1
0
mirror of https://github.com/KubaPro010/fm-dx-webserver.git synced 2026-02-26 22:13:53 +01:00

Compare commits

...

4 Commits

25 changed files with 978 additions and 512 deletions

View File

@@ -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: 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. - [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. All of these libraries are already bundled with the webserver.

7
package-lock.json generated
View File

@@ -16,7 +16,6 @@
"express-session": "1.18.2", "express-session": "1.18.2",
"ffmpeg-static": "5.2.0", "ffmpeg-static": "5.2.0",
"http": "0.0.1-security", "http": "0.0.1-security",
"koffi": "2.7.2",
"net": "1.0.2", "net": "1.0.2",
"serialport": "12.0.0", "serialport": "12.0.0",
"ws": "8.18.1" "ws": "8.18.1"
@@ -1181,12 +1180,6 @@
"node": ">=10" "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": { "node_modules/lru-cache": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",

View File

@@ -19,7 +19,6 @@
"express-session": "1.18.2", "express-session": "1.18.2",
"ffmpeg-static": "5.2.0", "ffmpeg-static": "5.2.0",
"http": "0.0.1-security", "http": "0.0.1-security",
"koffi": "2.7.2",
"net": "1.0.2", "net": "1.0.2",
"serialport": "12.0.0", "serialport": "12.0.0",
"ws": "8.18.1" "ws": "8.18.1"

View File

@@ -23,10 +23,7 @@ function createChatServer(storage) {
storage.chatHistory.forEach((message) => { storage.chatHistory.forEach((message) => {
const historyMessage = { ...message, history: true }; const historyMessage = { ...message, history: true };
if (!request.session?.isAdminAuthenticated) { if (!request.session?.isAdminAuthenticated) delete historyMessage.ip;
delete historyMessage.ip;
}
ws.send(JSON.stringify(historyMessage)); ws.send(JSON.stringify(historyMessage));
}); });

View File

@@ -39,9 +39,7 @@ const logMessage = (type, messages, verbose = false) => {
console.log(logMessage); console.log(logMessage);
} }
if(type !== 'FFMPEG') { if(type !== 'FFMPEG') appendLogToBuffer(logMessage);
appendLogToBuffer(logMessage);
}
}; };
const logDebug = (...messages) => logMessage('DEBUG', messages, verboseMode); const logDebug = (...messages) => logMessage('DEBUG', messages, verboseMode);

View File

