diff --git a/README.md b/README.md index da9367c..50f5108 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,7 @@ FM-DX Webserver is a cross-platform web server designed for FM DXers who want to This project utilizes these libraries: -- [3LAS](https://github.com/jojobond/3LAS) library by JoJoBond for Low Latency Audio Streaming. - [flat-flags](https://github.com/luishdez/flat-flags) library by luishdez for RDS country flags. -- [librdsparser](https://github.com/kkonradpl/librdsparser) library by Konrad Kosmatka for RDS parsing. All of these libraries are already bundled with the webserver. diff --git a/package-lock.json b/package-lock.json index 20fcdd0..01a5050 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,6 @@ "express-session": "1.18.2", "ffmpeg-static": "5.2.0", "http": "0.0.1-security", - "koffi": "2.7.2", "net": "1.0.2", "serialport": "12.0.0", "ws": "8.18.1" @@ -1181,12 +1180,6 @@ "node": ">=10" } }, - "node_modules/koffi": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.7.2.tgz", - "integrity": "sha512-AWcsEKETQuELxK0Wq/aXDkDiNFFY41TxZQSrKm2Nd6HO/KTHeohPOOIlh2OfQnBXJbRjx5etpWt8cbqMUZo2sg==", - "hasInstallScript": true - }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", diff --git a/package.json b/package.json index 989e5ef..bf120fc 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "express-session": "1.18.2", "ffmpeg-static": "5.2.0", "http": "0.0.1-security", - "koffi": "2.7.2", "net": "1.0.2", "serialport": "12.0.0", "ws": "8.18.1" diff --git a/server/chat.js b/server/chat.js index 9597abb..b160e7f 100644 --- a/server/chat.js +++ b/server/chat.js @@ -23,10 +23,7 @@ function createChatServer(storage) { storage.chatHistory.forEach((message) => { const historyMessage = { ...message, history: true }; - if (!request.session?.isAdminAuthenticated) { - delete historyMessage.ip; - } - + if (!request.session?.isAdminAuthenticated) delete historyMessage.ip; ws.send(JSON.stringify(historyMessage)); }); diff --git a/server/console.js b/server/console.js index cec1315..4c4397f 100644 --- a/server/console.js +++ b/server/console.js @@ -39,9 +39,7 @@ const logMessage = (type, messages, verbose = false) => { console.log(logMessage); } - if(type !== 'FFMPEG') { - appendLogToBuffer(logMessage); - } + if(type !== 'FFMPEG') appendLogToBuffer(logMessage); }; const logDebug = (...messages) => logMessage('DEBUG', messages, verboseMode); diff --git a/server/datahandler.js b/server/datahandler.js index 8a2f7bb..a4d85b7 100644 --- a/server/datahandler.js +++ b/server/datahandler.js @@ -1,213 +1,8 @@ /* Libraries / Imports */ -const fs = require('fs'); -const https = require('https'); -const koffi = require('koffi'); -const path = require('path'); -const os = require('os'); -const platform = os.platform(); -const cpuArchitecture = os.arch(); -const { configName, serverConfig, configUpdate, configSave } = require('./server_config'); -let unicode_type; -let shared_Library; +const { RDSDecoder } = require("./rds.js"); +const { serverConfig } = require('./server_config'); -if (platform === 'win32') { - unicode_type = 'int16_t'; - arch_type = (cpuArchitecture === 'x64' ? 'mingw64' : 'mingw32'); - shared_Library=path.join(__dirname, "libraries", arch_type, "librdsparser.dll"); -} else if (platform === 'linux') { - unicode_type = 'int32_t'; - arch_type = (cpuArchitecture === 'x64' ? 'x86_64' : - (cpuArchitecture === 'ia32' ? 'x86' : - (cpuArchitecture === 'arm64' ? 'aarch64' : cpuArchitecture))); - shared_Library=path.join(__dirname, "libraries", arch_type, "librdsparser.so"); -} else if (platform === 'darwin') { - unicode_type = 'int32_t'; - shared_Library=path.join(__dirname, "libraries", "macos", "librdsparser.dylib"); -} - -const lib = koffi.load(shared_Library); const { fetchTx } = require('./tx_search.js'); - -koffi.proto('void callback_pi(void *rds, void *user_data)'); -koffi.proto('void callback_pty(void *rds, void *user_data)'); -koffi.proto('void callback_tp(void *rds, void *user_data)'); -koffi.proto('void callback_ta(void *rds, void *user_data)'); -koffi.proto('void callback_ms(void *rds, void *user_data)'); -koffi.proto('void callback_ecc(void *rds, void *user_data)'); -koffi.proto('void callback_country(void *rds, void *user_data)'); -koffi.proto('void callback_af(void *rds, uint32_t af, void *user_data)'); -koffi.proto('void callback_ps(void *rds, void *user_data)'); -koffi.proto('void callback_rt(void *rds, int flag, void *user_data)'); -koffi.proto('void callback_ptyn(void *rds, void *user_data)'); -koffi.proto('void callback_ct(void *rds, void *ct, void *user_data)'); - -const rdsparser = { - new: lib.func('void* rdsparser_new()'), - free: lib.func('void rdsparser_free(void *rds)'), - clear: lib.func('void rdsparser_clear(void *rds)'), - parse_string: lib.func('bool rdsparser_parse_string(void *rds, const char *input)'), - set_text_correction: lib.func('void rdsparser_set_text_correction(void *rds, uint8_t text, uint8_t type, uint8_t error)'), - set_text_progressive: lib.func('void rdsparser_set_text_progressive(void *rds, uint8_t string, uint8_t state)'), - get_pi: lib.func('int32_t rdsparser_get_pi(void *rds)'), - get_pty: lib.func('int8_t rdsparser_get_pty(void *rds)'), - get_tp: lib.func('int8_t rdsparser_get_tp(void *rds)'), - get_ta: lib.func('int8_t rdsparser_get_ta(void *rds)'), - get_ms: lib.func('int8_t rdsparser_get_ms(void *rds)'), - get_ecc: lib.func('int16_t rdsparser_get_ecc(void *rds)'), - get_country: lib.func('int rdsparser_get_country(void *rds)'), - get_ps: lib.func('void* rdsparser_get_ps(void *rds)'), - get_rt: lib.func('void* rdsparser_get_rt(void *rds, int flag)'), - get_ptyn: lib.func('void* rdsparser_get_ptyn(void *rds)'), - register_pi: lib.func('void rdsparser_register_pi(void *rds, void *cb)'), - register_pty: lib.func('void rdsparser_register_pty(void *rds, void *cb)'), - register_tp: lib.func('void rdsparser_register_tp(void *rds, void *cb)'), - register_ta: lib.func('void rdsparser_register_ta(void *rds, void *cb)'), - register_ms: lib.func('void rdsparser_register_ms(void *rds, void *cb)'), - register_ecc: lib.func('void rdsparser_register_ecc(void *rds, void *cb)'), - register_country: lib.func('void rdsparser_register_country(void *rds, void *cb)'), - register_af: lib.func('void rdsparser_register_af(void *rds, void *cb)'), - register_ps: lib.func('void rdsparser_register_ps(void *rds, void *cb)'), - register_rt: lib.func('void rdsparser_register_rt(void *rds, void *cb)'), - register_ptyn: lib.func('void rdsparser_register_ptyn(void *rds, void *cb)'), - register_ct: lib.func('void rdsparser_register_ct(void *rds, void *cb)'), - string_get_content: lib.func(unicode_type + '* rdsparser_string_get_content(void *string)'), - string_get_errors: lib.func('uint8_t* rdsparser_string_get_errors(void *string)'), - string_get_length: lib.func('uint8_t rdsparser_string_get_length(void *string)'), - ct_get_year: lib.func('uint16_t rdsparser_ct_get_year(void *ct)'), - ct_get_month: lib.func('uint8_t rdsparser_ct_get_month(void *ct)'), - ct_get_day: lib.func('uint8_t rdsparser_ct_get_day(void *ct)'), - ct_get_hour: lib.func('uint8_t rdsparser_ct_get_hour(void *ct)'), - ct_get_minute: lib.func('uint8_t rdsparser_ct_get_minute(void *ct)'), - ct_get_offset: lib.func('int8_t rdsparser_ct_get_offset(void *ct)'), - pty_lookup_short: lib.func('const char* rdsparser_pty_lookup_short(int8_t pty, bool rbds)'), - pty_lookup_long: lib.func('const char* rdsparser_pty_lookup_long(int8_t pty, bool rbds)'), - country_lookup_name: lib.func('const char* rdsparser_country_lookup_name(int country)'), - country_lookup_iso: lib.func('const char* rdsparser_country_lookup_iso(int country)') -} - -const callbacks = { - pi: koffi.register(rds => ( - value = rdsparser.get_pi(rds) - //console.log('PI: ' + value.toString(16).toUpperCase()) - ), 'callback_pi*'), - - pty: koffi.register(rds => ( - value = rdsparser.get_pty(rds), - dataToSend.pty = value - ), 'callback_pty*'), - - tp: koffi.register(rds => ( - value = rdsparser.get_tp(rds), - dataToSend.tp = value - ), 'callback_tp*'), - - ta: koffi.register(rds => ( - value = rdsparser.get_ta(rds), - dataToSend.ta = value - ), 'callback_ta*'), - - ms: koffi.register(rds => ( - value = rdsparser.get_ms(rds), - dataToSend.ms = value - ), 'callback_ms*'), - - af: koffi.register((rds, value) => ( - dataToSend.af.push(value) - ), 'callback_af*'), - - ecc: koffi.register(rds => ( - value = rdsparser.get_ecc(rds), - dataToSend.ecc = value - ), 'callback_ecc*'), - - country: koffi.register(rds => ( - value = rdsparser.get_country(rds), - display = rdsparser.country_lookup_name(value), - iso = rdsparser.country_lookup_iso(value), - dataToSend.country_name = display, - dataToSend.country_iso = iso - ), 'callback_country*'), - - ps: koffi.register(rds => ( - ps = rdsparser.get_ps(rds), - dataToSend.ps = decode_unicode(ps), - dataToSend.ps_errors = decode_errors(ps) - ), 'callback_ps*'), - - rt: koffi.register((rds, flag) => { - const rt = rdsparser.get_rt(rds, flag); - - if (flag === 0) { - dataToSend.rt0 = decode_unicode(rt); - dataToSend.rt0_errors = decode_errors(rt); - } - - if (flag === 1) { - dataToSend.rt1 = decode_unicode(rt); - dataToSend.rt1_errors = decode_errors(rt); - } - dataToSend.rt_flag = flag; - }, 'callback_rt*'), - - ptyn: koffi.register((rds, flag) => ( - value = decode_unicode(rdsparser.get_ptyn(rds)) - /*console.log('PTYN: ' + value)*/ - ), 'callback_ptyn*'), - - ct: koffi.register((rds, ct) => ( - year = rdsparser.ct_get_year(ct), - month = String(rdsparser.ct_get_month(ct)).padStart(2, '0'), - day = String(rdsparser.ct_get_day(ct)).padStart(2, '0'), - hour = String(rdsparser.ct_get_hour(ct)).padStart(2, '0'), - minute = String(rdsparser.ct_get_minute(ct)).padStart(2, '0'), - offset = rdsparser.ct_get_offset(ct), - tz_sign = (offset >= 0 ? '+' : '-'), - tz_hour = String(Math.abs(Math.floor(offset / 60))).padStart(2, '0'), - tz_minute = String(Math.abs(offset % 60)).padStart(2, '0') - //console.log('CT: ' + year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ' (' + tz_sign + tz_hour + ':' + tz_minute + ')') - ), 'callback_ct*') -}; - -let rds = rdsparser.new() -rdsparser.set_text_correction(rds, 0, 0, 2); -rdsparser.set_text_correction(rds, 0, 1, 2); -rdsparser.set_text_correction(rds, 1, 0, 2); -rdsparser.set_text_correction(rds, 1, 1, 2); -rdsparser.set_text_progressive(rds, 0, 1) -rdsparser.set_text_progressive(rds, 1, 1) -rdsparser.register_pi(rds, callbacks.pi); -rdsparser.register_pty(rds, callbacks.pty); -rdsparser.register_tp(rds, callbacks.tp); -rdsparser.register_ta(rds, callbacks.ta); -rdsparser.register_ms(rds, callbacks.ms); -rdsparser.register_ecc(rds, callbacks.ecc); -rdsparser.register_country(rds, callbacks.country); -rdsparser.register_af(rds, callbacks.af); -rdsparser.register_ps(rds, callbacks.ps); -rdsparser.register_rt(rds, callbacks.rt); -rdsparser.register_ptyn(rds, callbacks.ptyn); -rdsparser.register_ct(rds, callbacks.ct); - -const decode_unicode = function(string) { - let length = rdsparser.string_get_length(string); - if (length) { - let content = rdsparser.string_get_content(string); - let array = koffi.decode(content, unicode_type + ' [' + length + ']'); - return String.fromCodePoint.apply(String, array); - } - return ''; -}; - -const decode_errors = function(string) { - let length = rdsparser.string_get_length(string); - if (length) { - let errors = rdsparser.string_get_errors(string); - let array = koffi.decode(errors, 'uint8_t [' + length + ']'); - return Uint8Array.from(array).toString(); - } - return ''; -}; - const updateInterval = 75; // Initialize the data object @@ -251,6 +46,8 @@ var dataToSend = { users: 0, }; +const rdsdec = new RDSDecoder(dataToSend); + const filterMappings = { 'G11': { eq: 1, ims: 1 }, 'G01': { eq: 0, ims: 1 }, @@ -264,8 +61,6 @@ var lastUpdateTime = Date.now(); const initialData = { ...dataToSend }; const resetToDefault = dataToSend => Object.assign(dataToSend, initialData); -// Serialport reconnect variables -const ServerStartTime = process.hrtime(); var serialportUpdateTime = process.hrtime(); let checkSerialport = false; let rdsTimeoutTimer = null; @@ -283,7 +78,7 @@ function rdsReceived() { function rdsReset() { resetToDefault(dataToSend); dataToSend.af.length = 0; - rdsparser.clear(rds); + rdsdec.clear(); if (rdsTimeoutTimer) { clearTimeout(rdsTimeoutTimer); rdsTimeoutTimer = null; @@ -307,17 +102,13 @@ function handleData(wss, receivedData, rdsWss) { rdsReceived(); modifiedData = receivedLine.slice(1); legacyRdsPiBuffer = modifiedData; - if (dataToSend.pi.length >= modifiedData.length || dataToSend.pi == '?') { - dataToSend.pi = modifiedData; - } + if (dataToSend.pi.length >= modifiedData.length || dataToSend.pi == '?') dataToSend.pi = modifiedData; break; case receivedLine.startsWith('T'): // Frequency modifiedData = receivedLine.substring(1).split(",")[0]; rdsReset(); - if((modifiedData / 1000).toFixed(3) == dataToSend.freq) { - return; // Prevent tune spamming using scrollwheel - } + if((modifiedData / 1000).toFixed(3) == dataToSend.freq) return; // Prevent tune spamming using scrollwheel parsedValue = parseFloat(modifiedData); @@ -360,8 +151,8 @@ function handleData(wss, receivedData, rdsWss) { processSignal(receivedLine, true, true); break; case receivedLine.startsWith('SM'): - processSignal(receivedLine, false, true); - break; + processSignal(receivedLine, false, true); + break; case receivedLine.startsWith('R'): // RDS HEX rdsReceived(); modifiedData = receivedLine.slice(1); @@ -372,8 +163,7 @@ function handleData(wss, receivedData, rdsWss) { var errorsNew = 0; var pi; - if (legacyRdsPiBuffer !== null && - legacyRdsPiBuffer.length >= 4) { + if(legacyRdsPiBuffer !== null && legacyRdsPiBuffer.length >= 4) { pi = legacyRdsPiBuffer.slice(0, 4); // PI message does not carry explicit information about // error correction, but this is a good substitute. @@ -404,7 +194,7 @@ function handleData(wss, receivedData, rdsWss) { client.send(finalBuffer); }); - rdsparser.parse_string(rds, modifiedData); + rdsdec.decodeGroup(parseInt(modifiedData.slice(0, 4), 16), parseInt(modifiedData.slice(4, 8), 16), parseInt(modifiedData.slice(8, 12), 16), parseInt(modifiedData.slice(12, 16), 16)); legacyRdsPiBuffer = null; break; } @@ -434,15 +224,15 @@ function handleData(wss, receivedData, rdsWss) { console.log("Error fetching Tx info:", error); }); - // Send the updated data to the client - const dataToSendJSON = JSON.stringify(dataToSend); - if (currentTime - lastUpdateTime >= updateInterval) { - wss.clients.forEach((client) => { - client.send(dataToSendJSON); - }); - lastUpdateTime = Date.now(); - serialportUpdateTime = process.hrtime(); - } + // Send the updated data to the client + const dataToSendJSON = JSON.stringify(dataToSend); + if (currentTime - lastUpdateTime >= updateInterval) { + wss.clients.forEach((client) => { + client.send(dataToSendJSON); + }); + lastUpdateTime = Date.now(); + serialportUpdateTime = process.hrtime(); + } } // Serialport retry code when port is open but communication is lost (additional code in index.js) @@ -469,10 +259,7 @@ async function checkSerialPortStatus() { while (!checkSerialport) { const ServerElapsedSeconds = process.hrtime(ServerStartTime)[0]; - if (ServerElapsedSeconds > 10) { - checkSerialport = true; - } - + if (ServerElapsedSeconds > 10) checkSerialport = true; await new Promise(resolve => setTimeout(resolve, 100)); } } @@ -507,11 +294,8 @@ function processSignal(receivedData, st, stForced) { // Convert highestSignal to a number for comparison var highestSignal = parseFloat(dataToSend.sigTop); - if (signal > highestSignal) { - dataToSend.sigTop = signal.toString(); // Convert back to string for consistency - } -} - + if (signal > highestSignal) dataToSend.sigTop = signal.toString(); // Convert back to string for consistency + } } module.exports = { diff --git a/server/endpoints.js b/server/endpoints.js index ffdf365..d6ecfb6 100644 --- a/server/endpoints.js +++ b/server/endpoints.js @@ -463,6 +463,7 @@ router.get('/tunnelservers', async (req, res) => { { value: "eu", host: "eu.fmtuner.org", label: "Europe" }, { value: "us", host: "us.fmtuner.org", label: "Americas" }, { value: "sg", host: "sg.fmtuner.org", label: "Asia & Oceania" }, + { value: "pldx", host: "pldx.fmtuner.org", label: "Poland (k201)" }, ]; const results = await Promise.all( diff --git a/server/fmdx_list.js b/server/fmdx_list.js index ffc21d0..70da4cf 100644 --- a/server/fmdx_list.js +++ b/server/fmdx_list.js @@ -1,8 +1,7 @@ /* Libraries / Imports */ -const fs = require('fs'); const fetch = require('node-fetch'); -const { logDebug, logError, logInfo, logWarn } = require('./console'); -const { serverConfig, configUpdate, configSave } = require('./server_config'); +const { logDebug, logInfo, logWarn } = require('./console'); +const { serverConfig, configSave } = require('./server_config'); var pjson = require('../package.json'); var os = require('os'); @@ -23,23 +22,15 @@ function send(request) { fetch(url, options) .then(response => response.json()) .then(data => { - if (data.success && data.token) - { + if (data.success && data.token) { if (!serverConfig.identification.token) { logInfo("Registered to FM-DX Server Map successfully."); serverConfig.identification.token = data.token; configSave(); } - else - { - logDebug("FM-DX Server Map update successful."); - } - } - else - { - logWarn("Failed to update FM-DX Server Map: " + (data.error ? data.error : 'unknown error')); - } + else logDebug("FM-DX Server Map update successful."); + } else logWarn("Failed to update FM-DX Server Map: " + (data.error ? data.error : 'unknown error')); }) .catch(error => { logWarn("Failed to update FM-DX Server Map: " + error); @@ -47,10 +38,7 @@ function send(request) { } function sendKeepalive() { - if (!serverConfig.identification.token) - { - return; - } + if (!serverConfig.identification.token) return; const request = { token: serverConfig.identification.token, @@ -64,9 +52,7 @@ function sendUpdate() { let currentOs = os.type() + ' ' + os.release(); let bwLimit = ''; - if (serverConfig.webserver.tuningLimit === true) { - bwLimit = serverConfig.webserver.tuningLowerLimit + ' - ' + serverConfig.webserver.tuningUpperLimit + ' MHz'; - } + if (serverConfig.webserver.tuningLimit === true) bwLimit = serverConfig.webserver.tuningLowerLimit + ' - ' + serverConfig.webserver.tuningUpperLimit + ' MHz'; const request = { status: ((serverConfig.lockToAdmin == 'true' || serverConfig.publicTuner == 'false') ? 2 : 1), @@ -82,37 +68,20 @@ function sendUpdate() { version: pjson.version }; - if (serverConfig.identification.token) - { - request.token = serverConfig.identification.token; - } + if (serverConfig.identification.token) request.token = serverConfig.identification.token; - if (serverConfig.identification.proxyIp.length) - { - request.url = serverConfig.identification.proxyIp; - } - else - { - request.port = serverConfig.webserver.webserverPort; - } + if (serverConfig.identification.proxyIp.length) request.url = serverConfig.identification.proxyIp; + else request.port = serverConfig.webserver.webserverPort; send(request); } function update() { - if (timeoutID !== null) { - clearTimeout(timeoutID); - } - - if (!serverConfig.identification.broadcastTuner) - { - return; - } + if (timeoutID !== null) clearTimeout(timeoutID); + if (!serverConfig.identification.broadcastTuner) return; sendUpdate(); timeoutID = setInterval(sendKeepalive, 5 * 60 * 1000); } -module.exports = { - update -}; +module.exports.update = update; diff --git a/server/helpers.js b/server/helpers.js index ce3758c..240c81d 100644 --- a/server/helpers.js +++ b/server/helpers.js @@ -137,9 +137,7 @@ function processConnection(clientIp, locationInfo, currentUsers, ws, callback) { const normalizedClientIp = clientIp?.replace(/^::ffff:/, ''); fetchBannedAS((error, bannedAS) => { - if (error) { - console.error("Error fetching banned AS list:", error); - } + if (error) console.error("Error fetching banned AS list:", error); if (bannedAS.some((as) => locationInfo.as?.includes(as))) { const now = Date.now(); @@ -165,9 +163,7 @@ function processConnection(clientIp, locationInfo, currentUsers, ws, callback) { instance: ws, }); - consoleCmd.logInfo( - `Web client \x1b[32mconnected\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${userLocation}` - ); + consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${userLocation}`); callback("User allowed"); }); @@ -200,13 +196,9 @@ function resolveDataBuffer(data, wss, rdsWss) { incompleteDataBuffer = receivedData.slice(position + 1); receivedData = receivedData.slice(0, position + 1); } - } else { - incompleteDataBuffer = ''; - } + } else incompleteDataBuffer = ''; - if (receivedData.length) { - dataHandler.handleData(wss, receivedData, rdsWss); - }; + if (receivedData.length) dataHandler.handleData(wss, receivedData, rdsWss); } function kickClient(ipAddress) { @@ -221,9 +213,7 @@ function kickClient(ipAddress) { targetClient.instance.close(); consoleCmd.logInfo(`Web client kicked (${ipAddress})`); }, 500); - } else { - consoleCmd.logInfo(`Kicking client ${ipAddress} failed. No suitable client found.`); - } + } else consoleCmd.logInfo(`Kicking client ${ipAddress} failed. No suitable client found.`); } function checkIPv6Support(callback) { @@ -232,11 +222,8 @@ function checkIPv6Support(callback) { server.listen(0, '::1', () => { server.close(() => callback(true)); }).on('error', (error) => { - if (error.code === 'EADDRNOTAVAIL') { - callback(false); - } else { - callback(false); - } + if (error.code === 'EADDRNOTAVAIL') callback(false); + else callback(false); }); } @@ -272,9 +259,7 @@ function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userC 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] = []; - } + if (!userCommandHistory[clientIp]) userCommandHistory[clientIp] = []; // Record the current timestamp for the user userCommandHistory[clientIp].push(now); diff --git a/server/index.js b/server/index.js index 9a998d4..11b1ed8 100644 --- a/server/index.js +++ b/server/index.js @@ -37,7 +37,7 @@ function findServerFiles(plugins) { 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); @@ -79,12 +79,13 @@ const terminalWidth = readline.createInterface({ }).output.columns; +// Couldn't get figlet.js or something like that? console.log(`\x1b[32m - _____ __ __ ______ __ __ __ _ -| ___| \\/ | | _ \\ \\/ / \\ \\ / /__| |__ ___ ___ _ ____ _____ _ __ + _____ __ __ ______ __ __ __ _ +| ___| \\/ | | _ \\ \\/ / \\ \\ / /__| |__ ___ ___ _ ____ _____ _ __ | |_ | |\\/| |_____| | | \\ / \\ \\ /\\ / / _ \\ '_ \\/ __|/ _ \\ '__\\ \\ / / _ \\ '__| -| _| | | | |_____| |_| / \\ \\ V V / __/ |_) \\__ \\ __/ | \\ V / __/ | -|_| |_| |_| |____/_/\\_\\ \\_/\\_/ \\___|_.__/|___/\\___|_| \\_/ \\___|_| +| _| | | | |_____| |_| / \\ \\ V V / __/ |_) \\__ \\ __/ | \\ V / __/ | +|_| |_| |_| |____/_/\\_\\ \\_/\\_/ \\___|_.__/|___/\\___|_| \\_/ \\___|_| `); console.log('\x1b[32m\x1b[2mby Noobish @ \x1b[4mFMDX.org\x1b[0m'); console.log("v" + pjson.version) @@ -124,21 +125,17 @@ setInterval(() => { logWarn('Communication lost from ' + serverConfig.xdrd.comPort + ', force closing serialport.'); setTimeout(() => { serialport.close((err) => { - if (err) { - logError('Error closing serialport: ', err.message); - } + if (err) logError('Error closing serialport: ', err.message); }); }, 1000); - } else { - logWarn('Communication lost from ' + serverConfig.xdrd.comPort + '.'); - } + } else logWarn('Communication lost from ' + serverConfig.xdrd.comPort + '.'); } }, 2000); // Serial Connection function connectToSerial() { -if (serverConfig.xdrd.wirelessConnection === false) { - + if (serverConfig.xdrd.wirelessConnection === true) return; + // Configure the SerialPort with DTR and RTS options serialport = new SerialPort({ path: serverConfig.xdrd.comPort, @@ -157,13 +154,13 @@ if (serverConfig.xdrd.wirelessConnection === false) { }, 5000); return; } - + logInfo('Using COM device: ' + serverConfig.xdrd.comPort); dataHandler.state.isSerialportAlive = true; setTimeout(() => { serialport.write('x\n'); }, 3000); - + setTimeout(() => { serialport.write('Q0\n'); serialport.write('M0\n'); @@ -175,9 +172,7 @@ if (serverConfig.xdrd.wirelessConnection === false) { dataHandler.dataToSend.freq = Number(serverConfig.defaultFreq).toFixed(3); } else if (dataHandler.state.lastFrequencyAlive && dataHandler.state.isSerialportRetrying) { // Serialport retry code when port is open but communication is lost serialport.write('T' + (dataHandler.state.lastFrequencyAlive * 1000) + '\n'); - } else { - serialport.write('T87500\n'); - } + } else serialport.write('T87500\n'); dataHandler.state.isSerialportRetrying = false; serialport.write('A0\n'); @@ -185,24 +180,17 @@ if (serverConfig.xdrd.wirelessConnection === false) { serialport.write('W0\n'); serverConfig.webserver.rdsMode ? serialport.write('D1\n') : serialport.write('D0\n'); // cEQ and iMS combinations - if (serverConfig.ceqStartup === "0" && serverConfig.imsStartup === "0") { - serialport.write("G00\n"); // Both Disabled - } else if (serverConfig.ceqStartup === "1" && serverConfig.imsStartup === "0") { - serialport.write(`G10\n`); - } else if (serverConfig.ceqStartup === "0" && serverConfig.imsStartup === "1") { - serialport.write(`G01\n`); - } else if (serverConfig.ceqStartup === "1" && serverConfig.imsStartup === "1") { - serialport.write("G11\n"); // Both Enabled - } + if (serverConfig.ceqStartup === "0" && serverConfig.imsStartup === "0") serialport.write("G00\n"); // Both Disabled + else if (serverConfig.ceqStartup === "1" && serverConfig.imsStartup === "0") serialport.write(`G10\n`); + else if (serverConfig.ceqStartup === "0" && serverConfig.imsStartup === "1") serialport.write(`G01\n`); + else if (serverConfig.ceqStartup === "1" && serverConfig.imsStartup === "1") serialport.write("G11\n"); // Both Enabled // Handle stereo mode - if (serverConfig.stereoStartup === "1") { - serialport.write("B1\n"); // Mono - } - serverConfig.audio.startupVolume - ? serialport.write('Y' + (serverConfig.audio.startupVolume * 100).toFixed(0) + '\n') + if (serverConfig.stereoStartup === "1") serialport.write("B1\n"); // Mono + serverConfig.audio.startupVolume + ? serialport.write('Y' + (serverConfig.audio.startupVolume * 100).toFixed(0) + '\n') : serialport.write('Y100\n'); }, 6000); - + serialport.on('data', (data) => { helpers.resolveDataBuffer(data, wss, rdsWss); }); @@ -222,7 +210,6 @@ if (serverConfig.xdrd.wirelessConnection === false) { }); return serialport; } -} // xdrd connection let authFlags = {}; @@ -233,7 +220,7 @@ function connectToXdrd() { if (xdrd.wirelessConnection && configExists()) { client.connect(xdrd.xdrdPort, xdrd.xdrdIp, () => { logInfo('Connection to xdrd established successfully.'); - + authFlags = { authMsg: false, firstClient: false, @@ -247,7 +234,7 @@ function connectToXdrd() { client.on('data', (data) => { const { xdrd } = serverConfig; - + helpers.resolveDataBuffer(data, wss, rdsWss); if (authFlags.authMsg == true && authFlags.messageCount > 1) { return; @@ -266,9 +253,8 @@ client.on('data', (data) => { if (line.startsWith('a')) { authFlags.authMsg = true; logWarn('Authentication with xdrd failed. Is your password set correctly?'); - } else if (line.startsWith('o1,')) { - authFlags.firstClient = true; - } else if (line.startsWith('T') && line.length <= 7) { + } else if (line.startsWith('o1,')) authFlags.firstClient = true; + else if (line.startsWith('T') && line.length <= 7) { const freq = line.slice(1) / 1000; dataHandler.dataToSend.freq = freq.toFixed(3); } else if (line.startsWith('OK')) { @@ -306,9 +292,7 @@ client.on('close', () => { setTimeout(function () { connectToXdrd(); }, 2000) - } else { - logWarn('Disconnected from xdrd.'); - } + } else logWarn('Disconnected from xdrd.'); }); client.on('error', (err) => { @@ -369,9 +353,7 @@ wss.on('connection', (ws, request) => { return; } - if (clientIp && clientIp.includes(',')) { - clientIp = clientIp.split(',')[0].trim(); - } + if (clientIp && clientIp.includes(',')) clientIp = clientIp.split(',')[0].trim(); // Per-IP limit connection open if (clientIp) { @@ -380,7 +362,7 @@ wss.on('connection', (ws, request) => { clientIp === '::1' || clientIp === '::ffff:127.0.0.1' || clientIp.startsWith('192.168.') || - clientIp.startsWith('10.') || + clientIp.startsWith('10.') || clientIp.startsWith('172.16.') ); if (!isLocalIp) { @@ -419,7 +401,7 @@ wss.on('connection', (ws, request) => { if (currentUsers === 1 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection) { serverConfig.xdrd.wirelessConnection ? connectToXdrd() : serialport.write('x\n'); } - }); + }); const userCommands = {}; let lastWarn = { time: 0 }; @@ -428,36 +410,34 @@ wss.on('connection', (ws, request) => { const command = helpers.antispamProtection(message, clientIp, ws, userCommands, lastWarn, userCommandHistory, '18', 'text'); if (!clientIp.includes("127.0.0.1")) { - if (((command.startsWith('X') || command.startsWith('Y')) && !request.session.isAdminAuthenticated) || + if (((command.startsWith('X') || command.startsWith('Y')) && !request.session.isAdminAuthenticated) || ((command.startsWith('F') || command.startsWith('W')) && serverConfig.bwSwitch === false)) { logWarn(`User \x1b[90m${clientIp}\x1b[0m attempted to send a potentially dangerous command: ${command.slice(0, 64)}.`); return; } } - if (command.includes("\'")) { - return; - } + if (command.includes("\'")) return; const { isAdminAuthenticated, isTuneAuthenticated } = request.session || {}; if (command.startsWith('w') && (isAdminAuthenticated || isTuneAuthenticated)) { switch (command) { - case 'wL1': - if (isAdminAuthenticated) serverConfig.lockToAdmin = true; + case 'wL1': + if (isAdminAuthenticated) serverConfig.lockToAdmin = true; break; - case 'wL0': - if (isAdminAuthenticated) serverConfig.lockToAdmin = false; + case 'wL0': + if (isAdminAuthenticated) serverConfig.lockToAdmin = false; break; - case 'wT0': - serverConfig.publicTuner = true; - if(!isAdminAuthenticated) tunerLockTracker.delete(ws); + case 'wT0': + serverConfig.publicTuner = true; + if(!isAdminAuthenticated) tunerLockTracker.delete(ws); break; - case 'wT1': + case 'wT1': serverConfig.publicTuner = false; if(!isAdminAuthenticated) tunerLockTracker.set(ws, true); break; - default: + default: break; } } @@ -465,15 +445,11 @@ wss.on('connection', (ws, request) => { if (command.startsWith('T')) { const tuneFreq = Number(command.slice(1)) / 1000; const { tuningLimit, tuningLowerLimit, tuningUpperLimit } = serverConfig.webserver; - - if (tuningLimit && (tuneFreq < tuningLowerLimit || tuneFreq > tuningUpperLimit) || isNaN(tuneFreq)) { - return; - } + + if (tuningLimit && (tuneFreq < tuningLowerLimit || tuneFreq > tuningUpperLimit) || isNaN(tuneFreq)) return; } - if ((serverConfig.publicTuner && !serverConfig.lockToAdmin) || isAdminAuthenticated || (!serverConfig.publicTuner && !serverConfig.lockToAdmin && isTuneAuthenticated)) { - output.write(`${command}\n`); - } + if ((serverConfig.publicTuner && !serverConfig.lockToAdmin) || isAdminAuthenticated || (!serverConfig.publicTuner && !serverConfig.lockToAdmin && isTuneAuthenticated)) output.write(`${command}\n`); }); ws.on('close', (code, reason) => { @@ -484,7 +460,7 @@ wss.on('connection', (ws, request) => { clientIp === '::1' || clientIp === '::ffff:127.0.0.1' || clientIp.startsWith('192.168.') || - clientIp.startsWith('10.') || + clientIp.startsWith('10.') || clientIp.startsWith('172.16.') ); if (!isLocalIp) { @@ -496,94 +472,69 @@ 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() !== '')) { currentUsers--; } - dataHandler.showOnlineUsers(currentUsers); + dataHandler.showOnlineUsers(currentUsers); - const index = storage.connectedUsers.findIndex(user => user.ip === clientIp); - if (index !== -1) { - storage.connectedUsers.splice(index, 1); - } + const index = storage.connectedUsers.findIndex(user => user.ip === clientIp); + if (index !== -1) storage.connectedUsers.splice(index, 1); - if (currentUsers === 0) { - storage.connectedUsers = []; + if (currentUsers === 0) { + storage.connectedUsers = []; - if (serverConfig.bwAutoNoUsers === "1") { - output.write("W0\n"); // Auto BW 'Enabled' - } + if (serverConfig.bwAutoNoUsers === "1") output.write("W0\n"); // Auto BW 'Enabled' - // cEQ and iMS combinations - if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "1") { - output.write("G00\n"); // Both Disabled - } else if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "0") { - output.write(`G0${dataHandler.dataToSend.ims}\n`); - } else if (serverConfig.ceqNoUsers === "0" && serverConfig.imsNoUsers === "1") { - output.write(`G${dataHandler.dataToSend.eq}0\n`); - } else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "0") { - output.write(`G1${dataHandler.dataToSend.ims}\n`); - } else if (serverConfig.ceqNoUsers === "0" && serverConfig.imsNoUsers === "2") { - output.write(`G${dataHandler.dataToSend.eq}1\n`); - } else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "1") { - output.write("G10\n"); // Only cEQ enabled - } else if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "2") { - output.write("G01\n"); // Only iMS enabled - } else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "2") { - output.write("G11\n"); // Both Enabled - } + // cEQ and iMS combinations + if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "1") output.write("G00\n"); // Both Disabled + else if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "0") output.write(`G0${dataHandler.dataToSend.ims}\n`); + else if (serverConfig.ceqNoUsers === "0" && serverConfig.imsNoUsers === "1") output.write(`G${dataHandler.dataToSend.eq}0\n`); + else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "0") output.write(`G1${dataHandler.dataToSend.ims}\n`); + else if (serverConfig.ceqNoUsers === "0" && serverConfig.imsNoUsers === "2") output.write(`G${dataHandler.dataToSend.eq}1\n`); + else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "1") output.write("G10\n"); // Only cEQ enabled + else if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "2") output.write("G01\n"); // Only iMS enabled + else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "2") output.write("G11\n"); // Both Enabled - // Handle stereo mode - if (serverConfig.stereoNoUsers === "1") { - output.write("B0\n"); - } else if (serverConfig.stereoNoUsers === "2") { - output.write("B1\n"); - } + // Handle stereo mode + if (serverConfig.stereoNoUsers === "1") output.write("B0\n"); + else if (serverConfig.stereoNoUsers === "2") output.write("B1\n"); - // Handle Antenna selection - if (timeoutAntenna) clearTimeout(timeoutAntenna); - timeoutAntenna = setTimeout(() => { - if (serverConfig.antennaNoUsers === "1") { - output.write("Z0\n"); - } else if (serverConfig.antennaNoUsers === "2") { - output.write("Z1\n"); - } else if (serverConfig.antennaNoUsers === "3") { - output.write("Z2\n"); - } else if (serverConfig.antennaNoUsers === "4") { - output.write("Z3\n"); - } - }, serverConfig.antennaNoUsersDelay ? 15000 : 0); - } + // Handle Antenna selection + if (timeoutAntenna) clearTimeout(timeoutAntenna); + timeoutAntenna = setTimeout(() => { + if (serverConfig.antennaNoUsers === "1") output.write("Z0\n"); + else if (serverConfig.antennaNoUsers === "2") output.write("Z1\n"); + else if (serverConfig.antennaNoUsers === "3") output.write("Z2\n"); + else if (serverConfig.antennaNoUsers === "4") output.write("Z3\n"); + }, serverConfig.antennaNoUsersDelay ? 15000 : 0); + } - if (tunerLockTracker.has(ws)) { - logInfo(`User who locked the tuner left. Unlocking the tuner.`); - output.write('wT0\n') - tunerLockTracker.delete(ws); - serverConfig.publicTuner = true; - } + if (tunerLockTracker.has(ws)) { + logInfo(`User who locked the tuner left. Unlocking the tuner.`); + output.write('wT0\n') + tunerLockTracker.delete(ws); + serverConfig.publicTuner = true; + } - if (currentUsers === 0 && serverConfig.enableDefaultFreq === true && - serverConfig.autoShutdown !== true && serverConfig.xdrd.wirelessConnection === true) { - setTimeout(function() { - if (currentUsers === 0) { - output.write('T' + Math.round(serverConfig.defaultFreq * 1000) + '\n'); - dataHandler.resetToDefault(dataHandler.dataToSend); - dataHandler.dataToSend.freq = Number(serverConfig.defaultFreq).toFixed(3); - dataHandler.initialData.freq = Number(serverConfig.defaultFreq).toFixed(3); - } - }, 10000); - } + if (currentUsers === 0 && serverConfig.enableDefaultFreq === true && + serverConfig.autoShutdown !== true && serverConfig.xdrd.wirelessConnection === true) { + setTimeout(function() { + if (currentUsers === 0) { + output.write('T' + Math.round(serverConfig.defaultFreq * 1000) + '\n'); + dataHandler.resetToDefault(dataHandler.dataToSend); + dataHandler.dataToSend.freq = Number(serverConfig.defaultFreq).toFixed(3); + dataHandler.initialData.freq = Number(serverConfig.defaultFreq).toFixed(3); + } + }, 10000); + } - if (currentUsers === 0 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection === true) { - client.write('X\n'); - } + if (currentUsers === 0 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection === true) client.write('X\n'); - if (code !== 1008) { - logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]`); - } + if (code !== 1008) logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]`); }); ws.on('error', console.error); }); // Additional web socket for using plugins -pluginsWss.on('connection', (ws, request) => { +pluginsWss.on('connection', (ws, request) => { const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress; const userCommandHistory = {}; if (serverConfig.webserver.banlist?.includes(clientIp)) { @@ -600,7 +551,7 @@ pluginsWss.on('connection', (ws, request) => { let messageData; - try { + try { // JS Requires the try statement to have braces, unlike the if statement. This extends the huge list of proofs that this is a fucking toy language messageData = JSON.parse(message); // Attempt to parse the JSON } catch (error) { // console.error("Failed to parse message:", error); // Log the error @@ -611,9 +562,7 @@ pluginsWss.on('connection', (ws, request) => { // Broadcast the message to all other clients pluginsWss.clients.forEach(client => { - if (client.readyState === WebSocket.OPEN) { - client.send(modifiedMessage); // Send the message to all clients - } + if (client.readyState === WebSocket.OPEN) client.send(modifiedMessage); // Send the message to all clients }); }); @@ -622,7 +571,7 @@ pluginsWss.on('connection', (ws, request) => { }); }); -// Websocket register for /text, /audio and /chat paths +// Websocket register for /text, /audio and /chat paths httpServer.on('upgrade', (request, socket, head) => { if (request.url === '/text') { sessionMiddleware(request, {}, () => { @@ -671,9 +620,7 @@ httpServer.on('upgrade', (request, socket, head) => { pluginsWss.emit('connection', ws, request); }); }); - } else { - socket.destroy(); - } + } else socket.destroy(); }); app.use(express.static(path.join(__dirname, '../web'))); // Serve the entire web folder to the user @@ -691,18 +638,13 @@ helpers.checkIPv6Support((isIPv6Supported) => { const startServer = (address, isIPv6) => { httpServer.listen(port, address, () => { - if (!isIPv6 && !configExists()) { - logInfo(`Open your browser and proceed to \x1b[34mhttp://${address}:${port}\x1b[0m to continue with setup.`); - } else { - logServerStart(address, isIPv6); - } + if (!isIPv6 && !configExists()) logInfo(`Open your browser and proceed to \x1b[34mhttp://${address}:${port}\x1b[0m to continue with setup.`); + else logServerStart(address, isIPv6); }); }; if (isIPv6Supported) { startServer(ipv4Address, false); // Start on IPv4 startServer(ipv6Address, true); // Start on IPv6 - } else { - startServer(ipv4Address, false); // Start only on IPv4 - } + } else startServer(ipv4Address, false); // Start only on IPv4 }); diff --git a/server/libraries/aarch64/librdsparser.so b/server/libraries/aarch64/librdsparser.so deleted file mode 100755 index 978dcdc..0000000 Binary files a/server/libraries/aarch64/librdsparser.so and /dev/null differ diff --git a/server/libraries/arm/librdsparser.so b/server/libraries/arm/librdsparser.so deleted file mode 100755 index 7f65101..0000000 Binary files a/server/libraries/arm/librdsparser.so and /dev/null differ diff --git a/server/libraries/macos/librdsparser.dylib b/server/libraries/macos/librdsparser.dylib deleted file mode 100755 index f2decd9..0000000 Binary files a/server/libraries/macos/librdsparser.dylib and /dev/null differ diff --git a/server/libraries/mingw32/librdsparser.dll b/server/libraries/mingw32/librdsparser.dll deleted file mode 100644 index 82283f1..0000000 Binary files a/server/libraries/mingw32/librdsparser.dll and /dev/null differ diff --git a/server/libraries/mingw64/librdsparser.dll b/server/libraries/mingw64/librdsparser.dll deleted file mode 100644 index 7b5da06..0000000 Binary files a/server/libraries/mingw64/librdsparser.dll and /dev/null differ diff --git a/server/libraries/x86/librdsparser.so b/server/libraries/x86/librdsparser.so deleted file mode 100755 index 4b7f90f..0000000 Binary files a/server/libraries/x86/librdsparser.so and /dev/null differ diff --git a/server/libraries/x86_64/librdsparser.so b/server/libraries/x86_64/librdsparser.so deleted file mode 100755 index 1692d60..0000000 Binary files a/server/libraries/x86_64/librdsparser.so and /dev/null differ diff --git a/server/rds.js b/server/rds.js new file mode 100644 index 0000000..a6f529f --- /dev/null +++ b/server/rds.js @@ -0,0 +1,184 @@ +const { rdsEccLookup, iso, countries } = require("./rds_country.js") + +class RDSDecoder { + constructor(data) { + this.data = data; + this.clear() + } + + clear() { + this.data.pi = '?'; + this.ps = Array(8).fill(' '); + this.ps_errors = Array(8).fill("0"); + this.rt0 = Array(64).fill(' '); + this.rt0_errors = Array(64).fill("0"); + this.rt1 = Array(64).fill(' '); + this.rt1_errors = Array(64).fill("0"); + this.data.ps = ''; + this.data.rt1 = ''; + this.data.rt0 = ''; + this.data.pty = 0; + this.data.tp = 0; + this.data.ta = 0; + this.data.ms = -1; + this.data.rt_flag = 0; + this.rt1_to_clear = false; + this.rt0_to_clear = false; + this.data.ecc = null; + this.data.country_name = "" + this.data.country_iso = "UN" + + this.af_len = 0; + this.data.af = [] + this.af_am_follows = false; + + this.last_pi_error = 0; + } + + decodeGroup(blockA, blockB, blockC, blockD, error) { + const a_error = (error & 0xC0) >> 6; + const b_error = (error & 0x30) >> 4; + const c_error = (error & 0xc) >> 2; + const d_error = error & 3; + + if(this.last_pi_error > a_error) { + this.data.pi = blockA.toString(16).toUpperCase().padStart(4, '0'); + this.last_pi_error = a_error; + } + + if(b_error != 0) return; // B chooses what group this is, if this has errors, we are screwed + + const group = (blockB >> 12) & 0xF; + const version = (blockB >> 11) & 0x1; + this.data.tp = Number((blockB >> 10) & 1); + this.data.pty = (blockB >> 5) & 0b11111; + + if (group === 0) { + this.data.ta = (blockB >> 4) & 1; + this.data.ms = (blockB >> 3) & 1; + + if(version === 0 && c_error !== 3) { + var af_high = blockC >> 8; + var af_low = blockC & 0xFF; + var BASE = 224; + var FILLER = 205; + var AM_FOLLOWS = 250; + + if(af_high >= BASE && af_high <= (BASE+25)) { + this.af_len = af_high-BASE; + if(this.af_len !== this.data.af.length) { + this.data.af = []; + this.af_am_follows = false; + + if(af_low != FILLER && af_low != AM_FOLLOWS) this.data.af.push((af_low+875)*100) + else if(af_low == AM_FOLLOWS) this.af_am_follows = true; + } + } else if(this.data.af.length != this.af_len) { + if(!(af_high == AM_FOLLOWS || this.af_am_follows)) { + var freq = (af_high+875)*100; + if(!this.data.af.includes(freq)) this.data.af.push(freq); + } + if(this.af_am_follows) this.af_am_follows = false; + if(!(af_high == AM_FOLLOWS || af_low == FILLER || af_low == AM_FOLLOWS)) { + var freq = (af_low+875)*100; + if(!this.data.af.includes(freq)) this.data.af.push(freq); + } + if(af_low == AM_FOLLOWS) this.af_am_follows = true; + } + } + + if(d_error === 3) return; // Don't risk it + + const idx = blockB & 0x3; + + 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.data.ps = this.ps.join(''); + this.data.ps_errors = this.ps_errors.join(','); + } else if (group === 1 && version === 0) { + var variant_code = (blockC >> 12) & 0x7; + switch (variant_code) { + case 0: + this.data.ecc = blockC & 0xff; + this.data.country_name = rdsEccLookup(blockA, this.data.ecc); + if(this.data.country_name.length === 0) this.data.country_iso = "UN"; + else this.data.country_iso = iso[countries.indexOf(this.data.country_name)] + break; + default: break; + } + } else if (group === 2) { + const idx = blockB & 0b1111; + this.rt_ab = Boolean((blockB >> 4) & 1); + var multiplier = (version == 0) ? 4 : 2; + if(this.rt_ab) { + if(this.rt1_to_clear) { + this.rt1 = Array(64).fill(' '); + this.rt1_errors = Array(64).fill("0"); + this.rt1_to_clear = false; + } + + if(c_error !== 3 && 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) { + 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; + } + + var i = this.rt1.indexOf("\r") + while(i != -1) { + this.rt1[i] = " "; + i = this.rt1.indexOf("\r"); + } + + this.data.rt1 = this.rt1.join(''); + this.data.rt1_errors = this.rt1_errors.join(','); + this.data.rt_flag = 1; + this.rt0_to_clear = true; + } else { + if(this.rt0_to_clear) { + this.rt0 = Array(64).fill(' '); + this.rt0_errors = Array(64).fill("0"); + this.rt0_to_clear = false; + } + + 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; + } + 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; + } + + var i = this.rt0.indexOf("\r"); + while(i != -1) { + this.rt0[i] = " "; + i = this.rt0.indexOf("\r"); + } + + this.data.rt0 = this.rt0.join(''); + this.data.rt0_errors = this.rt0_errors.join(','); + this.data.rt_flag = 0; + this.rt1_to_clear = true; + } + } else { + // console.log(group, version) + } + } +} +module.exports = RDSDecoder; \ No newline at end of file diff --git a/server/rds_country.js b/server/rds_country.js new file mode 100644 index 0000000..8ec2d02 --- /dev/null +++ b/server/rds_country.js @@ -0,0 +1,632 @@ +var countries = [ + "Albania", + "Estonia", + "Algeria", + "Ethiopia", + "Andorra", + "Angola", + "Finland", + "Armenia", + "France", + "Ascension Island", + "Gabon", + "Austria", + "Gambia", + "Azerbaijan", + "Georgia", + "Germany", + "Bahrein", + "Ghana", + "Belarus", + "Gibraltar", + "Belgium", + "Greece", + "Benin", + "Guinea", + "Bosnia Herzegovina", + "Guinea-Bissau", + "Botswana", + "Hungary", + "Bulgaria", + "Iceland", + "Burkina Faso", + "Iraq", + "Burundi", + "Ireland", + "Cabinda", + "Israel", + "Cameroon", + "Italy", + "Jordan", + "Cape Verde", + "Kazakhstan", + "Central African Republic", + "Kenya", + "Chad", + "Kosovo", + "Comoros", + "Kuwait", + "DR Congo", + "Kyrgyzstan", + "Republic of Congo", + "Latvia", + "Cote d'Ivoire", + "Lebanon", + "Croatia", + "Lesotho", + "Cyprus", + "Liberia", + "Czechia", + "Libya", + "Denmark", + "Liechtenstein", + "Djiboutia", + "Lithuania", + "Egypt", + "Luxembourg", + "Equatorial Guinea", + "Macedonia", + "Eritrea", + "Madagascar", + "Seychelles", + "Malawi", + "Sierra Leone", + "Mali", + "Slovakia", + "Malta", + "Slovenia", + "Mauritania", + "Somalia", + "Mauritius", + "South Africa", + "Moldova", + "South Sudan", + "Monaco", + "Spain", + "Mongolia", + "Sudan", + "Montenegro", + "Swaziland", + "Morocco", + "Sweden", + "Mozambique", + "Switzerland", + "Namibia", + "Syria", + "Netherlands", + "Tajikistan", + "Niger", + "Tanzania", + "Nigeria", + "Togo", + "Norway", + "Tunisia", + "Oman", + "Turkey", + "Palestine", + "Turkmenistan", + "Poland", + "Uganda", + "Portugal", + "Ukraine", + "Qatar", + "United Arab Emirates", + "Romania", + "United Kingdom", + "Russia", + "Uzbekistan", + "Rwanda", + "Vatican", + "San Marino", + "Western Sahara", + "Sao Tome and Principe", + "Yemen", + "Saudi Arabia", + "Zambia", + "Senegal", + "Zimbabwe", + "Serbia", + "Anguilla", + "Guyana", + "Antigua and Barbuda", + "Haiti", + "Argentina", + "Honduras", + "Aruba", + "Jamaica", + "Bahamas", + "Martinique", + "Barbados", + "Mexico", + "Belize", + "Montserrat", + "Brazil/Bermuda", + "Brazil/AN", + "Bolivia", + "Nicaragua", + "Brazil", + "Panama", + "Canada", + "Paraguay", + "Cayman Islands", + "Peru", + "Chile", + "USA/VI/PR", + "Colombia", + "St. Kitts", + "Costa Rica", + "St. Lucia", + "Cuba", + "St. Pierre and Miquelon", + "Dominica", + "St. Vincent", + "Dominican Republic", + "Suriname", + "El Salvador", + "Trinidad and Tobago", + "Turks and Caicos islands", + "Falkland Islands", + "Greenland", + "Uruguay", + "Grenada", + "Venezuela", + "Guadeloupe", + "Virgin Islands", + "Guatemala", + "Afghanistan", + "South Korea", + "Laos", + "Australia Capital Territory", + "Macao", + "Australia New South Wales", + "Malaysia", + "Australia Victoria", + "Maldives", + "Australia Queensland", + "Marshall Islands", + "Australia South Australia", + "Micronesia", + "Australia Western Australia", + "Myanmar", + "Australia Tasmania", + "Nauru", + "Australia Northern Territory", + "Nepal", + "Bangladesh", + "New Zealand", + "Bhutan", + "Pakistan", + "Brunei Darussalam", + "Papua New Guinea", + "Cambodia", + "Philippines", + "China", + "Samoa", + "Singapore", + "Solomon Islands", + "Fiji", + "Sri Lanka", + "Hong Kong", + "Taiwan", + "India", + "Thailand", + "Indonesia", + "Tonga", + "Iran", + "Vanuatu", + "Japan", + "Vietnam", + "Kiribati", + "North Korea", + "Brazil/Equator" +] + +var iso = [ + "AL", + "EE", + "DZ", + "ET", + "AD", + "AO", + "FI", + "AM", + "FR", + "SH", + "GA", + "AT", + "GM", + "AZ", + "GE", + "DE", + "BH", + "GH", + "BY", + "GI", + "BE", + "GR", + "BJ", + "GN", + "BA", + "GW", + "BW", + "HU", + "BG", + "IS", + "BF", + "IQ", + "BI", + "IE", + "--", + "IL", + "CM", + "IT", + "JO", + "CV", + "KZ", + "CF", + "KE", + "TD", + "XK", + "KM", + "KW", + "CD", + "KG", + "CG", + "LV", + "CI", + "LB", + "HR", + "LS", + "CY", + "LR", + "CZ", + "LY", + "DK", + "LI", + "DJ", + "LT", + "EG", + "LU", + "GQ", + "MK", + "ER", + "MG", + "SC", + "MW", + "SL", + "ML", + "SK", + "MT", + "SI", + "MR", + "SO", + "MU", + "ZA", + "MD", + "SS", + "MC", + "ES", + "MN", + "SD", + "ME", + "SZ", + "MA", + "SE", + "MZ", + "CH", + "NA", + "SY", + "NL", + "TJ", + "NE", + "TZ", + "NG", + "TG", + "NO", + "TN", + "OM", + "TR", + "PS", + "TM", + "PL", + "UG", + "PT", + "UA", + "QA", + "AE", + "RO", + "GB", + "RU", + "UZ", + "RW", + "VA", + "SM", + "EH", + "ST", + "YE", + "SA", + "ZM", + "SN", + "ZW", + "RS", + "AI", + "GY", + "AG", + "HT", + "AR", + "HN", + "AW", + "JM", + "BS", + "MQ", + "BB", + "MX", + "BZ", + "MS", + "--", + "--", + "BO", + "NI", + "BR", + "PA", + "CA", + "PY", + "KY", + "PE", + "CL", + "--", + "CO", + "KN", + "CR", + "LC", + "CU", + "PM", + "DM", + "VC", + "DO", + "SR", + "SN", + "TT", + "TB", + "FK", + "GL", + "UY", + "GD", + "VE", + "GP", + "VG", + "GT", + "AF", + "KR", + "LA", + "AU", + "MO", + "AU", + "MY", + "AU", + "MV", + "AU", + "MH", + "AU", + "FM", + "AU", + "MM", + "AU", + "NR", + "AU", + "NP", + "BD", + "NZ", + "BT", + "PK", + "BN", + "PG", + "KH", + "PH", + "CN", + "WS", + "SG", + "SB", + "FJ", + "LK", + "HK", + "TW", + "IN", + "TH", + "ID", + "TO", + "IR", + "VU", + "JP", + "VN", + "KI", + "KP", + "--" +] + +// RDS ECC Lookup Tables - Converted from C to JavaScript + +const rdsEccA0A6Lut = [ + // A0 + [ + "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", + "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", "USA/VI/PR", + "USA/VI/PR", null, "USA/VI/PR", "USA/VI/PR", null + ], + // A1 + [ + null, null, null, null, null, + null, null, null, null, null, + "Canada", "Canada", "Canada", "Canada", "Greenland" + ], + // A2 + [ + "Anguilla", "Antigua and Barbuda", "Brazil/Equator", "Falkland Islands", "Barbados", + "Belize", "Cayman Islands", "Costa Rica", "Cuba", "Argentina", + "Brazil", "Brazil/Bermuda", "Brazil/AN", "Guadeloupe", "Bahamas" + ], + // A3 + [ + "Bolivia", "Colombia", "Jamaica", "Martinique", null, + "Paraguay", "Nicaragua", null, "Panama", "Dominica", + "Dominican Republic", "Chile", "Grenada", "Turks and Caicos islands", "Guyana" + ], + // A4 + [ + "Guatemala", "Honduras", "Aruba", null, "Montserrat", + "Trinidad and Tobago", "Peru", "Suriname", "Uruguay", "St. Kitts", + "St. Lucia", "El Salvador", "Haiti", "Venezuela", "Virgin Islands" + ], + // A5 + [ + null, null, null, null, null, + null, null, null, null, null, + "Mexico", "St. Vincent", "Mexico", "Mexico", "Mexico" + ], + // A6 + [ + null, null, null, null, null, + null, null, null, null, null, + null, null, null, null, "St. Pierre and Miquelon" + ] +]; + +const rdsEccD0D4Lut = [ + // D0 + [ + "Cameroon", "Central African Republic", "Djiboutia", "Madagascar", "Mali", + "Angola", "Equatorial Guinea", "Gabon", "Guinea", "South Africa", + "Burkina Faso", "Republic of Congo", "Togo", "Benin", "Malawi" + ], + // D1 + [ + "Namibia", "Liberia", "Ghana", "Mauritania", "Sao Tome and Principe", + "Cape Verde", "Senegal", "Gambia", "Burundi", "Ascension Island", + "Botswana", "Comoros", "Tanzania", "Ethiopia", "Nigeria" + ], + // D2 + [ + "Sierra Leone", "Zimbabwe", "Mozambique", "Uganda", "Swaziland", + "Kenya", "Somalia", "Niger", "Chad", "Guinea-Bissau", + "DR Congo", "Cote d'Ivoire", null, "Zambia", "Eritrea" + ], + // D3 + [ + null, null, "Western Sahara", "Cabinda", "Rwanda", + "Lesotho", null, "Seychelles", null, "Mauritius", + null, "Sudan", null, null, null + ], + // D4 + [ + null, null, null, null, null, + null, null, null, null, "South Sudan", + null, null, null, null, null + ] +]; + +const rdsEccE0E5Lut = [ + // E0 + [ + "Germany", "Algeria", "Andorra", "Israel", "Italy", + "Belgium", "Russia", "Palestine", "Albania", "Austria", + "Hungary", "Malta", "Germany", null, "Egypt" + ], + // E1 + [ + "Greece", "Cyprus", "San Marino", "Switzerland", "Jordan", + "Finland", "Luxembourg", "Bulgaria", "Denmark", "Gibraltar", + "Iraq", "United Kingdom", "Libya", "Romania", "France" + ], + // E2 + [ + "Morocco", "Czechia", "Poland", "Vatican", "Slovakia", + "Syria", "Tunisia", null, "Liechtenstein", "Iceland", + "Monaco", "Lithuania", "Serbia", "Spain", "Norway" + ], + // E3 + [ + "Montenegro", "Ireland", "Turkey", null, "Tajikistan", + null, null, "Netherlands", "Latvia", "Lebanon", + "Azerbaijan", "Croatia", "Kazakhstan", "Sweden", "Belarus" + ], + // E4 + [ + "Moldova", "Estonia", "Macedonia", null, null, + "Ukraine", "Kosovo", "Portugal", "Slovenia", "Armenia", + "Uzbekistan", "Georgia", null, "Turkmenistan", "Bosnia Herzegovina" + ], + // E5 + [ + null, null, "Kyrgyzstan", null, null, + null, null, null, null, null, + null, null, null, null, null + ] +]; + +const rdsEccF0F4Lut = [ + // F0 + [ + "Australia Capital Territory", "Australia New South Wales", "Australia Victoria", "Australia Queensland", "Australia South Australia", + "Australia Western Australia", "Australia Tasmania", "Australia Northern Territory", "Saudi Arabia", "Afghanistan", + "Myanmar", "China", "North Korea", "Bahrein", "Malaysia" + ], + // F1 + [ + "Kiribati", "Bhutan", "Bangladesh", "Pakistan", "Fiji", + "Oman", "Nauru", "Iran", "New Zealand", "Solomon Islands", + "Brunei Darussalam", "Sri Lanka", "Taiwan", "South Korea", "Hong Kong" + ], + // F2 + [ + "Kuwait", "Qatar", "Cambodia", "Samoa", "India", + "Macao", "Vietnam", "Philippines", "Japan", "Singapore", + "Maldives", "Indonesia", "United Arab Emirates", "Nepal", "Vanuatu" + ], + // F3 + [ + "Laos", "Thailand", "Tonga", null, null, + null, null, "China", "Papua New Guinea", null, + "Yemen", null, null, "Micronesia", "Mongolia" + ], + // F4 + [ + null, null, null, null, null, + null, null, null, "China", null, + "Marshall Islands", null, null, null, null + ] +]; + +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) { + const eccId = ecc - range.min; + return range.lut[eccId][piId]; + } + } + + return "" +} + +module.exports = { + rdsEccLookup, + iso, + countries +}; \ No newline at end of file diff --git a/server/server_config.js b/server/server_config.js index 62466bc..203a5ee 100644 --- a/server/server_config.js +++ b/server/server_config.js @@ -94,7 +94,7 @@ let serverConfig = { enabled: false, username: "", token: "", - region: "eu", + region: "pldx", lowLatencyMode: false, subdomain: "", httpName: "", diff --git a/server/stream/checkFFmpeg.js b/server/stream/checkFFmpeg.js index 96d385e..601b3c9 100644 --- a/server/stream/checkFFmpeg.js +++ b/server/stream/checkFFmpeg.js @@ -11,11 +11,8 @@ function checkFFmpeg() { }); checkFFmpegProcess.on('exit', (code) => { - if (code === 0) { - resolve('ffmpeg'); - } else { - resolve(require('ffmpeg-static')); - } + if (code === 0) resolve('ffmpeg'); + else resolve(require('ffmpeg-static')); }); }); } diff --git a/server/stream/parser.js b/server/stream/parser.js index d0142bc..fa3d7b2 100644 --- a/server/stream/parser.js +++ b/server/stream/parser.js @@ -74,9 +74,7 @@ function parseAudioDevice(options, callback) { if (platform === 'win32' && line.search(/Alternative\sname/) > -1) { const lastDevice = deviceList[deviceList.length - 1]; const alt = line.match(alternativeName); - if (lastDevice && alt) { - lastDevice.alternativeName = alt[1]; - } + if (lastDevice && alt) lastDevice.alternativeName = alt[1]; return; } @@ -107,11 +105,8 @@ function parseAudioDevice(options, callback) { } }; - if (callbackExists) { - execute(); - } else { - return new Promise(execute); - } + if (callbackExists) execute(); + else return new Promise(execute); } module.exports = { parseAudioDevice }; \ No newline at end of file diff --git a/server/tunnel.js b/server/tunnel.js index 74f00b6..c7f0442 100644 --- a/server/tunnel.js +++ b/server/tunnel.js @@ -25,9 +25,7 @@ async function connect() { try { const res = await fetch('https://fmtuner.org/binaries/' + frpcFileName); - if (res.status === 404) { - throw new Error('404 error'); - } + if (res.status === 404) throw new Error('404 error'); const stream = fs2.createWriteStream(frpcPath); await finished(Readable.fromWeb(res.body).pipe(stream)); } catch (err) { @@ -41,7 +39,7 @@ async function connect() { } const cfg = ejs.render(frpcConfigTemplate, { cfg: serverConfig.tunnel, - host: serverConfig.tunnel.community.enabled ? serverConfig.tunnel.community.host : serverConfig.tunnel.region + ".fmtuner.org", + host: serverConfig.tunnel.community.enabled ? serverConfig.tunnel.community.host : ((serverConfig.tunnel.region == "pldx") ? "pldx.duckdns.org" : (serverConfig.tunnel.region + ".fmtuner.org")), server: { port: serverConfig.webserver.webserverPort } @@ -62,15 +60,10 @@ async function connect() { if (line.includes('connect to server error')) { const reason = line.substring(line.indexOf(': ')+2); logError('Failed to connect to tunnel, reason: ' + reason); - } else if (line.includes('invalid user or token')) { - logError('Failed to connect to tunnel, reason: invalid user or token'); - } else if (line.includes('start proxy success')) { - logInfo('Tunnel established successfully'); - } else if (line.includes('login to server success')) { - logInfo('Connection to tunnel server was successful'); - } else { - logDebug('Tunnel log:', line); - } + } else if (line.includes('invalid user or token')) logError('Failed to connect to tunnel, reason: invalid user or token'); + else if (line.includes('start proxy success')) logInfo('Tunnel established successfully'); + else if (line.includes('login to server success')) logInfo('Connection to tunnel server was successful'); + else logDebug('Tunnel log:', line); }); child.on('error', (err) => { diff --git a/web/js/setup.js b/web/js/setup.js index 231106d..16d737d 100644 --- a/web/js/setup.js +++ b/web/js/setup.js @@ -116,11 +116,8 @@ function initBanlist() { data: { ip: ipAddress, reason: reason }, success: function(response) { // Refresh the page if the request was successful - if (response.success) { - location.reload(); - } else { - console.error('Failed to add to banlist'); - } + if (response.success) location.reload(); + else console.error('Failed to add to banlist'); }, error: function() { console.error('Error occurred during the request'); diff --git a/web/setup.ejs b/web/setup.ejs index dc7a5e0..9ef2c04 100644 --- a/web/setup.ejs +++ b/web/setup.ejs @@ -699,7 +699,8 @@
When you become an FMDX.org supporter, you can host your webserver without the need of a public IP address & port forwarding.
- When you become a supporter, you can message the Founders on Discord for your login details.
You can also get an tunnel from kuba201 discord, one of the contributors of this version of the application.