1
0
mirror of https://github.com/KubaPro010/fm-dx-webserver.git synced 2026-02-26 14:11:59 +01:00

some changes

This commit is contained in:
2026-02-24 12:20:09 +01:00
parent 1f70b58295
commit 1d04719580
18 changed files with 160 additions and 259 deletions

25
package-lock.json generated
View File

@@ -15,6 +15,7 @@
"express": "5.2.1",
"express-session": "1.19.0",
"ffmpeg-static": "5.3.0",
"figlet": "^1.10.0",
"http": "0.0.1-security",
"koffi": "2.7.2",
"net": "1.0.2",
@@ -574,6 +575,15 @@
"node": ">=18"
}
},
"node_modules/commander": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/concat-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
@@ -874,6 +884,21 @@
"node": ">=16"
}
},
"node_modules/figlet": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/figlet/-/figlet-1.10.0.tgz",
"integrity": "sha512-aktIwEZZ6Gp9AWdMXW4YCi0J2Ahuxo67fNJRUIWD81w8pQ0t9TS8FFpbl27ChlTLF06VkwjDesZSzEVzN75rzA==",
"license": "MIT",
"dependencies": {
"commander": "^14.0.0"
},
"bin": {
"figlet": "bin/index.js"
},
"engines": {
"node": ">= 17.0.0"
}
},
"node_modules/filelist": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.5.tgz",

View File

@@ -18,6 +18,7 @@
"express": "5.2.1",
"express-session": "1.19.0",
"ffmpeg-static": "5.3.0",
"figlet": "^1.10.0",
"http": "0.0.1-security",
"koffi": "2.7.2",
"net": "1.0.2",

View File

@@ -4,14 +4,12 @@ const { logChat } = require('./console');
const helpers = require('./helpers');
function createChatServer(storage) {
if (!serverConfig.webserver.chatEnabled) {
return null;
}
if (!serverConfig.webserver.chatEnabled) return null;
const chatWss = new WebSocket.Server({ noServer: true });
chatWss.on('connection', (ws, request) => {
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress;
const userCommandHistory = {};
if (serverConfig.webserver.banlist?.includes(clientIp)) {
@@ -79,32 +77,19 @@ function createChatServer(storage) {
if (serverConfig.webserver.banlist?.includes(clientIp)) return;
if (request.session?.isAdminAuthenticated === true) {
messageData.admin = true;
}
if (messageData.nickname?.length > 32) {
messageData.nickname = messageData.nickname.substring(0, 32);
}
if (messageData.message?.length > 255) {
messageData.message = messageData.message.substring(0, 255);
}
if (request.session?.isAdminAuthenticated === true) messageData.admin = true;
if (messageData.nickname?.length > 32) messageData.nickname = messageData.nickname.substring(0, 32);
if (messageData.message?.length > 255) messageData.message = messageData.message.substring(0, 255);
storage.chatHistory.push(messageData);
if (storage.chatHistory.length > 50) {
storage.chatHistory.shift();
}
if (storage.chatHistory.length > 50) storage.chatHistory.shift();
logChat(messageData);
chatWss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
const responseMessage = { ...messageData };
if (!request.session?.isAdminAuthenticated) {
delete responseMessage.ip;
}
if (!request.session?.isAdminAuthenticated) delete responseMessage.ip;
client.send(JSON.stringify(responseMessage));
}
@@ -112,7 +97,7 @@ function createChatServer(storage) {
});
});
return chatWss; // ← VERY IMPORTANT
return chatWss;
}
module.exports = { createChatServer };

View File

