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

some changes again

This commit is contained in:
2026-02-24 14:15:52 +01:00
parent 1d04719580
commit ee25214160
11 changed files with 99 additions and 139 deletions

View File

@@ -3,7 +3,7 @@ require('./server/index.js');
/** /**
* FM-DX Webserver * FM-DX Webserver
* *
* Github repo: https://github.com/NoobishSVK/fm-dx-webserver * Github repo: https://github.com/KubaPro010/fm-dx-webserver
* Server files: /server * Server files: /server
* Client files (web): /web * Client files (web): /web
* Plugin files: /plugins * Plugin files: /plugins

View File

@@ -63,17 +63,12 @@ function createChatServer(storage) {
delete messageData.ip; delete messageData.ip;
delete messageData.time; delete messageData.time;
if (messageData.nickname != null) { if (messageData.nickname != null) messageData.nickname = helpers.escapeHtml(String(messageData.nickname));
messageData.nickname = helpers.escapeHtml(String(messageData.nickname));
}
messageData.ip = clientIp; messageData.ip = clientIp;
const now = new Date(); const now = new Date();
messageData.time = messageData.time = String(now.getHours()).padStart(2, '0') + ":" + String(now.getMinutes()).padStart(2, '0');
String(now.getHours()).padStart(2, '0') +
":" +
String(now.getMinutes()).padStart(2, '0');
if (serverConfig.webserver.banlist?.includes(clientIp)) return; if (serverConfig.webserver.banlist?.includes(clientIp)) return;

View File

@@ -2,7 +2,7 @@
const RDSDecoder = require("./rds.js"); const RDSDecoder = require("./rds.js");
const { serverConfig } = require('./server_config'); const { serverConfig } = require('./server_config');
const { fetchTx } = require('./tx_search.js'); const fetchTx = require('./tx_search.js');
const updateInterval = 75; const updateInterval = 75;
// Initialize the data object // Initialize the data object

View File

@@ -15,7 +15,7 @@ const tunerProfiles = require('./tuner_profiles');
const { logInfo, logs } = require('./console'); const { logInfo, logs } = require('./console');
const dataHandler = require('./datahandler'); const dataHandler = require('./datahandler');
const fmdxList = require('./fmdx_list'); const fmdxList = require('./fmdx_list');
const { allPluginConfigs } = require('./plugins'); const allPluginConfigs = require('./plugins');
// Endpoints // Endpoints
router.get('/', (req, res) => { router.get('/', (req, res) => {

View File

@@ -1,3 +1,4 @@
const fs = require('fs');
const http = require('http'); const http = require('http');
const https = require('https'); const https = require('https');
const net = require('net'); const net = require('net');
@@ -5,7 +6,7 @@ const crypto = require('crypto');
const dataHandler = require('./datahandler'); const dataHandler = require('./datahandler');
const storage = require('./storage'); const storage = require('./storage');
const consoleCmd = require('./console'); const consoleCmd = require('./console');
const { serverConfig, configExists, configSave } = require('./server_config'); const { serverConfig, configSave } = require('./server_config');
function parseMarkdown(parsed) { function parseMarkdown(parsed) {
parsed = parsed.replace(/<\/?[^>]+(>|$)/g, ''); parsed = parsed.replace(/<\/?[^>]+(>|$)/g, '');
@@ -93,9 +94,7 @@ let bannedASCache = { data: null, timestamp: 0 };
function fetchBannedAS(callback) { function fetchBannedAS(callback) {
const now = Date.now(); const now = Date.now();
if (bannedASCache.data && now - bannedASCache.timestamp < 10 * 60 * 1000) { if (bannedASCache.data && now - bannedASCache.timestamp < 10 * 60 * 1000) return callback(null, bannedASCache.data);
return callback(null, bannedASCache.data);
}
const req = https.get("https://fmdx.org/banned_as.json", { family: 4 }, (banResponse) => { const req = https.get("https://fmdx.org/banned_as.json", { family: 4 }, (banResponse) => {
let banData = ""; let banData = "";
@@ -152,9 +151,7 @@ function processConnection(clientIp, locationInfo, currentUsers, ws, callback) {
} }
const userLocation = const userLocation =
locationInfo.country === undefined locationInfo.country === undefined ? "Unknown" : `${locationInfo.city}, ${locationInfo.regionName}, ${locationInfo.countryCode}`;
? "Unknown"
: `${locationInfo.city}, ${locationInfo.regionName}, ${locationInfo.countryCode}`;
storage.connectedUsers.push({ storage.connectedUsers.push({
ip: clientIp, ip: clientIp,
@@ -269,7 +266,7 @@ function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userC
// Check if there are 8 or more commands in the last 20 ms // Check if there are 8 or more commands in the last 20 ms
if (userCommandHistory[clientIp].length >= 8) { if (userCommandHistory[clientIp].length >= 8) {
consoleCmd.logWarn(`User \x1b[90m${clientIp}\x1b[0m is spamming with rapid commands. Connection will be terminated and user will be banned.`); consoleCmd.logWarn(`User \x1b[90m${clientIp}\x1b[0m is spamming with rapid commands. Connection will be terminated and user will be banned.`);
// Check if the normalized IP is already in the banlist // Check if the normalized IP is already in the banlist
const isAlreadyBanned = serverConfig.webserver.banlist.some(banEntry => banEntry[0] === normalizedClientIp); const isAlreadyBanned = serverConfig.webserver.banlist.some(banEntry => banEntry[0] === normalizedClientIp);
@@ -281,17 +278,15 @@ function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userC
configSave(); configSave();
} }
ws.close(1008, 'Bot-like behavior detected'); ws.close(1008, 'Bot-like behavior detected');
return command; // Return command value before closing connection return command; // Return command value before closing connection
} }
// Update the last message time for general spam detection // Update the last message time for general spam detection
lastMessageTime = now; lastMessageTime = now;
// Initialize command history for rate-limiting checks // Initialize command history for rate-limiting checks
if (!userCommands[command]) { if (!userCommands[command]) userCommands[command] = [];
userCommands[command] = [];
}
// Record the current timestamp for this command // Record the current timestamp for this command
userCommands[command].push(now); userCommands[command].push(now);
@@ -313,15 +308,45 @@ function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userC
} }
const escapeHtml = (unsafe) => { const escapeHtml = (unsafe) => {
return unsafe return unsafe.replace(/&/g, "&amp;")
.replace(/&/g, "&amp;") .replace(/</g, "&lt;").replace(/>/g, "&gt;")
.replace(/</g, "&lt;") .replace(/"/g, "&quot;").replace(/'/g, "&#039;");
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}; };
// Start plugins with delay
function startPluginsWithDelay(plugins, delay) {
plugins.forEach((pluginPath, index) => {
setTimeout(() => {
const pluginName = path.basename(pluginPath, '.js'); // Extract plugin name from path
logInfo(`-----------------------------------------------------------------`);
logInfo(`Plugin ${pluginName} loaded successfully!`);
require(pluginPath);
}, delay * index);
});
// Add final log line after all plugins are loaded
setTimeout(() => {
logInfo(`-----------------------------------------------------------------`);
}, delay * plugins.length);
}
// Function to find server files based on the plugins listed in config
function findServerFiles(plugins) {
let results = [];
plugins.forEach(plugin => {
// Remove .js extension if present
if (plugin.endsWith('.js')) plugin = plugin.slice(0, -3);
const pluginPath = path.join(__dirname, '..', 'plugins', `${plugin}_server.js`);
if (fs.existsSync(pluginPath) && fs.statSync(pluginPath).isFile()) results.push(pluginPath);
});
return results;
}
module.exports = { module.exports = {
authenticateWithXdrd, parseMarkdown, handleConnect, removeMarkdown, formatUptime, resolveDataBuffer, kickClient, checkIPv6Support, checkLatency, antispamProtection, escapeHtml authenticateWithXdrd, parseMarkdown, handleConnect,
removeMarkdown, formatUptime, resolveDataBuffer,
kickClient, checkIPv6Support, checkLatency,
antispamProtection, escapeHtml, findServerFiles,
startPluginsWithDelay
} }

View File

@@ -1,4 +1,3 @@
// Library imports
const express = require('express'); const express = require('express');
const endpoints = require('./endpoints'); const endpoints = require('./endpoints');
const session = require('express-session'); const session = require('express-session');
@@ -8,21 +7,16 @@ const readline = require('readline');
const app = express(); const app = express();
const httpServer = http.createServer(app); const httpServer = http.createServer(app);
const WebSocket = require('ws'); const WebSocket = require('ws');
const wss = new WebSocket.Server({ noServer: true, perMessageDeflate: true });
const rdsWss = new WebSocket.Server({ noServer: true });
const pluginsWss = new WebSocket.Server({ noServer: true, perMessageDeflate: true });
const fs = require('fs');
const path = require('path'); const path = require('path');
const net = require('net'); const net = require('net');
const client = new net.Socket();
const { SerialPort } = require('serialport'); const { SerialPort } = require('serialport');
const tunnel = require('./tunnel'); const tunnel = require('./tunnel');
const { createChatServer } = require('./chat'); const { createChatServer } = require('./chat');
const { createAudioServer } = require('./stream/ws.js');
const figlet = require('figlet'); const figlet = require('figlet');
// File imports
const helpers = require('./helpers'); const helpers = require('./helpers');
const { findServerFiles, startPluginsWithDelay } = helpers;
const dataHandler = require('./datahandler'); const dataHandler = require('./datahandler');
const fmdxList = require('./fmdx_list'); const fmdxList = require('./fmdx_list');
const { logError, logInfo, logWarn } = require('./console'); const { logError, logInfo, logWarn } = require('./console');
@@ -31,35 +25,10 @@ const { serverConfig, configExists } = require('./server_config');
const pluginsApi = require('./plugins_api'); const pluginsApi = require('./plugins_api');
const pjson = require('../package.json'); const pjson = require('../package.json');
// Function to find server files based on the plugins listed in config const client = new net.Socket();
function findServerFiles(plugins) { const wss = new WebSocket.Server({ noServer: true, perMessageDeflate: true });
let results = []; const rdsWss = new WebSocket.Server({ noServer: true });
plugins.forEach(plugin => { const pluginsWss = new WebSocket.Server({ noServer: true, perMessageDeflate: true });
// Remove .js extension if present
if (plugin.endsWith('.js')) plugin = plugin.slice(0, -3);
const pluginPath = path.join(__dirname, '..', 'plugins', `${plugin}_server.js`);
if (fs.existsSync(pluginPath) && fs.statSync(pluginPath).isFile()) results.push(pluginPath);
});
return results;
}
// Start plugins with delay
function startPluginsWithDelay(plugins, delay) {
plugins.forEach((pluginPath, index) => {
setTimeout(() => {
const pluginName = path.basename(pluginPath, '.js'); // Extract plugin name from path
logInfo(`-----------------------------------------------------------------`);
logInfo(`Plugin ${pluginName} loaded successfully!`);
require(pluginPath);
}, delay * index);
});
// Add final log line after all plugins are loaded
setTimeout(() => {
logInfo(`-----------------------------------------------------------------`);
}, delay * plugins.length);
}
// Get all plugins from config and find corresponding server files // Get all plugins from config and find corresponding server files
const plugins = findServerFiles(serverConfig.plugins); const plugins = findServerFiles(serverConfig.plugins);
@@ -76,22 +45,12 @@ const terminalWidth = readline.createInterface({
output: process.stdout output: process.stdout
}).output.columns; }).output.columns;
console.log('\x1b[32m' + figlet.textSync("FM-DX Webserver"));
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('\x1b[32m\x1b[2mby Noobish @ \x1b[4mFMDX.org\x1b[0m');
console.log("v" + pjson.version) console.log("v" + pjson.version)
console.log('\x1b[90m' + '─'.repeat(terminalWidth - 1) + '\x1b[0m'); console.log('\x1b[90m' + '─'.repeat(terminalWidth - 1) + '\x1b[0m');
const chatWss = createChatServer(storage); const audioWss = require('./stream/ws.js');
const audioWss = createAudioServer();
// Start ffmpeg
require('./stream/index'); require('./stream/index');
require('./plugins'); require('./plugins');
@@ -107,6 +66,7 @@ const sessionMiddleware = session({
}); });
app.use(sessionMiddleware); app.use(sessionMiddleware);
app.use(bodyParser.json()); app.use(bodyParser.json());
const chatWss = createChatServer(storage);
connectToXdrd(); connectToXdrd();
connectToSerial(); connectToSerial();
@@ -237,9 +197,7 @@ client.on('data', (data) => {
const { xdrd } = serverConfig; const { xdrd } = serverConfig;
helpers.resolveDataBuffer(data, wss, rdsWss); helpers.resolveDataBuffer(data, wss, rdsWss);
if (authFlags.authMsg == true && authFlags.messageCount > 1) { if (authFlags.authMsg == true && authFlags.messageCount > 1) return;
return;
}
authFlags.messageCount++; authFlags.messageCount++;
const receivedData = data.toString(); const receivedData = data.toString();

View File

@@ -93,6 +93,4 @@ function createLinks() {
const allPluginConfigs = collectPluginConfigs(); const allPluginConfigs = collectPluginConfigs();
createLinks(); createLinks();
module.exports = { module.exports = allPluginConfigs;
allPluginConfigs
};

View File

@@ -93,8 +93,8 @@ class RDSDecoder {
this.ps[idx * 2] = String.fromCharCode(blockD >> 8); this.ps[idx * 2] = String.fromCharCode(blockD >> 8);
this.ps[idx * 2 + 1] = String.fromCharCode(blockD & 0xFF); this.ps[idx * 2 + 1] = String.fromCharCode(blockD & 0xFF);
this.ps_errors[idx * 2] = error; this.ps_errors[idx * 2] = Math.ceil(d_error * (10/3));
this.ps_errors[idx * 2 + 1] = error; this.ps_errors[idx * 2 + 1] = Math.ceil(d_error * (10/3));
this.data.ps = this.ps.join(''); this.data.ps = this.ps.join('');
this.data.ps_errors = this.ps_errors.join(','); this.data.ps_errors = this.ps_errors.join(',');
@@ -124,15 +124,15 @@ class RDSDecoder {
if(c_error < 2 && multiplier !== 2) { if(c_error < 2 && multiplier !== 2) {
this.rt1[idx * multiplier] = String.fromCharCode(blockC >> 8); this.rt1[idx * multiplier] = String.fromCharCode(blockC >> 8);
this.rt1[idx * multiplier + 1] = String.fromCharCode(blockC & 0xFF); this.rt1[idx * multiplier + 1] = String.fromCharCode(blockC & 0xFF);
this.rt1_errors[idx * multiplier] = error; this.rt1_errors[idx * multiplier] = Math.ceil(c_error * (10/3));
this.rt1_errors[idx * multiplier + 1] = error; this.rt1_errors[idx * multiplier + 1] = Math.ceil(c_error * (10/3));
} }
if(d_error < 2) { if(d_error < 2) {
var offset = (multiplier == 2) ? 0 : 2; var offset = (multiplier == 2) ? 0 : 2;
this.rt1[idx * multiplier + offset] = String.fromCharCode(blockD >> 8); this.rt1[idx * multiplier + offset] = String.fromCharCode(blockD >> 8);
this.rt1[idx * multiplier + offset + 1] = String.fromCharCode(blockD & 0xFF); this.rt1[idx * multiplier + offset + 1] = String.fromCharCode(blockD & 0xFF);
this.rt1_errors[idx * multiplier + offset] = error; this.rt1_errors[idx * multiplier + offset] = Math.ceil(d_error * (10/3));
this.rt1_errors[idx * multiplier + offset + 1] = error; this.rt1_errors[idx * multiplier + offset + 1] = Math.ceil(d_error * (10/3));
} }
var i = this.rt1.indexOf("\r") var i = this.rt1.indexOf("\r")
@@ -155,15 +155,15 @@ class RDSDecoder {
if(c_error !== 3 && multiplier !== 2) { if(c_error !== 3 && multiplier !== 2) {
this.rt0[idx * multiplier] = String.fromCharCode(blockC >> 8); this.rt0[idx * multiplier] = String.fromCharCode(blockC >> 8);
this.rt0[idx * multiplier + 1] = String.fromCharCode(blockC & 0xFF); this.rt0[idx * multiplier + 1] = String.fromCharCode(blockC & 0xFF);
this.rt0_errors[idx * multiplier] = error; this.rt0_errors[idx * multiplier] = Math.ceil(c_error * (10/3));
this.rt0_errors[idx * multiplier + 1] = error; this.rt0_errors[idx * multiplier + 1] = Math.ceil(c_error * (10/3));
} }
if(d_error !== 3) { if(d_error !== 3) {
var offset = (multiplier == 2) ? 0 : 2; var offset = (multiplier == 2) ? 0 : 2;
this.rt0[idx * multiplier + offset] = String.fromCharCode(blockD >> 8); this.rt0[idx * multiplier + offset] = String.fromCharCode(blockD >> 8);
this.rt0[idx * multiplier + offset + 1] = String.fromCharCode(blockD & 0xFF); this.rt0[idx * multiplier + offset + 1] = String.fromCharCode(blockD & 0xFF);
this.rt0_errors[idx * multiplier + offset] = error; this.rt0_errors[idx * multiplier + offset] = Math.ceil(d_error * (10/3));
this.rt0_errors[idx * multiplier + offset + 1] = error; this.rt0_errors[idx * multiplier + offset + 1] = Math.ceil(d_error * (10/3));
} }
var i = this.rt0.indexOf("\r"); var i = this.rt0.indexOf("\r");

View File

@@ -21,7 +21,7 @@ checkFFmpeg().then((ffmpegPath) => {
logInfo(`${consoleLogTitle} Using ${ffmpegPath === 'ffmpeg' ? 'system-installed FFmpeg' : 'ffmpeg-static'}`); logInfo(`${consoleLogTitle} Using ${ffmpegPath === 'ffmpeg' ? 'system-installed FFmpeg' : 'ffmpeg-static'}`);
logInfo(`${consoleLogTitle} Starting audio stream on device: \x1b[35m${serverConfig.audio.audioDevice}\x1b[0m`); logInfo(`${consoleLogTitle} Starting audio stream on device: \x1b[35m${serverConfig.audio.audioDevice}\x1b[0m`);
const sampleRate = Number(serverConfig.audio.sampleRate || 44100) + Number(serverConfig.audio.samplerateOffset || 0); const sampleRate = Number(serverConfig.audio.sampleRate || 44100) + Number(serverConfig.audio.samplerateOffset || 0); // Maybe even do 32 khz, we do not need higher than 15 khz precision
const channels = Number(serverConfig.audio.audioChannels || 2); const channels = Number(serverConfig.audio.audioChannels || 2);
@@ -139,4 +139,4 @@ checkFFmpeg().then((ffmpegPath) => {
logError(`${consoleLogTitle} Error: ${err.message}`); logError(`${consoleLogTitle} Error: ${err.message}`);
}); });
module.exports.audio_pipe = audio_pipe; module.exports = audio_pipe;

View File

@@ -1,32 +1,28 @@
const WebSocket = require('ws'); const WebSocket = require('ws');
const { serverConfig } = require('../server_config'); const { serverConfig } = require('../server_config');
const { audio_pipe } = require('./index.js'); const audio_pipe = require('./index.js');
function createAudioServer() { const audioWss = new WebSocket.Server({ noServer: true, skipUTF8Validation: true });
const audioWss = new WebSocket.Server({ noServer: true });
audioWss.on('connection', (ws, request) => { audioWss.on('connection', (ws, request) => {
const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress; const clientIp = request.headers['x-forwarded-for'] || request.socket.remoteAddress;
if (serverConfig.webserver.banlist?.includes(clientIp)) { if (serverConfig.webserver.banlist?.includes(clientIp)) {
ws.close(1008, 'Banned IP'); ws.close(1008, 'Banned IP');
return; return;
} }
});
audio_pipe.on('data', (chunk) => {
audioWss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) client.send(chunk, {binary: true, compress: false});
}); });
});
audio_pipe.on('data', (chunk) => { audio_pipe.on('end', () => {
audioWss.clients.forEach((client) => { audioWss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) client.send(chunk, {binary: true, compress: false }); client.close(1001, "Audio stream ended");
});
}); });
});
audio_pipe.on('end', () => { module.exports = audioWss;
audioWss.clients.forEach((client) => {
client.close(1001, "Audio stream ended");
});
});
return audioWss;
}
module.exports = { createAudioServer };

View File

@@ -80,9 +80,7 @@ async function buildTxDatabase() {
consoleCmd.logInfo('Fetching transmitter database...'); consoleCmd.logInfo('Fetching transmitter database...');
const response = await fetch(`https://maps.fmdx.org/api?qth=${serverConfig.identification.lat},${serverConfig.identification.lon}`, { const response = await fetch(`https://maps.fmdx.org/api?qth=${serverConfig.identification.lat},${serverConfig.identification.lon}`, {
method: 'GET', method: 'GET',
headers: { headers: {'Accept': 'application/json'}
'Accept': 'application/json'
}
}); });
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);
localDb = await response.json(); localDb = await response.json();
@@ -169,9 +167,7 @@ function getStateForCoordinates(lat, lon) {
for (const feature of usStatesGeoJson.features) { for (const feature of usStatesGeoJson.features) {
const boundingBox = getStateBoundingBox(feature.geometry.coordinates); const boundingBox = getStateBoundingBox(feature.geometry.coordinates);
if (isCityInState(lat, lon, boundingBox)) { if (isCityInState(lat, lon, boundingBox)) return feature.properties.name; // Return the state's name if city is inside bounding box
return feature.properties.name; // Return the state's name if city is inside bounding box
}
} }
return null; return null;
} }
@@ -208,22 +204,16 @@ function validPsCompare(rdsPs, stationPs) {
for (let i = 0; i < standardizedRdsPs.length; i++) { for (let i = 0; i < standardizedRdsPs.length; i++) {
// Skip this position if the character in standardizedRdsPs is an underscore. // Skip this position if the character in standardizedRdsPs is an underscore.
if (standardizedRdsPs[i] === '_') continue; if (standardizedRdsPs[i] === '_') continue;
if (token[i] === standardizedRdsPs[i]) { if (token[i] === standardizedRdsPs[i]) matchCount++;
matchCount++;
}
}
if (matchCount >= minMatchLen) {
return true;
} }
if (matchCount >= minMatchLen) return true;
} }
return false; return false;
} }
function evaluateStation(station, esMode) { function evaluateStation(station, esMode) {
let weightDistance = station.distanceKm; let weightDistance = station.distanceKm;
if (esMode && station.distanceKm > 700) { if (esMode && station.distanceKm > 700) weightDistance = Math.abs(station.distanceKm - 1500) + 200;
weightDistance = Math.abs(station.distanceKm - 1500) + 200;
}
let erp = station.erp && station.erp > 0 ? station.erp : 1; let erp = station.erp && station.erp > 0 ? station.erp : 1;
let extraWeight = erp > weightedErp && station.distanceKm <= weightDistance ? 0.3 : 0; let extraWeight = erp > weightedErp && station.distanceKm <= weightDistance ? 0.3 : 0;
let score = 0; let score = 0;
@@ -394,6 +384,4 @@ function deg2rad(deg) {
return deg * (Math.PI / 180); return deg * (Math.PI / 180);
} }
module.exports = { module.exports = fetchTx;
fetchTx
};