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:
@@ -10,6 +10,8 @@ proxyAllowedIps = ["127.0.0.1"]
|
|||||||
origin = false
|
origin = false
|
||||||
# Origins to accept connections from.
|
# Origins to accept connections from.
|
||||||
originAllowedDomains = ["computernewb.com"]
|
originAllowedDomains = ["computernewb.com"]
|
||||||
|
# Maximum amount of active connections allowed from the same IP.
|
||||||
|
maxConnections = 3
|
||||||
|
|
||||||
[vm]
|
[vm]
|
||||||
qemuArgs = "qemu-system-x86_64"
|
qemuArgs = "qemu-system-x86_64"
|
||||||
@@ -32,6 +34,8 @@ moderatorEnabled = true
|
|||||||
usernameblacklist = []
|
usernameblacklist = []
|
||||||
maxChatLength = 100
|
maxChatLength = 100
|
||||||
maxChatHistoryLength = 10
|
maxChatHistoryLength = 10
|
||||||
|
# Limit the amount of users allowed in the turn queue at the same time from the same IP
|
||||||
|
turnlimit = {enabled = true, maximum = 1}
|
||||||
# Temporarily mute a user if they send more than x messages in n seconds
|
# Temporarily mute a user if they send more than x messages in n seconds
|
||||||
automute = {enabled = true, seconds = 5, messages = 5}
|
automute = {enabled = true, seconds = 5, messages = 5}
|
||||||
# How long a temporary mute lasts, in seconds
|
# How long a temporary mute lasts, in seconds
|
||||||
|
|||||||
14
package.json
14
package.json
@@ -10,19 +10,19 @@
|
|||||||
"author": "Elijah R",
|
"author": "Elijah R",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "^18.11.18",
|
"@types/node": "^20.6.0",
|
||||||
"@types/sharp": "^0.31.1",
|
"@types/sharp": "^0.31.1",
|
||||||
"@types/ws": "^8.5.4",
|
"@types/ws": "^8.5.5",
|
||||||
"async-mutex": "^0.4.0",
|
"async-mutex": "^0.4.0",
|
||||||
"canvas": "^2.11.0",
|
"canvas": "^2.11.2",
|
||||||
"execa": "^6.1.0",
|
"execa": "^8.0.1",
|
||||||
"fs": "^0.0.1-security",
|
"fs": "^0.0.1-security",
|
||||||
"jimp": "^0.16.2",
|
"jimp": "^0.22.10",
|
||||||
"mnemonist": "^0.39.5",
|
"mnemonist": "^0.39.5",
|
||||||
"rfb2": "github:elijahr2411/node-rfb2",
|
"rfb2": "github:elijahr2411/node-rfb2",
|
||||||
"toml": "^3.0.0",
|
"toml": "^3.0.0",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^5.2.2",
|
||||||
"ws": "^8.12.0"
|
"ws": "^8.14.1"
|
||||||
},
|
},
|
||||||
"type": "module"
|
"type": "module"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export default interface IConfig {
|
|||||||
proxyAllowedIps : string[];
|
proxyAllowedIps : string[];
|
||||||
origin : boolean;
|
origin : boolean;
|
||||||
originAllowedDomains : string[];
|
originAllowedDomains : string[];
|
||||||
|
maxConnections: number;
|
||||||
};
|
};
|
||||||
vm : {
|
vm : {
|
||||||
qemuArgs : string;
|
qemuArgs : string;
|
||||||
@@ -24,6 +25,10 @@ export default interface IConfig {
|
|||||||
usernameblacklist : string[];
|
usernameblacklist : string[];
|
||||||
maxChatLength : number;
|
maxChatLength : number;
|
||||||
maxChatHistoryLength : number;
|
maxChatHistoryLength : number;
|
||||||
|
turnlimit : {
|
||||||
|
enabled: boolean,
|
||||||
|
maximum: number;
|
||||||
|
};
|
||||||
automute : {
|
automute : {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
seconds: number;
|
seconds: number;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export class IPData {
|
export class IPData {
|
||||||
tempMuteExpireTimeout? : NodeJS.Timer;
|
tempMuteExpireTimeout? : NodeJS.Timeout;
|
||||||
muted: Boolean;
|
muted: Boolean;
|
||||||
vote: boolean | null;
|
vote: boolean | null;
|
||||||
address: string;
|
address: string;
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { EventEmitter } from "events";
|
|
||||||
import IConfig from "./IConfig.js";
|
import IConfig from "./IConfig.js";
|
||||||
import * as rfb from 'rfb2';
|
import * as rfb from 'rfb2';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { execa, ExecaChildProcess, execaCommand } from "execa";
|
import { ExecaChildProcess, execaCommand } from "execa";
|
||||||
import QMPClient from "./QMPClient.js";
|
import QMPClient from "./QMPClient.js";
|
||||||
import BatchRects from "./RectBatcher.js";
|
import BatchRects from "./RectBatcher.js";
|
||||||
import { createCanvas, Canvas, CanvasRenderingContext2D, createImageData } from "canvas";
|
import { createCanvas, Canvas, CanvasRenderingContext2D, createImageData } from "canvas";
|
||||||
@@ -25,13 +24,13 @@ export default class QEMUVM extends VM {
|
|||||||
processRestartErrorLevel : number;
|
processRestartErrorLevel : number;
|
||||||
expectedExit : boolean;
|
expectedExit : boolean;
|
||||||
vncOpen : boolean;
|
vncOpen : boolean;
|
||||||
vncUpdateInterval? : NodeJS.Timer;
|
vncUpdateInterval? : NodeJS.Timeout;
|
||||||
rects : {height:number,width:number,x:number,y:number,data:Buffer}[];
|
rects : {height:number,width:number,x:number,y:number,data:Buffer}[];
|
||||||
rectMutex : Mutex;
|
rectMutex : Mutex;
|
||||||
|
|
||||||
vncReconnectTimeout? : NodeJS.Timer;
|
vncReconnectTimeout? : NodeJS.Timeout;
|
||||||
qmpReconnectTimeout? : NodeJS.Timer;
|
qmpReconnectTimeout? : NodeJS.Timeout;
|
||||||
qemuRestartTimeout? : NodeJS.Timer;
|
qemuRestartTimeout? : NodeJS.Timeout;
|
||||||
|
|
||||||
constructor(Config : IConfig) {
|
constructor(Config : IConfig) {
|
||||||
super();
|
super();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export default class RateLimiter extends EventEmitter {
|
|||||||
private limit : number;
|
private limit : number;
|
||||||
private interval : number;
|
private interval : number;
|
||||||
private requestCount : number;
|
private requestCount : number;
|
||||||
private limiter? : NodeJS.Timer;
|
private limiter? : NodeJS.Timeout;
|
||||||
private limiterSet : boolean;
|
private limiterSet : boolean;
|
||||||
constructor(limit : number, interval : number) {
|
constructor(limit : number, interval : number) {
|
||||||
super();
|
super();
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import RateLimiter from './RateLimiter.js';
|
|||||||
import { execaCommand } from 'execa';
|
import { execaCommand } from 'execa';
|
||||||
export class User {
|
export class User {
|
||||||
socket : WebSocket;
|
socket : WebSocket;
|
||||||
nopSendInterval : NodeJS.Timer;
|
nopSendInterval : NodeJS.Timeout;
|
||||||
msgRecieveInterval : NodeJS.Timer;
|
msgRecieveInterval : NodeJS.Timeout;
|
||||||
nopRecieveTimeout? : NodeJS.Timer;
|
nopRecieveTimeout? : NodeJS.Timeout;
|
||||||
username? : string;
|
username? : string;
|
||||||
connectedToNode : boolean;
|
connectedToNode : boolean;
|
||||||
viewMode : number;
|
viewMode : number;
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import Queue from 'mnemonist/queue.js';
|
|||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
import { isIP } from 'net';
|
import { isIP } from 'net';
|
||||||
import QEMUVM from './QEMUVM.js';
|
import QEMUVM from './QEMUVM.js';
|
||||||
import { Canvas, createCanvas, CanvasRenderingContext2D } from 'canvas';
|
import { Canvas, createCanvas } from 'canvas';
|
||||||
import { IPData } from './IPData.js';
|
import { IPData } from './IPData.js';
|
||||||
import { read, readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import log from './log.js';
|
import log from './log.js';
|
||||||
import VM from './VM.js';
|
import VM from './VM.js';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
@@ -32,19 +32,17 @@ export default class WSServer {
|
|||||||
// Time remaining on the current turn
|
// Time remaining on the current turn
|
||||||
private TurnTime : number;
|
private TurnTime : number;
|
||||||
// Interval to keep track of the current turn time
|
// Interval to keep track of the current turn time
|
||||||
private TurnInterval? : NodeJS.Timer;
|
private TurnInterval? : NodeJS.Timeout;
|
||||||
// Is the turn interval running?
|
|
||||||
private TurnIntervalRunning : boolean;
|
|
||||||
// If a reset vote is in progress
|
// If a reset vote is in progress
|
||||||
private voteInProgress : boolean;
|
private voteInProgress : boolean;
|
||||||
// Interval to keep track of vote resets
|
// Interval to keep track of vote resets
|
||||||
private voteInterval? : NodeJS.Timer;
|
private voteInterval? : NodeJS.Timeout;
|
||||||
// How much time is left on the vote
|
// How much time is left on the vote
|
||||||
private voteTime : number;
|
private voteTime : number;
|
||||||
// How much time until another reset vote can be cast
|
// How much time until another reset vote can be cast
|
||||||
private voteCooldown : number;
|
private voteCooldown : number;
|
||||||
// Interval to keep track
|
// Interval to keep track
|
||||||
private voteCooldownInterval? : NodeJS.Timer;
|
private voteCooldownInterval? : NodeJS.Timeout;
|
||||||
// Completely disable turns
|
// Completely disable turns
|
||||||
private turnsAllowed : boolean;
|
private turnsAllowed : boolean;
|
||||||
// Hide the screen
|
// 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.ChatHistory = new CircularBuffer<{user:string,msg:string}>(Array, this.Config.collabvm.maxChatHistoryLength);
|
||||||
this.TurnQueue = new Queue<User>();
|
this.TurnQueue = new Queue<User>();
|
||||||
this.TurnTime = 0;
|
this.TurnTime = 0;
|
||||||
this.TurnIntervalRunning = false;
|
|
||||||
this.clients = [];
|
this.clients = [];
|
||||||
this.ips = [];
|
this.ips = [];
|
||||||
this.voteInProgress = false;
|
this.voteInProgress = false;
|
||||||
@@ -140,7 +137,7 @@ export default class WSServer {
|
|||||||
// Get the first IP from the X-Forwarded-For variable
|
// Get the first IP from the X-Forwarded-For variable
|
||||||
_ip = req.headers["x-forwarded-for"]?.toString().replace(/\ /g, "").split(",")[0];
|
_ip = req.headers["x-forwarded-for"]?.toString().replace(/\ /g, "").split(",")[0];
|
||||||
} catch {
|
} catch {
|
||||||
// If we can't get the ip, kill the connection
|
// If we can't get the IP, kill the connection
|
||||||
killConnection();
|
killConnection();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -149,7 +146,7 @@ export default class WSServer {
|
|||||||
killConnection();
|
killConnection();
|
||||||
return;
|
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)) {
|
if (!isIP(_ip)) {
|
||||||
killConnection();
|
killConnection();
|
||||||
return;
|
return;
|
||||||
@@ -157,11 +154,8 @@ export default class WSServer {
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
req.proxiedIP = _ip;
|
req.proxiedIP = _ip;
|
||||||
}
|
}
|
||||||
this.socket.handleUpgrade(req, socket, head, (ws) => this.socket.emit('connection', ws, req));
|
|
||||||
}
|
|
||||||
|
|
||||||
private onConnection(ws : WebSocket, req : http.IncomingMessage) {
|
let ip: string;
|
||||||
var ip: string;
|
|
||||||
if (this.Config.http.proxying) {
|
if (this.Config.http.proxying) {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
if (!req.proxiedIP) return;
|
if (!req.proxiedIP) return;
|
||||||
@@ -172,23 +166,45 @@ export default class WSServer {
|
|||||||
ip = req.socket.remoteAddress;
|
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;
|
var ipdata;
|
||||||
if(_ipdata.length > 0) {
|
if(_ipdata.length > 0) {
|
||||||
ipdata = _ipdata[0];
|
ipdata = _ipdata[0];
|
||||||
}else{
|
}else{
|
||||||
ipdata = new IPData(ip);
|
//@ts-ignore
|
||||||
|
ipdata = new IPData(req.IP);
|
||||||
this.ips.push(ipdata);
|
this.ips.push(ipdata);
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = new User(ws, ipdata, this.Config);
|
var user = new User(ws, ipdata, this.Config);
|
||||||
this.clients.push(user);
|
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('close', () => this.connectionClosed(user));
|
||||||
ws.on('message', (e) => {
|
ws.on('message', (e) => {
|
||||||
var msg;
|
var msg;
|
||||||
try {msg = e.toString()}
|
try {msg = e.toString()}
|
||||||
catch {
|
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();
|
user.closeConnection();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -199,7 +215,10 @@ export default class WSServer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private connectionClosed(user : User) {
|
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;
|
if (this.indefiniteTurn === user) this.indefiniteTurn = null;
|
||||||
this.clients.splice(this.clients.indexOf(user), 1);
|
this.clients.splice(this.clients.indexOf(user), 1);
|
||||||
log("INFO", `Disconnect From ${user.IP.address}${user.username ? ` with username ${user.username}` : ""}`);
|
log("INFO", `Disconnect From ${user.IP.address}${user.username ? ` with username ${user.username}` : ""}`);
|
||||||
@@ -322,13 +341,17 @@ export default class WSServer {
|
|||||||
}
|
}
|
||||||
if (takingTurn) {
|
if (takingTurn) {
|
||||||
var currentQueue = this.TurnQueue.toArray();
|
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 (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
|
// Send them the turn queue to prevent client glitches
|
||||||
if (client.IP.muted) return;
|
if (client.IP.muted) return;
|
||||||
// Only allow one active turn per IP address
|
if(this.Config.collabvm.turnlimit.enabled) {
|
||||||
if(currentQueue.find(user => user.IP.address == client.IP.address)) return;
|
// 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);
|
this.TurnQueue.enqueue(client);
|
||||||
if (this.TurnQueue.size === 1) this.nextTurn();
|
if (this.TurnQueue.size === 1) this.nextTurn();
|
||||||
} else {
|
} else {
|
||||||
@@ -684,7 +707,6 @@ export default class WSServer {
|
|||||||
private nextTurn() {
|
private nextTurn() {
|
||||||
clearInterval(this.TurnInterval);
|
clearInterval(this.TurnInterval);
|
||||||
if (this.TurnQueue.size === 0) {
|
if (this.TurnQueue.size === 0) {
|
||||||
this.TurnIntervalRunning = false;
|
|
||||||
} else {
|
} else {
|
||||||
this.TurnTime = this.Config.collabvm.turnTime;
|
this.TurnTime = this.Config.collabvm.turnTime;
|
||||||
this.TurnInterval = setInterval(() => this.turnInterval(), 1000);
|
this.TurnInterval = setInterval(() => this.turnInterval(), 1000);
|
||||||
@@ -694,7 +716,6 @@ export default class WSServer {
|
|||||||
|
|
||||||
clearTurns() {
|
clearTurns() {
|
||||||
clearInterval(this.TurnInterval);
|
clearInterval(this.TurnInterval);
|
||||||
this.TurnIntervalRunning = false;
|
|
||||||
this.TurnQueue.clear();
|
this.TurnQueue.clear();
|
||||||
this.sendTurnUpdate();
|
this.sendTurnUpdate();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ try {
|
|||||||
|
|
||||||
|
|
||||||
async function start() {
|
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
|
// Fire up the VM
|
||||||
var VM = new QEMUVM(Config);
|
var VM = new QEMUVM(Config);
|
||||||
await VM.Start();
|
await VM.Start();
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
export default function log(loglevel : string, message : string) {
|
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}`);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user