@@ -1,213 +1,8 @@
/* Libraries / Imports */ /* Libraries / Imports */
const fs = require('fs'); const RDSDecoder = require("./rds.js");
const https = require('https'); const { serverConfig } = require('./server_config');
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;
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'); 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; const updateInterval = 75;
// Initialize the data object // Initialize the data object
@@ -251,6 +46,8 @@ var dataToSend = {
users: 0, users: 0,
}; };
const rdsdec = new RDSDecoder(dataToSend);
const filterMappings = { const filterMappings = {
'G11': { eq: 1, ims: 1 }, 'G11': { eq: 1, ims: 1 },
'G01': { eq: 0, ims: 1 }, 'G01': { eq: 0, ims: 1 },
@@ -264,8 +61,6 @@ var lastUpdateTime = Date.now();
const initialData = { ...dataToSend }; const initialData = { ...dataToSend };
const resetToDefault = dataToSend => Object.assign(dataToSend, initialData); const resetToDefault = dataToSend => Object.assign(dataToSend, initialData);
// Serialport reconnect variables
const ServerStartTime = process.hrtime();
var serialportUpdateTime = process.hrtime(); var serialportUpdateTime = process.hrtime();
let checkSerialport = false; let checkSerialport = false;
let rdsTimeoutTimer = null; let rdsTimeoutTimer = null;
@@ -283,7 +78,7 @@ function rdsReceived() {
function rdsReset() { function rdsReset() {
resetToDefault(dataToSend); resetToDefault(dataToSend);
dataToSend.af.length = 0; dataToSend.af.length = 0;
rdsparser.clear(rds); rdsdec.clear();
if (rdsTimeoutTimer) { if (rdsTimeoutTimer) {
clearTimeout(rdsTimeoutTimer); clearTimeout(rdsTimeoutTimer);
rdsTimeoutTimer = null; rdsTimeoutTimer = null;
@@ -307,17 +102,13 @@ function handleData(wss, receivedData, rdsWss) {
rdsReceived(); rdsReceived();
modifiedData = receivedLine.slice(1); modifiedData = receivedLine.slice(1);
legacyRdsPiBuffer = modifiedData; legacyRdsPiBuffer = modifiedData;
if (dataToSend.pi.length >= modifiedData.length || dataToSend.pi == '?') { if (dataToSend.pi.length >= modifiedData.length || dataToSend.pi == '?') dataToSend.pi = modifiedData;
dataToSend.pi = modifiedData;
}
break; break;
case receivedLine.startsWith('T'): // Frequency case receivedLine.startsWith('T'): // Frequency
modifiedData = receivedLine.substring(1).split(",")[0]; modifiedData = receivedLine.substring(1).split(",")[0];
rdsReset(); rdsReset();
if((modifiedData / 1000).toFixed(3) == dataToSend.freq) { if((modifiedData / 1000).toFixed(3) == dataToSend.freq) return; // Prevent tune spamming using scrollwheel
return; // Prevent tune spamming using scrollwheel
}
parsedValue = parseFloat(modifiedData); parsedValue = parseFloat(modifiedData);
@@ -372,8 +163,7 @@ function handleData(wss, receivedData, rdsWss) {
var errorsNew = 0; var errorsNew = 0;
var pi; var pi;
if (legacyRdsPiBuffer !== null && if(legacyRdsPiBuffer !== null && legacyRdsPiBuffer.length >= 4) {
legacyRdsPiBuffer.length >= 4) {
pi = legacyRdsPiBuffer.slice(0, 4); pi = legacyRdsPiBuffer.slice(0, 4);
// PI message does not carry explicit information about // PI message does not carry explicit information about
// error correction, but this is a good substitute. // error correction, but this is a good substitute.
@@ -404,7 +194,7 @@ function handleData(wss, receivedData, rdsWss) {
client.send(finalBuffer); 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; legacyRdsPiBuffer = null;
break; break;
} }
@@ -469,10 +259,7 @@ async function checkSerialPortStatus() {
while (!checkSerialport) { while (!checkSerialport) {
const ServerElapsedSeconds = process.hrtime(ServerStartTime)[0]; const ServerElapsedSeconds = process.hrtime(ServerStartTime)[0];
if (ServerElapsedSeconds > 10) { if (ServerElapsedSeconds > 10) checkSerialport = true;
checkSerialport = true;
}
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => setTimeout(resolve, 100));
} }
} }
@@ -507,13 +294,10 @@ function processSignal(receivedData, st, stForced) {
// Convert highestSignal to a number for comparison // Convert highestSignal to a number for comparison
var highestSignal = parseFloat(dataToSend.sigTop); var highestSignal = parseFloat(dataToSend.sigTop);
if (signal > highestSignal) { if (signal > highestSignal) dataToSend.sigTop = signal.toString(); // Convert back to string for consistency
dataToSend.sigTop = signal.toString(); // Convert back to string for consistency
} }
} }
}
module.exports = { module.exports = {
handleData, showOnlineUsers, dataToSend, initialData, resetToDefault, state handleData, showOnlineUsers, dataToSend, initialData, resetToDefault, state
}; };

View File

@@ -463,6 +463,7 @@ router.get('/tunnelservers', async (req, res) => {
{ value: "eu", host: "eu.fmtuner.org", label: "Europe" }, { value: "eu", host: "eu.fmtuner.org", label: "Europe" },
{ value: "us", host: "us.fmtuner.org", label: "Americas" }, { value: "us", host: "us.fmtuner.org", label: "Americas" },
{ value: "sg", host: "sg.fmtuner.org", label: "Asia & Oceania" }, { value: "sg", host: "sg.fmtuner.org", label: "Asia & Oceania" },
{ value: "pldx", host: "pldx.fmtuner.org", label: "Poland (k201)" },
]; ];
const results = await Promise.all( const results = await Promise.all(

View File

@@ -1,8 +1,7 @@
/* Libraries / Imports */ /* Libraries / Imports */
const fs = require('fs');
const fetch = require('node-fetch'); const fetch = require('node-fetch');
const { logDebug, logError, logInfo, logWarn } = require('./console'); const { logDebug, logInfo, logWarn } = require('./console');
const { serverConfig, configUpdate, configSave } = require('./server_config'); const { serverConfig, configSave } = require('./server_config');
var pjson = require('../package.json'); var pjson = require('../package.json');
var os = require('os'); var os = require('os');
@@ -23,23 +22,15 @@ function send(request) {
fetch(url, options) fetch(url, options)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success && data.token) if (data.success && data.token) {
{
if (!serverConfig.identification.token) if (!serverConfig.identification.token)
{ {
logInfo("Registered to FM-DX Server Map successfully."); logInfo("Registered to FM-DX Server Map successfully.");
serverConfig.identification.token = data.token; serverConfig.identification.token = data.token;
configSave(); configSave();
} }
else else logDebug("FM-DX Server Map update successful.");
{ } else logWarn("Failed to update FM-DX Server Map: " + (data.error ? data.error : 'unknown error'));
logDebug("FM-DX Server Map update successful.");
}
}
else
{
logWarn("Failed to update FM-DX Server Map: " + (data.error ? data.error : 'unknown error'));
}
}) })
.catch(error => { .catch(error => {
logWarn("Failed to update FM-DX Server Map: " + error); logWarn("Failed to update FM-DX Server Map: " + error);
@@ -47,10 +38,7 @@ function send(request) {
} }
function sendKeepalive() { function sendKeepalive() {
if (!serverConfig.identification.token) if (!serverConfig.identification.token) return;
{
return;
}
const request = { const request = {
token: serverConfig.identification.token, token: serverConfig.identification.token,
@@ -64,9 +52,7 @@ function sendUpdate() {
let currentOs = os.type() + ' ' + os.release(); let currentOs = os.type() + ' ' + os.release();
let bwLimit = ''; let bwLimit = '';
if (serverConfig.webserver.tuningLimit === true) { if (serverConfig.webserver.tuningLimit === true) bwLimit = serverConfig.webserver.tuningLowerLimit + ' - ' + serverConfig.webserver.tuningUpperLimit + ' MHz';
bwLimit = serverConfig.webserver.tuningLowerLimit + ' - ' + serverConfig.webserver.tuningUpperLimit + ' MHz';
}
const request = { const request = {
status: ((serverConfig.lockToAdmin == 'true' || serverConfig.publicTuner == 'false') ? 2 : 1), status: ((serverConfig.lockToAdmin == 'true' || serverConfig.publicTuner == 'false') ? 2 : 1),
@@ -82,37 +68,20 @@ function sendUpdate() {
version: pjson.version version: pjson.version
}; };
if (serverConfig.identification.token) if (serverConfig.identification.token) request.token = serverConfig.identification.token;
{
request.token = serverConfig.identification.token;
}
if (serverConfig.identification.proxyIp.length) if (serverConfig.identification.proxyIp.length) request.url = serverConfig.identification.proxyIp;
{ else request.port = serverConfig.webserver.webserverPort;
request.url = serverConfig.identification.proxyIp;
}
else
{
request.port = serverConfig.webserver.webserverPort;
}
send(request); send(request);
} }
function update() { function update() {
if (timeoutID !== null) { if (timeoutID !== null) clearTimeout(timeoutID);
clearTimeout(timeoutID); if (!serverConfig.identification.broadcastTuner) return;
}
if (!serverConfig.identification.broadcastTuner)
{
return;
}
sendUpdate(); sendUpdate();
timeoutID = setInterval(sendKeepalive, 5 * 60 * 1000); timeoutID = setInterval(sendKeepalive, 5 * 60 * 1000);
} }
module.exports = { module.exports.update = update;
update
};

View File

@@ -137,9 +137,7 @@ function processConnection(clientIp, locationInfo, currentUsers, ws, callback) {
const normalizedClientIp = clientIp?.replace(/^::ffff:/, ''); const normalizedClientIp = clientIp?.replace(/^::ffff:/, '');
fetchBannedAS((error, bannedAS) => { fetchBannedAS((error, bannedAS) => {
if (error) { if (error) console.error("Error fetching banned AS list:", error);
console.error("Error fetching banned AS list:", error);
}
if (bannedAS.some((as) => locationInfo.as?.includes(as))) { if (bannedAS.some((as) => locationInfo.as?.includes(as))) {
const now = Date.now(); const now = Date.now();
@@ -165,9 +163,7 @@ function processConnection(clientIp, locationInfo, currentUsers, ws, callback) {
instance: ws, instance: ws,
}); });
consoleCmd.logInfo( consoleCmd.logInfo(`Web client \x1b[32mconnected\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${userLocation}`);
`Web client \x1b[32mconnected\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${userLocation}`
);
callback("User allowed"); callback("User allowed");
}); });
@@ -200,13 +196,9 @@ function resolveDataBuffer(data, wss, rdsWss) {
incompleteDataBuffer = receivedData.slice(position + 1); incompleteDataBuffer = receivedData.slice(position + 1);
receivedData = receivedData.slice(0, position + 1); receivedData = receivedData.slice(0, position + 1);
} }
} else { } else incompleteDataBuffer = '';
incompleteDataBuffer = '';
}
if (receivedData.length) { if (receivedData.length) dataHandler.handleData(wss, receivedData, rdsWss);
dataHandler.handleData(wss, receivedData, rdsWss);
};
} }
function kickClient(ipAddress) { function kickClient(ipAddress) {
@@ -221,9 +213,7 @@ function kickClient(ipAddress) {
targetClient.instance.close(); targetClient.instance.close();
consoleCmd.logInfo(`Web client kicked (${ipAddress})`); consoleCmd.logInfo(`Web client kicked (${ipAddress})`);
}, 500); }, 500);
} else { } else consoleCmd.logInfo(`Kicking client ${ipAddress} failed. No suitable client found.`);
consoleCmd.logInfo(`Kicking client ${ipAddress} failed. No suitable client found.`);
}
} }
function checkIPv6Support(callback) { function checkIPv6Support(callback) {
@@ -232,11 +222,8 @@ function checkIPv6Support(callback) {
server.listen(0, '::1', () => { server.listen(0, '::1', () => {
server.close(() => callback(true)); server.close(() => callback(true));
}).on('error', (error) => { }).on('error', (error) => {
if (error.code === 'EADDRNOTAVAIL') { if (error.code === 'EADDRNOTAVAIL') callback(false);
callback(false); else 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}`); if (endpointName === 'text') consoleCmd.logDebug(`Command received from \x1b[90m${clientIp}\x1b[0m: ${command}`);
// Initialize user command history if not present // Initialize user command history if not present
if (!userCommandHistory[clientIp]) { if (!userCommandHistory[clientIp]) userCommandHistory[clientIp] = [];
userCommandHistory[clientIp] = [];
}
// Record the current timestamp for the user // Record the current timestamp for the user
userCommandHistory[clientIp].push(now); userCommandHistory[clientIp].push(now);

View File

@@ -79,6 +79,7 @@ const terminalWidth = readline.createInterface({
}).output.columns; }).output.columns;
// Couldn't get figlet.js or something like that?
console.log(`\x1b[32m console.log(`\x1b[32m
_____ __ __ ______ __ __ __ _ _____ __ __ ______ __ __ __ _
| ___| \\/ | | _ \\ \\/ / \\ \\ / /__| |__ ___ ___ _ ____ _____ _ __ | ___| \\/ | | _ \\ \\/ / \\ \\ / /__| |__ ___ ___ _ ____ _____ _ __
@@ -124,20 +125,16 @@ setInterval(() => {
logWarn('Communication lost from ' + serverConfig.xdrd.comPort + ', force closing serialport.'); logWarn('Communication lost from ' + serverConfig.xdrd.comPort + ', force closing serialport.');
setTimeout(() => { setTimeout(() => {
serialport.close((err) => { serialport.close((err) => {
if (err) { if (err) logError('Error closing serialport: ', err.message);
logError('Error closing serialport: ', err.message);
}
}); });
}, 1000); }, 1000);
} else { } else logWarn('Communication lost from ' + serverConfig.xdrd.comPort + '.');
logWarn('Communication lost from ' + serverConfig.xdrd.comPort + '.');
}
} }
}, 2000); }, 2000);
// Serial Connection // Serial Connection
function connectToSerial() { function connectToSerial() {
if (serverConfig.xdrd.wirelessConnection === false) { if (serverConfig.xdrd.wirelessConnection === true) return;
// Configure the SerialPort with DTR and RTS options // Configure the SerialPort with DTR and RTS options
serialport = new SerialPort({ serialport = new SerialPort({
@@ -175,9 +172,7 @@ if (serverConfig.xdrd.wirelessConnection === false) {
dataHandler.dataToSend.freq = Number(serverConfig.defaultFreq).toFixed(3); 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 } 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'); serialport.write('T' + (dataHandler.state.lastFrequencyAlive * 1000) + '\n');
} else { } else serialport.write('T87500\n');
serialport.write('T87500\n');
}
dataHandler.state.isSerialportRetrying = false; dataHandler.state.isSerialportRetrying = false;
serialport.write('A0\n'); serialport.write('A0\n');
@@ -185,19 +180,12 @@ if (serverConfig.xdrd.wirelessConnection === false) {
serialport.write('W0\n'); serialport.write('W0\n');
serverConfig.webserver.rdsMode ? serialport.write('D1\n') : serialport.write('D0\n'); serverConfig.webserver.rdsMode ? serialport.write('D1\n') : serialport.write('D0\n');
// cEQ and iMS combinations // cEQ and iMS combinations
if (serverConfig.ceqStartup === "0" && serverConfig.imsStartup === "0") { if (serverConfig.ceqStartup === "0" && serverConfig.imsStartup === "0") serialport.write("G00\n"); // Both Disabled
serialport.write("G00\n"); // Both Disabled else if (serverConfig.ceqStartup === "1" && serverConfig.imsStartup === "0") serialport.write(`G10\n`);
} else if (serverConfig.ceqStartup === "1" && serverConfig.imsStartup === "0") { else if (serverConfig.ceqStartup === "0" && serverConfig.imsStartup === "1") serialport.write(`G01\n`);
serialport.write(`G10\n`); else if (serverConfig.ceqStartup === "1" && serverConfig.imsStartup === "1") serialport.write("G11\n"); // Both Enabled
} 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 // Handle stereo mode
if (serverConfig.stereoStartup === "1") { if (serverConfig.stereoStartup === "1") serialport.write("B1\n"); // Mono
serialport.write("B1\n"); // Mono
}
serverConfig.audio.startupVolume serverConfig.audio.startupVolume
? serialport.write('Y' + (serverConfig.audio.startupVolume * 100).toFixed(0) + '\n') ? serialport.write('Y' + (serverConfig.audio.startupVolume * 100).toFixed(0) + '\n')
: serialport.write('Y100\n'); : serialport.write('Y100\n');
@@ -222,7 +210,6 @@ if (serverConfig.xdrd.wirelessConnection === false) {
}); });
return serialport; return serialport;
} }
}
// xdrd connection // xdrd connection
let authFlags = {}; let authFlags = {};
@@ -266,9 +253,8 @@ client.on('data', (data) => {
if (line.startsWith('a')) { if (line.startsWith('a')) {
authFlags.authMsg = true; authFlags.authMsg = true;
logWarn('Authentication with xdrd failed. Is your password set correctly?'); logWarn('Authentication with xdrd failed. Is your password set correctly?');
} else if (line.startsWith('o1,')) { } else if (line.startsWith('o1,')) authFlags.firstClient = true;
authFlags.firstClient = true; else if (line.startsWith('T') && line.length <= 7) {
} else if (line.startsWith('T') && line.length <= 7) {
const freq = line.slice(1) / 1000; const freq = line.slice(1) / 1000;
dataHandler.dataToSend.freq = freq.toFixed(3); dataHandler.dataToSend.freq = freq.toFixed(3);
} else if (line.startsWith('OK')) { } else if (line.startsWith('OK')) {
@@ -306,9 +292,7 @@ client.on('close', () => {
setTimeout(function () { setTimeout(function () {
connectToXdrd(); connectToXdrd();
}, 2000) }, 2000)
} else { } else logWarn('Disconnected from xdrd.');
logWarn('Disconnected from xdrd.');
}
}); });
client.on('error', (err) => { client.on('error', (err) => {
@@ -369,9 +353,7 @@ wss.on('connection', (ws, request) => {
return; return;
} }
if (clientIp && clientIp.includes(',')) { if (clientIp && clientIp.includes(',')) clientIp = clientIp.split(',')[0].trim();
clientIp = clientIp.split(',')[0].trim();
}
// Per-IP limit connection open // Per-IP limit connection open
if (clientIp) { if (clientIp) {
@@ -435,9 +417,7 @@ wss.on('connection', (ws, request) => {
} }
} }
if (command.includes("\'")) { if (command.includes("\'")) return;
return;
}
const { isAdminAuthenticated, isTuneAuthenticated } = request.session || {}; const { isAdminAuthenticated, isTuneAuthenticated } = request.session || {};
@@ -466,14 +446,10 @@ wss.on('connection', (ws, request) => {
const tuneFreq = Number(command.slice(1)) / 1000; const tuneFreq = Number(command.slice(1)) / 1000;
const { tuningLimit, tuningLowerLimit, tuningUpperLimit } = serverConfig.webserver; const { tuningLimit, tuningLowerLimit, tuningUpperLimit } = serverConfig.webserver;
if (tuningLimit && (tuneFreq < tuningLowerLimit || tuneFreq > tuningUpperLimit) || isNaN(tuneFreq)) { if (tuningLimit && (tuneFreq < tuningLowerLimit || tuneFreq > tuningUpperLimit) || isNaN(tuneFreq)) return;
return;
}
} }
if ((serverConfig.publicTuner && !serverConfig.lockToAdmin) || isAdminAuthenticated || (!serverConfig.publicTuner && !serverConfig.lockToAdmin && isTuneAuthenticated)) { if ((serverConfig.publicTuner && !serverConfig.lockToAdmin) || isAdminAuthenticated || (!serverConfig.publicTuner && !serverConfig.lockToAdmin && isTuneAuthenticated)) output.write(`${command}\n`);
output.write(`${command}\n`);
}
}); });
ws.on('close', (code, reason) => { ws.on('close', (code, reason) => {
@@ -499,55 +475,34 @@ wss.on('connection', (ws, request) => {
dataHandler.showOnlineUsers(currentUsers); dataHandler.showOnlineUsers(currentUsers);
const index = storage.connectedUsers.findIndex(user => user.ip === clientIp); const index = storage.connectedUsers.findIndex(user => user.ip === clientIp);
if (index !== -1) { if (index !== -1) storage.connectedUsers.splice(index, 1);
storage.connectedUsers.splice(index, 1);
}
if (currentUsers === 0) { if (currentUsers === 0) {
storage.connectedUsers = []; storage.connectedUsers = [];
if (serverConfig.bwAutoNoUsers === "1") { if (serverConfig.bwAutoNoUsers === "1") output.write("W0\n"); // Auto BW 'Enabled'
output.write("W0\n"); // Auto BW 'Enabled'
}
// cEQ and iMS combinations // cEQ and iMS combinations
if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "1") { if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "1") output.write("G00\n"); // Both Disabled
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 === "1" && serverConfig.imsNoUsers === "0") { else if (serverConfig.ceqNoUsers === "0" && serverConfig.imsNoUsers === "1") output.write(`G${dataHandler.dataToSend.eq}0\n`);
output.write(`G0${dataHandler.dataToSend.ims}\n`); else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "0") output.write(`G1${dataHandler.dataToSend.ims}\n`);
} else if (serverConfig.ceqNoUsers === "0" && serverConfig.imsNoUsers === "1") { else if (serverConfig.ceqNoUsers === "0" && serverConfig.imsNoUsers === "2") output.write(`G${dataHandler.dataToSend.eq}1\n`);
output.write(`G${dataHandler.dataToSend.eq}0\n`); else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "1") output.write("G10\n"); // Only cEQ enabled
} else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "0") { else if (serverConfig.ceqNoUsers === "1" && serverConfig.imsNoUsers === "2") output.write("G01\n"); // Only iMS enabled
output.write(`G1${dataHandler.dataToSend.ims}\n`); else if (serverConfig.ceqNoUsers === "2" && serverConfig.imsNoUsers === "2") output.write("G11\n"); // Both Enabled
} 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 // Handle stereo mode
if (serverConfig.stereoNoUsers === "1") { if (serverConfig.stereoNoUsers === "1") output.write("B0\n");
output.write("B0\n"); else if (serverConfig.stereoNoUsers === "2") output.write("B1\n");
} else if (serverConfig.stereoNoUsers === "2") {
output.write("B1\n");
}
// Handle Antenna selection // Handle Antenna selection
if (timeoutAntenna) clearTimeout(timeoutAntenna); if (timeoutAntenna) clearTimeout(timeoutAntenna);
timeoutAntenna = setTimeout(() => { timeoutAntenna = setTimeout(() => {
if (serverConfig.antennaNoUsers === "1") { if (serverConfig.antennaNoUsers === "1") output.write("Z0\n");
output.write("Z0\n"); else if (serverConfig.antennaNoUsers === "2") output.write("Z1\n");
} else if (serverConfig.antennaNoUsers === "2") { else if (serverConfig.antennaNoUsers === "3") output.write("Z2\n");
output.write("Z1\n"); else if (serverConfig.antennaNoUsers === "4") output.write("Z3\n");
} else if (serverConfig.antennaNoUsers === "3") {
output.write("Z2\n");
} else if (serverConfig.antennaNoUsers === "4") {
output.write("Z3\n");
}
}, serverConfig.antennaNoUsersDelay ? 15000 : 0); }, serverConfig.antennaNoUsersDelay ? 15000 : 0);
} }
@@ -570,13 +525,9 @@ wss.on('connection', (ws, request) => {
}, 10000); }, 10000);
} }
if (currentUsers === 0 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection === true) { if (currentUsers === 0 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection === true) client.write('X\n');
client.write('X\n');
}
if (code !== 1008) { if (code !== 1008) logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]`);
logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]`);
}
}); });
ws.on('error', console.error); ws.on('error', console.error);
@@ -600,7 +551,7 @@ pluginsWss.on('connection', (ws, request) => {
let messageData; 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 messageData = JSON.parse(message); // Attempt to parse the JSON
} catch (error) { } catch (error) {
// console.error("Failed to parse message:", error); // Log the 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 // Broadcast the message to all other clients
pluginsWss.clients.forEach(client => { pluginsWss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) { if (client.readyState === WebSocket.OPEN) client.send(modifiedMessage); // Send the message to all clients
client.send(modifiedMessage); // Send the message to all clients
}
}); });
}); });
@@ -671,9 +620,7 @@ httpServer.on('upgrade', (request, socket, head) => {
pluginsWss.emit('connection', ws, request); pluginsWss.emit('connection', ws, request);
}); });
}); });
} else { } else socket.destroy();
socket.destroy();
}
}); });
app.use(express.static(path.join(__dirname, '../web'))); // Serve the entire web folder to the user 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) => { const startServer = (address, isIPv6) => {
httpServer.listen(port, address, () => { httpServer.listen(port, address, () => {
if (!isIPv6 && !configExists()) { if (!isIPv6 && !configExists()) logInfo(`Open your browser and proceed to \x1b[34mhttp://${address}:${port}\x1b[0m to continue with setup.`);
logInfo(`Open your browser and proceed to \x1b[34mhttp://${address}:${port}\x1b[0m to continue with setup.`); else logServerStart(address, isIPv6);
} else {
logServerStart(address, isIPv6);
}
}); });
}; };
if (isIPv6Supported) { if (isIPv6Supported) {
startServer(ipv4Address, false); // Start on IPv4 startServer(ipv4Address, false); // Start on IPv4
startServer(ipv6Address, true); // Start on IPv6 startServer(ipv6Address, true); // Start on IPv6
} else { } else startServer(ipv4Address, false); // Start only on IPv4
startServer(ipv4Address, false); // Start only on IPv4
}
}); });

