diff --git a/index.js b/index.js index d0ed3bb..4fdb002 100644 --- a/index.js +++ b/index.js @@ -3,7 +3,7 @@ require('./server/index.js'); /** * FM-DX Webserver * - * Github repo: https://github.com/NoobishSVK/fm-dx-webserver + * Github repo: https://github.com/KubaPro010/fm-dx-webserver * Server files: /server * Client files (web): /web * Plugin files: /plugins diff --git a/server/chat.js b/server/chat.js index 6bbca1e..a538adc 100644 --- a/server/chat.js +++ b/server/chat.js @@ -63,17 +63,12 @@ function createChatServer(storage) { delete messageData.ip; delete messageData.time; - if (messageData.nickname != null) { - messageData.nickname = helpers.escapeHtml(String(messageData.nickname)); - } + if (messageData.nickname != null) messageData.nickname = helpers.escapeHtml(String(messageData.nickname)); messageData.ip = clientIp; const now = new Date(); - messageData.time = - String(now.getHours()).padStart(2, '0') + - ":" + - String(now.getMinutes()).padStart(2, '0'); + messageData.time = String(now.getHours()).padStart(2, '0') + ":" + String(now.getMinutes()).padStart(2, '0'); if (serverConfig.webserver.banlist?.includes(clientIp)) return; diff --git a/server/datahandler.js b/server/datahandler.js index f94c51f..05d332b 100644 --- a/server/datahandler.js +++ b/server/datahandler.js @@ -2,7 +2,7 @@ const RDSDecoder = require("./rds.js"); const { serverConfig } = require('./server_config'); -const { fetchTx } = require('./tx_search.js'); +const fetchTx = require('./tx_search.js'); const updateInterval = 75; // Initialize the data object diff --git a/server/endpoints.js b/server/endpoints.js index 3700347..ff6c3c3 100644 --- a/server/endpoints.js +++ b/server/endpoints.js @@ -15,7 +15,7 @@ const tunerProfiles = require('./tuner_profiles'); const { logInfo, logs } = require('./console'); const dataHandler = require('./datahandler'); const fmdxList = require('./fmdx_list'); -const { allPluginConfigs } = require('./plugins'); +const allPluginConfigs = require('./plugins'); // Endpoints router.get('/', (req, res) => { diff --git a/server/helpers.js b/server/helpers.js index 5714af9..31311ff 100644 --- a/server/helpers.js +++ b/server/helpers.js @@ -1,3 +1,4 @@ +const fs = require('fs'); const http = require('http'); const https = require('https'); const net = require('net'); @@ -5,7 +6,7 @@ const crypto = require('crypto'); const dataHandler = require('./datahandler'); const storage = require('./storage'); const consoleCmd = require('./console'); -const { serverConfig, configExists, configSave } = require('./server_config'); +const { serverConfig, configSave } = require('./server_config'); function parseMarkdown(parsed) { parsed = parsed.replace(/<\/?[^>]+(>|$)/g, ''); @@ -93,9 +94,7 @@ 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); - } + 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 = ""; @@ -152,9 +151,7 @@ function processConnection(clientIp, locationInfo, currentUsers, ws, callback) { } const userLocation = - locationInfo.country === undefined - ? "Unknown" - : `${locationInfo.city}, ${locationInfo.regionName}, ${locationInfo.countryCode}`; + locationInfo.country === undefined ? "Unknown" : `${locationInfo.city}, ${locationInfo.regionName}, ${locationInfo.countryCode}`; storage.connectedUsers.push({ ip: clientIp, @@ -269,7 +266,7 @@ function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userC // 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.`); + 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); @@ -281,17 +278,15 @@ function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userC configSave(); } - ws.close(1008, 'Bot-like behavior detected'); - return command; // Return command value before closing connection + 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] = []; - } + if (!userCommands[command]) userCommands[command] = []; // Record the current timestamp for this command userCommands[command].push(now); @@ -313,15 +308,45 @@ function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userC } const escapeHtml = (unsafe) => { - return unsafe - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); + 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 + logInfo(`-----------------------------------------------------------------`); + logInfo(`Plugin ${pluginName} loaded successfully!`); + require(pluginPath); + }, delay * index); + }); + + // Add final log line after all plugins are loaded + setTimeout(() => { + 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; +} module.exports = { - authenticateWithXdrd, parseMarkdown, handleConnect, removeMarkdown, formatUptime, resolveDataBuffer, kickClient, checkIPv6Support, checkLatency, antispamProtection, escapeHtml + authenticateWithXdrd, parseMarkdown, handleConnect, + removeMarkdown, formatUptime, resolveDataBuffer, + kickClient, checkIPv6Support, checkLatency, + antispamProtection, escapeHtml, findServerFiles, + startPluginsWithDelay } \ No newline at end of file diff --git a/server/index.js b/server/index.js index f22223e..734c3f2 100644 --- a/server/index.js +++ b/server/index.js @@ -1,4 +1,3 @@ -// Library imports const express = require('express'); const endpoints = require('./endpoints'); const session = require('express-session'); @@ -8,21 +7,16 @@ const readline = require('readline'); const app = express(); const httpServer = http.createServer(app); const WebSocket = require('ws'); -const wss = new WebSocket.Server({ noServer: true, perMessageDeflate: true }); -const rdsWss = new WebSocket.Server({ noServer: true }); -const pluginsWss = new WebSocket.Server({ noServer: true, perMessageDeflate: true }); -const fs = require('fs'); const path = require('path'); const net = require('net'); -const client = new net.Socket(); const { SerialPort } = require('serialport'); const tunnel = require('./tunnel'); const { createChatServer } = require('./chat'); -const { createAudioServer } = require('./stream/ws.js'); const figlet = require('figlet'); -// File imports const helpers = require('./helpers'); +const { findServerFiles, startPluginsWithDelay } = helpers; + const dataHandler = require('./datahandler'); const fmdxList = require('./fmdx_list'); const { logError, logInfo, logWarn } = require('./console'); @@ -31,35 +25,10 @@ const { serverConfig, configExists } = require('./server_config'); const pluginsApi = require('./plugins_api'); const pjson = require('../package.json'); -// 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; -} - -// Start plugins with delay -function startPluginsWithDelay(plugins, delay) { - plugins.forEach((pluginPath, index) => { - setTimeout(() => { - const pluginName = path.basename(pluginPath, '.js'); // Extract plugin name from path - logInfo(`-----------------------------------------------------------------`); - logInfo(`Plugin ${pluginName} loaded successfully!`); - require(pluginPath); - }, delay * index); - }); - - // Add final log line after all plugins are loaded - setTimeout(() => { - logInfo(`-----------------------------------------------------------------`); - }, delay * plugins.length); -} +const client = new net.Socket(); +const wss = new WebSocket.Server({ noServer: true, perMessageDeflate: true }); +const rdsWss = new WebSocket.Server({ noServer: true }); +const pluginsWss = new WebSocket.Server({ noServer: true, perMessageDeflate: true }); // Get all plugins from config and find corresponding server files const plugins = findServerFiles(serverConfig.plugins); @@ -76,22 +45,12 @@ const terminalWidth = readline.createInterface({ output: process.stdout }).output.columns; - -figlet("FM-DX Webserver", function (err, data) { - if (err) { - console.log("Something went wrong..."); - console.dir(err); - return; - } - console.log('\x1b[32m' + data); -}); +console.log('\x1b[32m' + figlet.textSync("FM-DX Webserver")); console.log('\x1b[32m\x1b[2mby Noobish @ \x1b[4mFMDX.org\x1b[0m'); console.log("v" + pjson.version) console.log('\x1b[90m' + '─'.repeat(terminalWidth - 1) + '\x1b[0m'); -const chatWss = createChatServer(storage); -const audioWss = createAudioServer(); -// Start ffmpeg +const audioWss = require('./stream/ws.js'); require('./stream/index'); require('./plugins'); @@ -107,6 +66,7 @@ const sessionMiddleware = session({ }); app.use(sessionMiddleware); app.use(bodyParser.json()); +const chatWss = createChatServer(storage); connectToXdrd(); connectToSerial(); @@ -237,9 +197,7 @@ client.on('data', (data) => { const { xdrd } = serverConfig; helpers.resolveDataBuffer(data, wss, rdsWss); - if (authFlags.authMsg == true && authFlags.messageCount > 1) { - return; - } + if (authFlags.authMsg == true && authFlags.messageCount > 1) return; authFlags.messageCount++; const receivedData = data.toString(); @@ -346,7 +304,7 @@ wss.on('connection', (ws, request) => { let clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress; const userCommandHistory = {}; const normalizedClientIp = clientIp?.replace(/^::ffff:/, ''); - + if (clientIp && serverConfig.webserver.banlist?.includes(clientIp)) { ws.close(1008, 'Banned IP'); return; diff --git a/server/plugins.js b/server/plugins.js index 369a4af..c42e064 100644 --- a/server/plugins.js +++ b/server/plugins.js @@ -93,6 +93,4 @@ function createLinks() { const allPluginConfigs = collectPluginConfigs(); createLinks(); -module.exports = { - allPluginConfigs -}; +module.exports = allPluginConfigs; diff --git a/server/rds.js b/server/rds.js index 9a478a1..70e2657 100644 --- a/server/rds.js +++ b/server/rds.js @@ -93,8 +93,8 @@ class RDSDecoder { this.ps[idx * 2] = String.fromCharCode(blockD >> 8); this.ps[idx * 2 + 1] = String.fromCharCode(blockD & 0xFF); - this.ps_errors[idx * 2] = error; - this.ps_errors[idx * 2 + 1] = error; + this.ps_errors[idx * 2] = Math.ceil(d_error * (10/3)); + this.ps_errors[idx * 2 + 1] = Math.ceil(d_error * (10/3)); this.data.ps = this.ps.join(''); this.data.ps_errors = this.ps_errors.join(','); @@ -124,15 +124,15 @@ class RDSDecoder { if(c_error < 2 && multiplier !== 2) { this.rt1[idx * multiplier] = String.fromCharCode(blockC >> 8); this.rt1[idx * multiplier + 1] = String.fromCharCode(blockC & 0xFF); - this.rt1_errors[idx * multiplier] = error; - this.rt1_errors[idx * multiplier + 1] = error; + this.rt1_errors[idx * multiplier] = Math.ceil(c_error * (10/3)); + this.rt1_errors[idx * multiplier + 1] = Math.ceil(c_error * (10/3)); } if(d_error < 2) { var offset = (multiplier == 2) ? 0 : 2; this.rt1[idx * multiplier + offset] = String.fromCharCode(blockD >> 8); this.rt1[idx * multiplier + offset + 1] = String.fromCharCode(blockD & 0xFF); - this.rt1_errors[idx * multiplier + offset] = error; - this.rt1_errors[idx * multiplier + offset + 1] = error; + this.rt1_errors[idx * multiplier + offset] = Math.ceil(d_error * (10/3)); + this.rt1_errors[idx * multiplier + offset + 1] = Math.ceil(d_error * (10/3)); } var i = this.rt1.indexOf("\r") @@ -155,15 +155,15 @@ class RDSDecoder { if(c_error !== 3 && multiplier !== 2) { this.rt0[idx * multiplier] = String.fromCharCode(blockC >> 8); this.rt0[idx * multiplier + 1] = String.fromCharCode(blockC & 0xFF); - this.rt0_errors[idx * multiplier] = error; - this.rt0_errors[idx * multiplier + 1] = error; + this.rt0_errors[idx * multiplier] = Math.ceil(c_error * (10/3)); + this.rt0_errors[idx * multiplier + 1] = Math.ceil(c_error * (10/3)); } if(d_error !== 3) { var offset = (multiplier == 2) ? 0 : 2; this.rt0[idx * multiplier + offset] = String.fromCharCode(blockD >> 8); this.rt0[idx * multiplier + offset + 1] = String.fromCharCode(blockD & 0xFF); - this.rt0_errors[idx * multiplier + offset] = error; - this.rt0_errors[idx * multiplier + offset + 1] = error; + this.rt0_errors[idx * multiplier + offset] = Math.ceil(d_error * (10/3)); + this.rt0_errors[idx * multiplier + offset + 1] = Math.ceil(d_error * (10/3)); } var i = this.rt0.indexOf("\r"); diff --git a/server/stream/index.js b/server/stream/index.js index 8d65672..e7c6cb3 100644 --- a/server/stream/index.js +++ b/server/stream/index.js @@ -21,7 +21,7 @@ checkFFmpeg().then((ffmpegPath) => { logInfo(`${consoleLogTitle} Using ${ffmpegPath === 'ffmpeg' ? 'system-installed FFmpeg' : 'ffmpeg-static'}`); logInfo(`${consoleLogTitle} Starting audio stream on device: \x1b[35m${serverConfig.audio.audioDevice}\x1b[0m`); - const sampleRate = Number(serverConfig.audio.sampleRate || 44100) + Number(serverConfig.audio.samplerateOffset || 0); + const sampleRate = Number(serverConfig.audio.sampleRate || 44100) + Number(serverConfig.audio.samplerateOffset || 0); // Maybe even do 32 khz, we do not need higher than 15 khz precision const channels = Number(serverConfig.audio.audioChannels || 2); @@ -139,4 +139,4 @@ checkFFmpeg().then((ffmpegPath) => { logError(`${consoleLogTitle} Error: ${err.message}`); }); -module.exports.audio_pipe = audio_pipe; \ No newline at end of file +module.exports = audio_pipe; \ No newline at end of file diff --git a/server/stream/ws.js b/server/stream/ws.js index fce010e..12ec920 100644 --- a/server/stream/ws.js +++ b/server/stream/ws.js @@ -1,32 +1,28 @@ const WebSocket = require('ws'); const { serverConfig } = require('../server_config'); -const { audio_pipe } = require('./index.js'); +const audio_pipe = require('./index.js'); -function createAudioServer() { - const audioWss = new WebSocket.Server({ noServer: true }); +const audioWss = new WebSocket.Server({ noServer: true, skipUTF8Validation: true }); - audioWss.on('connection', (ws, request) => { - const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress; +audioWss.on('connection', (ws, request) => { + const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress; - if (serverConfig.webserver.banlist?.includes(clientIp)) { - ws.close(1008, 'Banned IP'); - return; - } + if (serverConfig.webserver.banlist?.includes(clientIp)) { + ws.close(1008, 'Banned IP'); + return; + } +}); + +audio_pipe.on('data', (chunk) => { + audioWss.clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) client.send(chunk, {binary: true, compress: false}); }); +}); - audio_pipe.on('data', (chunk) => { - audioWss.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) client.send(chunk, {binary: true, compress: false }); - }); +audio_pipe.on('end', () => { + audioWss.clients.forEach((client) => { + client.close(1001, "Audio stream ended"); }); +}); - audio_pipe.on('end', () => { - audioWss.clients.forEach((client) => { - client.close(1001, "Audio stream ended"); - }); - }); - - return audioWss; -} - -module.exports = { createAudioServer }; \ No newline at end of file +module.exports = audioWss; \ No newline at end of file diff --git a/server/tx_search.js b/server/tx_search.js index e2bab15..d2cbc27 100644 --- a/server/tx_search.js +++ b/server/tx_search.js @@ -80,9 +80,7 @@ async function buildTxDatabase() { consoleCmd.logInfo('Fetching transmitter database...'); const response = await fetch(`https://maps.fmdx.org/api?qth=${serverConfig.identification.lat},${serverConfig.identification.lon}`, { method: 'GET', - headers: { - 'Accept': 'application/json' - } + headers: {'Accept': 'application/json'} }); if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); localDb = await response.json(); @@ -169,9 +167,7 @@ function getStateForCoordinates(lat, lon) { for (const feature of usStatesGeoJson.features) { const boundingBox = getStateBoundingBox(feature.geometry.coordinates); - if (isCityInState(lat, lon, boundingBox)) { - return feature.properties.name; // Return the state's name if city is inside bounding box - } + if (isCityInState(lat, lon, boundingBox)) return feature.properties.name; // Return the state's name if city is inside bounding box } return null; } @@ -208,22 +204,16 @@ function validPsCompare(rdsPs, stationPs) { for (let i = 0; i < standardizedRdsPs.length; i++) { // Skip this position if the character in standardizedRdsPs is an underscore. if (standardizedRdsPs[i] === '_') continue; - if (token[i] === standardizedRdsPs[i]) { - matchCount++; - } - } - if (matchCount >= minMatchLen) { - return true; + if (token[i] === standardizedRdsPs[i]) matchCount++; } + if (matchCount >= minMatchLen) return true; } return false; } function evaluateStation(station, esMode) { let weightDistance = station.distanceKm; - if (esMode && station.distanceKm > 700) { - weightDistance = Math.abs(station.distanceKm - 1500) + 200; - } + if (esMode && station.distanceKm > 700) weightDistance = Math.abs(station.distanceKm - 1500) + 200; let erp = station.erp && station.erp > 0 ? station.erp : 1; let extraWeight = erp > weightedErp && station.distanceKm <= weightDistance ? 0.3 : 0; let score = 0; @@ -394,6 +384,4 @@ function deg2rad(deg) { return deg * (Math.PI / 180); } -module.exports = { - fetchTx -}; \ No newline at end of file +module.exports = fetchTx; \ No newline at end of file