1
0
mirror of https://github.com/KubaPro010/fm-dx-webserver.git synced 2026-02-27 14:33:52 +01:00

Merge pull request #159 from AmateurAudioDude/fixes/v1.3.9-per-ip-attack-limit

Add per-ip limit
This commit is contained in:
Marek Farkaš
2025-08-30 17:29:37 +02:00
committed by GitHub
3 changed files with 99 additions and 30 deletions

View File

@@ -376,6 +376,24 @@ function antispamProtection(message, clientIp, ws, userCommands, lastWarn, userC
* WEBSOCKET BLOCK * WEBSOCKET BLOCK
*/ */
const tunerLockTracker = new WeakMap(); 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) => { wss.on('connection', (ws, request) => {
const output = serverConfig.xdrd.wirelessConnection ? client : serialport; const output = serverConfig.xdrd.wirelessConnection ? client : serialport;
@@ -392,6 +410,35 @@ wss.on('connection', (ws, request) => {
clientIp = clientIp.split(',')[0].trim(); 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() !== '')) { 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++; currentUsers++;
} }
@@ -465,6 +512,22 @@ wss.on('connection', (ws, request) => {
}); });
ws.on('close', (code, reason) => { 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() !== '')) { 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--; currentUsers--;
} }

View File

@@ -6,6 +6,7 @@ var parsedData, signalChart, previousFreq;
var data = []; var data = [];
var signalData = []; var signalData = [];
let updateCounter = 0; let updateCounter = 0;
let lastReconnectAttempt = 0;
let messageCounter = 0; // Count for WebSocket data length returning 0 let messageCounter = 0; // Count for WebSocket data length returning 0
let messageData = 800; // Initial value anything above 0 let messageData = 800; // Initial value anything above 0
let messageLength = 800; // Retain value of messageData until value is updated let messageLength = 800; // Retain value of messageData until value is updated
@@ -375,10 +376,16 @@ function sendPingRequest() {
messageCounter = 0; messageCounter = 0;
} }
// Automatic reconnection on WebSocket close // Automatic reconnection on WebSocket close with cooldown
if (socket.readyState === WebSocket.CLOSED || socket.readyState === WebSocket.CLOSING) { const now = Date.now();
if (
(socket.readyState === WebSocket.CLOSED || socket.readyState === WebSocket.CLOSING) &&
(now - lastReconnectAttempt > TIMEOUT_DURATION)
) {
lastReconnectAttempt = now;
socket = new WebSocket(socketAddress); socket = new WebSocket(socketAddress);
socket.onopen = () => { socket.onopen = () => {
sendToast('info', 'Connected', 'Reconnected successfully!', false, false); sendToast('info', 'Connected', 'Reconnected successfully!', false, false);
}; };

View File

@@ -1,30 +1,29 @@
var url = new URL('text', window.location.href); if (!window.socket || window.socket.readyState === WebSocket.CLOSED || window.socket.readyState === WebSocket.CLOSING) {
url.protocol = url.protocol.replace('http', 'ws'); var url = new URL('text', window.location.href);
var socketAddress = url.href; url.protocol = url.protocol.replace('http', 'ws');
var socket = new WebSocket(socketAddress); var socketAddress = url.href;
var socket = new WebSocket(socketAddress);
const socketPromise = new Promise((resolve, reject) => { window.socket = socket;
// Event listener for when the WebSocket connection is open
socket.addEventListener('open', () => { const socketPromise = new Promise((resolve, reject) => {
console.log('WebSocket connection open'); socket.addEventListener('open', () => {
resolve(socket); // Resolve the promise with the WebSocket instance 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 window.socketPromise = socketPromise;
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;