Binary file not shown.

Binary file not shown.

184
server/rds.js Normal file
View File

@@ -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;

632
server/rds_country.js Normal file
View File

@@ -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
};

View File

@@ -94,7 +94,7 @@ let serverConfig = {
enabled: false, enabled: false,
username: "", username: "",
token: "", token: "",
region: "eu", region: "pldx",
lowLatencyMode: false, lowLatencyMode: false,
subdomain: "", subdomain: "",
httpName: "", httpName: "",

View File

@@ -11,11 +11,8 @@ function checkFFmpeg() {
}); });
checkFFmpegProcess.on('exit', (code) => { checkFFmpegProcess.on('exit', (code) => {
if (code === 0) { if (code === 0) resolve('ffmpeg');
resolve('ffmpeg'); else resolve(require('ffmpeg-static'));
} else {
resolve(require('ffmpeg-static'));
}
}); });
}); });
} }

View File

@@ -74,9 +74,7 @@ function parseAudioDevice(options, callback) {
if (platform === 'win32' && line.search(/Alternative\sname/) > -1) { if (platform === 'win32' && line.search(/Alternative\sname/) > -1) {
const lastDevice = deviceList[deviceList.length - 1]; const lastDevice = deviceList[deviceList.length - 1];
const alt = line.match(alternativeName); const alt = line.match(alternativeName);
if (lastDevice && alt) { if (lastDevice && alt) lastDevice.alternativeName = alt[1];
lastDevice.alternativeName = alt[1];
}
return; return;
} }
@@ -107,11 +105,8 @@ function parseAudioDevice(options, callback) {
} }
}; };
if (callbackExists) { if (callbackExists) execute();
execute(); else return new Promise(execute);
} else {
return new Promise(execute);
}
} }
module.exports = { parseAudioDevice }; module.exports = { parseAudioDevice };

