const http = require('http'); 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, configExists, configSave } = require('./server_config'); 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'); parsed = parsed.replace(/\n/g, '
'); return parsed; } 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; parsed = parsed.replace(linkRegex, '$1'); return parsed; } 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(); function handleConnect(clientIp, currentUsers, ws, callback) { if (ipCache.has(clientIp)) { // Use cached location info processConnection(clientIp, ipCache.get(clientIp), currentUsers, ws, callback); return; } http.get(`http://ip-api.com/json/${clientIp}`, (response) => { let data = ""; response.on("data", (chunk) => { data += chunk; }); response.on("end", () => { try { const locationInfo = JSON.parse(data); ipCache.set(clientIp, locationInfo); // Store in cache processConnection(clientIp, locationInfo, currentUsers, ws, callback); } catch (error) { console.error("Error parsing location data:", error); callback("User allowed"); } }); }).on("error", (err) => { consoleCmd.logError("Error fetching location data:", err.code); callback("User allowed"); }); } let bannedASCache = { data: null, timestamp: 0 }; function fetchBannedAS(callback) { const now = Date.now(); if (bannedASCache.data && now - bannedASCache.timestamp < 10 * 60 * 1000) { return callback(null, bannedASCache.data); } const req = https.get("https://fmdx.org/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(null, bannedAS); } catch (error) { console.error("Error parsing banned AS list:", error); callback(null, []); // Default to allowing user } }); }); // Set timeout for the request (5 seconds) req.setTimeout(5000, () => { console.error("Error: Request timed out while fetching banned AS list."); req.abort(); callback(null, []); // Default to allowing user }); req.on("error", (err) => { console.error("Error fetching banned AS list:", err); callback(null, []); // Default to allowing user }); } 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((error, bannedAS) => { if (error) { console.error("Error fetching banned AS list:", error); } if (bannedAS.some((as) => locationInfo.as?.includes(as))) { return callback("User banned"); } const userLocation = locationInfo.country === undefined ? "Unknown" : `${locationInfo.city}, ${locationInfo.regionName}, ${locationInfo.countryCode}`; storage.connectedUsers.push({ ip: clientIp, location: userLocation, time: connectionTime, instance: ws, }); consoleCmd.logInfo( `Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${userLocation}` ); callback("User allowed"); }); } function formatUptime(uptimeInSeconds) { const secondsInMinute = 60; const secondsInHour = secondsInMinute * 60; const secondsInDay = secondsInHour * 24; const days = Math.floor(uptimeInSeconds / secondsInDay); const hours = Math.floor((uptimeInSeconds % secondsInDay) / secondsInHour); const minutes = Math.floor((uptimeInSeconds % secondsInHour) / secondsInMinute); return `${days}d ${hours}h ${minutes}m`; } let incompleteDataBuffer = ''; function resolveDataBuffer(data, wss, rdsWss) { 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 checkIPv6Support(callback) { const server = net.createServer(); server.listen(0, '::1', () => { server.close(() => callback(true)); }).on('error', (error) => { if (error.code === 'EADDRNOTAVAIL') { callback(false); } else { callback(false); } }); } function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, lengthCommands, endpointName) { const command = message.toString(); const now = Date.now(); const normalizedClientIp = clientIp?.replace(/^::ffff:/, ''); if (endpointName === 'text') consoleCmd.logDebug(`Command received from \x1b[90m${clientIp}\x1b[0m: ${command}`); // 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] === normalizedClientIp); if (!isAlreadyBanned) { // Add the normalized IP to the banlist serverConfig.webserver.banlist.push([normalizedClientIp, 'Unknown', Date.now(), '[Auto ban] Spam']); consoleCmd.logInfo(`User \x1b[90m${normalizedClientIp}\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, "'"); }; module.exports = { authenticateWithXdrd, parseMarkdown, handleConnect, removeMarkdown, formatUptime, resolveDataBuffer, kickClient, checkIPv6Support, antispamProtection, escapeHtml }