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

@@ -6,6 +6,7 @@ export default interface IConfig {
proxyAllowedIps : string[];
origin : boolean;
originAllowedDomains : string[];
maxConnections: number;
};
vm : {
qemuArgs : string;
@@ -24,6 +25,10 @@ export default interface IConfig {
usernameblacklist : string[];
maxChatLength : number;
maxChatHistoryLength : number;
turnlimit : {
enabled: boolean,
maximum: number;
};
automute : {
enabled: boolean;
seconds: number;

View File

@@ -1,5 +1,5 @@
export class IPData {
tempMuteExpireTimeout? : NodeJS.Timer;
tempMuteExpireTimeout? : NodeJS.Timeout;
muted: Boolean;
vote: boolean | null;
address: string;

View File

@@ -1,8 +1,7 @@
import { EventEmitter } from "events";
import IConfig from "./IConfig.js";
import * as rfb from 'rfb2';
import * as fs from 'fs';
import { execa, ExecaChildProcess, execaCommand } from "execa";
import { ExecaChildProcess, execaCommand } from "execa";
import QMPClient from "./QMPClient.js";
import BatchRects from "./RectBatcher.js";
import { createCanvas, Canvas, CanvasRenderingContext2D, createImageData } from "canvas";
@@ -25,13 +24,13 @@ export default class QEMUVM extends VM {
processRestartErrorLevel : number;
expectedExit : boolean;
vncOpen : boolean;
vncUpdateInterval? : NodeJS.Timer;
vncUpdateInterval? : NodeJS.Timeout;
rects : {height:number,width:number,x:number,y:number,data:Buffer}[];
rectMutex : Mutex;
vncReconnectTimeout? : NodeJS.Timer;
qmpReconnectTimeout? : NodeJS.Timer;
qemuRestartTimeout? : NodeJS.Timer;
vncReconnectTimeout? : NodeJS.Timeout;
qmpReconnectTimeout? : NodeJS.Timeout;
qemuRestartTimeout? : NodeJS.Timeout;
constructor(Config : IConfig) {
super();

View File

@@ -5,7 +5,7 @@ export default class RateLimiter extends EventEmitter {
private limit : number;
private interval : number;
private requestCount : number;
private limiter? : NodeJS.Timer;
private limiter? : NodeJS.Timeout;
private limiterSet : boolean;
constructor(limit : number, interval : number) {
super();

View File

@@ -7,9 +7,9 @@ import RateLimiter from './RateLimiter.js';
import { execaCommand } from 'execa';
export class User {
socket : WebSocket;
nopSendInterval : NodeJS.Timer;
msgRecieveInterval : NodeJS.Timer;
nopRecieveTimeout? : NodeJS.Timer;
nopSendInterval : NodeJS.Timeout;
msgRecieveInterval : NodeJS.Timeout;
nopRecieveTimeout? : NodeJS.Timeout;
username? : string;
connectedToNode : boolean;
viewMode : number;

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();
}

View File

@@ -25,6 +25,15 @@ try {
async function start() {
// Print a warning if qmpSockDir is set
// and the host OS is Windows, as this
// configuration will very likely not work.
if(process.platform === "win32" && Config.vm.qmpSockDir) {
log("WARN", "You appear to have the option 'qmpSockDir' enabled in the config.")
log("WARN", "This is not supported on Windows, and you will likely run into issues.");
log("WARN", "To remove this warning, use the qmpHost and qmpPort options instead.");
}
// Fire up the VM
var VM = new QEMUVM(Config);
await VM.Start();

View File

@@ -1,3 +1,7 @@
export default function log(loglevel : string, message : string) {
console[(loglevel === "ERROR" || loglevel === "FATAL") ? "error" : "log"](`[${new Date().toLocaleString()}] [${loglevel}] ${message}`);
console[
(loglevel === "ERROR" || loglevel === "FATAL") ? "error" :
(loglevel === "WARN") ? "warn" :
"log"
](`[${new Date().toLocaleString()}] [${loglevel}] ${message}`);
}