View File

@@ -25,9 +25,7 @@ async function connect() {
try { try {
const res = await fetch('https://fmtuner.org/binaries/' + frpcFileName); const res = await fetch('https://fmtuner.org/binaries/' + frpcFileName);
if (res.status === 404) { if (res.status === 404) throw new Error('404 error');
throw new Error('404 error');
}
const stream = fs2.createWriteStream(frpcPath); const stream = fs2.createWriteStream(frpcPath);
await finished(Readable.fromWeb(res.body).pipe(stream)); await finished(Readable.fromWeb(res.body).pipe(stream));
} catch (err) { } catch (err) {
@@ -41,7 +39,7 @@ async function connect() {
} }
const cfg = ejs.render(frpcConfigTemplate, { const cfg = ejs.render(frpcConfigTemplate, {
cfg: serverConfig.tunnel, 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: { server: {
port: serverConfig.webserver.webserverPort port: serverConfig.webserver.webserverPort
} }
@@ -62,15 +60,10 @@ async function connect() {
if (line.includes('connect to server error')) { if (line.includes('connect to server error')) {
const reason = line.substring(line.indexOf(': ')+2); const reason = line.substring(line.indexOf(': ')+2);
logError('Failed to connect to tunnel, reason: ' + reason); logError('Failed to connect to tunnel, reason: ' + reason);
} else if (line.includes('invalid user or token')) { } else if (line.includes('invalid user or token')) logError('Failed to connect to tunnel, reason: 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('start proxy success')) { else if (line.includes('login to server success')) logInfo('Connection to tunnel server was successful');
logInfo('Tunnel established successfully'); else logDebug('Tunnel log:', line);
} else if (line.includes('login to server success')) {
logInfo('Connection to tunnel server was successful');
} else {
logDebug('Tunnel log:', line);
}
}); });
child.on('error', (err) => { child.on('error', (err) => {

View File

@@ -116,11 +116,8 @@ function initBanlist() {
data: { ip: ipAddress, reason: reason }, data: { ip: ipAddress, reason: reason },
success: function(response) { success: function(response) {
// Refresh the page if the request was successful // Refresh the page if the request was successful
if (response.success) { if (response.success) location.reload();
location.reload(); else console.error('Failed to add to banlist');
} else {
console.error('Failed to add to banlist');
}
}, },
error: function() { error: function() {
console.error('Error occurred during the request'); console.error('Error occurred during the request');

View File

@@ -699,7 +699,8 @@
<div class="panel-100 p-bottom-20"> <div class="panel-100 p-bottom-20">
<h3>Tunnel</h3> <h3>Tunnel</h3>
<p>When you become an <a href="https://buymeacoffee.com/fmdx" target="_blank"><strong>FMDX.org supporter</strong></a>, you can host your webserver without the need of a public IP address & port forwarding.<br> <p>When you become an <a href="https://buymeacoffee.com/fmdx" target="_blank"><strong>FMDX.org supporter</strong></a>, you can host your webserver without the need of a public IP address & port forwarding.<br>
When you become a supporter, you can message the Founders on Discord for your login details.</p> When you become a supporter, you can message the Founders on Discord for your login details.</p><br>
<p>You can also get an tunnel from kuba201 discord, one of the contributors of this version of the application.</p>
<h4>Main tunnel settings</h4> <h4>Main tunnel settings</h4>
<%- include('_components', {component: 'checkbox', cssClass: 'm-right-10', label: 'Enable tunnel', id: 'tunnel-enabled'}) %><br> <%- include('_components', {component: 'checkbox', cssClass: 'm-right-10', label: 'Enable tunnel', id: 'tunnel-enabled'}) %><br>
<%- include('_components', { component: 'dropdown', id: 'tunnel-server', inputId: 'tunnel-serverSelect', label: 'Official server region', cssClass: '', placeholder: 'Europe', <%- include('_components', { component: 'dropdown', id: 'tunnel-server', inputId: 'tunnel-serverSelect', label: 'Official server region', cssClass: '', placeholder: 'Europe',
@@ -707,6 +708,7 @@
{ value: 'eu', label: 'Europe' }, { value: 'eu', label: 'Europe' },
{ value: 'us', label: 'Americas' }, { value: 'us', label: 'Americas' },
{ value: 'sg', label: 'Asia & Oceania' }, { value: 'sg', label: 'Asia & Oceania' },
{ value: 'pldx', label: 'Poland (k201)' },
] ]
}) %> }) %>
<%- include('_components', {component: 'text', cssClass: 'w-150 br-15', placeholder: '', label: 'Username', id: 'tunnel-username'}) %> <%- include('_components', {component: 'text', cssClass: 'w-150 br-15', placeholder: '', label: 'Username', id: 'tunnel-username'}) %>