const fs = require('fs'); const path = require('path'); const https = require('https'); const net = require('net'); const crypto = require('crypto'); const dataHandler = require('./datahandler'); const storage = require('./storage'); const consoleCmd = require('./console'); const { serverConfig, configSave } = require('./server_config'); let geoip = null; try { geoip = require('geoip-lite'); } catch (e) { geoip = null; } function parseMarkdown(parsed) { parsed = parsed.replace(/<\/?[^>]+(>|$)/g, ''); var grayTextRegex = /--(.*?)--/g; parsed = parsed.replace(grayTextRegex, '$1'); var boldRegex = /\*\*(.*?)\*\*/g; parsed = parsed.replace(boldRegex, '$1'); var italicRegex = /\*(.*?)\*/g; parsed = parsed.replace(italicRegex, '$1'); var linkRegex = /\[([^\]]+)]\(([^)]+)\)/g; parsed = parsed.replace(linkRegex, '$1'); return parsed.replace(/\n/g, '
'); } function removeMarkdown(parsed) { parsed = parsed.replace(/<\/?[^>]+(>|$)/g, ''); var grayTextRegex = /--(.*?)--/g; parsed = parsed.replace(grayTextRegex, '$1'); var boldRegex = /\*\*(.*?)\*\*/g; parsed = parsed.replace(boldRegex, '$1'); var italicRegex = /\*(.*?)\*/g; parsed = parsed.replace(italicRegex, '$1'); var linkRegex = /\[([^\]]+)]\(([^)]+)\)/g; return parsed.replace(linkRegex, '$1'); } function authenticateWithXdrd(client, salt, password) { const sha1 = crypto.createHash('sha1'); const saltBuffer = Buffer.from(salt, 'utf-8'); const passwordBuffer = Buffer.from(password, 'utf-8'); sha1.update(saltBuffer); sha1.update(passwordBuffer); const hashedPassword = sha1.digest('hex'); client.write(hashedPassword + '\n'); client.write('x\n'); } const ipCache = new Map(); const ipInfoInFlight = new Map(); function fetchIpWhoisInfo(ip, timeoutMs = 1500) { return new Promise((resolve) => { if (!ip || !net.isIP(ip)) return resolve({}); const url = `https://ipwho.is/${encodeURIComponent(ip)}`; const req = https.get(url, { timeout: timeoutMs }, (res) => { if (res.statusCode !== 200) { res.resume(); return resolve({}); } let body = ''; res.on('data', (chunk) => (body += chunk)); res.on('end', () => { try { const data = JSON.parse(body); if (!data || data.success === false) return resolve({}); const connection = data.connection || {}; const isp = connection.isp || connection.org; const asnRaw = connection.asn; const as = typeof asnRaw === 'string' ? asnRaw : typeof asnRaw === 'number' ? `AS${asnRaw}` : undefined; resolve({ isp: typeof isp === 'string' && isp.trim() ? isp.trim() : undefined, as, }); } catch (e) { resolve({}); } }); }); req.on('timeout', () => { req.destroy(); resolve({}); }); req.on('error', () => resolve({})); }); } function handleConnect(clientIp, currentUsers, ws, callback) { if (ipCache.has(clientIp)) { processConnection(clientIp, ipCache.get(clientIp), currentUsers, ws, callback); return; } if (ipInfoInFlight.has(clientIp)) { ipInfoInFlight .get(clientIp) .then((info) => processConnection(clientIp, info, currentUsers, ws, callback)) .catch(() => processConnection(clientIp, { country: undefined }, currentUsers, ws, callback)); return; } let locationInfo = { country: undefined }; if (geoip && clientIp && net.isIP(clientIp)) { const geo = geoip.lookup(clientIp); if (geo) { locationInfo = { country: geo.country, countryCode: geo.country, city: geo.city, regionName: geo.region, }; } } else if (!geoip) consoleCmd.logWarn('geoip-lite is not installed; location will be Unknown.'); const inFlightPromise = fetchIpWhoisInfo(clientIp) .then((whoisInfo) => { const merged = { ...locationInfo, ...whoisInfo }; ipCache.set(clientIp, merged); ipInfoInFlight.delete(clientIp); return merged; }) .catch(() => { ipCache.set(clientIp, locationInfo); ipInfoInFlight.delete(clientIp); return locationInfo; }); ipInfoInFlight.set(clientIp, inFlightPromise); inFlightPromise.then((info) => processConnection(clientIp, info, currentUsers, ws, callback)); } let bannedASCache = { data: null, timestamp: 0 }; function fetchBannedAS(callback) { const now = Date.now(); if (bannedASCache.data && now - bannedASCache.timestamp < 10 * 60 * 1000) return callback(bannedASCache.data); const req = https.get("https://flerken.zapto.org:5000/banned_as.json", { family: 4 }, (banResponse) => { let banData = ""; banResponse.on("data", (chunk) => banData += chunk); banResponse.on("end", () => { try { const bannedAS = JSON.parse(banData).banned_as || []; bannedASCache = { data: bannedAS, timestamp: now }; callback(bannedAS); } catch (error) { console.error("Error parsing banned AS list:", error); callback([]); // Default to allowing user } }); }); // Set timeout for the request (3 seconds) req.setTimeout(3000, () => { console.error("Error: Request timed out while fetching banned AS list."); req.abort(); callback([]); // Default to allowing user }); req.on("error", (err) => { console.error("Error fetching banned AS list:", err); callback([]); // Default to allowing user }); } const recentBannedIps = new Map(); // Store clientIp -> timestamp function processConnection(clientIp, locationInfo, currentUsers, ws, callback) { const options = { year: "numeric", month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" }; const connectionTime = new Date().toLocaleString([], options); fetchBannedAS((bannedAS) => { if (bannedAS.some((as) => locationInfo.as?.includes(as))) { const now = Date.now(); const lastSeen = recentBannedIps.get(clientIp) || 0; if (now - lastSeen > 300 * 1000) { consoleCmd.logWarn(`Banned AS list client kicked (${clientIp})`); recentBannedIps.set(clientIp, now); } return callback("User banned"); } const userLocation = locationInfo && locationInfo.country !== undefined ? [ locationInfo.city, locationInfo.regionName && /\p{L}/u.test(String(locationInfo.regionName)) ? locationInfo.regionName : undefined, locationInfo.countryCode, ] .filter(Boolean) .join(', ') : 'Unknown'; const userLocationForLog = locationInfo?.isp ? `${userLocation} (${locationInfo.isp})` : userLocation; storage.connectedUsers.push({ ip: clientIp, location: userLocation, isp: locationInfo?.isp, as: locationInfo?.as, time: connectionTime, instance: ws, }); consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${userLocationForLog}`); callback("User allowed"); }); } function formatUptime(uptimeInSeconds) { const secondsInHour = 60 ** 2; const secondsInDay = secondsInHour * 24; const days = Math.floor(uptimeInSeconds / secondsInDay); const hours = Math.floor((uptimeInSeconds % secondsInDay) / secondsInHour); const minutes = Math.floor((uptimeInSeconds % secondsInHour) / 60); return `${days}d ${hours}h ${minutes}m`; } let incompleteDataBuffer = ''; function resolveDataBuffer(data) { const wss = storage.websocket_delegation.get("/text"); // Genius const rdsWss = storage.websocket_delegation.get("/rds"); var receivedData = incompleteDataBuffer + data.toString(); const isIncomplete = (receivedData.slice(-1) != '\n'); if (isIncomplete) { const position = receivedData.lastIndexOf('\n'); if (position < 0) { incompleteDataBuffer = receivedData; receivedData = ''; } else { incompleteDataBuffer = receivedData.slice(position + 1); receivedData = receivedData.slice(0, position + 1); } } else incompleteDataBuffer = ''; if (receivedData.length) dataHandler.handleData(wss, receivedData, rdsWss); } function kickClient(ipAddress) { // Find the entry in connectedClients associated with the provided IP address const targetClient = storage.connectedUsers.find(client => client.ip === ipAddress); if (targetClient && targetClient.instance) { // Send a termination message to the client targetClient.instance.send('KICK'); // Close the WebSocket connection after a short delay to allow the client to receive the message setTimeout(() => { targetClient.instance.close(); consoleCmd.logInfo(`Web client kicked (${ipAddress})`); }, 500); } else consoleCmd.logInfo(`Kicking client ${ipAddress} failed. No suitable client found.`); } function checkLatency(host, port = 80, timeout = 2000) { return new Promise(resolve => { const start = Date.now(); const socket = net.connect({ host, port }); socket.setTimeout(timeout); socket.on("connect", () => { const latency = Date.now() - start; socket.destroy(); resolve(latency); // ms }); socket.on("timeout", () => { socket.destroy(); resolve(null); // timed out (yeah thanks, didn't see that but that comment had changed how i look on the world) }); socket.on("error", () => resolve(null)); }); } function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, lengthCommands, endpointName, maxPayloadSize = 1024 * 1024) { const rawCommand = message.toString(); const command = rawCommand.replace(/[\r\n]+/g, ''); const now = Date.now(); if (endpointName === 'text') consoleCmd.logDebug(`Command received from \x1b[90m${clientIp}\x1b[0m: ${command}`); if (command.length > maxPayloadSize) { consoleCmd.logWarn(`Command from \x1b[90m${clientIp}\x1b[0m on \x1b[90m/${endpointName}\x1b[0m exceeded maximum payload size (${parseInt(command.length / 1024)} KB / ${parseInt(maxPayloadSize / 1024)} KB).`); return ""; } // Initialize user command history if not present if (!userCommandHistory[clientIp]) userCommandHistory[clientIp] = []; // Record the current timestamp for the user userCommandHistory[clientIp].push(now); // Remove timestamps older than 20 ms from the history userCommandHistory[clientIp] = userCommandHistory[clientIp].filter(timestamp => now - timestamp <= 20); // Check if there are 8 or more commands in the last 20 ms if (userCommandHistory[clientIp].length >= 8) { consoleCmd.logWarn(`User \x1b[90m${clientIp}\x1b[0m is spamming with rapid commands. Connection will be terminated and user will be banned.`); // Check if the normalized IP is already in the banlist const isAlreadyBanned = serverConfig.webserver.banlist.some(banEntry => banEntry[0] === clientIp); if (!isAlreadyBanned) { // Add the normalized IP to the banlist serverConfig.webserver.banlist.push([clientIp, 'Unknown', Date.now(), '[Auto ban] Spam']); consoleCmd.logInfo(`User \x1b[90m${clientIp}\x1b[0m has been added to the banlist due to extreme spam.`); configSave(); } ws.close(1008, 'Bot-like behavior detected'); return command; // Return command value before closing connection } // Update the last message time for general spam detection lastMessageTime = now; // Initialize command history for rate-limiting checks if (!userCommands[command]) userCommands[command] = []; // Record the current timestamp for this command userCommands[command].push(now); // Remove timestamps older than 1 second userCommands[command] = userCommands[command].filter(timestamp => now - timestamp <= 1000); // If command count exceeds limit, close connection if (userCommands[command].length > lengthCommands) { if (now - lastWarn.time > 1000) { // Check if 1 second has passed consoleCmd.logWarn(`User \x1b[90m${clientIp}\x1b[0m is spamming command "${command}" in /${endpointName}. Connection will be terminated.`); lastWarn.time = now; // Update the last warning time } ws.close(1008, 'Spamming detected'); return command; // Return command value before closing connection } return command; // Return command value for normal execution } const escapeHtml = (unsafe) => { return unsafe.replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """).replace(/'/g, "'"); }; // Start plugins with delay function startPluginsWithDelay(plugins, delay) { plugins.forEach((pluginPath, index) => { setTimeout(() => { const pluginName = path.basename(pluginPath, '.js'); // Extract plugin name from path consoleCmd.logInfo(`-----------------------------------------------------------------`); consoleCmd.logInfo(`Plugin ${pluginName} loaded successfully!`); require(pluginPath); }, delay * index); }); // Add final log line after all plugins are loaded setTimeout(() => { consoleCmd.logInfo(`-----------------------------------------------------------------`); }, delay * plugins.length); } // Function to find server files based on the plugins listed in config function findServerFiles(plugins) { let results = []; plugins.forEach(plugin => { // Remove .js extension if present if (plugin.endsWith('.js')) plugin = plugin.slice(0, -3); const pluginPath = path.join(__dirname, '..', 'plugins', `${plugin}_server.js`); if (fs.existsSync(pluginPath) && fs.statSync(pluginPath).isFile()) results.push(pluginPath); }); return results; } function normalizeIp(ip) { if(ip && ip.startsWith('::ffff:')) return ip.substring(7); return ip; } function isLocalhost(ip) { const normalized = normalizeIp(ip); return normalized === '127.0.0.1' || normalized === '::1'; } function isTrustedProxy(ip) { return serverConfig.trustedProxies.includes(normalizeIp(ip)); } function getIpAddress(request) { const remoteIpRaw = request.socket.remoteAddress; const remoteIp = normalizeIp(remoteIpRaw); const xff = request.headers['x-forwarded-for']; if (xff && !isLocalhost(remoteIp) && !isTrustedProxy(remoteIp)) { consoleCmd.logSecurity(`Untrusted proxy tried to set X-Forwarded-For: ${xff} (remote: ${remoteIpRaw})`); return remoteIp; } if (xff) return normalizeIp(xff.split(',')[0].trim()); return remoteIp; } module.exports = { authenticateWithXdrd, parseMarkdown, handleConnect, removeMarkdown, formatUptime, resolveDataBuffer, kickClient, checkLatency, antispamProtection, escapeHtml, findServerFiles, startPluginsWithDelay, getIpAddress }