@@ -8,7 +8,7 @@ const updateInterval = 75;
// Initialize the data object
var dataToSend = {
pi: '?',
freq: 87.500.toFixed(3),
freq: (87.500).toFixed(3),
sig: 0,
sigRaw: '',
sigTop: -Infinity,
@@ -71,9 +71,7 @@ function rdsReceived() {
clearTimeout(rdsTimeoutTimer);
rdsTimeoutTimer = null;
}
if (serverConfig.webserver.rdsTimeout && serverConfig.webserver.rdsTimeout != 0) {
rdsTimeoutTimer = setTimeout(rdsReset, serverConfig.webserver.rdsTimeout * 1000);
}
if (serverConfig.webserver.rdsTimeout && serverConfig.webserver.rdsTimeout != 0) rdsTimeoutTimer = setTimeout(rdsReset, serverConfig.webserver.rdsTimeout * 1000);
}
function rdsReset() {

View File

@@ -19,7 +19,7 @@ const { allPluginConfigs } = require('./plugins');
// Endpoints
router.get('/', (req, res) => {
let requestIp = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
let requestIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
const normalizedIp = requestIp?.replace(/^::ffff:/, '');
const ipList = (normalizedIp || '').split(',').map(ip => ip.trim()).filter(Boolean); // in case there are multiple IPs (proxy), we need to check all of them
@@ -175,11 +175,11 @@ router.get('/wizard', (req, res) => {
router.get('/rds', (req, res) => {
res.send('Please connect using a WebSocket compatible app to obtain RDS stream.');
res.send('Please connect using a WebSocket compatible app to obtain the RDS stream.');
});
router.get('/rdsspy', (req, res) => {
res.send('Please connect using a WebSocket compatible app to obtain RDS stream.');
res.send('Please connect using a WebSocket compatible app to obtain the RDS stream.');
});
router.get('/api', (req, res) => {
@@ -195,24 +195,17 @@ router.get('/api', (req, res) => {
const loginAttempts = {}; // Format: { 'ip': { count: 1, lastAttempt: 1234567890 } }
const MAX_ATTEMPTS = 25;
const MAX_ATTEMPTS = 15;
const WINDOW_MS = 15 * 60 * 1000;
const authenticate = (req, res, next) => {
const ip = req.ip || req.connection.remoteAddress;
const now = Date.now();
if (!loginAttempts[ip]) {
loginAttempts[ip] = { count: 0, lastAttempt: now };
} else if (now - loginAttempts[ip].lastAttempt > WINDOW_MS) {
loginAttempts[ip] = { count: 0, lastAttempt: now };
}
if (!loginAttempts[ip]) loginAttempts[ip] = { count: 0, lastAttempt: now };
else if (now - loginAttempts[ip].lastAttempt > WINDOW_MS) loginAttempts[ip] = { count: 0, lastAttempt: now };
if (loginAttempts[ip].count >= MAX_ATTEMPTS) {
return res.status(403).json({
message: 'Too many login attempts. Please try again later.'
});
}
if (loginAttempts[ip].count >= MAX_ATTEMPTS) return res.status(403).json({message: 'Too many login attempts. Please try again later.'});
const { password } = req.body;
@@ -250,11 +243,9 @@ router.get('/logout', (req, res) => {
});
router.get('/kick', (req, res) => {
const ipAddress = req.query.ip; // Extract the IP address parameter from the query string
const ipAddress = req.query.ip;
// Terminate the WebSocket connection for the specified IP address
if(req.session.isAdminAuthenticated) {
helpers.kickClient(ipAddress);
}
if(req.session.isAdminAuthenticated) helpers.kickClient(ipAddress);
setTimeout(() => {
res.redirect('/setup');
}, 500);
@@ -269,9 +260,7 @@ router.get('/addToBanlist', (req, res) => {
userBanData = [ipAddress, location, date, reason];
if (typeof serverConfig.webserver.banlist !== 'object') {
serverConfig.webserver.banlist = [];
}
if (typeof serverConfig.webserver.banlist !== 'object') serverConfig.webserver.banlist = [];
serverConfig.webserver.banlist.push(userBanData);
configSave();
@@ -281,17 +270,14 @@ router.get('/addToBanlist', (req, res) => {
router.get('/removeFromBanlist', (req, res) => {
if (!req.session.isAdminAuthenticated) return;
const ipAddress = req.query.ip;
if (typeof serverConfig.webserver.banlist !== 'object') {
serverConfig.webserver.banlist = [];
}
if (typeof serverConfig.webserver.banlist !== 'object') serverConfig.webserver.banlist = [];
const banIndex = serverConfig.webserver.banlist.findIndex(ban => ban[0] === ipAddress);
if (banIndex === -1) {
return res.status(404).json({ success: false, message: 'IP address not found in banlist.' });
}
if (banIndex === -1) return res.status(404).json({ success: false, message: 'IP address not found in banlist.' });
serverConfig.webserver.banlist.splice(banIndex, 1);
configSave();
@@ -303,19 +289,14 @@ router.get('/removeFromBanlist', (req, res) => {
router.post('/saveData', (req, res) => {
const data = req.body;
let firstSetup;
if(req.session.isAdminAuthenticated || configExists() === false) {
if(req.session.isAdminAuthenticated || !configExists()) {
configUpdate(data);
fmdxList.update();
if(configExists() === false) {
firstSetup = true;
}
if(!configExists()) firstSetup = true;
logInfo('Server config changed successfully.');
if(firstSetup === true) {
res.status(200).send('Data saved successfully!\nPlease, restart the server to load your configuration.');
} else {
res.status(200).send('Data saved successfully!\nSome settings may need a server restart to apply.');
}
if(firstSetup === true) res.status(200).send('Data saved successfully!\nPlease, restart the server to load your configuration.');
else res.status(200).send('Data saved successfully!\nSome settings may need a server restart to apply.');
}
});
@@ -327,9 +308,8 @@ router.get('/getData', (req, res) => {
if(req.session.isAdminAuthenticated) {
// Check if the file exists
fs.access(configPath, fs.constants.F_OK, (err) => {
if (err) {
console.log(err);
} else {
if (err) console.log(err);
else {
// File exists, send it as the response
res.sendFile(path.join(__dirname, '../' + configName + '.json'));
}
@@ -342,9 +322,7 @@ router.get('/getDevices', (req, res) => {
parseAudioDevice((result) => {
res.json(result);
});
} else {
res.status(403).json({ error: 'Unauthorized' });
}
} else res.status(403).json({ error: 'Unauthorized' });
});
/* Static data are being sent through here on connection - these don't change when the server is running */
@@ -389,9 +367,7 @@ function canLog(id) {
}
}
if (logHistory[id] && (now - logHistory[id]) < sixtyMinutes) {
return false; // Deny logging if less than 60 minutes have passed
}
if (logHistory[id] && (now - logHistory[id]) < sixtyMinutes) return false; // Deny logging if less than 60 minutes have passed
logHistory[id] = now; // Update with the current timestamp
return true;
}

View File

@@ -19,6 +19,7 @@ const { SerialPort } = require('serialport');
const tunnel = require('./tunnel');
const { createChatServer } = require('./chat');
const { createAudioServer } = require('./stream/ws.js');
const figlet = require('figlet');
// File imports
const helpers = require('./helpers');
@@ -35,14 +36,10 @@ function findServerFiles(plugins) {
let results = [];
plugins.forEach(plugin => {
// Remove .js extension if present
if (plugin.endsWith('.js')) {
plugin = plugin.slice(0, -3);
}
if (plugin.endsWith('.js')) plugin = plugin.slice(0, -3);
const pluginPath = path.join(__dirname, '..', 'plugins', `${plugin}_server.js`);
if (fs.existsSync(pluginPath) && fs.statSync(pluginPath).isFile()) {
results.push(pluginPath);
}
if (fs.existsSync(pluginPath) && fs.statSync(pluginPath).isFile()) results.push(pluginPath);
});
return results;
}
@@ -80,14 +77,14 @@ const terminalWidth = readline.createInterface({
}).output.columns;
// Couldn't get figlet.js or something like that?
console.log(`\x1b[32m
_____ __ __ ______ __ __ __ _
| ___| \\/ | | _ \\ \\/ / \\ \\ / /__| |__ ___ ___ _ ____ _____ _ __
| |_ | |\\/| |_____| | | \\ / \\ \\ /\\ / / _ \\ '_ \\/ __|/ _ \\ '__\\ \\ / / _ \\ '__|
| _| | | | |_____| |_| / \\ \\ V V / __/ |_) \\__ \\ __/ | \\ V / __/ |
|_| |_| |_| |____/_/\\_\\ \\_/\\_/ \\___|_.__/|___/\\___|_| \\_/ \\___|_|
`);
figlet("FM-DX Webserver", function (err, data) {
if (err) {
console.log("Something went wrong...");
console.dir(err);
return;
}
console.log('\x1b[32m' + data);
});
console.log('\x1b[32m\x1b[2mby Noobish @ \x1b[4mFMDX.org\x1b[0m');
console.log("v" + pjson.version)
console.log('\x1b[90m' + '─'.repeat(terminalWidth - 1) + '\x1b[0m');
@@ -104,7 +101,7 @@ let timeoutAntenna;
app.use(bodyParser.urlencoded({ extended: true }));
const sessionMiddleware = session({
secret: 'GTce3tN6U8odMwoI',
secret: 'GTce3tN6U8odMwoI', // Cool
resave: false,
saveUninitialized: true,
});
@@ -324,9 +321,6 @@ app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, '../web'));
app.use('/', endpoints);
/**
* WEBSOCKET BLOCK
*/
const tunerLockTracker = new WeakMap();
const ipConnectionCounts = new Map(); // Per-IP limit variables
const ipLogTimestamps = new Map();
@@ -349,7 +343,7 @@ setInterval(() => {
wss.on('connection', (ws, request) => {
const output = serverConfig.xdrd.wirelessConnection ? client : serialport;
let clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
let clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress;
const userCommandHistory = {};
const normalizedClientIp = clientIp?.replace(/^::ffff:/, '');
@@ -363,17 +357,10 @@ wss.on('connection', (ws, request) => {
// Per-IP limit connection open
if (clientIp) {
const isLocalIp = (
clientIp === '127.0.0.1' ||
clientIp === '::1' ||
clientIp === '::ffff:127.0.0.1' ||
clientIp.startsWith('192.168.') ||
clientIp.startsWith('10.') ||
clientIp.startsWith('172.16.')
);
clientIp === '127.0.0.1' || clientIp === '::1' || clientIp === '::ffff:127.0.0.1' ||
clientIp.startsWith('192.168.') || clientIp.startsWith('10.') || clientIp.startsWith('172.16.'));
if (!isLocalIp) {
if (!ipConnectionCounts.has(clientIp)) {
ipConnectionCounts.set(clientIp, 0);
}
if (!ipConnectionCounts.has(clientIp)) ipConnectionCounts.set(clientIp, 0);
const currentCount = ipConnectionCounts.get(clientIp);
if (currentCount >= MAX_CONNECTIONS_PER_IP) {
ws.close(1008, 'Too many open connections from this IP');
@@ -389,7 +376,9 @@ wss.on('connection', (ws, request) => {
}
}
if (clientIp !== '::ffff:127.0.0.1' || (request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.0.1') || (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
if (clientIp !== '::ffff:127.0.0.1' ||
(request.socket && request.socket.remoteAddress && request.socket.remoteAddress !== '::ffff:127.0.0.1') ||
(request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
currentUsers++;
}
@@ -400,12 +389,9 @@ wss.on('connection', (ws, request) => {
ws.close(1008, 'Banned IP');
return;
}
dataHandler.showOnlineUsers(currentUsers);
if (currentUsers === 1 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection) {
serverConfig.xdrd.wirelessConnection ? connectToXdrd() : serialport.write('x\n');
}
if (currentUsers === 1 && serverConfig.autoShutdown === true && serverConfig.xdrd.wirelessConnection) serverConfig.xdrd.wirelessConnection ? connectToXdrd() : serialport.write('x\n');
});
const userCommands = {};
@@ -474,7 +460,9 @@ wss.on('connection', (ws, request) => {
}
}
if (clientIp !== '::ffff:127.0.0.1' || (request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.0.1') || (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
if (clientIp !== '::ffff:127.0.0.1' ||
(request.socket && request.socket.remoteAddress && request.socket.remoteAddress !== '::ffff:127.0.0.1') ||
(request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) {
currentUsers--;
}
dataHandler.showOnlineUsers(currentUsers);
@@ -540,7 +528,7 @@ wss.on('connection', (ws, request) => {
// Additional web socket for using plugins
pluginsWss.on('connection', (ws, request) => {
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress;
const userCommandHistory = {};
if (serverConfig.webserver.banlist?.includes(clientIp)) {
ws.close(1008, 'Banned IP');
@@ -578,39 +566,23 @@ pluginsWss.on('connection', (ws, request) => {
// Websocket register for /text, /audio and /chat paths
httpServer.on('upgrade', (request, socket, head) => {
const clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress;
const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress;
if (serverConfig.webserver.banlist?.includes(clientIp)) {
socket.destroy();
return;
}
if (request.url === '/text') {
var upgradeWss = undefined;
if (request.url === '/text') upgradeWss = wss;
else if (request.url === '/audio') upgradeWss = audioWss;
else if (request.url === '/chat' && serverConfig.webserver.chatEnabled === true) upgradeWss = chatWss;
else if (request.url === '/rds' || request.url === '/rdsspy') upgradeWss = rdsWss;
else if (request.url === '/data_plugins') upgradeWss = pluginsWss;
if(upgradeWss) {
sessionMiddleware(request, {}, () => {
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request);
});
});
} else if (request.url === '/audio') {
sessionMiddleware(request, {}, () => {
audioWss.handleUpgrade(request, socket, head, (ws) => {
audioWss.emit('connection', ws, request);
});
});
} else if (request.url === '/chat' && serverConfig.webserver.chatEnabled === true) {
sessionMiddleware(request, {}, () => {
chatWss.handleUpgrade(request, socket, head, (ws) => {
chatWss.emit('connection', ws, request);
});
});
} else if (request.url === '/rds' || request.url === '/rdsspy') {
sessionMiddleware(request, {}, () => {
rdsWss.handleUpgrade(request, socket, head, (ws) => {
rdsWss.emit('connection', ws, request);
});
});
} else if (request.url === '/data_plugins') {
sessionMiddleware(request, {}, () => {
pluginsWss.handleUpgrade(request, socket, head, (ws) => {
pluginsWss.emit('connection', ws, request);
upgradeWss.handleUpgrade(request, socket, head, (ws) => {
upgradeWss.emit('connection', ws, request);
});
});
} else socket.destroy();

View File

@@ -1,7 +1,6 @@
const fs = require('fs');
const path = require('path');
const consoleCmd = require('./console');
const { serverConfig } = require('./server_config');
// Function to read all .js files in a directory
function readJSFiles(dir) {
@@ -11,7 +10,6 @@ function readJSFiles(dir) {
// Function to parse plugin config from a file
function parsePluginConfig(filePath) {
const fileContent = fs.readFileSync(filePath, 'utf8');
const pluginConfig = {};
// Assuming pluginConfig is a JavaScript object defined in each .js file
@@ -31,9 +29,7 @@ function parsePluginConfig(filePath) {
}
// Check if the destination directory exists, if not, create it
if (!fs.existsSync(destinationDir)) {
fs.mkdirSync(destinationDir, { recursive: true }); // Create directory recursively
}
if (!fs.existsSync(destinationDir)) fs.mkdirSync(destinationDir, { recursive: true }); // Create directory recursively
const destinationFile = path.join(destinationDir, path.basename(sourcePath));
@@ -41,9 +37,7 @@ function parsePluginConfig(filePath) {
if (process.platform !== 'win32') {
// On Linux, create a symlink
try {
if (fs.existsSync(destinationFile)) {
fs.unlinkSync(destinationFile); // Remove existing file/symlink
}
if (fs.existsSync(destinationFile)) fs.unlinkSync(destinationFile); // Remove existing file/symlink
fs.symlinkSync(sourcePath, destinationFile);
setTimeout(function() {
consoleCmd.logInfo(`Plugin ${pluginConfig.name} ${pluginConfig.version} initialized successfully.`);
@@ -52,9 +46,7 @@ function parsePluginConfig(filePath) {
console.error(`Error creating symlink at ${destinationFile}: ${err.message}`);
}
}
} else {
console.error(`Error: frontEndPath is not defined in ${filePath}`);
}
} else console.error(`Error: frontEndPath is not defined in ${filePath}`);
} catch (err) {
console.error(`Error parsing plugin config from ${filePath}: ${err.message}`);
}
@@ -71,9 +63,7 @@ function collectPluginConfigs() {
jsFiles.forEach(file => {
const filePath = path.join(pluginsDir, file);
const config = parsePluginConfig(filePath);
if (Object.keys(config).length > 0) {
pluginConfigs.push(config);
}
if (Object.keys(config).length > 0) pluginConfigs.push(config);
});
return pluginConfigs;
@@ -81,9 +71,7 @@ function collectPluginConfigs() {
// Ensure the web/js/plugins directory exists
const webJsPluginsDir = path.join(__dirname, '../web/js/plugins');
if (!fs.existsSync(webJsPluginsDir)) {
fs.mkdirSync(webJsPluginsDir, { recursive: true });
}
if (!fs.existsSync(webJsPluginsDir)) fs.mkdirSync(webJsPluginsDir, { recursive: true });
// Main function to create symlinks/junctions for plugins
function createLinks() {
@@ -93,13 +81,8 @@ function createLinks() {
if (process.platform === 'win32') {
// On Windows, create a junction
try {
if (fs.existsSync(destinationPluginsDir)) {
fs.rmSync(destinationPluginsDir, { recursive: true });
}
if (fs.existsSync(destinationPluginsDir)) fs.rmSync(destinationPluginsDir, { recursive: true });
fs.symlinkSync(pluginsDir, destinationPluginsDir, 'junction');
setTimeout(function() {
//consoleCmd.logInfo(`Plugin ${pluginConfig.name} ${pluginConfig.version} initialized successfully.`);
}, 500)
} catch (err) {
console.error(`Error creating junction at ${destinationPluginsDir}: ${err.message}`);
}

View File

@@ -5,7 +5,7 @@
// - Optionally broadcasts events to connected plugin WebSocket clients
const { EventEmitter } = require('events');
const { logInfo, logWarn, logError } = require('./console');
const { logWarn, logError } = require('./console');
let output = null;
let wss = null;

View File

@@ -51,7 +51,7 @@ class RDSDecoder {
const group = (blockB >> 12) & 0xF;
const version = (blockB >> 11) & 0x1;
this.data.tp = Number((blockB >> 10) & 1);
this.data.pty = (blockB >> 5) & 0b11111;
this.data.pty = (blockB >> 5) & 31;
if (group === 0) {
this.data.ta = (blockB >> 4) & 1;
@@ -87,7 +87,7 @@ class RDSDecoder {
}
}
if(d_error === 3) return; // Don't risk it
if(d_error > 2) return; // Don't risk it
const idx = blockB & 0x3;
@@ -99,6 +99,7 @@ class RDSDecoder {
this.data.ps = this.ps.join('');
this.data.ps_errors = this.ps_errors.join(',');
} else if (group === 1 && version === 0) {
if(c_error > 2) return;
var variant_code = (blockC >> 12) & 0x7;
switch (variant_code) {
case 0:
@@ -120,13 +121,13 @@ class RDSDecoder {
this.rt1_to_clear = false;
}
if(c_error !== 3 && multiplier !== 2) {
if(c_error < 2 && multiplier !== 2) {
this.rt1[idx * multiplier] = String.fromCharCode(blockC >> 8);
this.rt1[idx * multiplier + 1] = String.fromCharCode(blockC & 0xFF);
this.rt1_errors[idx * multiplier] = error;
this.rt1_errors[idx * multiplier + 1] = error;
}
if(d_error !== 3) {
if(d_error < 2) {
var offset = (multiplier == 2) ? 0 : 2;
this.rt1[idx * multiplier + offset] = String.fromCharCode(blockD >> 8);
this.rt1[idx * multiplier + offset + 1] = String.fromCharCode(blockD & 0xFF);

View File

@@ -133,14 +133,10 @@ let serverConfig = {
function addMissingFields(target, source) {
Object.keys(source).forEach(function(key) {
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
if (!target[key]) {
target[key] = {}; // Create missing object
}
if (!target[key]) target[key] = {}; // Create missing object
addMissingFields(target[key], source[key]); // Recursively add missing fields
} else {
if (target[key] === undefined) {
target[key] = source[key]; // Add missing fields only
}
if (target[key] === undefined) target[key] = source[key]; // Add missing fields only
}
});
}
@@ -149,13 +145,9 @@ function addMissingFields(target, source) {
function deepMerge(target, source) {
Object.keys(source).forEach(function(key) {
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
if (!target[key] || typeof target[key] !== 'object') {
target[key] = {}; // Ensure target[key] is an object before merging
}
if (!target[key] || typeof target[key] !== 'object') target[key] = {}; // Ensure target[key] is an object before merging
deepMerge(target[key], source[key]); // Recursively merge objects
} else {
target[key] = source[key]; // Overwrite or add the value
}
} else target[key] = source[key]; // Overwrite or add the value
});
}

View File

@@ -21,9 +21,9 @@ checkFFmpeg().then((ffmpegPath) => {
logInfo(`${consoleLogTitle} Using ${ffmpegPath === 'ffmpeg' ? 'system-installed FFmpeg' : 'ffmpeg-static'}`);
logInfo(`${consoleLogTitle} Starting audio stream on device: \x1b[35m${serverConfig.audio.audioDevice}\x1b[0m`);
const sampleRate = Number(this?.Server?.SampleRate || serverConfig.audio.sampleRate || 44100) + Number(serverConfig.audio.samplerateOffset || 0);
const sampleRate = Number(serverConfig.audio.sampleRate || 44100) + Number(serverConfig.audio.samplerateOffset || 0);
const channels = Number(this?.Server?.Channels || serverConfig.audio.audioChannels || 2);
const channels = Number(serverConfig.audio.audioChannels || 2);
let ffmpeg = null;
let restartTimer = null;
@@ -48,7 +48,7 @@ checkFFmpeg().then((ffmpegPath) => {
...inputArgs,
"-thread_queue_size", "1024",
"-thread_queue_size", "1536",
"-ar", String(sampleRate),
"-ac", String(channels),

View File

@@ -28,9 +28,7 @@ function parseAudioDevice(options, callback) {
const matches = (data.match(regex) || []).map(match => 'hw:' + match.replace(/\s+/g, '').slice(1, -1));
matches.forEach(match => {
if (typeof match === 'string') {
audioDevices.push({ name: match });
}
if (typeof match === 'string') audioDevices.push({ name: match });
});
} catch (err) {
console.error(`Error reading file: ${err.message}`);

View File

@@ -1,15 +1,12 @@
const WebSocket = require('ws');
const { serverConfig } = require('../server_config');
const { audio_pipe } = require('./index.js');
const { PassThrough } = require('stream');
function createAudioServer() {
const audioWss = new WebSocket.Server({ noServer: true });
audioWss.on('connection', (ws, request) => {
const clientIp =
request.headers['x-forwarded-for'] ||
request.connection.remoteAddress;
const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress;
if (serverConfig.webserver.banlist?.includes(clientIp)) {
ws.close(1008, 'Banned IP');
@@ -19,12 +16,7 @@ function createAudioServer() {
audio_pipe.on('data', (chunk) => {
audioWss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(chunk, {
binary: true,
compress: false
});
}
if (client.readyState === WebSocket.OPEN) client.send(chunk, {binary: true, compress: false });
});
});

View File

@@ -15,12 +15,10 @@ const fileExists = path => new Promise(resolve => fs.access(path, fs.constants.F
async function connect() {
if (serverConfig.tunnel?.enabled === true) {
const librariesDir = path.resolve(__dirname, '../libraries');
if (!await fileExists(librariesDir)) {
await fs.mkdir(librariesDir);
}
if (!await fileExists(librariesDir)) await fs.mkdir(librariesDir);
const frpcPath = path.resolve(librariesDir, 'frpc' + (os.platform() === 'win32' ? '.exe' : ''));
if (!await fileExists(frpcPath)) {
logInfo('frpc binary required for tunnel is not available. Downloading now...');
logInfo('frpc binary, required for tunnel is not available. Downloading now...');
const frpcFileName = `frpc_${os.platform}_${os.arch}` + (os.platform() === 'win32' ? '.exe' : '');
try {
@@ -33,9 +31,7 @@ async function connect() {
return;
}
logInfo('Downloading of frpc is completed.')
if (os.platform() === 'linux' || os.platform() === 'darwin') {
await fs.chmod(frpcPath, 0o770);
}
if (os.platform() === 'linux' || os.platform() === 'darwin') await fs.chmod(frpcPath, 0o770);
}
const cfg = ejs.render(frpcConfigTemplate, {
cfg: serverConfig.tunnel,
@@ -73,7 +69,6 @@ async function connect() {
child.on('close', (code) => {
logInfo(`Tunnel process exited with code ${code}`);
});
}
}

View File

@@ -96,9 +96,7 @@ async function buildTxDatabase() {
consoleCmd.logInfo('Retrying transmitter database download...');
}
}
} else {
consoleCmd.logInfo('Server latitude and longitude must be set before transmitter database can be built');
}
} else consoleCmd.logInfo('Server latitude and longitude must be set before transmitter database can be built');
}
// Function to build index map of PI+Freq combinations
@@ -162,8 +160,7 @@ function getStateBoundingBox(coordinates) {
// Function to check if a city (lat, lon) falls within the bounding box of a state
function isCityInState(lat, lon, boundingBox) {
return lat >= boundingBox.minLat && lat <= boundingBox.maxLat &&
lon >= boundingBox.minLon && lon <= boundingBox.maxLon;
return lat >= boundingBox.minLat && lat <= boundingBox.maxLat && lon >= boundingBox.minLon && lon <= boundingBox.maxLon;
}
// Function to check if a city (lat, lon) is inside any US state and return the state name
@@ -231,11 +228,8 @@ function evaluateStation(station, esMode) {
let extraWeight = erp > weightedErp && station.distanceKm <= weightDistance ? 0.3 : 0;
let score = 0;
// If ERP is 1W, use a simpler formula to avoid zero-scoring.
if (erp === 0.001) {
score = erp / station.distanceKm;
} else {
score = ((10 * (Math.log10(erp * 1000))) / weightDistance) + extraWeight;
}
if (erp === 0.001) score = erp / station.distanceKm;
else score = ((10 * (Math.log10(erp * 1000))) / weightDistance) + extraWeight;
return score;
}
@@ -304,12 +298,8 @@ async function fetchTx(freq, piCode, rdsPs) {
loc => loc.distanceKm < 700 && loc.erp >= 10
);
let esMode = false;
if (!tropoPriority) {
esMode = checkEs();
}
for (let loc of filteredLocations) {
loc.score = evaluateStation(loc, esMode);
}
if (!tropoPriority) esMode = checkEs();
for (let loc of filteredLocations) loc.score = evaluateStation(loc, esMode);
// Sort by score in descending order
filteredLocations.sort((a, b) => b.score - a.score);
match = filteredLocations[0];
@@ -323,11 +313,9 @@ async function fetchTx(freq, piCode, rdsPs) {
}
if (match) {
if (match.itu === 'USA') {
if (match.itu == 'USA') { // Also known as Dumbfuckinstan. they should not go to hell, but hell+ (it is NOT better)
const state = getStateForCoordinates(match.lat, match.lon);
if (state) {
match.state = state; // Add state to matchingCity
}
if (state) match.state = state; // Add state to matchingCity
}
const result = {
station: match.detectedByPireg
@@ -360,9 +348,7 @@ function checkEs() {
const now = Date.now();
const url = "https://fmdx.org/includes/tools/get_muf.php";
if (esSwitchCache.lastCheck && now - esSwitchCache.lastCheck < esFetchInterval) {
return esSwitchCache.esSwitch;
}
if (esSwitchCache.lastCheck && now - esSwitchCache.lastCheck < esFetchInterval) return esSwitchCache.esSwitch;
if (Latitude > 20) {
esSwitchCache.lastCheck = now;
@@ -389,15 +375,12 @@ function haversine(lat1, lon1, lat2, lon2) {
const R = 6371;
const dLat = deg2rad(lat2 - lat1);
const dLon = deg2rad(lon2 - lon1);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const distance = R * c;
const y = Math.sin(dLon) * Math.cos(deg2rad(lat2));
const x = Math.cos(deg2rad(lat1)) * Math.sin(deg2rad(lat2)) -
Math.sin(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.cos(dLon);
const x = Math.cos(deg2rad(lat1)) * Math.sin(deg2rad(lat2)) - Math.sin(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.cos(dLon);
const azimuth = Math.atan2(y, x);
const azimuthDegrees = (azimuth * 180 / Math.PI + 360) % 360;