From 2ab7dd33dd334bc2dbbcff33996aa0096ca6796b Mon Sep 17 00:00:00 2001 From: Amateur Audio Dude <168192910+AmateurAudioDude@users.noreply.github.com> Date: Mon, 7 Jul 2025 22:38:20 +1000 Subject: [PATCH] add per-ip limit --- server/index.js | 67 +++++++++++++++++++++++++++++++++++++++++++-- web/js/main.js | 13 +++++++-- web/js/websocket.js | 53 ++++++++++++++++++----------------- 3 files changed, 101 insertions(+), 32 deletions(-) diff --git a/server/index.js b/server/index.js index d9ea23e..4cfae4c 100644 --- a/server/index.js +++ b/server/index.js @@ -383,6 +383,24 @@ function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userC * WEBSOCKET BLOCK */ const tunerLockTracker = new WeakMap(); +const ipConnectionCounts = new Map(); // Per-IP limit variables +const ipLogTimestamps = new Map(); +const MAX_CONNECTIONS_PER_IP = 5; +const IP_LOG_INTERVAL_MS = 60000; +// Remove old per-IP limit addresses +setInterval(() => { + const now = Date.now(); + + for (const [ip, count] of ipConnectionCounts.entries()) { + const lastSeen = ipLogTimestamps.get(ip) || 0; + const inactive = now - lastSeen > 60 * 60 * 1000; + + if (count === 0 && inactive) { + ipConnectionCounts.delete(ip); + ipLogTimestamps.delete(ip); + } + } +}, 60 * 60 * 1000); // Run every hour wss.on('connection', (ws, request) => { const output = serverConfig.xdrd.wirelessConnection ? client : serialport; @@ -390,15 +408,44 @@ wss.on('connection', (ws, request) => { const userCommandHistory = {}; const normalizedClientIp = clientIp?.replace(/^::ffff:/, ''); - if (serverConfig.webserver.banlist?.includes(clientIp)) { + if (clientIp && serverConfig.webserver.banlist?.includes(clientIp)) { ws.close(1008, 'Banned IP'); return; } - if (clientIp.includes(',')) { + if (clientIp && clientIp.includes(',')) { clientIp = clientIp.split(',')[0].trim(); } + // 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.') + ); + if (!isLocalIp) { + 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'); + const lastLogTime = ipLogTimestamps.get(clientIp) || 0; + const now = Date.now(); + if (now - lastLogTime > IP_LOG_INTERVAL_MS) { + logWarn(`Web client \x1b[31mclosed: limit exceeded\x1b[0m (${normalizedClientIp}) \x1b[90m[${currentUsers}]`); + ipLogTimestamps.set(clientIp, now); + } + return; + } + ipConnectionCounts.set(clientIp, currentCount + 1); + } + } + if (clientIp !== '::ffff:127.0.0.1' || (request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.0.1') || (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) { currentUsers++; } @@ -472,6 +519,22 @@ wss.on('connection', (ws, request) => { }); ws.on('close', (code, reason) => { + // Per-IP limit connection closed + 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.') + ); + if (!isLocalIp) { + const current = ipConnectionCounts.get(clientIp) || 1; + ipConnectionCounts.set(clientIp, Math.max(0, current - 1)); + } + } + if (clientIp !== '::ffff:127.0.0.1' || (request.connection && request.connection.remoteAddress && request.connection.remoteAddress !== '::ffff:127.0.0.1') || (request.headers && request.headers['origin'] && request.headers['origin'].trim() !== '')) { currentUsers--; } diff --git a/web/js/main.js b/web/js/main.js index 532bce2..ffde72d 100644 --- a/web/js/main.js +++ b/web/js/main.js @@ -6,6 +6,7 @@ var parsedData, signalChart, previousFreq; var data = []; var signalData = []; let updateCounter = 0; +let lastReconnectAttempt = 0; let messageCounter = 0; // Count for WebSocket data length returning 0 let messageData = 800; // Initial value anything above 0 let messageLength = 800; // Retain value of messageData until value is updated @@ -375,10 +376,16 @@ function sendPingRequest() { messageCounter = 0; } - // Automatic reconnection on WebSocket close - if (socket.readyState === WebSocket.CLOSED || socket.readyState === WebSocket.CLOSING) { + // Automatic reconnection on WebSocket close with cooldown + const now = Date.now(); + if ( + (socket.readyState === WebSocket.CLOSED || socket.readyState === WebSocket.CLOSING) && + (now - lastReconnectAttempt > TIMEOUT_DURATION) + ) { + lastReconnectAttempt = now; + socket = new WebSocket(socketAddress); - + socket.onopen = () => { sendToast('info', 'Connected', 'Reconnected successfully!', false, false); }; diff --git a/web/js/websocket.js b/web/js/websocket.js index d965d33..a120ce4 100644 --- a/web/js/websocket.js +++ b/web/js/websocket.js @@ -1,30 +1,29 @@ -var url = new URL('text', window.location.href); -url.protocol = url.protocol.replace('http', 'ws'); -var socketAddress = url.href; -var socket = new WebSocket(socketAddress); +if (!window.socket || window.socket.readyState === WebSocket.CLOSED || window.socket.readyState === WebSocket.CLOSING) { + var url = new URL('text', window.location.href); + url.protocol = url.protocol.replace('http', 'ws'); + var socketAddress = url.href; + var socket = new WebSocket(socketAddress); -const socketPromise = new Promise((resolve, reject) => { - // Event listener for when the WebSocket connection is open - socket.addEventListener('open', () => { - console.log('WebSocket connection open'); - resolve(socket); // Resolve the promise with the WebSocket instance + window.socket = socket; + + const socketPromise = new Promise((resolve, reject) => { + socket.addEventListener('open', () => { + console.log('WebSocket connection open'); + resolve(socket); + }); + + socket.addEventListener('error', (error) => { + console.error('WebSocket error', error); + reject(error); + }); + + socket.addEventListener('close', () => { + setTimeout(() => { + console.warn('WebSocket connection closed'); + }, 100); + reject(new Error('WebSocket connection closed')); + }); }); - // Event listener for WebSocket errors - socket.addEventListener('error', (error) => { - console.error('WebSocket error', error); - reject(error); // Reject the promise on error - }); - - // Event listener for WebSocket connection closure - socket.addEventListener('close', () => { - console.warn('WebSocket connection closed'); - reject(new Error('WebSocket connection closed')); // Reject with closure warning - }); -}); - -// Assign the socketPromise to window.socketPromise for global access -window.socketPromise = socketPromise; - -// Assign the socket instance to window.socket for global access -window.socket = socket; + window.socketPromise = socketPromise; +}