Add IPData, Windows support, bugfixes (#2)
* Add IPData, Windows support, bugfixes Changes: - Added IPData: mutes and votes are now tracked per-IP instead of per-user. - Windows support: QMP over TCP can now be enabled in the config. - Vote cooldown can now be specified in the config. - Fixed bugs: "Username is already taken" message appearing when it shouldn't * Remove vote from closed connections --------- Co-authored-by: Elijah R <62162399+elijahr2411@users.noreply.github.com>
This commit is contained in:
@@ -9,7 +9,9 @@ export default interface IConfig {
|
||||
qemuArgs : string;
|
||||
vncPort : number;
|
||||
snapshots : boolean;
|
||||
qmpSockDir : string;
|
||||
qmpHost : string | null;
|
||||
qmpPort : number | null;
|
||||
qmpSockDir : string | null;
|
||||
};
|
||||
collabvm : {
|
||||
node : string;
|
||||
@@ -27,6 +29,7 @@ export default interface IConfig {
|
||||
tempMuteTime : number;
|
||||
turnTime : number;
|
||||
voteTime : number;
|
||||
voteCooldown: number;
|
||||
adminpass : string;
|
||||
modpass : string;
|
||||
moderatorPermissions : Permissions;
|
||||
|
||||
12
src/IPData.ts
Normal file
12
src/IPData.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export class IPData {
|
||||
tempMuteExpireTimeout? : NodeJS.Timer;
|
||||
muted: Boolean;
|
||||
vote: boolean | null;
|
||||
address: string;
|
||||
|
||||
constructor(address: string) {
|
||||
this.address = address;
|
||||
this.muted = false;
|
||||
this.vote = null;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ export default class QEMUVM extends EventEmitter {
|
||||
framebuffer : Canvas;
|
||||
framebufferCtx : CanvasRenderingContext2D;
|
||||
qmpSock : string;
|
||||
qmpType: string;
|
||||
qmpClient : QMPClient;
|
||||
qemuCmd : string;
|
||||
qemuProcess? : ExecaChildProcess;
|
||||
@@ -36,9 +37,14 @@ export default class QEMUVM extends EventEmitter {
|
||||
console.error("[FATAL] VNC Port must be 5900 or higher");
|
||||
process.exit(1);
|
||||
}
|
||||
this.qmpSock = `${Config.vm.qmpSockDir}collab-vm-qmp-${Config.collabvm.node}.sock`;
|
||||
Config.vm.qmpSockDir == null ? this.qmpType = "tcp:" : this.qmpType = "unix:";
|
||||
if(this.qmpType == "tcp:") {
|
||||
this.qmpSock = `${Config.vm.qmpHost}:${Config.vm.qmpPort}`;
|
||||
}else{
|
||||
this.qmpSock = `${Config.vm.qmpSockDir}collab-vm-qmp-${Config.collabvm.node}.sock`;
|
||||
}
|
||||
this.vncPort = Config.vm.vncPort;
|
||||
this.qemuCmd = `${Config.vm.qemuArgs} -snapshot -no-shutdown -vnc 127.0.0.1:${this.vncPort - 5900} -qmp unix:${this.qmpSock},server`;
|
||||
this.qemuCmd = `${Config.vm.qemuArgs} -snapshot -no-shutdown -vnc 127.0.0.1:${this.vncPort - 5900} -qmp ${this.qmpType}${this.qmpSock},server`;
|
||||
this.qmpErrorLevel = 0;
|
||||
this.vncErrorLevel = 0;
|
||||
this.vncOpen = true;
|
||||
@@ -48,7 +54,7 @@ export default class QEMUVM extends EventEmitter {
|
||||
this.framebufferCtx = this.framebuffer.getContext("2d");
|
||||
this.processRestartErrorLevel = 0;
|
||||
this.expectedExit = false;
|
||||
this.qmpClient = new QMPClient(this.qmpSock);
|
||||
this.qmpClient = new QMPClient(this.qmpSock, this.qmpType);
|
||||
this.qmpClient.on('connected', () => this.qmpConnected());
|
||||
this.qmpClient.on('close', () => this.qmpClosed());
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ import { Mutex } from "async-mutex";
|
||||
|
||||
export default class QMPClient extends EventEmitter {
|
||||
socketfile : string;
|
||||
sockettype: string;
|
||||
socket : Socket;
|
||||
connected : boolean;
|
||||
sentConnected : boolean;
|
||||
cmdMutex : Mutex; // So command outputs don't get mixed up
|
||||
constructor(socketfile : string) {
|
||||
constructor(socketfile : string, sockettype: string) {
|
||||
super();
|
||||
this.sockettype = sockettype;
|
||||
this.socketfile = socketfile;
|
||||
this.socket = new Socket();
|
||||
this.connected = false;
|
||||
@@ -20,7 +22,12 @@ export default class QMPClient extends EventEmitter {
|
||||
return new Promise((res, rej) => {
|
||||
if (this.connected) {res(); return;}
|
||||
try {
|
||||
this.socket.connect(this.socketfile);
|
||||
if(this.sockettype == "tcp:") {
|
||||
let _sock = this.socketfile.split(':');
|
||||
this.socket.connect(parseInt(_sock[1]), _sock[0]);
|
||||
}else{
|
||||
this.socket.connect(this.socketfile);
|
||||
}
|
||||
} catch (e) {
|
||||
this.onClose();
|
||||
}
|
||||
|
||||
22
src/User.ts
22
src/User.ts
@@ -1,6 +1,7 @@
|
||||
import * as Utilities from './Utilities.js';
|
||||
import * as guacutils from './guacutils.js';
|
||||
import {WebSocket} from 'ws';
|
||||
import {IPData} from './IPData.js';
|
||||
import IConfig from './IConfig.js';
|
||||
import RateLimiter from './RateLimiter.js';
|
||||
import { execaCommand } from 'execa';
|
||||
@@ -12,25 +13,20 @@ export class User {
|
||||
username? : string;
|
||||
connectedToNode : boolean;
|
||||
rank : Rank;
|
||||
muted : Boolean;
|
||||
tempMuteExpireTimeout? : NodeJS.Timer;
|
||||
msgsSent : number;
|
||||
Config : IConfig;
|
||||
IP : string;
|
||||
vote : boolean | null;
|
||||
IP : IPData;
|
||||
// Rate limiters
|
||||
ChatRateLimit : RateLimiter;
|
||||
LoginRateLimit : RateLimiter;
|
||||
RenameRateLimit : RateLimiter;
|
||||
TurnRateLimit : RateLimiter;
|
||||
constructor(ws : WebSocket, ip : string, config : IConfig, username? : string, node? : string) {
|
||||
constructor(ws : WebSocket, ip : IPData, config : IConfig, username? : string, node? : string) {
|
||||
this.IP = ip;
|
||||
this.connectedToNode = false;
|
||||
this.Config = config;
|
||||
this.socket = ws;
|
||||
this.muted = false;
|
||||
this.msgsSent = 0;
|
||||
this.vote = null;
|
||||
this.socket.on('close', () => {
|
||||
clearInterval(this.nopSendInterval);
|
||||
});
|
||||
@@ -86,22 +82,22 @@ export class User {
|
||||
this.ChatRateLimit.request();
|
||||
}
|
||||
mute(permanent : boolean) {
|
||||
this.muted = true;
|
||||
this.IP.muted = true;
|
||||
this.sendMsg(guacutils.encode("chat", "", `You have been muted${permanent ? "" : ` for ${this.Config.collabvm.tempMuteTime} seconds`}.`));
|
||||
if (!permanent) {
|
||||
clearTimeout(this.tempMuteExpireTimeout);
|
||||
this.tempMuteExpireTimeout = setTimeout(() => this.unmute(), this.Config.collabvm.tempMuteTime * 1000);
|
||||
clearTimeout(this.IP.tempMuteExpireTimeout);
|
||||
this.IP.tempMuteExpireTimeout = setTimeout(() => this.unmute(), this.Config.collabvm.tempMuteTime * 1000);
|
||||
}
|
||||
}
|
||||
unmute() {
|
||||
clearTimeout(this.tempMuteExpireTimeout);
|
||||
this.muted = false;
|
||||
clearTimeout(this.IP.tempMuteExpireTimeout);
|
||||
this.IP.muted = false;
|
||||
this.sendMsg(guacutils.encode("chat", "", "You are no longer muted."));
|
||||
}
|
||||
|
||||
async ban() {
|
||||
// Prevent the user from taking turns or chatting, in case the ban command takes a while
|
||||
this.muted = true;
|
||||
this.IP.muted = true;
|
||||
//@ts-ignore
|
||||
var cmd = this.Config.collabvm.bancmd.replace(/\$IP/g, this.IP).replace(/\$NAME/g, this.username);
|
||||
await execaCommand(cmd);
|
||||
|
||||
@@ -12,12 +12,14 @@ import { createHash } from 'crypto';
|
||||
import { isIP } from 'net';
|
||||
import QEMUVM from './QEMUVM.js';
|
||||
import { Canvas, createCanvas, CanvasRenderingContext2D } from 'canvas';
|
||||
import { IPData } from './IPData.js';
|
||||
|
||||
export default class WSServer {
|
||||
private Config : IConfig;
|
||||
private server : http.Server;
|
||||
private socket : WebSocketServer;
|
||||
private clients : User[];
|
||||
private ips : IPData[];
|
||||
private ChatHistory : CircularBuffer<{user:string,msg:string}>
|
||||
private TurnQueue : Queue<User>;
|
||||
// Time remaining on the current turn
|
||||
@@ -33,9 +35,9 @@ export default class WSServer {
|
||||
// How much time is left on the vote
|
||||
private voteTime : number;
|
||||
// How much time until another reset vote can be cast
|
||||
private voteTimeout : number;
|
||||
private voteCooldown : number;
|
||||
// Interval to keep track
|
||||
private voteTimeoutInterval? : NodeJS.Timer;
|
||||
private voteCooldownInterval? : NodeJS.Timer;
|
||||
// Completely disable turns
|
||||
private turnsAllowed : boolean;
|
||||
// Indefinite turn
|
||||
@@ -48,10 +50,11 @@ export default class WSServer {
|
||||
this.TurnTime = 0;
|
||||
this.TurnIntervalRunning = false;
|
||||
this.clients = [];
|
||||
this.ips = [];
|
||||
this.Config = config;
|
||||
this.voteInProgress = false;
|
||||
this.voteTime = 0;
|
||||
this.voteTimeout = 0;
|
||||
this.voteCooldown = 0;
|
||||
this.turnsAllowed = true;
|
||||
this.indefiniteTurn = null;
|
||||
this.ModPerms = Utilities.MakeModPerms(this.Config.collabvm.moderatorPermissions);
|
||||
@@ -120,7 +123,7 @@ export default class WSServer {
|
||||
}
|
||||
|
||||
private onConnection(ws : WebSocket, req : http.IncomingMessage) {
|
||||
var ip;
|
||||
var ip: string;
|
||||
if (this.Config.http.proxying) {
|
||||
//@ts-ignore
|
||||
if (!req.proxiedIP) return;
|
||||
@@ -130,7 +133,17 @@ export default class WSServer {
|
||||
if (!req.socket.remoteAddress) return;
|
||||
ip = req.socket.remoteAddress;
|
||||
}
|
||||
var user = new User(ws, ip, this.Config);
|
||||
|
||||
var _ipdata = this.ips.filter(data => data.address == ip);
|
||||
var ipdata;
|
||||
if(_ipdata.length > 0) {
|
||||
ipdata = _ipdata[0];
|
||||
}else{
|
||||
ipdata = new IPData(ip);
|
||||
this.ips.push(ipdata);
|
||||
}
|
||||
|
||||
var user = new User(ws, ipdata, this.Config);
|
||||
this.clients.push(user);
|
||||
ws.on('close', () => this.connectionClosed(user));
|
||||
ws.on('message', (e) => {
|
||||
@@ -144,12 +157,13 @@ export default class WSServer {
|
||||
this.onMessage(user, msg);
|
||||
});
|
||||
user.sendMsg(this.getAdduserMsg());
|
||||
console.log(`[Connect] From ${user.IP}`);
|
||||
console.log(`[Connect] From ${user.IP.address}`);
|
||||
};
|
||||
|
||||
private connectionClosed(user : User) {
|
||||
if(user.IP.vote != null) user.IP.vote = null;
|
||||
this.clients.splice(this.clients.indexOf(user), 1);
|
||||
console.log(`[DISCONNECT] From ${user.IP}${user.username ? ` with username ${user.username}` : ""}`);
|
||||
console.log(`[DISCONNECT] From ${user.IP.address}${user.username ? ` with username ${user.username}` : ""}`);
|
||||
if (!user.username) return;
|
||||
if (this.TurnQueue.toArray().indexOf(user) !== -1) {
|
||||
var hadturn = (this.TurnQueue.peek() === user);
|
||||
@@ -189,7 +203,7 @@ export default class WSServer {
|
||||
break;
|
||||
case "chat":
|
||||
if (!client.username) return;
|
||||
if (client.muted) return;
|
||||
if (client.IP.muted) return;
|
||||
if (msgArr.length !== 2) return;
|
||||
var msg = Utilities.HTMLSanitize(msgArr[1]);
|
||||
// One of the things I hated most about the old server is it completely discarded your message if it was too long
|
||||
@@ -226,7 +240,7 @@ export default class WSServer {
|
||||
if (this.TurnQueue.toArray().indexOf(client) !== -1) return;
|
||||
// If they're muted, also fuck them off.
|
||||
// Send them the turn queue to prevent client glitches
|
||||
if (client.muted) return;
|
||||
if (client.IP.muted) return;
|
||||
this.TurnQueue.enqueue(client);
|
||||
if (this.TurnQueue.size === 1) this.nextTurn();
|
||||
} else {
|
||||
@@ -261,22 +275,22 @@ export default class WSServer {
|
||||
switch (msgArr[1]) {
|
||||
case "1":
|
||||
if (!this.voteInProgress) {
|
||||
if (this.voteTimeout !== 0) {
|
||||
client.sendMsg(guacutils.encode("vote", "3", this.voteTimeout.toString()));
|
||||
if (this.voteCooldown !== 0) {
|
||||
client.sendMsg(guacutils.encode("vote", "3", this.voteCooldown.toString()));
|
||||
return;
|
||||
}
|
||||
this.startVote();
|
||||
this.clients.forEach(c => c.sendMsg(guacutils.encode("chat", "", `${client.username} has started a vote to reset the VM.`)));
|
||||
}
|
||||
else if (client.vote !== true)
|
||||
else if (client.IP.vote !== true)
|
||||
this.clients.forEach(c => c.sendMsg(guacutils.encode("chat", "", `${client.username} has voted yes.`)));
|
||||
client.vote = true;
|
||||
client.IP.vote = true;
|
||||
break;
|
||||
case "0":
|
||||
if (!this.voteInProgress) return;
|
||||
if (client.vote !== false)
|
||||
if (client.IP.vote !== false)
|
||||
this.clients.forEach(c => c.sendMsg(guacutils.encode("chat", "", `${client.username} has voted no.`)));
|
||||
client.vote = false;
|
||||
client.IP.vote = false;
|
||||
break;
|
||||
}
|
||||
this.sendVoteUpdate();
|
||||
@@ -397,7 +411,7 @@ export default class WSServer {
|
||||
if (msgArr.length !== 3) return;
|
||||
var user = this.clients.find(c => c.username === msgArr[2]);
|
||||
if (!user) return;
|
||||
client.sendMsg(guacutils.encode("admin", "19", msgArr[2], user.IP));
|
||||
client.sendMsg(guacutils.encode("admin", "19", msgArr[2], user.IP.address));
|
||||
break;
|
||||
case "20":
|
||||
// Steal turn
|
||||
@@ -473,7 +487,9 @@ export default class WSServer {
|
||||
}
|
||||
if (this.getUsernameList().indexOf(newName) !== -1) {
|
||||
client.assignGuestName(this.getUsernameList());
|
||||
status = "1";
|
||||
if(client.connectedToNode) {
|
||||
status = "1";
|
||||
}
|
||||
} else
|
||||
if (!/^[a-zA-Z0-9\ \-\_\.]+$/.test(newName) || newName.length > 20 || newName.length < 3) {
|
||||
client.assignGuestName(this.getUsernameList());
|
||||
@@ -487,12 +503,12 @@ export default class WSServer {
|
||||
//@ts-ignore
|
||||
client.sendMsg(guacutils.encode("rename", "0", status, client.username));
|
||||
if (hadName) {
|
||||
console.log(`[RENAME] ${client.IP} from ${oldname} to ${client.username}`);
|
||||
console.log(`[RENAME] ${client.IP.address} from ${oldname} to ${client.username}`);
|
||||
this.clients.filter(c => c.username !== client.username).forEach((c) =>
|
||||
//@ts-ignore
|
||||
c.sendMsg(guacutils.encode("rename", "1", oldname, client.username)));
|
||||
} else {
|
||||
console.log(`[RENAME] ${client.IP} to ${client.username}`);
|
||||
console.log(`[RENAME] ${client.IP.address} to ${client.username}`);
|
||||
this.clients.forEach((c) =>
|
||||
//@ts-ignore
|
||||
c.sendMsg(guacutils.encode("adduser", "1", client.username, client.rank)));
|
||||
@@ -624,12 +640,14 @@ export default class WSServer {
|
||||
} else {
|
||||
this.clients.forEach(c => c.sendMsg(guacutils.encode("chat", "", "The vote to reset the VM has lost.")));
|
||||
}
|
||||
this.clients.forEach(c => c.vote = null);
|
||||
this.voteTimeout = 180;
|
||||
this.voteTimeoutInterval = setInterval(() => {
|
||||
this.voteTimeout--;
|
||||
if (this.voteTimeout < 1)
|
||||
clearInterval(this.voteTimeoutInterval);
|
||||
this.clients.forEach(c => {
|
||||
c.IP.vote = null;
|
||||
});
|
||||
this.voteCooldown = this.Config.collabvm.voteCooldown;
|
||||
this.voteCooldownInterval = setInterval(() => {
|
||||
this.voteCooldown--;
|
||||
if (this.voteCooldown < 1)
|
||||
clearInterval(this.voteCooldownInterval);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
@@ -646,7 +664,7 @@ export default class WSServer {
|
||||
getVoteCounts() : {yes:number,no:number} {
|
||||
var yes = 0;
|
||||
var no = 0;
|
||||
this.clients.forEach((c) => {
|
||||
this.ips.forEach((c) => {
|
||||
if (c.vote === true) yes++;
|
||||
if (c.vote === false) no++;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user