diff --git a/package.json b/package.json index 6bc1bdb..417e13a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fm-dx-webserver", - "version": "1.3.6.1", + "version": "1.3.7", "description": "FM DX Webserver", "main": "index.js", "scripts": { @@ -12,10 +12,10 @@ "author": "", "license": "ISC", "dependencies": { - "@mapbox/node-pre-gyp": "1.0.11", - "body-parser": "1.20.3", + "@mapbox/node-pre-gyp": "2.0.0", + "body-parser": "2.2.0", "ejs": "3.1.10", - "express": "4.21.2", + "express": "5.1.0", "express-session": "1.18.1", "ffmpeg-static": "5.2.0", "http": "0.0.1-security", @@ -23,6 +23,6 @@ "koffi": "2.7.2", "net": "1.0.2", "serialport": "12.0.0", - "ws": "8.18.0" + "ws": "8.18.1" } } diff --git a/server/endpoints.js b/server/endpoints.js index fa85f57..50ed624 100644 --- a/server/endpoints.js +++ b/server/endpoints.js @@ -179,21 +179,44 @@ router.get('/api', (req, res) => { }); +const loginAttempts = {}; // Format: { 'ip': { count: 1, lastAttempt: 1234567890 } } +const MAX_ATTEMPTS = 25; +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].count >= MAX_ATTEMPTS) { + return res.status(403).json({ + message: 'Too many login attempts. Please try again later.' + }); + } + const { password } = req.body; - - // Check if the entered password matches the admin password + + loginAttempts[ip].lastAttempt = now; + if (password === serverConfig.password.adminPass) { req.session.isAdminAuthenticated = true; req.session.isTuneAuthenticated = true; - logInfo('User from ' + req.connection.remoteAddress + ' logged in as an administrator.'); + logInfo(`User from ${ip} logged in as an administrator.`); + loginAttempts[ip].count = 0; next(); } else if (password === serverConfig.password.tunePass) { req.session.isAdminAuthenticated = false; req.session.isTuneAuthenticated = true; - logInfo('User from ' + req.connection.remoteAddress + ' logged in with tune permissions.'); + logInfo(`User from ${ip} logged in with tune permissions.`); + loginAttempts[ip].count = 0; next(); } else { + loginAttempts[ip].count += 1; res.status(403).json({ message: 'Login failed. Wrong password?' }); } }; diff --git a/server/helpers.js b/server/helpers.js index a6e2bef..4f7c98c 100644 --- a/server/helpers.js +++ b/server/helpers.js @@ -132,6 +132,7 @@ function fetchBannedAS(callback) { function processConnection(clientIp, locationInfo, currentUsers, ws, callback) { const options = { year: "numeric", month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit" }; const connectionTime = new Date().toLocaleString([], options); + const normalizedClientIp = clientIp?.replace(/^::ffff:/, ''); fetchBannedAS((error, bannedAS) => { if (error) { @@ -155,7 +156,7 @@ function processConnection(clientIp, locationInfo, currentUsers, ws, callback) { }); consoleCmd.logInfo( - `Web client \x1b[32mconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${userLocation}` + `Web client \x1b[32mconnected\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]\x1b[0m Location: ${userLocation}` ); callback("User allowed"); diff --git a/server/index.js b/server/index.js index 6fd0f3a..4ee3ef9 100644 --- a/server/index.js +++ b/server/index.js @@ -388,6 +388,7 @@ wss.on('connection', (ws, request) => { const output = serverConfig.xdrd.wirelessConnection ? client : serialport; let clientIp = request.headers['x-forwarded-for'] || request.connection.remoteAddress; const userCommandHistory = {}; + const normalizedClientIp = clientIp?.replace(/^::ffff:/, ''); if (serverConfig.webserver.banlist?.includes(clientIp)) { ws.close(1008, 'Banned IP'); @@ -509,7 +510,7 @@ wss.on('connection', (ws, request) => { } if (code !== 1008) { - logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${clientIp}) \x1b[90m[${currentUsers}]`); + logInfo(`Web client \x1b[31mdisconnected\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]`); } }); diff --git a/server/tx_search.js b/server/tx_search.js index cf48bcd..de31960 100644 --- a/server/tx_search.js +++ b/server/tx_search.js @@ -87,14 +87,11 @@ async function fetchTx(freq, piCode, rdsPs) { const now = Date.now(); freq = parseFloat(freq); - if (isNaN(freq)) { - return; - } + if (isNaN(freq)) return; if (now - lastFetchTime < fetchInterval || serverConfig.identification.lat.length < 2 || freq < 87 - || (currentPiCode == piCode && currentRdsPs == rdsPs)) - { + || (currentPiCode == piCode && currentRdsPs == rdsPs)) { return Promise.resolve(); } @@ -107,15 +104,33 @@ async function fetchTx(freq, piCode, rdsPs) { const url = "https://maps.fmdx.org/api/?freq=" + freq; try { - const response = await fetch(url, { redirect: 'manual' }); - if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`); - const data = await response.json(); + // Try POST first + const postResponse = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ freq }), // You can omit or customize this + redirect: 'manual' + }); + + if (!postResponse.ok) throw new Error(`POST failed: ${postResponse.status}`); + const data = await postResponse.json(); cachedData[freq] = data; - if(serverConfig.webserver.rdsMode == true) await loadUsStatesGeoJson(); + if (serverConfig.webserver.rdsMode === true) await loadUsStatesGeoJson(); return processData(data, piCode, rdsPs); - } catch (error) { - console.error("Error fetching data:", error); - return null; // Return null to indicate failure + } catch (postError) { + console.warn("POST failed, trying GET:", postError); + + try { + const getResponse = await fetch(url, { redirect: 'manual' }); + if (!getResponse.ok) throw new Error(`GET failed: ${getResponse.status}`); + const data = await getResponse.json(); + cachedData[freq] = data; + if (serverConfig.webserver.rdsMode === true) await loadUsStatesGeoJson(); + return processData(data, piCode, rdsPs); + } catch (getError) { + console.error("GET also failed:", getError); + return null; + } } } diff --git a/web/_components.ejs b/web/_components.ejs index 64f003e..bc60799 100644 --- a/web/_components.ejs +++ b/web/_components.ejs @@ -29,10 +29,13 @@ switch (component) { * @param cssClass Custom CSS class if needed */ case 'checkbox': -%> -
+ Current identity: +
+You are logged in as an adminstrator.
@@ -500,29 +529,6 @@- Current identity: -
-Legacy option for Linux / macOS that could resolve audio issues, but will consume additional CPU and RAM usage.
<%- include('_components', {component: 'checkbox', cssClass: '', label: 'Additional FFmpeg', id: 'audio-ffmpeg'}) %>Using a negative value could eliminate audio buffering issues during long periods of listening. However, a value that’s too low might increase the buffer over time.
+Using a negative value could eliminate audio buffering issues during long periods of listening.
+ However, a value that’s too low might increase the buffer over time.
Leave the IP at 0.0.0.0 unless you explicitly know you have to change it.
Don't enter your public IP here.
You can switch between American (RBDS) / Global (RDS) mode here.
<%- include('_components', {component: 'checkbox', cssClass: 'bottom-20', iconClass: '', label: 'American RDS mode (RBDS)', id: 'webserver-rdsMode'}) %>Different modes may help with more accurate transmitter identification depending on your region.
<%- include('_components', { component: 'dropdown', id: 'server-tx-id-algo', inputId: 'webserver-txIdAlgorithm', label: 'Transmitter ID Algorithm', cssClass: '', placeholder: 'Algorithm 1', @@ -553,9 +570,9 @@ When you become a supporter, you can message the Founders on Discord for your login details. <%- include('_components', {component: 'checkbox', cssClass: 'm-right-10', label: 'Enable tunnel', id: 'tunnel-enabled'}) %>Enabling low latency mode may provide better experience, however it will also use more bandwidth.
<%- include('_components', {component: 'checkbox', cssClass: 'm-right-10', label: 'Low latency mode', id: 'tunnel-lowLatencyMode'}) %>