Various bug fixes and small additions

IMPORTANT:
This commit contains new config options.
Make sure your local config file is up to date with config.example.toml to avoid issues.

CHANGELOG:
- Fix crash exploit caused by abnormally long WebSocket messages
- Added config option for limiting the maximum amount of active connections from the same IP
- Made per-IP turn limiter configurable
- Fixed an issue where vote updates were not sent to clients in some cases
- Added warning about using qmpSockDir on Windows hosts
- Updated dependencies
- Removed unused code/imports
This commit is contained in:
MDMCK10
2023-09-12 00:25:57 +02:00
parent d80d2c8ed9
commit 43fddbc521
10 changed files with 85 additions and 43 deletions

View File

@@ -11,9 +11,9 @@ import Queue from 'mnemonist/queue.js';
import { createHash } from 'crypto';
import { isIP } from 'net';
import QEMUVM from './QEMUVM.js';
import { Canvas, createCanvas, CanvasRenderingContext2D } from 'canvas';
import { Canvas, createCanvas } from 'canvas';
import { IPData } from './IPData.js';
import { read, readFileSync } from 'fs';
import { readFileSync } from 'fs';
import log from './log.js';
import VM from './VM.js';
import { fileURLToPath } from 'url';
@@ -32,19 +32,17 @@ export default class WSServer {
// Time remaining on the current turn
private TurnTime : number;
// Interval to keep track of the current turn time
private TurnInterval? : NodeJS.Timer;
// Is the turn interval running?
private TurnIntervalRunning : boolean;
private TurnInterval? : NodeJS.Timeout;
// If a reset vote is in progress
private voteInProgress : boolean;
// Interval to keep track of vote resets
private voteInterval? : NodeJS.Timer;
private voteInterval? : NodeJS.Timeout;
// How much time is left on the vote
private voteTime : number;
// How much time until another reset vote can be cast
private voteCooldown : number;
// Interval to keep track
private voteCooldownInterval? : NodeJS.Timer;
private voteCooldownInterval? : NodeJS.Timeout;
// Completely disable turns
private turnsAllowed : boolean;
// Hide the screen
@@ -61,7 +59,6 @@ export default class WSServer {
this.ChatHistory = new CircularBuffer<{user:string,msg:string}>(Array, this.Config.collabvm.maxChatHistoryLength);
this.TurnQueue = new Queue<User>();
this.TurnTime = 0;
this.TurnIntervalRunning = false;
this.clients = [];
this.ips = [];
this.voteInProgress = false;
@@ -140,7 +137,7 @@ export default class WSServer {
// Get the first IP from the X-Forwarded-For variable
_ip = req.headers["x-forwarded-for"]?.toString().replace(/\ /g, "").split(",")[0];
} catch {
// If we can't get the ip, kill the connection
// If we can't get the IP, kill the connection
killConnection();
return;
}
@@ -149,7 +146,7 @@ export default class WSServer {
killConnection();
return;
}
// Make sure the ip is valid. If not, kill the connection.
// Make sure the IP is valid. If not, kill the connection.
if (!isIP(_ip)) {
killConnection();
return;
@@ -157,11 +154,8 @@ export default class WSServer {
//@ts-ignore
req.proxiedIP = _ip;
}
this.socket.handleUpgrade(req, socket, head, (ws) => this.socket.emit('connection', ws, req));
}
private onConnection(ws : WebSocket, req : http.IncomingMessage) {
var ip: string;
let ip: string;
if (this.Config.http.proxying) {
//@ts-ignore
if (!req.proxiedIP) return;
@@ -172,23 +166,45 @@ export default class WSServer {
ip = req.socket.remoteAddress;
}
var _ipdata = this.ips.filter(data => data.address == ip);
//@ts-ignore
req.IP = ip;
// Get the amount of active connections coming from the requesting IP.
let connections = this.clients.filter(client => client.IP.address == ip);
// If it exceeds the limit set in the config, reject the connection with a 429.
if(connections.length + 1 > this.Config.http.maxConnections) {
socket.write("HTTP/1.1 429 Too Many Requests\n\n429 Too Many Requests");
socket.destroy();
}
this.socket.handleUpgrade(req, socket, head, (ws: WebSocket) => this.socket.emit('connection', ws, req));
}
private onConnection(ws : WebSocket, req: http.IncomingMessage) {
//@ts-ignore
var _ipdata = this.ips.filter(data => data.address == req.IP);
var ipdata;
if(_ipdata.length > 0) {
ipdata = _ipdata[0];
}else{
ipdata = new IPData(ip);
//@ts-ignore
ipdata = new IPData(req.IP);
this.ips.push(ipdata);
}
var user = new User(ws, ipdata, this.Config);
this.clients.push(user);
ws.on('error', (e) => {
//@ts-ignore
log("ERROR", `${e} (caused by connection ${req.IP})`);
ws.close();
});
ws.on('close', () => this.connectionClosed(user));
ws.on('message', (e) => {
var msg;
try {msg = e.toString()}
catch {
// Fuck the user off if they send a non-string message
// Close the user's connection if they send a non-string message
user.closeConnection();
return;
}
@@ -199,7 +215,10 @@ export default class WSServer {
};
private connectionClosed(user : User) {
if(user.IP.vote != null) user.IP.vote = null;
if(user.IP.vote != null) {
user.IP.vote = null;
this.sendVoteUpdate();
};
if (this.indefiniteTurn === user) this.indefiniteTurn = null;
this.clients.splice(this.clients.indexOf(user), 1);
log("INFO", `Disconnect From ${user.IP.address}${user.username ? ` with username ${user.username}` : ""}`);
@@ -322,13 +341,17 @@ export default class WSServer {
}
if (takingTurn) {
var currentQueue = this.TurnQueue.toArray();
// If the user is already in the queue, fuck them off
// If the user is already in the turn queue, ignore the turn request.
if (currentQueue.indexOf(client) !== -1) return;
// If they're muted, also fuck them off.
// If they're muted, also ignore the turn request.
// Send them the turn queue to prevent client glitches
if (client.IP.muted) return;
// Only allow one active turn per IP address
if(currentQueue.find(user => user.IP.address == client.IP.address)) return;
if(this.Config.collabvm.turnlimit.enabled) {
// Get the amount of users in the turn queue with the same IP as the user requesting a turn.
let turns = currentQueue.filter(user => user.IP.address == client.IP.address);
// If it exceeds the limit set in the config, ignore the turn request.
if(turns.length + 1 > this.Config.collabvm.turnlimit.maximum) return;
}
this.TurnQueue.enqueue(client);
if (this.TurnQueue.size === 1) this.nextTurn();
} else {
@@ -684,7 +707,6 @@ export default class WSServer {
private nextTurn() {
clearInterval(this.TurnInterval);
if (this.TurnQueue.size === 0) {
this.TurnIntervalRunning = false;
} else {
this.TurnTime = this.Config.collabvm.turnTime;
this.TurnInterval = setInterval(() => this.turnInterval(), 1000);
@@ -694,7 +716,6 @@ export default class WSServer {
clearTurns() {
clearInterval(this.TurnInterval);
this.TurnIntervalRunning = false;
this.TurnQueue.clear();
this.sendTurnUpdate();
}