diff --git a/package-lock.json b/package-lock.json index 009bf83..12c7186 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "express": "5.2.1", "express-session": "1.19.0", "ffmpeg-static": "5.3.0", + "figlet": "^1.10.0", "http": "0.0.1-security", "koffi": "2.7.2", "net": "1.0.2", @@ -574,6 +575,15 @@ "node": ">=18" } }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/concat-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", @@ -874,6 +884,21 @@ "node": ">=16" } }, + "node_modules/figlet": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.10.0.tgz", + "integrity": "sha512-aktIwEZZ6Gp9AWdMXW4YCi0J2Ahuxo67fNJRUIWD81w8pQ0t9TS8FFpbl27ChlTLF06VkwjDesZSzEVzN75rzA==", + "license": "MIT", + "dependencies": { + "commander": "^14.0.0" + }, + "bin": { + "figlet": "bin/index.js" + }, + "engines": { + "node": ">= 17.0.0" + } + }, "node_modules/filelist": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.5.tgz", diff --git a/package.json b/package.json index 0f89f91..753331c 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,11 @@ "express": "5.2.1", "express-session": "1.19.0", "ffmpeg-static": "5.3.0", + "figlet": "^1.10.0", "http": "0.0.1-security", "koffi": "2.7.2", "net": "1.0.2", "serialport": "13.0.0", "ws": "8.19.0" } -} \ No newline at end of file +} diff --git a/server/chat.js b/server/chat.js index b160e7f..6bbca1e 100644 --- a/server/chat.js +++ b/server/chat.js @@ -4,14 +4,12 @@ const { logChat } = require('./console'); const helpers = require('./helpers'); function createChatServer(storage) { - if (!serverConfig.webserver.chatEnabled) { - return null; - } + if (!serverConfig.webserver.chatEnabled) return null; const chatWss = new WebSocket.Server({ noServer: true }); chatWss.on('connection', (ws, request) => { - const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress; + const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress; const userCommandHistory = {}; if (serverConfig.webserver.banlist?.includes(clientIp)) { @@ -79,32 +77,19 @@ function createChatServer(storage) { if (serverConfig.webserver.banlist?.includes(clientIp)) return; - if (request.session?.isAdminAuthenticated === true) { - messageData.admin = true; - } - - if (messageData.nickname?.length > 32) { - messageData.nickname = messageData.nickname.substring(0, 32); - } - - if (messageData.message?.length > 255) { - messageData.message = messageData.message.substring(0, 255); - } + if (request.session?.isAdminAuthenticated === true) messageData.admin = true; + if (messageData.nickname?.length > 32) messageData.nickname = messageData.nickname.substring(0, 32); + if (messageData.message?.length > 255) messageData.message = messageData.message.substring(0, 255); storage.chatHistory.push(messageData); - if (storage.chatHistory.length > 50) { - storage.chatHistory.shift(); - } + if (storage.chatHistory.length > 50) storage.chatHistory.shift(); logChat(messageData); chatWss.clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { const responseMessage = { ...messageData }; - - if (!request.session?.isAdminAuthenticated) { - delete responseMessage.ip; - } + if (!request.session?.isAdminAuthenticated) delete responseMessage.ip; client.send(JSON.stringify(responseMessage)); } @@ -112,7 +97,7 @@ function createChatServer(storage) { }); }); - return chatWss; // ← VERY IMPORTANT + return chatWss; } module.exports = { createChatServer }; \ No newline at end of file diff --git a/server/datahandler.js b/server/datahandler.js index aac93bd..f94c51f 100644 --- a/server/datahandler.js +++ b/server/datahandler.js @@ -8,7 +8,7 @@ const updateInterval = 75; // Initialize the data object var dataToSend = { pi: '?', - freq: 87.500.toFixed(3), + freq: (87.500).toFixed(3), sig: 0, sigRaw: '', sigTop: -Infinity, @@ -71,9 +71,7 @@ function rdsReceived() { clearTimeout(rdsTimeoutTimer); rdsTimeoutTimer = null; } - if (serverConfig.webserver.rdsTimeout && serverConfig.webserver.rdsTimeout != 0) { - rdsTimeoutTimer = setTimeout(rdsReset, serverConfig.webserver.rdsTimeout * 1000); - } + if (serverConfig.webserver.rdsTimeout && serverConfig.webserver.rdsTimeout != 0) rdsTimeoutTimer = setTimeout(rdsReset, serverConfig.webserver.rdsTimeout * 1000); } function rdsReset() { diff --git a/server/endpoints.js b/server/endpoints.js index d4f17c5..3700347 100644 --- a/server/endpoints.js +++ b/server/endpoints.js @@ -19,7 +19,7 @@ const { allPluginConfigs } = require('./plugins'); // Endpoints router.get('/', (req, res) => { - let requestIp = req.headers['x-forwarded-for'] || req.connection.remoteAddress; + let requestIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress; const normalizedIp = requestIp?.replace(/^::ffff:/, ''); const ipList = (normalizedIp || '').split(',').map(ip => ip.trim()).filter(Boolean); // in case there are multiple IPs (proxy), we need to check all of them @@ -175,11 +175,11 @@ router.get('/wizard', (req, res) => { router.get('/rds', (req, res) => { - res.send('Please connect using a WebSocket compatible app to obtain RDS stream.'); + res.send('Please connect using a WebSocket compatible app to obtain the RDS stream.'); }); router.get('/rdsspy', (req, res) => { - res.send('Please connect using a WebSocket compatible app to obtain RDS stream.'); + res.send('Please connect using a WebSocket compatible app to obtain the RDS stream.'); }); router.get('/api', (req, res) => { @@ -195,24 +195,17 @@ router.get('/api', (req, res) => { const loginAttempts = {}; // Format: { 'ip': { count: 1, lastAttempt: 1234567890 } } -const MAX_ATTEMPTS = 25; +const MAX_ATTEMPTS = 15; const WINDOW_MS = 15 * 60 * 1000; const authenticate = (req, res, next) => { const ip = req.ip || req.connection.remoteAddress; const now = Date.now(); - if (!loginAttempts[ip]) { - loginAttempts[ip] = { count: 0, lastAttempt: now }; - } else if (now - loginAttempts[ip].lastAttempt > WINDOW_MS) { - loginAttempts[ip] = { count: 0, lastAttempt: now }; - } + if (!loginAttempts[ip]) loginAttempts[ip] = { count: 0, lastAttempt: now }; + else if (now - loginAttempts[ip].lastAttempt > WINDOW_MS) loginAttempts[ip] = { count: 0, lastAttempt: now }; - if (loginAttempts[ip].count >= MAX_ATTEMPTS) { - return res.status(403).json({ - message: 'Too many login attempts. Please try again later.' - }); - } + if (loginAttempts[ip].count >= MAX_ATTEMPTS) return res.status(403).json({message: 'Too many login attempts. Please try again later.'}); const { password } = req.body; @@ -250,11 +243,9 @@ router.get('/logout', (req, res) => { }); router.get('/kick', (req, res) => { - const ipAddress = req.query.ip; // Extract the IP address parameter from the query string + const ipAddress = req.query.ip; // Terminate the WebSocket connection for the specified IP address - if(req.session.isAdminAuthenticated) { - helpers.kickClient(ipAddress); - } + if(req.session.isAdminAuthenticated) helpers.kickClient(ipAddress); setTimeout(() => { res.redirect('/setup'); }, 500); @@ -269,9 +260,7 @@ router.get('/addToBanlist', (req, res) => { userBanData = [ipAddress, location, date, reason]; - if (typeof serverConfig.webserver.banlist !== 'object') { - serverConfig.webserver.banlist = []; - } + if (typeof serverConfig.webserver.banlist !== 'object') serverConfig.webserver.banlist = []; serverConfig.webserver.banlist.push(userBanData); configSave(); @@ -281,17 +270,14 @@ router.get('/addToBanlist', (req, res) => { router.get('/removeFromBanlist', (req, res) => { if (!req.session.isAdminAuthenticated) return; + const ipAddress = req.query.ip; - if (typeof serverConfig.webserver.banlist !== 'object') { - serverConfig.webserver.banlist = []; - } + if (typeof serverConfig.webserver.banlist !== 'object') serverConfig.webserver.banlist = []; const banIndex = serverConfig.webserver.banlist.findIndex(ban => ban[0] === ipAddress); - if (banIndex === -1) { - return res.status(404).json({ success: false, message: 'IP address not found in banlist.' }); - } + if (banIndex === -1) return res.status(404).json({ success: false, message: 'IP address not found in banlist.' }); serverConfig.webserver.banlist.splice(banIndex, 1); configSave(); @@ -303,19 +289,14 @@ router.get('/removeFromBanlist', (req, res) => { router.post('/saveData', (req, res) => { const data = req.body; let firstSetup; - if(req.session.isAdminAuthenticated || configExists() === false) { + if(req.session.isAdminAuthenticated || !configExists()) { configUpdate(data); fmdxList.update(); - if(configExists() === false) { - firstSetup = true; - } + if(!configExists()) firstSetup = true; logInfo('Server config changed successfully.'); - if(firstSetup === true) { - res.status(200).send('Data saved successfully!\nPlease, restart the server to load your configuration.'); - } else { - res.status(200).send('Data saved successfully!\nSome settings may need a server restart to apply.'); - } + if(firstSetup === true) res.status(200).send('Data saved successfully!\nPlease, restart the server to load your configuration.'); + else res.status(200).send('Data saved successfully!\nSome settings may need a server restart to apply.'); } }); @@ -327,9 +308,8 @@ router.get('/getData', (req, res) => { if(req.session.isAdminAuthenticated) { // Check if the file exists fs.access(configPath, fs.constants.F_OK, (err) => { - if (err) { - console.log(err); - } else { + if (err) console.log(err); + else { // File exists, send it as the response res.sendFile(path.join(__dirname, '../' + configName + '.json')); } @@ -342,9 +322,7 @@ router.get('/getDevices', (req, res) => { parseAudioDevice((result) => { res.json(result); }); - } else { - res.status(403).json({ error: 'Unauthorized' }); - } + } else res.status(403).json({ error: 'Unauthorized' }); }); /* Static data are being sent through here on connection - these don't change when the server is running */ @@ -389,9 +367,7 @@ function canLog(id) { } } - if (logHistory[id] && (now - logHistory[id]) < sixtyMinutes) { - return false; // Deny logging if less than 60 minutes have passed - } + if (logHistory[id] && (now - logHistory[id]) < sixtyMinutes) return false; // Deny logging if less than 60 minutes have passed logHistory[id] = now; // Update with the current timestamp return true; } diff --git a/server/fmdx_list.js b/server/fmdx_list.js index 70da4cf..ec57f93 100644 --- a/server/fmdx_list.js +++ b/server/fmdx_list.js @@ -65,7 +65,7 @@ function sendUpdate() { tuner: serverConfig.device || '', bwLimit: bwLimit, os: currentOs, - version: pjson.version + version: pjson.version }; if (serverConfig.identification.token) request.token = serverConfig.identification.token; diff --git a/server/helpers.js b/server/helpers.js index 240c81d..5714af9 100644 --- a/server/helpers.js +++ b/server/helpers.js @@ -9,39 +9,39 @@ 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; } @@ -173,11 +173,11 @@ 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`; } @@ -186,7 +186,7 @@ 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) { @@ -197,7 +197,7 @@ function resolveDataBuffer(data, wss, rdsWss) { receivedData = receivedData.slice(0, position + 1); } } else incompleteDataBuffer = ''; - + if (receivedData.length) dataHandler.handleData(wss, receivedData, rdsWss); } @@ -207,7 +207,7 @@ function kickClient(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(); @@ -260,17 +260,17 @@ function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userC // 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); @@ -280,7 +280,7 @@ function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userC 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 } diff --git a/server/index.js b/server/index.js index 48addef..f22223e 100644 --- a/server/index.js +++ b/server/index.js @@ -19,6 +19,7 @@ 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'); @@ -35,14 +36,10 @@ function findServerFiles(plugins) { let results = []; plugins.forEach(plugin => { // Remove .js extension if present - if (plugin.endsWith('.js')) { - plugin = plugin.slice(0, -3); - } + 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); - } + if (fs.existsSync(pluginPath) && fs.statSync(pluginPath).isFile()) results.push(pluginPath); }); return results; } @@ -80,14 +77,14 @@ const terminalWidth = readline.createInterface({ }).output.columns; -// Couldn't get figlet.js or something like that? -console.log(`\x1b[32m - _____ __ __ ______ __ __ __ _ -| ___| \\/ | | _ \\ \\/ / \\ \\ / /__| |__ ___ ___ _ ____ _____ _ __ -| |_ | |\\/| |_____| | | \\ / \\ \\ /\\ / / _ \\ '_ \\/ __|/ _ \\ '__\\ \\ / / _ \\ '__| -| _| | | | |_____| |_| / \\ \\ V V / __/ |_) \\__ \\ __/ | \\ V / __/ | -|_| |_| |_| |____/_/\\_\\ \\_/\\_/ \\___|_.__/|___/\\___|_| \\_/ \\___|_| -`); +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\x1b[2mby Noobish @ \x1b[4mFMDX.org\x1b[0m'); console.log("v" + pjson.version) console.log('\x1b[90m' + '─'.repeat(terminalWidth - 1) + '\x1b[0m'); @@ -104,7 +101,7 @@ let timeoutAntenna; app.use(bodyParser.urlencoded({ extended: true })); const sessionMiddleware = session({ - secret: 'GTce3tN6U8odMwoI', + secret: 'GTce3tN6U8odMwoI', // Cool resave: false, saveUninitialized: true, }); @@ -324,9 +321,6 @@ app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, '../web')); app.use('/', endpoints); -/** - * WEBSOCKET BLOCK - */ const tunerLockTracker = new WeakMap(); const ipConnectionCounts = new Map(); // Per-IP limit variables const ipLogTimestamps = new Map(); @@ -349,7 +343,7 @@ setInterval(() => { wss.on('connection', (ws, request) => { const output = serverConfig.xdrd.wirelessConnection ? client : serialport; - let clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress; + let clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress; const userCommandHistory = {}; const normalizedClientIp = clientIp?.replace(/^::ffff:/, ''); @@ -363,17 +357,10 @@ wss.on('connection', (ws, request) => { // Per-IP limit connection open if (clientIp) { const isLocalIp = ( - clientIp === '127.0.0.1' || - clientIp === '::1' || - clientIp === '::ffff:127.0.0.1' || - clientIp.startsWith('192.168.') || - clientIp.startsWith('10.') || - clientIp.startsWith('172.16.') - ); + clientIp === '127.0.0.1' || clientIp === '::1' || clientIp === '::ffff:127.0.0.1' || + clientIp.startsWith('192.168.') || clientIp.startsWith('10.') || clientIp.startsWith('172.16.')); if (!isLocalIp) { - if (!ipConnectionCounts.has(clientIp)) { - ipConnectionCounts.set(clientIp, 0); - } + if (!ipConnectionCounts.has(clientIp)) ipConnectionCounts.set(clientIp, 0); const currentCount = ipConnectionCounts.get(clientIp); if (currentCount >= MAX_CONNECTIONS_PER_IP) { ws.close(1008, 'Too many open connections from this IP'); @@ -389,7 +376,9 @@ wss.on('connection', (ws, request) => { } } - if (clientIp !== '::ffff:127.0.0.1' || (request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.0.1') || (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) { + if (clientIp !== '::ffff:127.0.0.1' || + (request.socket && request.socket.remoteAddress && request.socket.remoteAddress !== '::ffff:127.0.0.1') || + (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) { currentUsers++; } @@ -400,13 +389,10 @@ wss.on('connection', (ws, request) => { ws.close(1008, 'Banned IP'); return; } + dataHandler.showOnlineUsers(currentUsers); - dataHandler.showOnlineUsers(currentUsers); - - if (currentUsers === 1 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection) { - serverConfig.xdrd.wirelessConnection ? connectToXdrd() : serialport.write('x\n'); - } - }); + if (currentUsers === 1 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection) serverConfig.xdrd.wirelessConnection ? connectToXdrd() : serialport.write('x\n'); + }); const userCommands = {}; let lastWarn = { time: 0 }; @@ -474,7 +460,9 @@ wss.on('connection', (ws, request) => { } } - if (clientIp !== '::ffff:127.0.0.1' || (request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.0.1') || (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) { + if (clientIp !== '::ffff:127.0.0.1' || + (request.socket && request.socket.remoteAddress && request.socket.remoteAddress !== '::ffff:127.0.0.1') || + (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) { currentUsers--; } dataHandler.showOnlineUsers(currentUsers); @@ -540,7 +528,7 @@ wss.on('connection', (ws, request) => { // Additional web socket for using plugins pluginsWss.on('connection', (ws, request) => { - const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress; + const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress; const userCommandHistory = {}; if (serverConfig.webserver.banlist?.includes(clientIp)) { ws.close(1008, 'Banned IP'); @@ -578,39 +566,23 @@ pluginsWss.on('connection', (ws, request) => { // Websocket register for /text, /audio and /chat paths httpServer.on('upgrade', (request, socket, head) => { - const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress; + const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress; if (serverConfig.webserver.banlist?.includes(clientIp)) { socket.destroy(); return; } - if (request.url === '/text') { + + var upgradeWss = undefined; + if (request.url === '/text') upgradeWss = wss; + else if (request.url === '/audio') upgradeWss = audioWss; + else if (request.url === '/chat' && serverConfig.webserver.chatEnabled === true) upgradeWss = chatWss; + else if (request.url === '/rds' || request.url === '/rdsspy') upgradeWss = rdsWss; + else if (request.url === '/data_plugins') upgradeWss = pluginsWss; + + if(upgradeWss) { sessionMiddleware(request, {}, () => { - wss.handleUpgrade(request, socket, head, (ws) => { - wss.emit('connection', ws, request); - }); - }); - } else if (request.url === '/audio') { - sessionMiddleware(request, {}, () => { - audioWss.handleUpgrade(request, socket, head, (ws) => { - audioWss.emit('connection', ws, request); - }); - }); - } else if (request.url === '/chat' && serverConfig.webserver.chatEnabled === true) { - sessionMiddleware(request, {}, () => { - chatWss.handleUpgrade(request, socket, head, (ws) => { - chatWss.emit('connection', ws, request); - }); - }); - } else if (request.url === '/rds' || request.url === '/rdsspy') { - sessionMiddleware(request, {}, () => { - rdsWss.handleUpgrade(request, socket, head, (ws) => { - rdsWss.emit('connection', ws, request); - }); - }); - } else if (request.url === '/data_plugins') { - sessionMiddleware(request, {}, () => { - pluginsWss.handleUpgrade(request, socket, head, (ws) => { - pluginsWss.emit('connection', ws, request); + upgradeWss.handleUpgrade(request, socket, head, (ws) => { + upgradeWss.emit('connection', ws, request); }); }); } else socket.destroy(); diff --git a/server/plugins.js b/server/plugins.js index 292af35..369a4af 100644 --- a/server/plugins.js +++ b/server/plugins.js @@ -1,7 +1,6 @@ const fs = require('fs'); const path = require('path'); const consoleCmd = require('./console'); -const { serverConfig } = require('./server_config'); // Function to read all .js files in a directory function readJSFiles(dir) { @@ -11,7 +10,6 @@ function readJSFiles(dir) { // Function to parse plugin config from a file function parsePluginConfig(filePath) { - const fileContent = fs.readFileSync(filePath, 'utf8'); const pluginConfig = {}; // Assuming pluginConfig is a JavaScript object defined in each .js file @@ -31,9 +29,7 @@ function parsePluginConfig(filePath) { } // Check if the destination directory exists, if not, create it - if (!fs.existsSync(destinationDir)) { - fs.mkdirSync(destinationDir, { recursive: true }); // Create directory recursively - } + if (!fs.existsSync(destinationDir)) fs.mkdirSync(destinationDir, { recursive: true }); // Create directory recursively const destinationFile = path.join(destinationDir, path.basename(sourcePath)); @@ -41,9 +37,7 @@ function parsePluginConfig(filePath) { if (process.platform !== 'win32') { // On Linux, create a symlink try { - if (fs.existsSync(destinationFile)) { - fs.unlinkSync(destinationFile); // Remove existing file/symlink - } + if (fs.existsSync(destinationFile)) fs.unlinkSync(destinationFile); // Remove existing file/symlink fs.symlinkSync(sourcePath, destinationFile); setTimeout(function() { consoleCmd.logInfo(`Plugin ${pluginConfig.name} ${pluginConfig.version} initialized successfully.`); @@ -52,9 +46,7 @@ function parsePluginConfig(filePath) { console.error(`Error creating symlink at ${destinationFile}: ${err.message}`); } } - } else { - console.error(`Error: frontEndPath is not defined in ${filePath}`); - } + } else console.error(`Error: frontEndPath is not defined in ${filePath}`); } catch (err) { console.error(`Error parsing plugin config from ${filePath}: ${err.message}`); } @@ -71,9 +63,7 @@ function collectPluginConfigs() { jsFiles.forEach(file => { const filePath = path.join(pluginsDir, file); const config = parsePluginConfig(filePath); - if (Object.keys(config).length > 0) { - pluginConfigs.push(config); - } + if (Object.keys(config).length > 0) pluginConfigs.push(config); }); return pluginConfigs; @@ -81,9 +71,7 @@ function collectPluginConfigs() { // Ensure the web/js/plugins directory exists const webJsPluginsDir = path.join(__dirname, '../web/js/plugins'); -if (!fs.existsSync(webJsPluginsDir)) { - fs.mkdirSync(webJsPluginsDir, { recursive: true }); -} +if (!fs.existsSync(webJsPluginsDir)) fs.mkdirSync(webJsPluginsDir, { recursive: true }); // Main function to create symlinks/junctions for plugins function createLinks() { @@ -93,13 +81,8 @@ function createLinks() { if (process.platform === 'win32') { // On Windows, create a junction try { - if (fs.existsSync(destinationPluginsDir)) { - fs.rmSync(destinationPluginsDir, { recursive: true }); - } + if (fs.existsSync(destinationPluginsDir)) fs.rmSync(destinationPluginsDir, { recursive: true }); fs.symlinkSync(pluginsDir, destinationPluginsDir, 'junction'); - setTimeout(function() { - //consoleCmd.logInfo(`Plugin ${pluginConfig.name} ${pluginConfig.version} initialized successfully.`); - }, 500) } catch (err) { console.error(`Error creating junction at ${destinationPluginsDir}: ${err.message}`); } diff --git a/server/plugins_api.js b/server/plugins_api.js index 506fcb3..20cc8f7 100644 --- a/server/plugins_api.js +++ b/server/plugins_api.js @@ -5,7 +5,7 @@ // - Optionally broadcasts events to connected plugin WebSocket clients const { EventEmitter } = require('events'); -const { logInfo, logWarn, logError } = require('./console'); +const { logWarn, logError } = require('./console'); let output = null; let wss = null; diff --git a/server/rds.js b/server/rds.js index a6f529f..9a478a1 100644 --- a/server/rds.js +++ b/server/rds.js @@ -51,7 +51,7 @@ class RDSDecoder { const group = (blockB >> 12) & 0xF; const version = (blockB >> 11) & 0x1; this.data.tp = Number((blockB >> 10) & 1); - this.data.pty = (blockB >> 5) & 0b11111; + this.data.pty = (blockB >> 5) & 31; if (group === 0) { this.data.ta = (blockB >> 4) & 1; @@ -87,7 +87,7 @@ class RDSDecoder { } } - if(d_error === 3) return; // Don't risk it + if(d_error > 2) return; // Don't risk it const idx = blockB & 0x3; @@ -99,6 +99,7 @@ class RDSDecoder { this.data.ps = this.ps.join(''); this.data.ps_errors = this.ps_errors.join(','); } else if (group === 1 && version === 0) { + if(c_error > 2) return; var variant_code = (blockC >> 12) & 0x7; switch (variant_code) { case 0: @@ -120,13 +121,13 @@ class RDSDecoder { this.rt1_to_clear = false; } - if(c_error !== 3 && multiplier !== 2) { + 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; } - if(d_error !== 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); diff --git a/server/rds_country.js b/server/rds_country.js index 8ec2d02..71d98df 100644 --- a/server/rds_country.js +++ b/server/rds_country.js @@ -598,22 +598,22 @@ const rdsEccF0F4Lut = [ function rdsEccLookup(pi, ecc) { const PI_UNKNOWN = -1; - + const piCountry = (pi >> 12) & 0xF; - + if (pi === PI_UNKNOWN || piCountry === 0) { return "" } - + const piId = piCountry - 1; - + const eccRanges = [ { min: 0xA0, max: 0xA6, lut: rdsEccA0A6Lut }, { min: 0xD0, max: 0xD4, lut: rdsEccD0D4Lut }, { min: 0xE0, max: 0xE5, lut: rdsEccE0E5Lut }, { min: 0xF0, max: 0xF4, lut: rdsEccF0F4Lut } ]; - + // Check each range for (const range of eccRanges) { if (ecc >= range.min && ecc <= range.max) { @@ -621,7 +621,7 @@ function rdsEccLookup(pi, ecc) { return range.lut[eccId][piId]; } } - + return "" } diff --git a/server/server_config.js b/server/server_config.js index 73bfb3c..231044e 100644 --- a/server/server_config.js +++ b/server/server_config.js @@ -133,14 +133,10 @@ let serverConfig = { function addMissingFields(target, source) { Object.keys(source).forEach(function(key) { if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) { - if (!target[key]) { - target[key] = {}; // Create missing object - } + if (!target[key]) target[key] = {}; // Create missing object addMissingFields(target[key], source[key]); // Recursively add missing fields } else { - if (target[key] === undefined) { - target[key] = source[key]; // Add missing fields only - } + if (target[key] === undefined) target[key] = source[key]; // Add missing fields only } }); } @@ -149,13 +145,9 @@ function addMissingFields(target, source) { function deepMerge(target, source) { Object.keys(source).forEach(function(key) { if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) { - if (!target[key] || typeof target[key] !== 'object') { - target[key] = {}; // Ensure target[key] is an object before merging - } + if (!target[key] || typeof target[key] !== 'object') target[key] = {}; // Ensure target[key] is an object before merging deepMerge(target[key], source[key]); // Recursively merge objects - } else { - target[key] = source[key]; // Overwrite or add the value - } + } else target[key] = source[key]; // Overwrite or add the value }); } diff --git a/server/stream/index.js b/server/stream/index.js index 4dae63f..8d65672 100644 --- a/server/stream/index.js +++ b/server/stream/index.js @@ -21,9 +21,9 @@ 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(this?.Server?.SampleRate || serverConfig.audio.sampleRate || 44100) + Number(serverConfig.audio.samplerateOffset || 0); + const sampleRate = Number(serverConfig.audio.sampleRate || 44100) + Number(serverConfig.audio.samplerateOffset || 0); - const channels = Number(this?.Server?.Channels || serverConfig.audio.audioChannels || 2); + const channels = Number(serverConfig.audio.audioChannels || 2); let ffmpeg = null; let restartTimer = null; @@ -48,7 +48,7 @@ checkFFmpeg().then((ffmpegPath) => { ...inputArgs, - "-thread_queue_size", "1024", + "-thread_queue_size", "1536", "-ar", String(sampleRate), "-ac", String(channels), diff --git a/server/stream/parser.js b/server/stream/parser.js index fa3d7b2..465cebb 100644 --- a/server/stream/parser.js +++ b/server/stream/parser.js @@ -28,9 +28,7 @@ function parseAudioDevice(options, callback) { const matches = (data.match(regex) || []).map(match => 'hw:' + match.replace(/\s+/g, '').slice(1, -1)); matches.forEach(match => { - if (typeof match === 'string') { - audioDevices.push({ name: match }); - } + if (typeof match === 'string') audioDevices.push({ name: match }); }); } catch (err) { console.error(`Error reading file: ${err.message}`); diff --git a/server/stream/ws.js b/server/stream/ws.js index 2ab0f03..fce010e 100644 --- a/server/stream/ws.js +++ b/server/stream/ws.js @@ -1,15 +1,12 @@ const WebSocket = require('ws'); const { serverConfig } = require('../server_config'); const { audio_pipe } = require('./index.js'); -const { PassThrough } = require('stream'); function createAudioServer() { const audioWss = new WebSocket.Server({ noServer: true }); audioWss.on('connection', (ws, request) => { - const clientIp = - request.headers['x-forwarded-for'] || - request.connection.remoteAddress; + const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress; if (serverConfig.webserver.banlist?.includes(clientIp)) { ws.close(1008, 'Banned IP'); @@ -19,12 +16,7 @@ function createAudioServer() { audio_pipe.on('data', (chunk) => { audioWss.clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - client.send(chunk, { - binary: true, - compress: false - }); - } + if (client.readyState === WebSocket.OPEN) client.send(chunk, {binary: true, compress: false }); }); }); diff --git a/server/tunnel.js b/server/tunnel.js index c7f0442..18009dc 100644 --- a/server/tunnel.js +++ b/server/tunnel.js @@ -15,12 +15,10 @@ const fileExists = path => new Promise(resolve => fs.access(path, fs.constants.F async function connect() { if (serverConfig.tunnel?.enabled === true) { const librariesDir = path.resolve(__dirname, '../libraries'); - if (!await fileExists(librariesDir)) { - await fs.mkdir(librariesDir); - } + if (!await fileExists(librariesDir)) await fs.mkdir(librariesDir); const frpcPath = path.resolve(librariesDir, 'frpc' + (os.platform() === 'win32' ? '.exe' : '')); if (!await fileExists(frpcPath)) { - logInfo('frpc binary required for tunnel is not available. Downloading now...'); + logInfo('frpc binary, required for tunnel is not available. Downloading now...'); const frpcFileName = `frpc_${os.platform}_${os.arch}` + (os.platform() === 'win32' ? '.exe' : ''); try { @@ -33,9 +31,7 @@ async function connect() { return; } logInfo('Downloading of frpc is completed.') - if (os.platform() === 'linux' || os.platform() === 'darwin') { - await fs.chmod(frpcPath, 0o770); - } + if (os.platform() === 'linux' || os.platform() === 'darwin') await fs.chmod(frpcPath, 0o770); } const cfg = ejs.render(frpcConfigTemplate, { cfg: serverConfig.tunnel, @@ -73,7 +69,6 @@ async function connect() { child.on('close', (code) => { logInfo(`Tunnel process exited with code ${code}`); }); - } } diff --git a/server/tx_search.js b/server/tx_search.js index b18541e..e2bab15 100644 --- a/server/tx_search.js +++ b/server/tx_search.js @@ -96,9 +96,7 @@ async function buildTxDatabase() { consoleCmd.logInfo('Retrying transmitter database download...'); } } - } else { - consoleCmd.logInfo('Server latitude and longitude must be set before transmitter database can be built'); - } + } else consoleCmd.logInfo('Server latitude and longitude must be set before transmitter database can be built'); } // Function to build index map of PI+Freq combinations @@ -162,8 +160,7 @@ function getStateBoundingBox(coordinates) { // Function to check if a city (lat, lon) falls within the bounding box of a state function isCityInState(lat, lon, boundingBox) { - return lat >= boundingBox.minLat && lat <= boundingBox.maxLat && - lon >= boundingBox.minLon && lon <= boundingBox.maxLon; + return lat >= boundingBox.minLat && lat <= boundingBox.maxLat && lon >= boundingBox.minLon && lon <= boundingBox.maxLon; } // Function to check if a city (lat, lon) is inside any US state and return the state name @@ -231,11 +228,8 @@ function evaluateStation(station, esMode) { let extraWeight = erp > weightedErp && station.distanceKm <= weightDistance ? 0.3 : 0; let score = 0; // If ERP is 1W, use a simpler formula to avoid zero-scoring. - if (erp === 0.001) { - score = erp / station.distanceKm; - } else { - score = ((10 * (Math.log10(erp * 1000))) / weightDistance) + extraWeight; - } + if (erp === 0.001) score = erp / station.distanceKm; + else score = ((10 * (Math.log10(erp * 1000))) / weightDistance) + extraWeight; return score; } @@ -304,12 +298,8 @@ async function fetchTx(freq, piCode, rdsPs) { loc => loc.distanceKm < 700 && loc.erp >= 10 ); let esMode = false; - if (!tropoPriority) { - esMode = checkEs(); - } - for (let loc of filteredLocations) { - loc.score = evaluateStation(loc, esMode); - } + if (!tropoPriority) esMode = checkEs(); + for (let loc of filteredLocations) loc.score = evaluateStation(loc, esMode); // Sort by score in descending order filteredLocations.sort((a, b) => b.score - a.score); match = filteredLocations[0]; @@ -323,11 +313,9 @@ async function fetchTx(freq, piCode, rdsPs) { } if (match) { - if (match.itu === 'USA') { + if (match.itu == 'USA') { // Also known as Dumbfuckinstan. they should not go to hell, but hell+ (it is NOT better) const state = getStateForCoordinates(match.lat, match.lon); - if (state) { - match.state = state; // Add state to matchingCity - } + if (state) match.state = state; // Add state to matchingCity } const result = { station: match.detectedByPireg @@ -360,9 +348,7 @@ function checkEs() { const now = Date.now(); const url = "https://fmdx.org/includes/tools/get_muf.php"; - if (esSwitchCache.lastCheck && now - esSwitchCache.lastCheck < esFetchInterval) { - return esSwitchCache.esSwitch; - } + if (esSwitchCache.lastCheck && now - esSwitchCache.lastCheck < esFetchInterval) return esSwitchCache.esSwitch; if (Latitude > 20) { esSwitchCache.lastCheck = now; @@ -389,15 +375,12 @@ function haversine(lat1, lon1, lat2, lon2) { const R = 6371; const dLat = deg2rad(lat2 - lat1); const dLon = deg2rad(lon2 - lon1); - const a = - Math.sin(dLat / 2) * Math.sin(dLat / 2) + - Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); + const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); const distance = R * c; const y = Math.sin(dLon) * Math.cos(deg2rad(lat2)); - const x = Math.cos(deg2rad(lat1)) * Math.sin(deg2rad(lat2)) - - Math.sin(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.cos(dLon); + const x = Math.cos(deg2rad(lat1)) * Math.sin(deg2rad(lat2)) - Math.sin(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.cos(dLon); const azimuth = Math.atan2(y, x); const azimuthDegrees = (azimuth * 180 / Math.PI + 360) % 360;