Version 1.3.0

- Implemented authentication and billing routes for Jpn region.
- Refactored and changed the project structure from CommonJS to ES Modules
This commit is contained in:
Junior 2025-04-29 16:20:09 -03:00
parent 9584e58143
commit c3d9e7afb5
76 changed files with 3847 additions and 1109 deletions

View file

@ -1,163 +1,48 @@
// Load environment variables
const env = require('./utils/env');
import config from './config.js';
const { logger } = config;
import { logSystemInfo } from './utils/systemInfo.js';
import memoryLogger from './utils/memoryLogger.js';
const { setupMemoryLogging } = memoryLogger;
// Import modules
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const compression = require('compression');
const rateLimit = require('express-rate-limit');
const expressWinston = require('express-winston');
const moment = require('moment-timezone');
const { logger } = require('./utils/logger');
const path = require('path');
// Import servers
import { startServer as startMainApp } from './servers/mainApp.js';
import { startServer as startUsaApp } from './servers/usaApp.js';
import { startServer as startJpnApp } from './servers/jpnApp.js';
import { startServer as startProxyServer } from './servers/proxyServer.js';
// Import routes
const authRouter = require('./routes/auth');
const billingRouter = require('./routes/billing');
const gatewayRouter = require('./routes/gateway');
const loginRouter = require('./routes/launcher/login');
const registerRouter = require('./routes/launcher/register');
const codeVerificationRouter = require('./routes/launcher/codeVerification');
const passwordResetEmailRouter = require('./routes/launcher/passwordResetEmail');
const passwordChangeRouter = require('./routes/launcher/changePassword');
const verificationEmailRouter = require('./routes/launcher/verificationEmail');
const launcherUpdaterRouter = require('./routes/launcher/launcherUpdater');
const onlineCountRouter = require('./routes/onlineCount');
// Parse command line arguments
const args = process.argv.slice(2);
const serversToStart = args.length > 0 ? args : ['mainApp'];
// Set up rate limiter
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 60, // limit each IP to 60 requests per minute
message: 'Too many requests from this IP, please try again later'
});
// Start selected servers
const activeServers = [];
const app = express();
// Set up middleware
const middleware = [
cors(),
compression(),
express.json(),
express.urlencoded({ extended: false }),
];
if (process.env.ENABLE_HELMET === 'true') {
middleware.unshift(helmet());
if (serversToStart.includes('mainApp')) {
activeServers.push(startMainApp());
}
app.use(...middleware);
const authPort = process.env.AUTH_PORT || 8070;
const billingPort = process.env.BILLING_PORT || 8080;
// Set up routes
app.use('/serverApi/auth', limiter, authRouter).listen(authPort, '127.0.0.1');
app.use('/serverApi/billing', limiter , billingRouter).listen(billingPort, '127.0.0.1');
app.use('/serverApi/gateway', limiter , gatewayRouter);
app.use('/accountApi/register', limiter , registerRouter);
app.use('/accountApi/login', limiter , loginRouter);
app.use('/accountApi/codeVerification', limiter , codeVerificationRouter);
app.use('/accountApi/sendPasswordResetEmail', limiter , passwordResetEmailRouter);
app.use('/accountApi/changePassword', limiter , passwordChangeRouter);
app.use('/accountApi/sendVerificationEmail', limiter , verificationEmailRouter);
app.use('/launcherApi/launcherUpdater', launcherUpdaterRouter);
app.use('/serverApi/onlineCount', limiter , onlineCountRouter);
// Serve static files from public folder
app.use(express.static('../public'));
// Serve static files for the launcher
app.get('/launcher/news', (req, res) => {
res.sendFile(path.join(__dirname, '../public/launcher/news/news-panel.html'));
});
app.get('/launcher/agreement', (req, res) => {
res.sendFile(path.join(__dirname, '../public/launcher/news/agreement.html'));
});
app.get('/favicon.ico', (req, res) => {
res.sendFile(path.join(__dirname, '../public/launcher/news/favicon.ico'));
});
app.use('/launcher/news/images', express.static(path.join(__dirname, '../public/launcher/news/images')));
app.use('/launcher/news', express.static(path.join(__dirname, '../public/launcher/news')));
app.use('/launcher/patch', express.static(path.join(__dirname, '../public/launcher/patch')));
app.use('/launcher/client', express.static(path.join(__dirname, '../public/launcher/client')));
// Set up error handling middleware
app.use((err, req, res, next) => {
if (env.LOG_LEVEL && env.LOG_LEVEL === 'error') {
logger.error(err.stack);
} else {
logger.info(err.stack);
}
res.status(500).send('A error ocurred. Try again later.');
});
// Node.js version
const nodeVersion = process.version;
// timezone
const timezone = process.env.TZ || new Date().toLocaleString('en-US', { timeZoneName: 'short' });
const offsetInMinutes = moment.tz(timezone).utcOffset();
const offsetHours = Math.floor(Math.abs(offsetInMinutes) / 60);
const offsetMinutes = Math.abs(offsetInMinutes) % 60;
const offsetSign = offsetInMinutes >= 0 ? '+' : '-';
const offsetString = `${offsetSign}${offsetHours.toString().padStart(2, '0')}:${offsetMinutes.toString().padStart(2, '0')}`;
const memoryUsage = process.memoryUsage();
// Function to format bytes as human-readable string
function formatBytes(bytes) {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
if (bytes === 0) {
return '0 B';
}
const i = Math.floor(Math.log2(bytes) / 10);
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
if (serversToStart.includes('usaApp')) {
activeServers.push(startUsaApp());
}
// Start server
const port = process.env.PORT || 3000;
const publicIP = process.env.PUBLIC_IP || '0.0.0.0';
console.log('--------------------------------------------------');
console.log(`Rusty Hearts API Version: 1.2`)
console.log(`Node.js Version: ${nodeVersion}`);
console.log(`Timezone: ${timezone} (${offsetString})`);
console.log('Memory Usage:');
console.log(` RSS: ${formatBytes(memoryUsage.rss)}`);
console.log(` Heap Total: ${formatBytes(memoryUsage.heapTotal)}`);
console.log(` Heap Used: ${formatBytes(memoryUsage.heapUsed)}`);
console.log(` External: ${formatBytes(memoryUsage.external)}`);
console.log(` Array Buffers: ${formatBytes(memoryUsage.arrayBuffers)}`);
console.log('--------------------------------------------------');
// Function to log memory usage
function logMemoryUsage() {
const now = new Date();
const formattedDateTime = moment(now).format('YYYY-MM-DD HH:mm:ss');
const memoryUsage = process.memoryUsage();
console.log(`Memory Usage at ${formattedDateTime}:`);
console.log(` RSS : ${formatBytes(memoryUsage.rss)}`);
console.log(` Heap Total : ${formatBytes(memoryUsage.heapTotal)}`);
console.log(` Heap Used : ${formatBytes(memoryUsage.heapUsed)}`);
console.log(` External : ${formatBytes(memoryUsage.external)}`);
console.log(` Array Buffers: ${formatBytes(memoryUsage.arrayBuffers)}`);
console.log('--------------------------------------------------');
if (serversToStart.includes('jpnApp')) {
activeServers.push(startJpnApp());
}
// Log memory usage every 30 minutes (1800000 milliseconds)
const memoryLogInterval = 1800000;
if (serversToStart.includes('proxyServer')) {
activeServers.push(startProxyServer());
}
setInterval(logMemoryUsage, memoryLogInterval);
// System Info
logSystemInfo();
app.listen(port, publicIP, () => {
logger.info(`API listening on ${publicIP}:${port}`);
logger.info(`Auth API listening on 127.0.0.1:${authPort}`);
logger.info(`Billing API listening on 127.0.0.1:${billingPort}`);
});
// Memory Logging
const stopMemoryLogging = setupMemoryLogging();
// Handle shutdown
process.on('SIGINT', () => {
logger.info('Shutting down servers...');
activeServers.forEach(server => server.close());
stopMemoryLogging();
process.exit();
});

100
src/config.js Normal file
View file

@ -0,0 +1,100 @@
import dotenv from 'dotenv';
dotenv.config();
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import cors from 'cors';
import compression from 'compression';
import express from 'express';
import helmet from 'helmet';
import { logger } from './utils/logger.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export default {
// Port configurations
ports: {
main: process.env.API_LISTEN_PORT || 80,
usaApp: process.env.API_USA_PORT || 8070,
jpnApp: process.env.API_JPN_PORT || 8080,
proxy: process.env.API_PROXY_PORT || 8090,
gate: process.env.GATESERVER_PORT || 50001,
},
// IP configurations
ips: {
public: process.env.API_LISTEN_HOST || '0.0.0.0',
local: process.env.API_LOCAL_LISTEN_HOST || '127.0.0.1',
gate: process.env.GATESERVER_IP,
},
// configurations
config: {
serverId: Number(process.env.SERVER_ID) || 10101,
shopBalance: process.env.API_SHOP_INITIAL_BALANCE || 0,
timeZone: process.env.TZ,
},
// API configurations
apiConfig: {
trustProxyEnabled: process.env.API_TRUSTPROXY_ENABLE || 'false',
trustProxyHosts: process.env.API_TRUSTPROXY_HOSTS || [],
logIPAddresses: process.env.LOG_IP_ADDRESSES || 'false',
},
// Mailer configurations
mailer: {
host: process.env.SMTP_HOST,
port: Number(process.env.SMTP_PORT),
secure: process.env.SMTP_ENCRYPTION === 'ssl' || process.env.SMTP_ENCRYPTION === 'tls',
auth: {
user: process.env.SMTP_USERNAME,
pass: process.env.SMTP_PASSWORD
}
},
// middleware
middleware: {
baseMiddleware: [
cors(),
compression(),
express.json(),
express.urlencoded({ extended: true }),
],
getMiddleware: function () {
const middleware = [...this.baseMiddleware];
if (process.env.API_ENABLE_HELMET === 'true') {
middleware.unshift(helmet());
}
return middleware;
}
},
// Static file paths
staticPaths: {
public: path.join(__dirname, '../public'),
launcherNews: path.join(__dirname, '../public/launcher/news'),
launcherNewsImages: path.join(__dirname, '../public/launcher/news/images'),
launcherPatch: path.join(__dirname, '../public/launcher/patch'),
launcherClient: path.join(__dirname, '../public/launcher/client'),
site: path.join(__dirname, '../public/site'),
},
// Logger
logger,
// Backend configuration for proxy
BACKENDS: {
AUTH: {
paths: ['/Auth/cgi-bin/auth_rest_oem.cgi']
},
BILLING: {
paths: ['/Billing/S1/ApiPointTotalGetS.php', '/Billing/S1/ApiPointMoveS.php']
}
}
};

View file

@ -0,0 +1,41 @@
import { RateLimiterMemory } from 'rate-limiter-flexible';
import { api } from '../../config/rateLimits.default.js';
class RateLimitConfig {
constructor() {
this.limiters = new Map();
this.loadConfig();
}
loadConfig() {
// API Limiters for /launcher
for (const [endpoint, config] of Object.entries(api.launcher)) {
this.limiters.set(
`/launcher/${endpoint.charAt(0).toUpperCase()}${endpoint.slice(1)}`,
new RateLimiterMemory({
points: config.points,
duration: config.duration,
blockDuration: config.blockDuration
})
);
}
// API Limiters for /launcherAction
for (const [endpoint, config] of Object.entries(api.launcherAction)) {
this.limiters.set(
`/launcherAction/${endpoint}`,
new RateLimiterMemory({
points: config.points,
duration: config.duration,
blockDuration: config.blockDuration
})
);
}
}
getLimiter(path) {
return this.limiters.get(path);
}
}
export default new RateLimitConfig();

View file

@ -0,0 +1,7 @@
// This middleware sets the "Connection" header to "close" for all responses.
// It ensures that the connection is closed after the response is sent, which can help with resource management and performance.
// The 'next()' function is called to pass control to the next middleware or route handler in the stack.
export const closeConnection = (req, res, next) => {
res.set("Connection", "close");
next();
};

43
src/lib/rateLimiter.js Normal file
View file

@ -0,0 +1,43 @@
import rateLimitConfig from "../config/rateLimitConfig.js";
import { getClientIp } from "../utils/getClientIp.js";
async function rateLimiter(req, res, next) {
try {
const path = (req.baseUrl + req.path).replace(/\/+$/, "");
const limiterKey = Array.from(rateLimitConfig.limiters.keys()).find((key) =>
path.startsWith(key)
);
if (!limiterKey) {
console.warn(`[RateLimiter] No limiter found for path: "${path}"`);
return next();
}
const limiter = rateLimitConfig.getLimiter(limiterKey);
const clientIP = getClientIp(req);
const rateLimitRes = await limiter.consume(clientIP);
res.set({
"Retry-After": rateLimitRes.msBeforeNext / 1000,
"X-RateLimit-Limit": limiter.points,
"X-RateLimit-Remaining": rateLimitRes.remainingPoints,
"X-RateLimit-Reset": new Date(Date.now() + rateLimitRes.msBeforeNext),
});
next();
} catch (rateLimitRes) {
res.set({
"Retry-After": rateLimitRes.msBeforeNext / 1000,
});
return res.status(429).json({
error: "Too many requests",
message: `You've exceeded the rate limit. Please try again in ${Math.ceil(
rateLimitRes.msBeforeNext / 1000
)} seconds.`,
});
}
}
export default rateLimiter;

View file

@ -1,18 +1,24 @@
const nodemailer = require('nodemailer');
const handlebars = require('handlebars');
const fs = require('fs');
const path = require('path');
const { mailerLogger } = require('../utils/logger');
import { createTransport } from 'nodemailer';
import handlebars from 'handlebars';
import { readFileSync } from 'fs';
import { logger, mailerLogger } from '../utils/logger.js';
import path from 'path';
import config from '../config.js';
const { mailer } = config;
import { fileURLToPath } from 'url';
import { dirname } from 'path';
// Load the email templates
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Load and compile email templates
const emailTemplates = {
confirmation: fs.readFileSync(path.join(__dirname, 'templates', 'confirmationTemplate.hbs'), 'utf-8'),
verification: fs.readFileSync(path.join(__dirname, 'templates', 'verificationTemplate.hbs'), 'utf-8'),
passwordReset: fs.readFileSync(path.join(__dirname, 'templates', 'passwordResetTemplate.hbs'), 'utf-8'),
passwordChanged: fs.readFileSync(path.join(__dirname, 'templates', 'passwordChangedTemplate.hbs'), 'utf-8')
confirmation: readFileSync(path.join(__dirname, 'templates', 'confirmationTemplate.hbs'), 'utf-8'),
verification: readFileSync(path.join(__dirname, 'templates', 'verificationTemplate.hbs'), 'utf-8'),
passwordReset: readFileSync(path.join(__dirname, 'templates', 'passwordResetTemplate.hbs'), 'utf-8'),
passwordChanged: readFileSync(path.join(__dirname, 'templates', 'passwordChangedTemplate.hbs'), 'utf-8')
};
// Compile the email templates
const compiledTemplates = {
confirmation: handlebars.compile(emailTemplates.confirmation),
verification: handlebars.compile(emailTemplates.verification),
@ -20,23 +26,47 @@ const compiledTemplates = {
passwordChanged: handlebars.compile(emailTemplates.passwordChanged)
};
// SMTP transport configuration
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
secure: process.env.SMTP_ENCRYPTION === 'ssl' || process.env.SMTP_ENCRYPTION === 'tls',
auth: {
user: process.env.SMTP_USERNAME,
pass: process.env.SMTP_PASSWORD
}
});
// Check for required environment variables
function isMailerConfigured() {
const requiredEnvVars = [
'SMTP_HOST',
'SMTP_PORT',
'SMTP_ENCRYPTION',
'SMTP_USERNAME',
'SMTP_PASSWORD',
'SMTP_FROM_NAME'
];
const missing = requiredEnvVars.filter(key => !process.env[key]);
if (missing.length) {
logger.warn(`[Mailer] SMTP server is not configured. Missing environment variables: ${missing.join(', ')}`);
return false;
}
return true;
}
// Create transporter
function getTransporter() {
return createTransport({
host: mailer.host,
port: Number(mailer.port),
secure: mailer.secure,
auth: {
user: mailer.auth.user,
pass: mailer.auth.pass
},
});
}
// Email send functions
function sendConfirmationEmail(email, windyCode) {
const template = compiledTemplates.confirmation;
const emailContent = template({ windyCode });
if (!isMailerConfigured()) return;
const transporter = getTransporter();
const emailContent = compiledTemplates.confirmation({ windyCode });
const mailOptions = {
from: `"${process.env.SMTP_FROMNAME}" <${process.env.SMTP_USERNAME}>`,
from: `"${process.env.SMTP_FROM_NAME}" <${process.env.SMTP_EMAIL_FROM_ADDRESS}>`,
to: email,
subject: '[Rusty Hearts] Account Creation Confirmation',
html: emailContent
@ -44,19 +74,21 @@ function sendConfirmationEmail(email, windyCode) {
transporter.sendMail(mailOptions, (error, info) => {
if (error) {
mailerLogger.error('[Mailer] Error sending confirmation email: ' + error.message);
mailerLogger.error('[Mailer] Error sending confirmation email: ' + error.message);
} else {
mailerLogger.info('[Mailer] Confirmation email sent: ' + info.response);
mailerLogger.info('[Mailer] Confirmation email sent: ' + info.response);
}
});
}
function sendVerificationEmail(email, verificationCode) {
const template = compiledTemplates.verification;
const emailContent = template({ verificationCode });
if (!isMailerConfigured()) return;
const transporter = getTransporter();
const emailContent = compiledTemplates.verification({ verificationCode });
const mailOptions = {
from: `"${process.env.SMTP_FROMNAME}" <${process.env.SMTP_USERNAME}>`,
from: `"${process.env.SMTP_FROM_NAME}" <${process.env.SMTP_EMAIL_FROM_ADDRESS}>`,
to: email,
subject: '[Rusty Hearts] Account Creation',
html: emailContent
@ -72,11 +104,13 @@ function sendVerificationEmail(email, verificationCode) {
}
function sendPasswordResetEmail(email, verificationCode) {
const template = compiledTemplates.passwordReset;
const emailContent = template({ verificationCode });
if (!isMailerConfigured()) return;
const transporter = getTransporter();
const emailContent = compiledTemplates.passwordReset({ verificationCode });
const mailOptions = {
from: `"${process.env.SMTP_FROMNAME}" <${process.env.SMTP_USERNAME}>`,
from: `"${process.env.SMTP_FROM_NAME}" <${process.env.SMTP_EMAIL_FROM_ADDRESS}>`,
to: email,
subject: '[Rusty Hearts] Password Reset Request',
html: emailContent
@ -92,11 +126,13 @@ function sendPasswordResetEmail(email, verificationCode) {
}
function sendPasswordChangedEmail(email, windyCode) {
const template = compiledTemplates.passwordChanged;
const emailContent = template({ windyCode });
if (!isMailerConfigured()) return;
const transporter = getTransporter();
const emailContent = compiledTemplates.passwordChanged({ windyCode });
const mailOptions = {
from: `"${process.env.SMTP_FROMNAME}" <${process.env.SMTP_USERNAME}>`,
from: `"${process.env.SMTP_FROM_NAME}" <${process.env.SMTP_EMAIL_FROM_ADDRESS}>`,
to: email,
subject: '[Rusty Hearts] Account Password Changed',
html: emailContent
@ -111,4 +147,9 @@ function sendPasswordChangedEmail(email, windyCode) {
});
}
module.exports = {sendConfirmationEmail, sendVerificationEmail, sendPasswordResetEmail, sendPasswordChangedEmail};
export {
sendConfirmationEmail,
sendVerificationEmail,
sendPasswordResetEmail,
sendPasswordChangedEmail
};

View file

@ -1,86 +0,0 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const xml2js = require('xml2js');
const sql = require('mssql');
const Joi = require('joi');
const { authLogger } = require('../utils/logger');
const { connAccount } = require('../utils/dbConfig');
const router = express.Router();
const parser = new xml2js.Parser({ explicitArray: false });
// Joi schema for request body validation
const schema = Joi.object({
'login-request': Joi.object({
account: Joi.string().required(),
password: Joi.string().required(),
game: Joi.string().required(),
ip: Joi.string().required(),
}).required(),
});
// Route for handling login requests
router.post('/', express.text({ type: '*/xml' }), async (req, res) => {
try {
const xml = req.body;
const result = await parser.parseStringPromise(xml);
// Validate the request body against the schema
const { error, value } = schema.validate(result);
if (error) {
authLogger.info(`[Auth] Invalid login request: ${error.message}`);
return res.send('<status>failed</status>');
}
const { 'login-request': loginRequest } = value;
const { account, password, game, ip } = loginRequest;
authLogger.info(`[Auth] Account [${account}] is trying to login from [${ip}]`);
// Create a connection pool for the database
const pool = await connAccount;
// Get the account information from the database
const { recordset } = await pool
.request()
.input('Identifier', sql.VarChar(50), account)
.execute('GetAccount');
// Check if the account exists
const row = recordset[0];
if (!row || row.Result !== 'AccountExists') {
authLogger.info(`[Auth] Account [${account}] login failed from [${ip}]`);
return res.send('<status>failed</status>');
}
// Verify the password using bcrypt
const passwordMatch = await bcrypt.compare(password, row.AccountPwd);
// Authenticate the user and update the database
const { recordset: authRecordset } = await pool
.request()
.input('Identifier', sql.VarChar(50), account)
.input('password_verify_result', sql.Bit, passwordMatch ? 1 : 0)
.input('LastLoginIP', sql.VarChar(50), ip)
.execute('AuthenticateUser');
const authRow = authRecordset[0];
if (!authRow || authRow.Result !== 'LoginSuccess') {
authLogger.info(`[Auth] Account [${account}] login failed from [${ip}]`);
return res.send('<status>failed</status>');
}
// Send the authentication response
const response = `<userid>${authRow.AuthID}</userid><user-type>F</user-type><status>success</status>`;
res.set('Content-Type', 'text/xml; charset=utf-8');
res.send(response);
authLogger.info(`[Auth] Account [${account}] successfully logged in from [${ip}]`);
} catch (error) {
authLogger.error(`Error handling login request: ${error.message}`);
res.status(500).send('<status>failed</status>');
}
});
module.exports = router;

114
src/routes/authJpn.js Normal file
View file

@ -0,0 +1,114 @@
import { Router, urlencoded } from "express";
import joi from "joi";
import config from "../config.js";
const { apiConfig } = config;
import { authLogger } from "../utils/logger.js";
import { authenticateUser } from "../services/accountDBService.js";
import { getClientIp } from "../utils/getClientIp.js";
const router = Router();
const schema = joi.object({
service_id: joi.string().required(),
product_name: joi.string().allow("").optional(),
original_id: joi.string().required(),
original_password: joi.string().required(),
ip: joi.string().ip().required(),
});
function buildSuccessResponse(statusCode, idFlg, userId, authToken) {
return `<response>
<status>${statusCode}</status>
<id_flg>${idFlg}</id_flg>
<user_id>${userId}</user_id>
<auth_token>${authToken}</auth_token>
</response>`;
}
function buildErrorResponse(statusCode, idFlg) {
return `<response>
<status>${statusCode}</status>
<id_flg>${idFlg}</id_flg>
</response>`;
}
router.post(
"/cgi-bin/auth_rest_oem.cgi",
urlencoded({ extended: true }),
async (req, res) => {
try {
res.set({
"Content-Type": "text/xml",
Connection: "close",
});
const ip = getClientIp(req);
// Validate request
const { error } = schema.validate({ ...req.body, ip });
if (error) {
authLogger.warn(`[Auth] [JPN] Invalid request: ${error.message}`);
return res.send(buildErrorResponse(1, 0));
}
const { original_id, original_password } = req.body;
authLogger.info(
apiConfig.logIPAddresses === "true"
? `[Auth] [JPN] Account [${original_id}] is trying to login from [${ip}]`
: `[Auth] [JPN] Account [${original_id}] is trying to login`
);
// Authenticate user
const authResult = await authenticateUser(
original_id,
original_password,
ip
);
// Handle different authentication results
switch (authResult.status) {
case "LoginSuccess":
authLogger.info(
`[Auth] [JPN] Account [${original_id}] login success`
);
return res.send(
buildSuccessResponse(0, 0, authResult.authId, authResult.token)
);
case "Locked":
authLogger.warn(
`[Auth] [JPN] Account [${original_id}] login failed - account locked`
);
return res.send(buildErrorResponse(0, 2));
case "InvalidCredentials":
authLogger.warn(
`[Auth] [JPN] Account [${original_id}] login failed - invalid credentials`
);
return res.send(buildErrorResponse(1, 0));
case "TooManyAttempts":
authLogger.warn(
`[Auth] [JPN] Account [${original_id}] login failed - too many attempts`
);
return res.send(buildErrorResponse(8, 0));
case "AccountNotFound":
authLogger.warn(`[Auth] [JPN] Account [${original_id}] not found`);
return res.send(buildErrorResponse(2, 0));
default:
authLogger.error(
`[Auth] [JPN] Unknown authentication status: ${authResult.status}`
);
return res.send(buildErrorResponse(18, 0));
}
} catch (error) {
authLogger.error(`[Auth] [JPN] Error handling auth request: ${error.message}`);
return res.send(buildErrorResponse(18, 0));
}
}
);
export default router;

110
src/routes/authUsa.js Normal file
View file

@ -0,0 +1,110 @@
import { Router, text } from "express";
import { Parser } from "xml2js";
import joi from "joi";
import config from "../config.js";
const { apiConfig } = config;
import { authLogger } from "../utils/logger.js";
import { authenticateUser } from "../services/accountDBService.js";
import { getClientIp } from "../utils/getClientIp.js";
const router = Router();
const parser = new Parser({ explicitArray: false });
// body validation
const schema = joi.object({
"login-request": joi
.object({
account: joi.string().required(),
password: joi.string().required(),
game: joi.string().required(),
ip: joi.string().required(),
})
.required(),
});
function buildSuccessResponse(userId, userType, authToken) {
return `<userid>${userId}</userid>
<user-type>${userType}</user-type>
<auth_token>${authToken}</auth_token>
<status>success</status>`;
}
function buildErrorResponse(message) {
return `<status>failed</status>
<message>${message}</message>`;
}
router.post("/", text({ type: "*/xml" }), async (req, res) => {
try {
res.set({
"Content-Type": "text/xml",
Connection: "close",
});
const ip = getClientIp(req);
const xml = req.body;
const result = await parser.parseStringPromise(xml);
const { error, value } = schema.validate(result);
if (error) {
authLogger.info(`[Auth] [USA] Invalid login request: ${error.message}`);
return res.send(buildErrorResponse("ValidationError"));
}
const { "login-request": loginRequest } = value;
const { account, password } = loginRequest;
authLogger.info(
apiConfig.logIPAddresses === "true"
? `[Auth] [USA] Account [${account}] is trying to login from [${ip}]`
: `[Auth] [USA] Account [${account}] is trying to login`
);
// Authenticate user
const authResult = await authenticateUser(account, password, ip, true);
// Handle results
switch (authResult.status) {
case "LoginSuccess":
authLogger.info(`[Auth] [USA] Account [${account}] login success`);
return res.send(
buildSuccessResponse(authResult.authId, "F", authResult.token)
);
case "Locked":
authLogger.warn(
`[Auth] [USA] Account [${account}] login failed - account locked`
);
return res.send(buildErrorResponse(authResult.status));
case "InvalidCredentials":
authLogger.warn(
`[Auth] [USA] Account [${account}] login failed - invalid credentials`
);
return res.send(buildErrorResponse(authResult.status));
case "TooManyAttempts":
authLogger.warn(
`[Auth] [USA] Account [${account}] login failed - too many attempts`
);
return res.send(buildErrorResponse(authResult.status));
case "AccountNotFound":
authLogger.warn(`[Auth] [USA] Account [${account}] not found`);
return res.send(buildErrorResponse(authResult.status));
default:
authLogger.error(
`[Auth] [USA] Unknown authentication status: ${authResult.status}`
);
return res.send(buildErrorResponse(authResult.status));
}
} catch (error) {
authLogger.error(
`[Auth] [USA] Error handling auth request: ${error.message}`
);
return res.send(buildErrorResponse("ServerError"));
}
});
export default router;

View file

@ -1,138 +0,0 @@
const express = require('express');
const bodyParser = require('body-parser');
const xml2js = require('xml2js');
const sql = require('mssql');
const router = express.Router();
const { billingLogger } = require('../utils/logger');
const Joi = require('joi');
// Set up database connection
const { connAccount } = require('../utils/dbConfig');
// Define the validation schema for currency requests
const currencySchema = Joi.object({
userid: Joi.string().required(),
server: Joi.string().required(),
game: Joi.string().required(),
}).required();
// Define the validation schema for item purchase requests
const itemPurchaseSchema = Joi.object({
userid: Joi.string().required(),
server: Joi.string().required(),
charid: Joi.string().required(),
game: Joi.number().required(),
uniqueid: Joi.string().required(),
amount: Joi.number().required(),
itemid: Joi.string().required(),
count: Joi.number().required(),
}).required();
// Route for handling billing requests
router.post('/', bodyParser.text({
type: '*/xml'
}), async (req, res) => {
try {
const xml = req.body;
const result = await xml2js.parseStringPromise(xml, { explicitArray: false });
const name = result['currency-request'] ? 'currency-request' : 'item-purchase-request';
const request = result[name];
// Validate the request against the appropriate schema
const { error, value } = name === 'currency-request'
? currencySchema.validate(request)
: itemPurchaseSchema.validate(request);
if (error) {
billingLogger.info(`[Billing] Invalid request: ${error.message}`);
return res.status(400).send('<status>failed</status>');
}
const { userid, server, game, charid, uniqueid, amount, itemid, count } = value;
// Create a connection pool for the database
const pool = await connAccount;
switch (name) {
case 'currency-request':
const { recordset } = await pool
.request()
.input('UserId', sql.VarChar(50), userid)
.input('ServerId', sql.VarChar(50), server)
.execute('GetCurrency');
const row = recordset[0];
if (row && row.Result === 'Success') {
const response = `<result><balance>${row.Zen}</balance></result>`;
res.set('Content-Type', 'text/xml; charset=utf-8');
return res.send(response);
} else {
billingLogger.error(`[Billing] Currency request from user [${userid}] failed: ${row.Result}`);
return res.status(400).send('<status>failed</status>');
}
case 'item-purchase-request':
billingLogger.info(`[Billing] Received [${name}] from user [${userid}]`);
const { recordset: currencyRecordset } = await pool
.request()
.input('UserId', sql.VarChar(50), userid)
.input('ServerId', sql.VarChar(50), server)
.execute('GetCurrency');
const currencyRow = currencyRecordset[0];
if (currencyRow && currencyRow.Result === 'Success') {
const balance = currencyRow.Zen;
if (amount > 0) {
if (amount > balance) {
billingLogger.warn(`[Billing] Item purchase with id [${uniqueid}] from user [${userid}] failed. Not enough Zen [${balance}]. charid: [${charid}] itemid: [${itemid}] itemcount: [${count}] price: [${amount}]`);
return res.status(400).send('<status>failed</status>');
} else {
const newbalance = balance - amount;
await pool
.request()
.input('UserId', sql.VarChar(50), userid)
.input('ServerId', sql.VarChar(50), server)
.input('NewBalance', sql.BigInt, newbalance)
.execute('SetCurrency');
await pool
.request()
.input('userid', sql.VarChar(50), userid)
.input('charid', sql.VarChar(50), charid)
.input('uniqueid', sql.VarChar(50), uniqueid)
.input('amount', sql.BigInt, amount)
.input('itemid', sql.VarChar(50), itemid)
.input('itemcount', sql.Int, count)
.execute('SetBillingLog');
billingLogger.info(`[Billing] Item purchase with id [${uniqueid}] from user [${userid}] success. charid: [${charid}] itemid: [${itemid}] itemcount: [${count}] price: [${amount}]`);
billingLogger.info(`[Billing] [${userid}] Zen balance before purchase: [${balance}] | New zen balance: [${newbalance}]`);
const response = `<result><status>success</status><new-balance>${newbalance}</new-balance></result>`;
res.set('Content-Type', 'text/xml; charset=utf-8');
return res.send(response);
}
} else {
const response = `<result><balance>${currencyRow.Zen}</balance></result>`;
res.set('Content-Type', 'text/xml; charset=utf-8');
return res.send(response);
}
} else {
return res.status(400).send('<status>failed</status>');
}
default:
return res.status(400).send('<status>failed</status>');
}
} catch (error) {
billingLogger.error(`[Billing] Error handling request: ${error.message}`);
return res.status(500).send('<status>failed</status>');
}
});
module.exports = router;

117
src/routes/billingJpn.js Normal file
View file

@ -0,0 +1,117 @@
import { Router, urlencoded } from 'express';
import joi from 'joi';
import { billingLogger } from '../utils/logger.js';
import { validateCredentials, getCurrency, setCurrency } from '../services/accountDBService.js';
const router = Router();
function buildSuccessResponse(balance) {
return `<response>
<status>0</status>
<total_point>${balance}</total_point>
</response>`;
}
function buildErrorResponse(statusCode, errorMessage) {
return `<response>
<status>${statusCode}</status>
<error>${errorMessage}</error>
</response>`;
}
function extractItemId(itemCode) {
if (!itemCode || itemCode.length !== 17 || !itemCode.startsWith('rsty')) {
throw new Error('Invalid item_code format');
}
return parseInt(itemCode.substring(4, 14), 10);
}
// Validation Schemas
const baseSchema = {
product_name: joi.string().allow('').optional(),
original_id: joi.string().required(),
original_password: joi.string().required(),
auto_charge_exec: joi.number().integer().optional()
};
const purchaseSchema = joi.object({
...baseSchema,
move_point: joi.number().integer().required(),
move_kind: joi.string().valid('06').required(),
item_code: joi.string().pattern(/^rsty\d{13}$/).required()
});
const currencySchema = joi.object(baseSchema);
async function handleBalanceRequest(req, res) {
try {
res.set({
'Content-Type': 'text/xml',
'Connection': 'close'
});
const { error, value } = currencySchema.validate(req.body);
if (error) throw new Error(`Invalid request: ${error.message}`);
const { original_id, original_password } = value;
if (!await validateCredentials(original_id, original_password)) {
throw new Error('Invalid credentials');
}
const balance = await getCurrency(original_id);
return res.send(buildSuccessResponse(balance));
} catch (error) {
billingLogger.error(`[Billing] [JPN] Balance request error: ${error.message}`);
return res.send(buildErrorResponse(1, error.message.includes('Invalid') ? error.message : 'Invalid request'));
}
}
async function handlePurchaseRequest(req, res) {
try {
res.set({
'Content-Type': 'text/xml',
'Connection': 'close'
});
const { error, value } = purchaseSchema.validate(req.body);
if (error) throw new Error(`Invalid request: ${error.message}`);
const { original_id, original_password, move_point, item_code } = value;
if (!await validateCredentials(original_id, original_password)) {
throw new Error('Invalid credentials');
}
const currentBalance = await getCurrency(original_id);
const newBalance = currentBalance + move_point;
if (newBalance < 0) {
throw new Error('Insufficient balance');
}
const item_id = extractItemId(item_code);
await setCurrency(original_id, newBalance);
billingLogger.info(`[Billing] [JPN] Purchase processed for [${original_id}]
Shop Item: ${item_id}
Price: ${move_point}
Balance: ${currentBalance} ${newBalance}`);
return res.send(buildSuccessResponse(newBalance));
} catch (error) {
billingLogger.error(`[Billing] [JPN] Purchase error: ${error.message}`);
return res.send(buildErrorResponse(
error.message.includes('Insufficient') ? 1 : 18,
error.message.includes('Invalid') ? error.message : 'Transaction failed'
));
}
}
// Routes
router.post('/S1/ApiPointTotalGetS.php', urlencoded({ extended: true }), handleBalanceRequest);
router.post('/S1/ApiPointMoveS.php', urlencoded({ extended: true }), handlePurchaseRequest);
export default router;

146
src/routes/billingUsa.js Normal file
View file

@ -0,0 +1,146 @@
import { Router } from "express";
import bodyParser from "body-parser";
import { parseStringPromise } from "xml2js";
const router = Router();
import { billingLogger } from "../utils/logger.js";
import Joi from "joi";
import {
getCurrency,
setCurrency,
logBillingTransaction,
} from "../services/accountDBService.js";
// Schemas
const currencySchema = Joi.object({
userid: Joi.string().required(),
server: Joi.string().required(),
game: Joi.string().required(),
}).required();
const itemPurchaseSchema = Joi.object({
userid: Joi.string().required(),
server: Joi.string().required(),
charid: Joi.string().required(),
game: Joi.number().required(),
uniqueid: Joi.string().required(),
amount: Joi.number().required(),
itemid: Joi.string().required(),
count: Joi.number().required(),
}).required();
// XML response builders
function buildSuccessResponse(balance) {
return `<result><balance>${balance}</balance></result>`;
}
function buildPurchaseSuccessResponse(newBalance) {
return `<result>
<status>success</status>
<new-balance>${newBalance}</new-balance>
</result>`;
}
function buildErrorResponse(message) {
return `<status>failed</status><message>${message}</message>`;
}
// Currency request handler
async function handleCurrencyRequest(data, res) {
const { error, value } = currencySchema.validate(data);
if (error) {
billingLogger.warn(`[Billing] Invalid currency request: ${error.message}`);
return res.status(400).send(buildErrorResponse("Invalid request"));
}
const { userid, server } = value;
try {
const currency = await getCurrency(userid);
return res.send(buildSuccessResponse(currency));
} catch (err) {
billingLogger.error(
`[Billing] Balance request error for [${userid}]: ${err.message}`
);
return res.status(400).send(buildErrorResponse("Failed to get balance"));
}
}
// Item purchase handler
async function handleItemPurchaseRequest(data, res) {
const { error, value } = itemPurchaseSchema.validate(data);
if (error) {
billingLogger.warn(`[Billing] Invalid purchase request: ${error.message}`);
return res.status(400).send(buildErrorResponse("Invalid request"));
}
const { userid, server, charid, uniqueid, amount, itemid, count } = value;
billingLogger.info(
`[Billing] Processing purchase [${uniqueid}] for user [${userid}]`
);
try {
const balance = await getCurrency(userid);
if (amount <= 0) {
return res.send(buildSuccessResponse(balance));
}
if (amount > balance) {
billingLogger.warn(
`[Billing] Insufficient funds for user [${userid}]: Has ${balance}, needs ${amount}`
);
return res.status(400).send(buildErrorResponse("Insufficient funds"));
}
const newBalance = balance - amount;
await setCurrency(userid, newBalance);
await logBillingTransaction({
userId: userid,
charId: charid,
uniqueId: uniqueid,
amount,
itemId: itemid,
count,
});
billingLogger.info(`[Billing] Purchase successful for user [${userid}]
Purchase ID: ${uniqueid}
CharacterId: [${charid}]
Item: ${itemid} (x${count})
Price: ${amount}
Balance: ${balance} ${newBalance}`);
return res.send(buildPurchaseSuccessResponse(newBalance));
} catch (err) {
billingLogger.error(
`[Billing] Purchase failed for user [${userid}]: ${err.message}`
);
return res.status(400).send(buildErrorResponse("Transaction failed"));
}
}
router.post("/", bodyParser.text({ type: "*/xml" }), async (req, res) => {
res.set({
"Content-Type": "text/xml",
Connection: "close",
});
try {
const xml = req.body;
const result = await parseStringPromise(xml, { explicitArray: false });
if (result["currency-request"]) {
return handleCurrencyRequest(result["currency-request"], res);
} else if (result["item-purchase-request"]) {
return handleItemPurchaseRequest(result["item-purchase-request"], res);
} else {
return res.status(400).send(buildErrorResponse("Invalid request type"));
}
} catch (err) {
billingLogger.error(`[Billing] Error handling billing request: ${err.message}`);
return res.status(500).send(buildErrorResponse("Internal server error"));
}
});
export default router;

View file

@ -1,58 +1,103 @@
const express = require('express');
const router = express.Router();
const net = require('net');
const { logger } = require('../utils/logger');
import { Router } from 'express';
const router = Router();
import { Socket } from 'net';
import config from '../config.js';
const { ports, ips, logger } = config;
import { create } from 'xmlbuilder2';
// Define the gateway route
// Constants
const SOCKET_TIMEOUT = 2000;
const GATEWAY_STATUS = {
ONLINE: 'online',
OFFLINE: 'offline'
};
/**
* Build XML response for gateway info
*/
function buildGatewayXml() {
return create({ version: '1.0', encoding: 'ISO-8859-1' })
.ele('network')
.ele('gateserver')
.att('ip', ips.gate)
.att('port', ports.gate)
.up()
.end({ prettyPrint: true });
}
/**
* Build gateway info route response
*/
function buildGatewayInfo(req) {
const baseUrl = `${req.protocol}://${req.headers.host}`;
return `1|${baseUrl}/launcher/GetGatewayAction|${baseUrl}/launcher/GetGatewayAction|`;
}
/**
* Check gateway server status via socket connection
*/
async function checkGatewayStatus() {
return new Promise((resolve) => {
const socket = new Socket();
socket.setTimeout(SOCKET_TIMEOUT);
socket.on('connect', () => {
socket.destroy();
resolve({ status: GATEWAY_STATUS.ONLINE });
});
socket.on('timeout', () => {
socket.destroy();
resolve({ status: GATEWAY_STATUS.OFFLINE, code: 408 });
});
socket.on('error', () => {
socket.destroy();
resolve({ status: GATEWAY_STATUS.OFFLINE, code: 503 });
});
socket.connect(ports.gate, ips.gate);
});
}
// Main gateway route
router.get('/', (req, res) => {
const ip = process.env.GATESERVER_IP;
const port = process.env.GATESERVER_PORT || '50001';
// Generate the XML content with the IP and port values
const xml = `<?xml version="1.0" encoding="ISO-8859-1"?>
<network>
<gateserver ip="${ip}" port="${port}" />
</network>`;
res.set('Content-Type', 'application/xml');
res.send(xml);
try {
res.set('Content-Type', 'application/xml');
res.send(buildGatewayXml());
logger.debug(`[Gateway] XML served to ${req.ip}`);
} catch (error) {
logger.error(`[Gateway] XML generation failed: ${error.message}`);
res.status(500).send('Internal Server Error');
}
});
// Define the gateway info route
// Gateway info route
router.get('/info', (req, res) => {
const gatewayRoute = `1|${req.protocol}://${req.headers.host}/serverApi/gateway|${req.protocol}://${req.headers.host}/serverApi/gateway|`;
res.send(gatewayRoute);
try {
res.send(buildGatewayInfo(req));
logger.debug(`[Gateway] Info served to ${req.ip}`);
} catch (error) {
logger.error(`[Gateway] Info generation failed: ${error.message}`);
res.status(500).send('Internal Server Error');
}
});
// Define the gateway status route
// Gateway status route
router.get('/status', async (req, res) => {
const ip = process.env.GATESERVER_IP;
const port = process.env.GATESERVER_PORT || '50001';
try {
const { status, code } = await checkGatewayStatus();
logger[status === GATEWAY_STATUS.ONLINE ? 'info' : 'warn'](
`[Gateway] Status check from ${req.ip}: ${status}`
);
const timeout = 2000;
// Create a new socket and connect to the gateserver
const socket = new net.Socket();
socket.setTimeout(timeout);
socket.connect(port, ip);
// Handle the socket events to check the connection status
socket.on('connect', () => {
logger.info(`[Gateway] Connection attempt success from IP: ${req.ip}`);
res.status(200).json({ status: 'online' });
socket.destroy();
});
socket.on('timeout', () => {
logger.warn(`[Gateway] Connection attempt timeout from IP: ${req.ip}`);
res.status(408).json({ status: 'offline' });
socket.destroy();
});
socket.on('error', () => {
logger.error(`[Gateway] Connection failed from IP: ${req.ip}`);
res.status(503).json({ status: 'offline' });
socket.destroy();
});
res.status(status === GATEWAY_STATUS.ONLINE ? 200 : code || 503)
.json({ status });
} catch (error) {
logger.error(`[Gateway] Status check failed: ${error.message}`);
res.status(500).json({ status: GATEWAY_STATUS.OFFLINE });
}
});
module.exports = router;
export default router;

View file

@ -1,108 +1,60 @@
const sql = require('mssql');
const express = require('express');
const router = express.Router();
const bcrypt = require('bcryptjs');
const crypto = require('crypto');
const { logger, accountLogger } = require('../../utils/logger');
const { sendPasswordChangedEmail } = require('../../mailer/mailer');
const Joi = require('joi');
import { Router } from "express";
const router = Router();
import joi from "joi";
import { logger, accountLogger } from "../../utils/logger.js";
import { changeAccountPassword } from "../../services/accountDBService.js";
// Set up database connection
const { connAccount } = require('../../utils/dbConfig');
// Joi schema for request body validation
const schema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().required(),
verification_code: Joi.string().pattern(new RegExp('^[0-9]+$')).required()
const schema = joi.object({
email: joi.string().email().required(),
password: joi.string().required(),
verificationCode: joi.string().pattern(new RegExp("^[0-9]+$")).required(),
});
// Route for registering an account
router.post('/', async (req, res) => {
router.post("/", async (req, res) => {
try {
// Validate request body
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(400).send(error.details[0].message);
return res.status(400).json({
success: false,
result: "ValidationError",
message: error.details[0].message,
});
}
const email = value.email;
const password = value.password;
const verificationCode = value.verification_code;
const { email, password, verificationCode } = value;
// Use a prepared statement to get the verification code
const pool = await connAccount;
const request = pool.request();
request.input('Email', sql.VarChar, email);
request.input('VerificationCode', sql.VarChar, verificationCode);
request.input('VerificationCodeType', sql.VarChar, 'Password');
const inputResult = await request.execute('GetVerificationCode');
const inputRow = inputResult.recordset[0];
const changeAccountPasswordStatus = await changeAccountPassword(
email,
password,
verificationCode
);
if (inputRow && inputRow.Result === 'ValidVerificationCode') {
// Use a prepared statement to retrieve the account information
const pool = await connAccount;
const request = pool.request();
request.input('Identifier', sql.VarChar, email);
const getResult = await request.execute('GetAccount');
const getRow = getResult.recordset[0];
if (getRow && getRow.Result === 'AccountExists') {
const windyCode = getRow.WindyCode
const hash = getRow.AccountPwd;
// Verify the password
const md5_password = crypto
.createHash('md5')
.update(windyCode + password)
.digest('hex');
const password_verify_result = await bcrypt.compare(
md5_password,
hash
switch (changeAccountPasswordStatus) {
case "PasswordChanged":
accountLogger.info(
`[Account] Account [${email}] password changed successfully`
);
if (password_verify_result === true) {
return res.status(400).send('SamePassword');
} else {
const passwordHash = await bcrypt.hash(md5_password, 10);
// Use a prepared statement to update the password
const pool = await connAccount;
const request = pool.request();
request.input('Email', sql.VarChar, email);
request.input('AccountPwd', sql.VarChar, passwordHash);
const updateResult = await request.execute('UpdateAccountPassword');
const updateRow = updateResult.recordset[0];
if (updateRow && updateRow.Result === 'PasswordChanged') {
accountLogger.info(`[Account] Password for [${windyCode}] changed successfully`);
sendPasswordChangedEmail(email, windyCode);
const pool = await connAccount;
const request = pool.request();
request.input('Email', sql.VarChar, email);
const clearResult = await request.execute('ClearVerificationCode');
const clearRow = clearResult.recordset[0];
return res.status(200).send('PasswordChanged');
} else {
accountLogger.info(`[Account] Password change for [${windyCode}] failed: ${row.Result}`);
return res.status(400).send(updateRow.Result);
}
}
} else {
return res.status(400).send(getRow.Result);
}
} else {
return res.status(400).send(inputRow.Result);
return res.status(200).json({
success: true,
result: changeAccountPasswordStatus,
});
default:
accountLogger.info(
`[Account] Account [${email}] password change failed: ${changeAccountPasswordStatus}`
);
return res.status(200).json({
success: false,
result: changeAccountPasswordStatus,
});
}
} catch (error) {
logger.error('[Account] A error ocourred: ' + error.message);
return res.status(500).send('A error ocourred. Please try again later.');
logger.error(`Account password change failed: ${error.message}`);
return res.status(500).json({
result: "ServerError",
message: "A server error occurred. Please try again later.",
});
}
});
module.exports = router;
export default router;

View file

@ -1,53 +1,63 @@
const sql = require('mssql');
const express = require('express');
const router = express.Router();
const { logger } = require('../../utils/logger');
const Joi = require('joi');
// Set up database connection
const { connAccount } = require('../../utils/dbConfig');
import { Router } from "express";
const router = Router();
import joi from "joi";
import { logger } from "../../utils/logger.js";
import { verifyCode } from "../../services/accountDBService.js";
// Joi schema for request body validation
const schema = Joi.object({
email: Joi.string().email().required(),
verification_code_type: Joi.string().required(),
verification_code: Joi.string().pattern(new RegExp('^[0-9]+$')).required()
const schema = joi.object({
email: joi.string().email().required(),
verificationCodeType: joi.string().required(),
verificationCode: joi.string().pattern(new RegExp("^[0-9]+$")).required(),
});
// Route for registering an account
router.post('/', async (req, res) => {
router.post("/", async (req, res) => {
try {
// Validate request body
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(400).send(error.details[0].message);
return res.status(400).json({
success: false,
result: "ValidationError",
message: error.details[0].message,
});
}
const email = req.body.email;
const verificationCode = req.body.verification_code;
const verificationCodeType = req.body.verification_code_type;
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return res.status(400).send('InvalidEmailFormat');
}
if (!/^\d+$/.test(verificationCode)) {
return res.status(400).send('InvalidVerificationCodeFormat');
}
const {
email,
verificationCode: verificationCode,
verificationCodeType: verificationCodeType,
} = value;
// Use a prepared statement to check verification code
const pool = await connAccount;
const request = pool.request();
request.input('Email', sql.VarChar, email);
request.input('VerificationCode', sql.VarChar, verificationCode);
request.input('VerificationCodeType', sql.VarChar, verificationCodeType);
const result = await request.execute('GetVerificationCode');
const row = result.recordset[0];
return res.status(200).send(row.Result);
} catch (error) {
logger.error('Database query failed: ' + error.message);
return res.status(500).send('A error ocourred. Please try again later.');
// Verify code with database
const verificationResult = await verifyCode(
email,
verificationCode,
verificationCodeType
);
if (verificationResult !== "ValidVerificationCode") {
logger.info(
`[Account] Verification failed for ${email}. Status: ${verificationResult}`
);
return res.status(200).json({
success: false,
result: verificationResult
});
}
logger.info(
`[Account] Verification successful for ${email}. Status: ${verificationResult}`
);
return res.status(200).json({
success: true,
result: verificationResult,
});
} catch (error) {
logger.error(`Verification failed: ${error.message}`);
return res.status(500).json({
result: "ServerError",
message: "A server error occurred. Please try again later.",
});
}
});
module.exports = router;
export default router;

View file

@ -1,41 +1,120 @@
const express = require('express');
const fs = require('fs');
const path = require('path');
const { logger } = require('../../utils/logger');
import { Router } from "express";
import { readFile, existsSync } from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { dirname } from "path";
import { logger } from "../../utils/logger.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const router = express.Router();
const router = Router();
// Endpoint to get the launcher version from the launcher_info.ini file
router.get('/getLauncherVersion', (req, res) => {
const launcherInfoPath = path.join(__dirname, '..', '..', '..', 'public', 'launcher', 'launcher_update', 'launcher_info.ini');
fs.readFile(launcherInfoPath, 'utf8', (err, data) => {
if (err) {
console.error(err);
return res.status(500).send('Error reading launcher_info.ini');
}
const versionRegex = /version=(.*)/i;
const match = data.match(versionRegex);
if (match) {
const launcherVersion = match[1];
return res.json({ version: launcherVersion });
}
return res.status(500).send('Invalid launcher_info.ini format');
});
router.get("/getLauncherVersion", (req, res) => {
try {
const launcherInfoPath = path.join(
__dirname,
"..",
"..",
"..",
"public",
"launcher",
"launcher_update",
"launcher_info.ini"
);
readFile(launcherInfoPath, "utf8", (err, data) => {
try {
if (err) {
logger.error(err);
return res.status(400).json({
result: "FileReadError",
message: "Error reading launcher_info.ini file",
});
}
const versionRegex = /version=(.*)/i;
const match = data.match(versionRegex);
if (match) {
const launcherVersion = match[1];
return res.status(200).json({
version: launcherVersion,
});
}
return res.status(400).json({
result: "VersionNotFound",
message: "Version not found in launcher_info.ini file",
});
} catch (error) {
logger.error(`[getLauncherVersion] Processing error: ${error}`);
return res.status(500).json({
result: "InternalError",
message: "An error occurred while processing the file",
});
}
});
} catch (error) {
logger.error(`[getLauncherVersion] Initialization error: ${error}`);
return res.status(500).json({
result: "InternalError",
message: "An error occurred while initializing the version check",
});
}
});
// Endpoint to download the new launcher version from the launcher_update folder
router.post('/updateLauncherVersion', (req, res) => {
const launcherUpdatePath = path.join(__dirname, '..', '..', '..', 'public', 'launcher', 'launcher_update');
const version = req.body.version;
if (!req.body.version) {
return res.status(400).send('Missing version parameter');
}
const file = path.join(launcherUpdatePath, `launcher_${version}.zip`);
if (!fs.existsSync(file)) {
return res.status(404).send(`File ${file} not found`);
logger.error(`[Launcher Updater] File ${file} not found`);
router.post("/updateLauncherVersion", (req, res) => {
try {
const launcherUpdatePath = path.join(
__dirname,
"..",
"..",
"..",
"public",
"launcher",
"launcher_update"
);
const version = req.body.version;
if (!version) {
return res.status(400).json({
result: "MissingVersion",
message: "Missing version parameter",
});
}
const fileName = `launcher_${version}.zip`;
const file = path.join(launcherUpdatePath, fileName);
if (!existsSync(file)) {
logger.error(`[Launcher Updater] File ${fileName} not found`);
return res.status(404).json({
result: "FileNotFound",
message: `File ${fileName} not found on the server`,
});
}
res.download(file, (err) => {
if (err) {
logger.error(`[updateLauncherVersion] Download error: ${err}`);
if (!res.headersSent) {
return res.status(500).json({
result: "DownloadError",
message: "Error occurred while downloading the file",
});
}
}
});
} catch (error) {
logger.error(`[updateLauncherVersion] Error: ${error}`);
if (!res.headersSent) {
return res.status(500).json({
result: "InternalError",
message: "An unexpected error occurred",
});
}
}
res.download(file);
});
module.exports = router;
export default router;

View file

@ -1,99 +1,63 @@
const sql = require('mssql');
const bcrypt = require('bcryptjs');
const crypto = require('crypto');
const express = require('express');
const router = express.Router();
const { logger, accountLogger } = require('../../utils/logger');
const Joi = require('joi');
import { Router } from "express";
const router = Router();
import joi from "joi";
import config from "../../config.js";
const { apiConfig } = config;
import { logger } from "../../utils/logger.js";
import { getClientIp } from "../../utils/getClientIp.js";
import { authenticateUser } from "../../services/accountDBService.js";
// Set up database connection
const { connAccount } = require('../../utils/dbConfig');
// Define the validation schema for the request body
const schema = Joi.object({
account: Joi.string().required(),
password: Joi.string().required(),
const schema = joi.object({
account: joi.string().required(),
password: joi.string().min(8).max(16).required(),
});
router.post('/', async (req, res) => {
router.post("/", async (req, res) => {
try {
// Validate the request body against the schema
// Validate request
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(400).send(error.details[0].message);
return res.status(400).json({
result: "InvalidRequest",
message: error.details[0].message,
});
}
const account = req.body.account;
const password = req.body.password;
const userIp = req.ip;
const { account, password } = value;
const ip = getClientIp(req);
// Check the format of the account identifier
if (
!/^[a-z0-9_-]{6,50}$/.test(account) &&
!/^[\w\d._%+-]+@[\w\d.-]+\.[\w]{2,}$/i.test(account)
) {
return res.status(400).json({ Result: 'InvalidUsernameFormat' });
}
// Use a prepared statement to retrieve the account information
const pool = await connAccount;
const request = pool.request();
request.input('Identifier', sql.VarChar, account);
const result = await request.execute('GetAccount');
const row = result.recordset[0];
if (row && row.Result === 'AccountExists') {
const windyCode = row.WindyCode;
const hash = row.AccountPwd;
// Verify the password
const md5_password = crypto
.createHash('md5')
.update(windyCode + password)
.digest('hex');
const password_verify_result = await bcrypt.compare(
md5_password,
hash
);
const authRequest = pool.request();
authRequest.input('Identifier', sql.VarChar, account);
authRequest.input(
'password_verify_result',
sql.Bit,
password_verify_result
);
authRequest.input('LastLoginIP', sql.VarChar, userIp);
const authResult = await authRequest.execute('AuthenticateUser');
const authRow = authResult.recordset[0];
if (authRow && authRow.Result === 'LoginSuccess') {
accountLogger.info(
`[Account] Launcher Login: Account [${windyCode}] successfully logged in from [${userIp}]`
);
return res.status(200).json({
Result: authRow.Result,
Token: authRow.Token,
WindyCode: authRow.WindyCode,
});
} else {
accountLogger.info(
`[Account] Launcher Login: Account [${windyCode}] login failed: ${authRow.Result} `
);
return res.status(400).json({
Result: authRow.Result,
});
}
} else {
return res.status(400).json({ Result: 'AccountNotFound' });
}
} catch (error) {
logger.error(
'[Account] Launcher Login: Database query failed: ' + error.message
logger.info(
apiConfig.logIPAddresses === "true"
? `[Launcher Login] Account [${account}] is trying to login from [${ip}]`
: `[Launcher Login] Account [${account}] is trying to login`
);
return res.status(500).send('Login failed. Please try again later.');
// Authenticate user
const authResult = await authenticateUser(account, password, ip);
if (!authResult || authResult.status !== "LoginSuccess") {
logger.warn(
`[Launcher Login] Authentication failed for user [${account}]: ${authResult?.status}`
);
return res.status(200).json({
result: authResult?.status || "AuthenticationFailed",
});
}
logger.info(
`[Launcher Login] Authentication successful for user [${account}]`
);
return res.status(200).json({
result: authResult.status,
token: authResult.token,
windyCode: account,
});
} catch (error) {
logger.error(`[Launcher Login] Authentication failed: ${error.message}`);
return res.status(500).json({
result: "ServerError",
message: "A server error occurred. Please try again later.",
});
}
});
module.exports = router;
export default router;

View file

@ -1,74 +1,52 @@
const sql = require('mssql');
const express = require('express');
const router = express.Router();
const { logger, accountLogger } = require('../../utils/logger');
const { sendPasswordResetEmail } = require('../../mailer/mailer');
const Joi = require('joi');
import { Router } from "express";
const router = Router();
import { logger, accountLogger } from "../../utils/logger.js";
import joi from "joi";
import { sendPasswordVerificationEmail } from "../../services/accountDBService.js";
// Set up database connection
const { connAccount } = require('../../utils/dbConfig');
// Joi schema for request body validation
const schema = Joi.object({
email: Joi.string().email().required()
const schema = joi.object({
email: joi.string().email().required(),
});
// Route for registering an account
router.post('/', async (req, res) => {
// Route for sending verification email for password reset
router.post("/", async (req, res) => {
try {
// Validate request body
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(400).send(error.details[0].message);
return res.status(400).json({
success: false,
result: "ValidationError",
message: error.details[0].message,
});
}
const email = req.body.email;
const { email } = value;
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
logger.info('Invalid email format');
return res.status(400).send('InvalidEmailFormat');
}
const sendEmailStatus = await sendPasswordVerificationEmail(email);
// Use a prepared statement to retrieve the account information
const pool = await connAccount;
const request = pool.request();
request.input('Identifier', sql.VarChar, email);
const result = await request.execute('GetAccount');
const row = result.recordset[0];
if (row && row.Result === 'AccountExists') {
const emailAdress = row.Email;
const windycode = row.WindyCode;
const verificationCode = Math.floor(10000 + Math.random() * 90000).toString();
const expirationTime = new Date(Date.now() + 600000).toISOString(); // 10 minutes from now
// Prepare the second statement to insert the verification code information
const insertRequest = pool.request();
insertRequest.input('Email', sql.VarChar, email);
insertRequest.input('VerificationCode', sql.VarChar, verificationCode);
insertRequest.input('ExpirationTime', sql.DateTime, expirationTime);
const insertResult = await insertRequest.execute('SetPasswordVerificationCode');
const insertRow = insertResult.recordset[0];
if (insertRow && insertRow.Result === 'Success') {
// Send verification code email
sendPasswordResetEmail(email, verificationCode);
return res.status(200).send('EmailSent');
}
else {
accountLogger.error(`[Account] Failed to insert verification code for email: ${email}`);
return res.status(500).send(insertRow.Result);
}
} else if (row && row.Result === 'AccountNotFound') {
return res.status(400).send('AccountNotFound');
} else {
return res.status(500).send(row.Result);
}
} catch (error) {
logger.error('[Account] Database query failed: ' + error.message);
return res.status(500).send('A error ocourred. Please try again later.');
if (sendEmailStatus !== "Success") {
accountLogger.info(
`[Account] Password reset request failed to [${email}]. Status: ${sendEmailStatus}`
);
return res.status(200).json({
success: true,
result: sendEmailStatus,
});
}
accountLogger.info(
`[Account] Password reset request sent to [${email}]. Status: ${sendEmailStatus}`
);
return res.status(200).json({
success: true,
result: sendEmailStatus,
});
} catch (error) {
logger.error(`[Account] Password reset request failed: ${error.message}`);
return res.status(500).json({
result: 'ServerError',
message: 'A server error occurred. Please try again later.'
});
}
});
module.exports = router;
export default router;

View file

@ -1,74 +0,0 @@
const sql = require('mssql');
const express = require('express');
const router = express.Router();
const bcrypt = require('bcryptjs');
const crypto = require('crypto');
const { logger, accountLogger } = require('../../utils/logger');
const { sendConfirmationEmail } = require('../../mailer/mailer');
const Joi = require('joi');
// Set up database connection
const { connAccount } = require('../../utils/dbConfig');
// Joi schema for validating request data
const schema = Joi.object({
windyCode: Joi.string().alphanum().min(6).max(16).required(),
email: Joi.string().email().required(),
password: Joi.string().min(6).required(),
});
// Route for registering an account
router.post('/', async (req, res) => {
try {
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(400).send(error.details[0].message);
}
const windyCode = value.windyCode;
const email = value.email;
const password = value.password;
const userIp = req.ip;
if (
!/^[a-z0-9_-]{6,50}$/.test(windyCode) &&
!/^[\w\d._%+-]+@[\w\d.-]+\.[\w]{2,}$/i.test(email)
) {
return res.status(400).send('InvalidUsernameFormat');
}
const md5_password = crypto.createHash('md5').update(windyCode + password).digest('hex'); // Generate MD5 hash
const passwordHash = await bcrypt.hash(md5_password, 10);
// Use a prepared statement to create the account
const pool = await connAccount;
const request = pool.request();
request.input('WindyCode', sql.VarChar, windyCode);
request.input('AccountPwd', sql.VarChar, passwordHash);
request.input('Email', sql.VarChar, email);
request.input('RegisterIP', sql.VarChar, userIp);
const result = await request.execute('CreateAccount');
const row = result.recordset[0];
if (row && row.Result === 'AccountCreated') {
accountLogger.info(`[Account] Account [${windyCode}] created successfully`);
sendConfirmationEmail(email, windyCode);
const clearRequest = pool.request();
clearRequest.input('Email', sql.VarChar, email);
const clearResult = await clearRequest.execute('ClearVerificationCode');
const clearRow = clearResult.recordset[0];
return res.status(200).send('Success');
} else {
accountLogger.error(`[Account] Account [${windyCode}] creation failed: ${row.Result}`);
return res.status(400).send(row.Result);
}
} catch (error) {
logger.error('[Account] Database query failed: ' + error.message);
return res.status(500).send('A error ocourred. Please try again later.');
}
});
module.exports = router;

View file

@ -0,0 +1,65 @@
import { Router } from "express";
const router = Router();
import joi from "joi";
import { logger, accountLogger } from "../../utils/logger.js";
import { createAccount } from "../../services/accountDBService.js";
import { getClientIp } from "../../utils/getClientIp.js";
const schema = joi.object({
username: joi.string().alphanum().lowercase().min(6).max(16).required(),
email: joi.string().email().required(),
password: joi.string().min(8).max(16).required(),
verificationCode: joi.string().pattern(new RegExp("^[0-9]+$")).required(),
});
// Route for registering an account
router.post("/", async (req, res) => {
try {
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(400).json({
success: false,
result: "ValidationError",
message: error.details[0].message,
});
}
const ip = getClientIp(req);
const { username, email, password, verificationCode } = value;
const createAccountStatus = await createAccount(
username,
email,
password,
ip,
verificationCode
);
switch (createAccountStatus) {
case "AccountCreated":
accountLogger.info(
`[Account] Account [${username}] created successfully.`
);
return res.status(200).json({
success: true,
result: createAccountStatus,
});
default:
accountLogger.info(
`[Account] Account [${username}] creation failed - ${createAccountStatus}`
);
return res.status(200).json({
success: false,
result: createAccountStatus,
});
}
} catch (error) {
logger.error("[Account] Account creation failed: " + error.message);
return res.status(500).json({
result: "ServerError",
message: "A server error occurred. Please try again later.",
});
}
});
export default router;

View file

@ -1,68 +1,54 @@
// Load environment variables
const env = require('../../utils/env');
import { Router } from "express";
const router = Router();
import { logger, accountLogger } from "../../utils/logger.js";
import joi from "joi";
import { sendAccountVerificationEmail } from "../../services/accountDBService.js";
const sql = require('mssql');
const express = require('express');
const router = express.Router();
const { logger } = require('../../utils/logger');
const { sendVerificationEmail } = require('../../mailer/mailer');
const Joi = require('joi');
// Set up database connection
const { connAccount } = require('../../utils/dbConfig');
// Joi schema for request body validation
const schema = Joi.object({
email: Joi.string().email().required()
const schema = joi.object({
email: joi.string().email().required(),
});
// Route for registering an account
router.post('/', async (req, res) => {
// Route for sending verification email for account creation
router.post("/", async (req, res) => {
try {
// Validate request body
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(400).send(error.details[0].message);
return res.status(400).json({
success: false,
result: "ValidationError",
message: error.details[0].message,
});
}
const email = req.body.email;
const { email } = value;
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return res.status(400).send('InvalidEmailFormat');
}
const sendEmailStatus = await sendAccountVerificationEmail(email);
// Use a prepared statement to retrieve the account information
const pool = await connAccount;
const request = pool.request();
request.input('Identifier', sql.VarChar, email);
const result = await request.execute('GetAccount');
const row = result.recordset[0];
if (row && row.Result === 'AccountNotFound') {
const verificationCode = Math.floor(10000 + Math.random() * 90000).toString();
const timeZone = process.env.TZ;
// Set the expiration time 10 minutes from now in the specified timezone
const expirationTime = new Date(Date.now() + 600000).toLocaleString('en-US', { timeZone });
// Prepare the second statement to insert the verification code information
const insertRequest = pool.request();
insertRequest.input('Email', sql.VarChar, email);
insertRequest.input('VerificationCode', sql.VarChar, verificationCode);
insertRequest.input('ExpirationTime', sql.DateTime, expirationTime);
const insertResult = await insertRequest.execute('SetAccountVerificationCode');
const insertRow = insertResult.recordset[0];
// Send verification code email
sendVerificationEmail(email, verificationCode);
return res.status(200).send('EmailSent');
} else {
return res.status(400).send(row.Result);
}
} catch (error) {
logger.error('[Account] Database query failed: ' + error.message);
return res.status(500).send('A error ocourred. Please try again later.');
if (sendEmailStatus !== "Success") {
accountLogger.info(
`[Account] Verification email sending failed to ${email}. Sending status: ${sendEmailStatus}`
);
return res.status(200).json({
success: false,
result: sendEmailStatus,
});
}
accountLogger.info(
`[Account] Verification email sent to ${email}. Sending status: ${sendEmailStatus}`
);
return res.status(200).json({
success: true,
result: sendEmailStatus,
});
} catch (error) {
logger.error(
`[Account] Verification email sending failed: ${error.message}`
);
return res.status(500).json({
result: 'ServerError',
message: 'A server error occurred. Please try again later.'
});
}
});
module.exports = router;
export default router;

View file

@ -1,47 +1,96 @@
const express = require('express');
const router = express.Router();
const NodeCache = require('node-cache');
const { logger } = require('../utils/logger');
const sql = require('mssql');
const { authDBConfig } = require('../utils/dbConfig.js');
import { Router } from 'express';
import sql from 'mssql';
const router = Router();
import NodeCache from 'node-cache';
import { logger } from '../utils/logger.js';
import { authDBConfig } from '../utils/dbConfig.js';
import { fetchOnlineCount } from "../services/authDBService.js";
// Set up the cache
const cache = new NodeCache({ stdTTL: 60, checkperiod: 120 });
const CACHE_KEY = 'onlineCount';
const CACHE_TTL = 60; // 1 minute
const CHECK_PERIOD = 120; // 2 minutes
// Route for getting the count of online players
// Cache configuration
const cache = new NodeCache({
stdTTL: CACHE_TTL,
checkperiod: CHECK_PERIOD,
useClones: false
});
// Database connection pool
let pool;
let poolReady = (async () => {
try {
pool = await new sql.ConnectionPool(authDBConfig).connect();
} catch (error) {
logger.error('Failed to create database connection pool:', error);
process.exit(1);
}
})();
/**
* Gets online count, using cache when possible
*/
async function getOnlineCount() {
try {
// Try to get from cache first
let count = cache.get(CACHE_KEY);
if (count === undefined) {
logger.debug('Cache miss for online count, querying database');
count = await fetchOnlineCount(pool);
cache.set(CACHE_KEY, count);
} else {
logger.debug('Online count retrieved from cache');
}
return count;
} catch (error) {
// If we have a cached value, return it even if the query fails
const cached = cache.get(CACHE_KEY);
if (cached !== undefined) {
logger.warn('Using cached online count due to database error');
return cached;
}
throw error;
}
}
// Route for getting online players count
router.get('/', async (req, res) => {
try {
// Check if the count exists in the cache
const cacheKey = 'onlineCount';
let count = cache.get(cacheKey);
if (count === undefined) {
// Count not found in cache, fetch it from the database
const connAuth = new sql.ConnectionPool(authDBConfig);
await connAuth.connect();
const request = connAuth.request();
// Declare the @online parameter and set its value to 1
request.input('online', sql.Int, 1);
const result = await request.query('SELECT COUNT(*) AS OnlineCount FROM AuthTable WHERE online = @online');
count = result.recordset[0].OnlineCount;
// Store the count in the cache
cache.set(cacheKey, count);
// Close the database connection
await connAuth.close();
}
// Return the count as the response
return res.status(200).json({ count });
if (!pool) await poolReady;
const count = await getOnlineCount();
// Set cache-control headers
res.set('Cache-Control', `public, max-age=${CACHE_TTL}`);
return res.status(200).json({
count,
cached: cache.has(CACHE_KEY),
timestamp: new Date().toISOString()
});
} catch (error) {
logger.error('Database query failed: ' + error.message);
return res.status(500).send('Database query failed. Please try again later.');
logger.error('Failed to get online count:', error);
return res.status(500).json({
error: 'Internal server error',
message: 'Unable to retrieve online player count'
});
}
});
module.exports = router;
// Cleanup on process exit
process.on('SIGINT', async () => {
try {
if (pool) {
await pool.close();
}
process.exit(0);
} catch (error) {
logger.error('Error closing connection pool:', error);
process.exit(1);
}
});
export default router;

18
src/servers/jpnApp.js Normal file
View file

@ -0,0 +1,18 @@
import express, { urlencoded } from 'express';
import config from '../config.js';
const { ports, ips, logger } = config;
import authJpnRouter from '../routes/authJpn.js';
import billingJpnRouter from '../routes/billingJpn.js';
const app = express();
app.use(urlencoded({ extended: false, type: 'application/x-www-form-urlencoded' }));
app.use('/Auth', authJpnRouter);
app.use('/Billing', billingJpnRouter);
const startServer = () => {
return app.listen(ports.jpnApp, ips.local, () => {
logger.info(`API (JPN) listening on ${ips.local}:${ports.jpnApp}`);
});
};
export { app, startServer };

85
src/servers/mainApp.js Normal file
View file

@ -0,0 +1,85 @@
import express from 'express';
import config from '../config.js';
const { ports, ips, apiConfig, middleware, staticPaths, logger } = config;
import rateLimiter from '../lib/rateLimiter.js';
import { closeConnection } from '../lib/closeConnection.js';
import path from 'path';
// Routers
import gatewayRouter from '../routes/gateway.js';
import loginRouter from '../routes/launcher/login.js';
import registerRouter from '../routes/launcher/registerAccount.js';
import codeVerificationRouter from '../routes/launcher/codeVerification.js';
import passwordResetEmailRouter from '../routes/launcher/passwordResetEmail.js';
import passwordChangeRouter from '../routes/launcher/changePassword.js';
import verificationEmailRouter from '../routes/launcher/verificationEmail.js';
import launcherUpdaterRouter from '../routes/launcher/launcherUpdater.js';
import onlineCountRouter from '../routes/onlineCount.js';
const app = express();
if (apiConfig.trustProxyEnabled) {
const trustProxyHosts = apiConfig.trustProxyHosts || [];
if (trustProxyHosts.length > 0) {
app.set("trust proxy", trustProxyHosts);
} else {
app.set("trust proxy", true);
}
}
app.disable("x-powered-by");
app.disable("etag");
// Middleware
app.use(...middleware.getMiddleware());
// Routes
app.use('/launcher/GetGatewayAction', closeConnection, rateLimiter, gatewayRouter);
app.use('/launcher/SignupAction', closeConnection, rateLimiter, registerRouter);
app.use('/launcher/LoginAction', closeConnection, rateLimiter, loginRouter);
app.use('/launcher/VerifyCodeAction', closeConnection, rateLimiter, codeVerificationRouter);
app.use('/launcher/ResetPasswordAction', closeConnection, rateLimiter, passwordChangeRouter);
app.use('/launcher/SendPasswordResetEmailAction', closeConnection, rateLimiter, passwordResetEmailRouter);
app.use('/launcher/SendVerificationEmailAction', closeConnection, rateLimiter, verificationEmailRouter);
app.use('/launcherAction', closeConnection, rateLimiter, launcherUpdaterRouter);
app.use('/launcher/GetOnlineCountAction', closeConnection, rateLimiter, onlineCountRouter);
// Static files
app.use(express.static(staticPaths.public));
app.use('/launcher/news', express.static(staticPaths.launcherNews));
app.use('/site', express.static(staticPaths.site));
app.use('/launcher/patch', express.static(staticPaths.launcherPatch));
app.use('/launcher/client', express.static(staticPaths.launcherClient));
// HTML routes
app.get('/launcher/news', (req, res) => {
res.sendFile(path.join(staticPaths.launcherNews, 'news-panel.html'));
});
app.get('/launcher/agreement', (req, res) => {
res.sendFile(path.join(staticPaths.launcherNews, 'agreement.html'));
});
app.get('/favicon.ico', (req, res) => {
res.sendFile(path.join(staticPaths.launcherNews, 'favicon.ico'));
});
app.get('/Register', (req, res) => {
res.sendFile(path.join(staticPaths.site, 'Signup.html'));
});
// Error handler
app.use((err, req, res, next) => {
logger.error(err.stack);
return res.status(500).json({
result: 'ServerError',
message: 'A server error occurred. Please try again later.'
});
});
const startServer = () => {
return app.listen(ports.main, ips.public, () => {
logger.info(`API listening on ${ips.public}:${ports.main}`);
});
};
export { app, startServer };

151
src/servers/proxyServer.js Normal file
View file

@ -0,0 +1,151 @@
import { createServer } from 'net';
import { request } from 'http';
import config from '../config.js';
const { ports, ips, logger, BACKENDS } = config;
const parseRequest = (data) => {
const lines = data.split('\r\n');
let realPath = '/';
let isMalformed = false;
if (lines.some(line => line.startsWith('POST /') && line.includes('HTTP/1.1Content-Type:'))) {
isMalformed = true;
for (const line of lines) {
if (line.startsWith('POST /') && line.includes('HTTP/1.1Content-Type:')) {
const match = line.match(/POST (\/[^ ]*) HTTP\/1\.1/);
if (match) realPath = match[1];
break;
}
}
} else {
const requestLine = lines[0];
const match = requestLine.match(/^(POST|GET) (\/(?:[^ ]*)) HTTP\/1\.1$/i);
if (match) realPath = match[2];
}
if (realPath.startsWith('/cgi-bin/')) {
realPath = '/Auth' + realPath;
} else if (realPath.startsWith('/S1/')) {
realPath = '/Billing' + realPath;
}
return { realPath, isMalformed, lines };
};
const findBackend = (realPath) => {
if (BACKENDS.AUTH.paths.includes(realPath)) {
return { ...BACKENDS.AUTH, port: ports.jpnApp };
} else if (BACKENDS.BILLING.paths.some(path => realPath.startsWith(path))) {
return { ...BACKENDS.BILLING, port: ports.jpnApp };
} else {
return null;
}
};
const buildHeaders = (lines, body, backend, isMalformed) => {
const headers = {
'Host': `${ips.local}:${backend.port}`,
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(body),
'User-Agent': 'Mozilla/4.0 (ISAO/1.00;Auth)',
};
if (!isMalformed) {
const headerLines = lines.slice(1, lines.indexOf(''));
for (const line of headerLines) {
const colonIndex = line.indexOf(':');
if (colonIndex > 0) {
const key = line.substring(0, colonIndex).trim().toLowerCase();
const value = line.substring(colonIndex + 1).trim();
if (['content-type', 'user-agent', 'content-length'].includes(key)) {
headers[key] = value;
}
}
}
}
return headers;
};
const handleProxyRequest = (socket, data) => {
try {
const { realPath, isMalformed, lines } = parseRequest(data);
const backend = findBackend(realPath);
if (!backend) {
logger.error('[Proxy] Unknown path:', realPath);
socket.end('HTTP/1.1 404 Not Found\r\n\r\n');
return;
}
const body = data.split('\r\n\r\n')[1] || '';
const headers = buildHeaders(lines, body, backend, isMalformed);
const options = {
hostname: ips.local,
port: backend.port,
path: realPath,
method: 'POST',
headers,
};
const proxyReq = request(options, (proxyRes) => {
socket.write(`HTTP/1.1 ${proxyRes.statusCode} ${proxyRes.statusMessage}\r\n`);
Object.entries(proxyRes.headers).forEach(([key, value]) => {
socket.write(`${key}: ${value}\r\n`);
});
socket.write('\r\n');
proxyRes.pipe(socket);
});
proxyReq.on('error', (err) => {
logger.error('[Proxy] Proxy error:', err);
socket.end('HTTP/1.1 500 Internal Server Error\r\n\r\n');
});
proxyReq.write(body);
proxyReq.end();
} catch (err) {
logger.error('[Proxy] Error parsing request:', err);
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
}
};
const createProxyServer = () => {
const server = createServer((socket) => {
let data = '';
socket.on('data', (chunk) => {
data += chunk.toString('binary');
if (data.includes('\r\n\r\n')) {
handleProxyRequest(socket, data);
}
});
socket.on('error', (err) => {
logger.error('[Proxy] Socket error:', err);
});
socket.on('timeout', () => {
logger.warn('[Proxy] Socket timeout.');
socket.end();
});
});
return server;
};
const startServer = () => {
const server = createProxyServer();
return server.listen(ports.proxy, ips.local, () => {
logger.info(`Proxy (JPN) listening on ${ips.local}:${ports.proxy}`);
console.log('Configured backends:');
console.log(`- AUTH (${ports.jpnApp}):`, BACKENDS.AUTH.paths);
console.log(`- BILLING (${ports.jpnApp}):`, BACKENDS.BILLING.paths);
});
};
export { createProxyServer, startServer };

17
src/servers/usaApp.js Normal file
View file

@ -0,0 +1,17 @@
import express from 'express';
import config from '../config.js';
const { ports, ips, logger } = config;
import authUsaRouter from '../routes/authUsa.js';
import billingRouter from '../routes/billingUsa.js';
const app = express();
app.use('/Auth', authUsaRouter);
app.use('/Billing', billingRouter);
const startServer = () => {
return app.listen(ports.usaApp, ips.local, () => {
logger.info(`API (USA) listening on ${ips.local}:${ports.usaApp}`);
});
};
export { app, startServer };

View file

@ -0,0 +1,334 @@
import sql from "mssql";
import {
generateMD5Hash,
comparePassword,
hashPassword,
} from "../utils/hashUtils.js";
import { connAccount } from "../utils/dbConfig.js";
import configs from "../config.js";
const { config } = configs;
import {
sendConfirmationEmail,
sendVerificationEmail,
sendPasswordResetEmail,
sendPasswordChangedEmail,
} from "../mailer/mailer.js";
import { logger } from "../utils/logger.js";
// ==============================================
// Account Management Functions
// ==============================================
export async function getAccount(identifier) {
const pool = connAccount;
const result = await pool
.request()
.input("Identifier", sql.VarChar(50), identifier)
.execute("GetAccount");
const row = result.recordset[0];
if (!row || row.Result !== "AccountExists") {
throw new Error(row?.Result || "AccountNotFound");
}
return row;
}
export async function createAccount(account, email, password, ip, verificationCode) {
const isValidVerificationCode = await verifyCode(
email,
verificationCode,
"Account"
);
if (isValidVerificationCode !== "ValidVerificationCode") {
return isValidVerificationCode;
}
const md5Password = generateMD5Hash(account, password);
const passwordHash = await hashPassword(md5Password, 10);
const pool = connAccount;
const request = pool.request();
request.input("WindyCode", sql.VarChar, account);
request.input("AccountPwd", sql.VarChar, passwordHash);
request.input("Email", sql.VarChar, email);
request.input("RegisterIP", sql.VarChar, ip);
request.input("ServerId", sql.Int, config.serverId);
request.input("ShopBalance", sql.BigInt, config.shopBalance);
const result = await request.execute("CreateAccount");
const row = result.recordset[0];
if (row.Result == "AccountCreated") {
sendConfirmationEmail(email, account);
await clearVerificationCode(email);
}
return row.Result;
}
export async function validateCredentials(account, password) {
try {
const accountStatus = await getAccount(account);
const passwordHash = generateMD5Hash(account, password);
const passwordMatch = await comparePassword(
passwordHash,
accountStatus.AccountPwd
);
return accountStatus.Result == "AccountExists" && passwordMatch;
} catch (error) {
return false;
}
}
// ==============================================
// Authentication Functions
// ==============================================
export async function authenticateUser(account, password, ip, isMd5 = false) {
const pool = connAccount;
// Get account info
const { recordset } = await pool
.request()
.input("Identifier", sql.VarChar(50), account)
.execute("GetAccount");
const row = recordset[0];
if (!row || row.Result !== "AccountExists") {
return { status: row.Result };
}
// Verify password
const passwordHash = isMd5 ? password : generateMD5Hash(account, password);
const passwordMatch = await comparePassword(passwordHash, row.AccountPwd);
// Authenticate
const { recordset: authRecordset } = await pool
.request()
.input("Identifier", sql.VarChar(50), account)
.input("password_verify_result", sql.Bit, passwordMatch ? 1 : 0)
.input("LastLoginIP", sql.VarChar(50), ip)
.execute("AuthenticateUser");
const authRow = authRecordset[0];
if (!authRow || authRow.Result !== "LoginSuccess") {
return { status: authRow.Result };
}
return {
status: authRow.Result,
authId: authRow.AuthID,
token: authRow.Token,
};
}
// ==============================================
// Password Management Functions
// ==============================================
export async function changeAccountPassword(email, password, verificationCode) {
// Get account information
const accountInfo = await getAccount(email);
if (!accountInfo || accountInfo.Result !== "AccountExists") {
return accountInfo;
}
// Check verification code
const verificationResult = await verifyCode(
email,
verificationCode,
"Password"
);
if (!verificationResult || verificationResult !== "ValidVerificationCode") {
return verificationResult;
}
const accountName = accountInfo.WindyCode;
const currentHash = accountInfo.AccountPwd;
const passwordHash = generateMD5Hash(accountName, password);
// Verify if password is the same
const isSamePassword = await comparePassword(passwordHash, currentHash);
if (isSamePassword) {
return "SamePassword";
}
// Hash and update password
const newPasswordHash = await hashPassword(passwordHash);
const updateResult = await updatePassword(email, newPasswordHash);
if (!updateResult || updateResult.Result !== "PasswordChanged") {
return updateResult;
}
// Clear verification code and send email
await clearVerificationCode(email);
sendPasswordChangedEmail(email, accountName);
return updateResult.Result;
}
export async function updatePassword(email, newPasswordHash) {
const pool = connAccount;
const result = await pool
.request()
.input("Email", sql.VarChar, email)
.input("AccountPwd", sql.VarChar, newPasswordHash)
.execute("UpdateAccountPassword");
return result.recordset[0];
}
// ==============================================
// Verification Code Functions
// ==============================================
export function generateVerificationCode() {
const verificationCode = Math.floor(10000 + Math.random() * 90000).toString();
// Set the expiration time 10 minutes from now in the specified timezone
const expirationTime = new Date(Date.now() + 600000).toLocaleString(
"en-US",
config.timeZone
);
return {
code: verificationCode,
expiration: expirationTime,
};
}
export async function sendAccountVerificationEmail(email) {
const verificationCode = generateVerificationCode();
const insertRow = await setAccountVerificationCode(
email,
verificationCode.code,
verificationCode.expiration
);
if (insertRow.Result == "Success") {
// Send verification code email
sendVerificationEmail(email, verificationCode.code);
return "EmailSent";
} else {
return insertRow.Result;
}
}
export async function sendPasswordVerificationEmail(email) {
const verificationCode = generateVerificationCode();
const insertRow = await setPasswordVerificationCode(
email,
verificationCode.code,
verificationCode.expiration
);
if (insertRow.Result == "Success") {
// Send verification code email
sendPasswordResetEmail(email, verificationCode.code);
return "EmailSent";
} else {
return insertRow.Result;
}
}
export async function setAccountVerificationCode(
email,
verificationCode,
expirationTime
) {
const pool = connAccount;
const result = await pool
.request()
.input("Email", sql.VarChar, email)
.input("VerificationCode", sql.VarChar, verificationCode)
.input("ExpirationTime", sql.DateTime, expirationTime)
.execute("SetAccountVerificationCode");
const row = result.recordset[0];
return row;
}
export async function setPasswordVerificationCode(
email,
verificationCode,
expirationTime
) {
const pool = connAccount;
const result = await pool
.request()
.input("Email", sql.VarChar, email)
.input("VerificationCode", sql.VarChar, verificationCode)
.input("ExpirationTime", sql.DateTime, expirationTime)
.execute("SetPasswordVerificationCode");
return result.recordset[0];
}
export async function verifyCode(email, verificationCode, verificationCodeType) {
const pool = connAccount;
const result = await pool
.request()
.input("Email", sql.VarChar, email)
.input("VerificationCode", sql.VarChar, verificationCode)
.input("VerificationCodeType", sql.VarChar, verificationCodeType)
.execute("GetVerificationCode");
const row = result.recordset[0];
return row.Result;
}
export async function clearVerificationCode(email) {
const pool = connAccount;
await pool
.request()
.input("Email", sql.VarChar, email)
.execute("ClearVerificationCode");
}
// ==============================================
// Billing Functions
// ==============================================
export async function getCurrency(userId) {
const pool = connAccount;
const result = await pool
.request()
.input("UserId", sql.VarChar(50), userId)
.input("ServerId", sql.Int, config.serverId)
.execute("GetCurrency");
const row = result.recordset[0];
if (!row || row.Result !== "Success") {
throw new Error(row?.Result || "Failed to get balance");
}
return row.Zen;
}
export async function setCurrency(userId, newBalance) {
const pool = connAccount;
const result = await pool
.request()
.input("UserId", sql.VarChar(50), userId)
.input("ServerId", sql.Int, config.serverId)
.input("NewBalance", sql.BigInt, newBalance)
.execute("SetCurrency");
if (result.rowsAffected[0] === 0) {
throw new Error("Balance update failed");
}
}
export async function logBillingTransaction(transaction) {
const pool = connAccount;
await pool
.request()
.input("userid", sql.VarChar(50), transaction.userId)
.input("charid", sql.VarChar(50), transaction.charId)
.input("uniqueid", sql.VarChar(50), transaction.uniqueId)
.input("amount", sql.BigInt, transaction.amount)
.input("itemid", sql.VarChar(50), transaction.itemId)
.input("itemcount", sql.Int, transaction.count)
.execute("SetBillingLog");
}

View file

@ -0,0 +1,20 @@
import sql from "mssql";
import configs from "../config.js";
const { config } = configs;
import { logger } from "../utils/logger.js";
// ==============================================
// Fetch online count from the database
// ==============================================
export async function fetchOnlineCount(pool) {
try {
const QUERY = 'SELECT COUNT(*) AS OnlineCount FROM AuthTable WHERE online = @online';
const request = pool.request();
request.input('online', sql.Int, 1);
const result = await request.query(QUERY);
return result.recordset[0].OnlineCount;
} catch (error) {
logger.error('Online count query failed:', error);
throw error;
}
}

View file

@ -1,6 +1,5 @@
const sql = require('mssql');
const env = require('./env');
const { logger } = require('../utils/logger');
import sql from 'mssql';
import { logger } from '../utils/logger.js';
const dbConfig = {
user: process.env.DB_USER,
@ -32,7 +31,7 @@ const authDBConfig = {
},
};
module.exports = {
export {
connAccount,
authDBConfig
};

View file

@ -1,6 +0,0 @@
require('dotenv').config();
// Access environment variables with a function
module.exports = {
get: (key) => process.env[key],
};

36
src/utils/getClientIp.js Normal file
View file

@ -0,0 +1,36 @@
import config from "../config.js";
const { apiConfig } = config;
export function getClientIp(req) {
let ip;
if (apiConfig.trustProxyEnabled) {
const forwarded = req.headers["x-forwarded-for"];
if (forwarded) {
// Grab the first IP from x-forwarded-for list
ip = forwarded.split(",")[0].trim();
}
}
// Fallback to connection-based IP
if (!ip) {
ip = req.socket?.remoteAddress || req.connection?.remoteAddress || null;
}
// Optional fallback (only if explicitly included in body)
if (!ip && req.body?.ip) {
ip = req.body.ip;
}
// Normalize IPv6 localhost
if (ip === "::1" || ip === "0:0:0:0:0:0:0:1") {
ip = "127.0.0.1";
}
// Remove IPv6 prefix if needed (e.g. "::ffff:192.168.0.1")
if (ip && ip.startsWith("::ffff:")) {
ip = ip.substring(7);
}
return ip;
}

12
src/utils/hashUtils.js Normal file
View file

@ -0,0 +1,12 @@
import { createHash } from 'crypto';
import bcrypt from 'bcryptjs';
export function generateMD5Hash(account, password) {
return createHash('md5').update(account + password).digest('hex');
}
export async function hashPassword(password, saltRounds = 10) {
return await bcrypt.hash(password, saltRounds);
}
export async function comparePassword(password, hash) {
return await bcrypt.compare(password, hash);
}

View file

@ -1,15 +1,17 @@
const fs = require("fs");
const winston = require("winston");
import dotenv from 'dotenv';
dotenv.config();
import { existsSync, mkdirSync } from "fs";
import { transports as _transports, format as _format, createLogger as _createLogger } from "winston";
const logsDirectory = 'logs';
if (!fs.existsSync(logsDirectory)) {
fs.mkdirSync(logsDirectory);
if (!existsSync(logsDirectory)) {
mkdirSync(logsDirectory);
}
function createLogger(filename, level, filter, showConsole) {
const transports = [
new winston.transports.File({
new _transports.File({
filename: `${logsDirectory}/${filename}-${new Date().toISOString().slice(0, 10)}.log`,
level,
filter
@ -17,20 +19,20 @@ function createLogger(filename, level, filter, showConsole) {
];
if (showConsole) {
transports.push(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple(),
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}]`)
transports.push(new _transports.Console({
format: _format.combine(
_format.colorize(),
_format.simple(),
_format.printf(info => `${info.level}: ${info.message} [${info.timestamp}]`)
)
}));
}
const logger = winston.createLogger({
const logger = _createLogger({
level,
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.printf(info => `${info.level}: ${info.message} [${info.timestamp}] `)
format: _format.combine(
_format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
_format.printf(info => `${info.level}: ${info.message} [${info.timestamp}] `)
),
transports
});
@ -46,7 +48,7 @@ const mailerLogger = createLogger('mailer', logLevel, log => log.message.include
const accountLogger = createLogger('account', logLevel, log => log.message.includes('[Account]'), process.env.LOG_ACCOUNT_CONSOLE === 'true');
const logger = createLogger('api', logLevel, null, true);
module.exports = {
export {
authLogger,
billingLogger,
mailerLogger,

31
src/utils/memoryLogger.js Normal file
View file

@ -0,0 +1,31 @@
import moment from 'moment-timezone';
import { formatBytes } from './systemInfo.js';
function setupMemoryLogging(interval = 1800000) { // 30 minutes
// Set up periodic logging
const intervalId = setInterval(logMemoryUsage, interval);
// Return function to stop logging
return () => {
clearInterval(intervalId);
console.log('Stopped memory logging');
};
}
function logMemoryUsage() {
const now = moment().format('YYYY-MM-DD HH:mm:ss');
const mem = process.memoryUsage();
console.log(`Memory Usage at ${now}:`);
console.log(` RSS : ${formatBytes(mem.rss)}`);
console.log(` Heap Total : ${formatBytes(mem.heapTotal)}`);
console.log(` Heap Used : ${formatBytes(mem.heapUsed)}`);
console.log(` External : ${formatBytes(mem.external)}`);
console.log(` Array Buffers: ${formatBytes(mem.arrayBuffers)}`);
console.log('--------------------------------------------------');
}
export default {
setupMemoryLogging,
logMemoryUsage
};

61
src/utils/systemInfo.js Normal file
View file

@ -0,0 +1,61 @@
import tz from 'moment-timezone';
import fs from 'fs/promises';
const data = await fs.readFile(new URL('../../package.json', import.meta.url), 'utf-8');
const { version: APP_VERSION } = JSON.parse(data);
function formatBytes(bytes) {
const units = ['B', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 B';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
}
function getSystemInfo() {
const nodeVersion = process.version;
const timezone = process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone;
const offsetInMinutes = tz.tz(timezone).utcOffset();
const offsetSign = offsetInMinutes >= 0 ? '+' : '-';
const offsetHours = Math.floor(Math.abs(offsetInMinutes) / 60).toString().padStart(2, '0');
const offsetMinutes = (Math.abs(offsetInMinutes) % 60).toString().padStart(2, '0');
const offsetString = `${offsetSign}${offsetHours}:${offsetMinutes}`;
const memoryUsage = process.memoryUsage();
const githubLink = 'https://github.com/JuniorDark/RustyHearts-API';
return {
version: `Rusty Hearts API Version: ${APP_VERSION}`,
github: `Github: ${githubLink}`,
nodeVersion: `Node.js Version: ${nodeVersion}`,
timezone: `Timezone: ${timezone} (${offsetString})`,
memory: {
rss: formatBytes(memoryUsage.rss),
heapTotal: formatBytes(memoryUsage.heapTotal),
heapUsed: formatBytes(memoryUsage.heapUsed),
external: formatBytes(memoryUsage.external),
arrayBuffers: formatBytes(memoryUsage.arrayBuffers)
}
};
}
function logSystemInfo() {
const info = getSystemInfo();
console.log('--------------------------------------------------');
console.log(info.version);
console.log(info.github);
console.log(info.nodeVersion);
console.log(info.timezone);
console.log('Memory Usage:');
console.log(` RSS : ${info.memory.rss}`);
console.log(` Heap Total : ${info.memory.heapTotal}`);
console.log(` Heap Used : ${info.memory.heapUsed}`);
console.log(` External : ${info.memory.external}`);
console.log(` Array Buffers: ${info.memory.arrayBuffers}`);
console.log('--------------------------------------------------');
}
export {
getSystemInfo,
logSystemInfo,
formatBytes
};