add all the staff actions and resets and uhh i forgot to commit but everything works now
This commit is contained in:
@@ -29,6 +29,8 @@ automute = {enabled = true, seconds = 5, messages = 5}
|
|||||||
tempMuteTime = 30
|
tempMuteTime = 30
|
||||||
# How long a turn lasts, in seconds
|
# How long a turn lasts, in seconds
|
||||||
turnTime = 20
|
turnTime = 20
|
||||||
|
# How long a reset vote lasts, in seconds
|
||||||
|
voteTime = 100
|
||||||
# SHA256 sum of the admin and mod passwords. This can be generated with the following command:
|
# SHA256 sum of the admin and mod passwords. This can be generated with the following command:
|
||||||
# printf "<password>" | sha256sum -
|
# printf "<password>" | sha256sum -
|
||||||
# Example hash is hunter2 and hunter3
|
# Example hash is hunter2 and hunter3
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export default interface IConfig {
|
|||||||
};
|
};
|
||||||
tempMuteTime : number;
|
tempMuteTime : number;
|
||||||
turnTime : number;
|
turnTime : number;
|
||||||
|
voteTime : number;
|
||||||
adminpass : string;
|
adminpass : string;
|
||||||
modpass : string;
|
modpass : string;
|
||||||
moderatorPermissions : Permissions;
|
moderatorPermissions : Permissions;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export default class QEMUVM extends EventEmitter {
|
|||||||
vncErrorLevel : number;
|
vncErrorLevel : number;
|
||||||
processRestartErrorLevel : number;
|
processRestartErrorLevel : number;
|
||||||
expectedExit : boolean;
|
expectedExit : boolean;
|
||||||
|
vncOpen : boolean;
|
||||||
|
|
||||||
vncReconnectTimeout? : NodeJS.Timer;
|
vncReconnectTimeout? : NodeJS.Timer;
|
||||||
qmpReconnectTimeout? : NodeJS.Timer;
|
qmpReconnectTimeout? : NodeJS.Timer;
|
||||||
@@ -32,6 +33,7 @@ export default class QEMUVM extends EventEmitter {
|
|||||||
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 unix:${this.qmpSock},server`;
|
||||||
this.qmpErrorLevel = 0;
|
this.qmpErrorLevel = 0;
|
||||||
this.vncErrorLevel = 0;
|
this.vncErrorLevel = 0;
|
||||||
|
this.vncOpen = true;
|
||||||
this.processRestartErrorLevel = 0;
|
this.processRestartErrorLevel = 0;
|
||||||
this.expectedExit = false;
|
this.expectedExit = false;
|
||||||
this.qmpClient = new QMPClient(this.qmpSock);
|
this.qmpClient = new QMPClient(this.qmpSock);
|
||||||
@@ -50,13 +52,14 @@ export default class QEMUVM extends EventEmitter {
|
|||||||
}
|
}
|
||||||
var qemuArr = this.qemuCmd.split(" ");
|
var qemuArr = this.qemuCmd.split(" ");
|
||||||
this.qemuProcess = execa(qemuArr[0], qemuArr.slice(1));
|
this.qemuProcess = execa(qemuArr[0], qemuArr.slice(1));
|
||||||
|
this.qemuProcess.catch(() => false);
|
||||||
this.qemuProcess.stderr?.on('data', (d) => console.log(d.toString()));
|
this.qemuProcess.stderr?.on('data', (d) => console.log(d.toString()));
|
||||||
this.qemuProcess.on('spawn', () => {
|
this.qemuProcess.once('spawn', () => {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await this.qmpClient.connect();
|
await this.qmpClient.connect();
|
||||||
}, 1000)
|
}, 2000)
|
||||||
});
|
});
|
||||||
this.qemuProcess.on('exit', () => {
|
this.qemuProcess.once('exit', () => {
|
||||||
if (this.expectedExit) return;
|
if (this.expectedExit) return;
|
||||||
clearTimeout(this.qmpReconnectTimeout);
|
clearTimeout(this.qmpReconnectTimeout);
|
||||||
clearTimeout(this.vncReconnectTimeout);
|
clearTimeout(this.vncReconnectTimeout);
|
||||||
@@ -70,6 +73,7 @@ export default class QEMUVM extends EventEmitter {
|
|||||||
this.vnc?.end();
|
this.vnc?.end();
|
||||||
this.qemuRestartTimeout = setTimeout(() => this.Start(), 3000);
|
this.qemuRestartTimeout = setTimeout(() => this.Start(), 3000);
|
||||||
});
|
});
|
||||||
|
this.qemuProcess.on('error', () => false);
|
||||||
this.once('vncconnect', () => res());
|
this.once('vncconnect', () => res());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -109,6 +113,7 @@ export default class QEMUVM extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private vncClosed() {
|
private vncClosed() {
|
||||||
|
this.vncOpen = false;
|
||||||
if (this.expectedExit) return;
|
if (this.expectedExit) return;
|
||||||
this.vncErrorLevel++;
|
this.vncErrorLevel++;
|
||||||
if (this.vncErrorLevel > 4) {
|
if (this.vncErrorLevel > 4) {
|
||||||
@@ -123,8 +128,11 @@ export default class QEMUVM extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private vncConnected() {
|
private vncConnected() {
|
||||||
|
this.vncOpen = true;
|
||||||
this.emit('vncconnect');
|
this.emit('vncconnect');
|
||||||
this.vncErrorLevel = 0;
|
this.vncErrorLevel = 0;
|
||||||
|
//@ts-ignore
|
||||||
|
this.onVNCSize({height: this.vnc.height, width: this.vnc.width});
|
||||||
}
|
}
|
||||||
private async onVNCRect(rect : any) {
|
private async onVNCRect(rect : any) {
|
||||||
var buff = Buffer.alloc(rect.height * rect.width * 4)
|
var buff = Buffer.alloc(rect.height * rect.width * 4)
|
||||||
@@ -137,6 +145,7 @@ export default class QEMUVM extends EventEmitter {
|
|||||||
}
|
}
|
||||||
this.emit("dirtyrect", buff, rect.x, rect.y, rect.width, rect.height);
|
this.emit("dirtyrect", buff, rect.x, rect.y, rect.width, rect.height);
|
||||||
if (!this.vnc) throw new Error();
|
if (!this.vnc) throw new Error();
|
||||||
|
if (this.vncOpen)
|
||||||
this.vnc.requestUpdate(true, 0, 0, this.vnc.height, this.vnc.width);
|
this.vnc.requestUpdate(true, 0, 0, this.vnc.height, this.vnc.width);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +158,7 @@ export default class QEMUVM extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async Restore() {
|
async Restore() {
|
||||||
|
if (this.expectedExit) return;
|
||||||
await this.Stop();
|
await this.Stop();
|
||||||
this.expectedExit = false;
|
this.expectedExit = false;
|
||||||
this.Start();
|
this.Start();
|
||||||
@@ -156,14 +166,23 @@ export default class QEMUVM extends EventEmitter {
|
|||||||
|
|
||||||
Stop() : Promise<void> {
|
Stop() : Promise<void> {
|
||||||
return new Promise<void>(async (res, rej) => {
|
return new Promise<void>(async (res, rej) => {
|
||||||
|
if (this.expectedExit) {res(); return;}
|
||||||
|
if (!this.qemuProcess) throw new Error("VM was not running");
|
||||||
this.expectedExit = true;
|
this.expectedExit = true;
|
||||||
|
this.vncOpen = false;
|
||||||
this.vnc?.end();
|
this.vnc?.end();
|
||||||
this.qmpClient.disconnect();
|
|
||||||
var killTimeout = setTimeout(() => {
|
var killTimeout = setTimeout(() => {
|
||||||
console.log("Force killing QEMU after 10 seconds of waiting for shutdown");
|
console.log("Force killing QEMU after 10 seconds of waiting for shutdown");
|
||||||
this.qemuProcess?.kill(9);
|
this.qemuProcess?.kill(9);
|
||||||
}, 10000)
|
}, 10000);
|
||||||
await this.qemuProcess;
|
var closep = new Promise<void>(async (reso, reje) => {
|
||||||
|
this.qemuProcess?.once('exit', () => reso());
|
||||||
|
await this.qmpClient.execute({ "execute": "quit" });
|
||||||
|
});
|
||||||
|
var qmpclosep = new Promise<void>((reso, rej) => {
|
||||||
|
this.qmpClient.once('close', () => reso());
|
||||||
|
});
|
||||||
|
await Promise.all([closep, qmpclosep]);
|
||||||
clearTimeout(killTimeout);
|
clearTimeout(killTimeout);
|
||||||
res();
|
res();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ export default class QMPClient extends EventEmitter {
|
|||||||
this.onClose();
|
this.onClose();
|
||||||
}
|
}
|
||||||
this.connected = true;
|
this.connected = true;
|
||||||
this.socket.on('error', (err) => false); // Disable throwing if QMP errors
|
this.socket.on('error', (err) => console.log(err)); // Disable throwing if QMP errors
|
||||||
this.socket.on('data', (data) => this.onData(data));
|
this.socket.on('data', (data) => this.onData(data));
|
||||||
this.socket.on('close', () => this.onClose());
|
this.socket.on('close', () => this.onClose());
|
||||||
this.once('connected', () => res());
|
this.once('connected', () => {res();});
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,10 +39,13 @@ export default class QMPClient extends EventEmitter {
|
|||||||
|
|
||||||
private async onData(data : Buffer) {
|
private async onData(data : Buffer) {
|
||||||
var msgraw = data.toString();
|
var msgraw = data.toString();
|
||||||
var msg = JSON.parse(msgraw);
|
var msg;
|
||||||
|
try {msg = JSON.parse(msgraw);}
|
||||||
|
catch {return;}
|
||||||
if (msg.QMP) {
|
if (msg.QMP) {
|
||||||
if (this.sentConnected) return;
|
if (this.sentConnected) {return;};
|
||||||
await this.execute({ execute: "qmp_capabilities" });
|
await this.execute({ execute: "qmp_capabilities" });
|
||||||
|
|
||||||
this.emit('connected');
|
this.emit('connected');
|
||||||
this.sentConnected = true;
|
this.sentConnected = true;
|
||||||
}
|
}
|
||||||
@@ -52,6 +55,11 @@ export default class QMPClient extends EventEmitter {
|
|||||||
private onClose() {
|
private onClose() {
|
||||||
this.connected = false;
|
this.connected = false;
|
||||||
this.sentConnected = false;
|
this.sentConnected = false;
|
||||||
|
if (this.socket.readyState === 'open')
|
||||||
|
this.socket.destroy();
|
||||||
|
this.cmdMutex.cancel();
|
||||||
|
this.cmdMutex.release();
|
||||||
|
this.socket = new Socket();
|
||||||
this.emit('close');
|
this.emit('close');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +75,9 @@ export default class QMPClient extends EventEmitter {
|
|||||||
|
|
||||||
execute(args : object) {
|
execute(args : object) {
|
||||||
return new Promise(async (res, rej) => {
|
return new Promise(async (res, rej) => {
|
||||||
var result:any = await this.cmdMutex.runExclusive(() => {
|
var result:any;
|
||||||
|
try {
|
||||||
|
result = await this.cmdMutex.runExclusive(() => {
|
||||||
// I kinda hate having two promises but IDK how else to do it /shrug
|
// I kinda hate having two promises but IDK how else to do it /shrug
|
||||||
return new Promise((reso, reje) => {
|
return new Promise((reso, reje) => {
|
||||||
this.once('qmpreturn', (e) => {
|
this.once('qmpreturn', (e) => {
|
||||||
@@ -76,6 +86,7 @@ export default class QMPClient extends EventEmitter {
|
|||||||
this.socket.write(JSON.stringify(args));
|
this.socket.write(JSON.stringify(args));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
} catch {res({})};
|
||||||
res(result);
|
res(result);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/User.ts
17
src/User.ts
@@ -3,6 +3,7 @@ import * as guacutils from './guacutils.js';
|
|||||||
import {WebSocket} from 'ws';
|
import {WebSocket} from 'ws';
|
||||||
import IConfig from './IConfig.js';
|
import IConfig from './IConfig.js';
|
||||||
import RateLimiter from './RateLimiter.js';
|
import RateLimiter from './RateLimiter.js';
|
||||||
|
import { execaCommand } from 'execa';
|
||||||
export class User {
|
export class User {
|
||||||
socket : WebSocket;
|
socket : WebSocket;
|
||||||
nopSendInterval : NodeJS.Timer;
|
nopSendInterval : NodeJS.Timer;
|
||||||
@@ -16,6 +17,7 @@ export class User {
|
|||||||
msgsSent : number;
|
msgsSent : number;
|
||||||
Config : IConfig;
|
Config : IConfig;
|
||||||
IP : string;
|
IP : string;
|
||||||
|
vote : boolean | null;
|
||||||
// Rate limiters
|
// Rate limiters
|
||||||
ChatRateLimit : RateLimiter;
|
ChatRateLimit : RateLimiter;
|
||||||
LoginRateLimit : RateLimiter;
|
LoginRateLimit : RateLimiter;
|
||||||
@@ -28,6 +30,7 @@ export class User {
|
|||||||
this.socket = ws;
|
this.socket = ws;
|
||||||
this.muted = false;
|
this.muted = false;
|
||||||
this.msgsSent = 0;
|
this.msgsSent = 0;
|
||||||
|
this.vote = null;
|
||||||
this.socket.on('close', () => {
|
this.socket.on('close', () => {
|
||||||
clearInterval(this.nopSendInterval);
|
clearInterval(this.nopSendInterval);
|
||||||
});
|
});
|
||||||
@@ -95,6 +98,20 @@ export class User {
|
|||||||
this.muted = false;
|
this.muted = false;
|
||||||
this.sendMsg(guacutils.encode("chat", "", "You are no longer muted."));
|
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;
|
||||||
|
//@ts-ignore
|
||||||
|
var cmd = this.Config.collabvm.bancmd.replace(/\$IP/g, this.IP).replace(/\$NAME/g, this.username);
|
||||||
|
await execaCommand(cmd);
|
||||||
|
this.kick();
|
||||||
|
}
|
||||||
|
|
||||||
|
async kick() {
|
||||||
|
this.sendMsg("10.disconnect");
|
||||||
|
this.socket.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Rank {
|
export enum Rank {
|
||||||
|
|||||||
334
src/WSServer.ts
334
src/WSServer.ts
@@ -5,7 +5,6 @@ import internal from 'stream';
|
|||||||
import * as Utilities from './Utilities.js';
|
import * as Utilities from './Utilities.js';
|
||||||
import { User, Rank } from './User.js';
|
import { User, Rank } from './User.js';
|
||||||
import * as guacutils from './guacutils.js';
|
import * as guacutils from './guacutils.js';
|
||||||
import * as fs from 'fs';
|
|
||||||
// I hate that you have to do it like this
|
// I hate that you have to do it like this
|
||||||
import CircularBuffer from 'mnemonist/circular-buffer.js';
|
import CircularBuffer from 'mnemonist/circular-buffer.js';
|
||||||
import Queue from 'mnemonist/queue.js';
|
import Queue from 'mnemonist/queue.js';
|
||||||
@@ -13,7 +12,7 @@ import { createHash } from 'crypto';
|
|||||||
import { isIP } from 'net';
|
import { isIP } from 'net';
|
||||||
import QEMUVM from './QEMUVM.js';
|
import QEMUVM from './QEMUVM.js';
|
||||||
import Framebuffer from './Framebuffer.js';
|
import Framebuffer from './Framebuffer.js';
|
||||||
import sharp, { Sharp } from 'sharp';
|
import sharp from 'sharp';
|
||||||
|
|
||||||
export default class WSServer {
|
export default class WSServer {
|
||||||
private Config : IConfig;
|
private Config : IConfig;
|
||||||
@@ -22,9 +21,22 @@ export default class WSServer {
|
|||||||
private clients : User[];
|
private clients : User[];
|
||||||
private ChatHistory : CircularBuffer<{user:string,msg:string}>
|
private ChatHistory : CircularBuffer<{user:string,msg:string}>
|
||||||
private TurnQueue : Queue<User>;
|
private TurnQueue : Queue<User>;
|
||||||
|
// Time remaining on the current turn
|
||||||
private TurnTime : number;
|
private TurnTime : number;
|
||||||
|
// Interval to keep track of the current turn time
|
||||||
private TurnInterval? : NodeJS.Timer;
|
private TurnInterval? : NodeJS.Timer;
|
||||||
|
// Is the turn interval running?
|
||||||
private TurnIntervalRunning : boolean;
|
private TurnIntervalRunning : boolean;
|
||||||
|
// If a reset vote is in progress
|
||||||
|
private voteInProgress : boolean;
|
||||||
|
// Interval to keep track of vote resets
|
||||||
|
private voteInterval? : NodeJS.Timer;
|
||||||
|
// How much time is left on the vote
|
||||||
|
private voteTime : number;
|
||||||
|
// How much time until another reset vote can be cast
|
||||||
|
private voteTimeout : number;
|
||||||
|
// Interval to keep track
|
||||||
|
private voteTimeoutInterval? : NodeJS.Timer;
|
||||||
private ModPerms : number;
|
private ModPerms : number;
|
||||||
private VM : QEMUVM;
|
private VM : QEMUVM;
|
||||||
private framebuffer : Framebuffer;
|
private framebuffer : Framebuffer;
|
||||||
@@ -35,6 +47,9 @@ export default class WSServer {
|
|||||||
this.TurnIntervalRunning = false;
|
this.TurnIntervalRunning = false;
|
||||||
this.clients = [];
|
this.clients = [];
|
||||||
this.Config = config;
|
this.Config = config;
|
||||||
|
this.voteInProgress = false;
|
||||||
|
this.voteTime = 0;
|
||||||
|
this.voteTimeout = 0;
|
||||||
this.ModPerms = Utilities.MakeModPerms(this.Config.collabvm.moderatorPermissions);
|
this.ModPerms = Utilities.MakeModPerms(this.Config.collabvm.moderatorPermissions);
|
||||||
this.server = http.createServer();
|
this.server = http.createServer();
|
||||||
this.socket = new WebSocketServer({noServer: true});
|
this.socket = new WebSocketServer({noServer: true});
|
||||||
@@ -156,49 +171,12 @@ export default class WSServer {
|
|||||||
var jpg = await sharp(await this.framebuffer.getFb(), {raw: {height: this.framebuffer.size.height, width: this.framebuffer.size.width, channels: 4}}).jpeg().toBuffer();
|
var jpg = await sharp(await this.framebuffer.getFb(), {raw: {height: this.framebuffer.size.height, width: this.framebuffer.size.width, channels: 4}}).jpeg().toBuffer();
|
||||||
var jpg64 = jpg.toString("base64");
|
var jpg64 = jpg.toString("base64");
|
||||||
client.sendMsg(guacutils.encode("png", "0", "0", "0", "0", jpg64));
|
client.sendMsg(guacutils.encode("png", "0", "0", "0", "0", jpg64));
|
||||||
|
if (this.voteInProgress) this.sendVoteUpdate(client);
|
||||||
|
this.sendTurnUpdate(client);
|
||||||
break;
|
break;
|
||||||
case "rename":
|
case "rename":
|
||||||
if (!client.RenameRateLimit.request()) return;
|
if (!client.RenameRateLimit.request()) return;
|
||||||
// This shouldn't need a ternary but it does for some reason
|
this.renameUser(client, msgArr[1]);
|
||||||
var hadName : boolean = client.username ? true : false;
|
|
||||||
var oldname : any;
|
|
||||||
if (hadName) oldname = client.username;
|
|
||||||
if (msgArr.length === 1) {
|
|
||||||
client.assignGuestName(this.getUsernameList());
|
|
||||||
} else {
|
|
||||||
var newName = msgArr[1];
|
|
||||||
if (hadName && newName === oldname) {
|
|
||||||
//@ts-ignore
|
|
||||||
client.sendMsg(guacutils.encode("rename", "0", "0", client.username));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.getUsernameList().indexOf(newName) !== -1) {
|
|
||||||
client.sendMsg(guacutils.encode("rename", "0", "1"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!/^[a-zA-Z0-9\ \-\_\.]+$/.test(newName)) {
|
|
||||||
client.sendMsg(guacutils.encode("rename", "0", "2"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.Config.collabvm.usernameblacklist.indexOf(newName) !== -1) {
|
|
||||||
client.sendMsg(guacutils.encode("rename", "0", "3"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
client.username = newName;
|
|
||||||
}
|
|
||||||
//@ts-ignore
|
|
||||||
client.sendMsg(guacutils.encode("rename", "0", "0", client.username));
|
|
||||||
if (hadName) {
|
|
||||||
console.log(`[RENAME] ${client.IP} 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}`);
|
|
||||||
this.clients.forEach((c) =>
|
|
||||||
//@ts-ignore
|
|
||||||
c.sendMsg(guacutils.encode("adduser", "1", client.username, client.rank)));
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case "chat":
|
case "chat":
|
||||||
if (!client.username) return;
|
if (!client.username) return;
|
||||||
@@ -247,6 +225,7 @@ export default class WSServer {
|
|||||||
break;
|
break;
|
||||||
case "mouse":
|
case "mouse":
|
||||||
if (this.TurnQueue.peek() !== client && client.rank !== Rank.Admin) return;
|
if (this.TurnQueue.peek() !== client && client.rank !== Rank.Admin) return;
|
||||||
|
if (!this.VM.vncOpen) return;
|
||||||
if (!this.VM.vnc) throw new Error("VNC Client was undefined");
|
if (!this.VM.vnc) throw new Error("VNC Client was undefined");
|
||||||
var x = parseInt(msgArr[1]);
|
var x = parseInt(msgArr[1]);
|
||||||
var y = parseInt(msgArr[2]);
|
var y = parseInt(msgArr[2]);
|
||||||
@@ -256,16 +235,44 @@ export default class WSServer {
|
|||||||
break;
|
break;
|
||||||
case "key":
|
case "key":
|
||||||
if (this.TurnQueue.peek() !== client && client.rank !== Rank.Admin) return;
|
if (this.TurnQueue.peek() !== client && client.rank !== Rank.Admin) return;
|
||||||
|
if (!this.VM.vncOpen) return;
|
||||||
if (!this.VM.vnc) throw new Error("VNC Client was undefined");
|
if (!this.VM.vnc) throw new Error("VNC Client was undefined");
|
||||||
var keysym = parseInt(msgArr[1]);
|
var keysym = parseInt(msgArr[1]);
|
||||||
var down = parseInt(msgArr[2]);
|
var down = parseInt(msgArr[2]);
|
||||||
if (keysym === undefined || (down !== 0 && down !== 1)) return;
|
if (keysym === undefined || (down !== 0 && down !== 1)) return;
|
||||||
this.VM.vnc.keyEvent(keysym, down);
|
this.VM.vnc.keyEvent(keysym, down);
|
||||||
break;
|
break;
|
||||||
|
case "vote":
|
||||||
|
if (!client.connectedToNode) return;
|
||||||
|
if (msgArr.length !== 2) return;
|
||||||
|
switch (msgArr[1]) {
|
||||||
|
case "1":
|
||||||
|
if (!this.voteInProgress) {
|
||||||
|
if (this.voteTimeout !== 0) {
|
||||||
|
client.sendMsg(guacutils.encode("vote", "3", this.voteTimeout.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)
|
||||||
|
this.clients.forEach(c => c.sendMsg(guacutils.encode("chat", "", `${client.username} has voted yes.`)));
|
||||||
|
client.vote = true;
|
||||||
|
break;
|
||||||
|
case "0":
|
||||||
|
if (!this.voteInProgress) return;
|
||||||
|
if (client.vote !== false)
|
||||||
|
this.clients.forEach(c => c.sendMsg(guacutils.encode("chat", "", `${client.username} has voted no.`)));
|
||||||
|
client.vote = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.sendVoteUpdate();
|
||||||
|
break;
|
||||||
case "admin":
|
case "admin":
|
||||||
if (msgArr.length < 2) return;
|
if (msgArr.length < 2) return;
|
||||||
switch (msgArr[1]) {
|
switch (msgArr[1]) {
|
||||||
case "2":
|
case "2":
|
||||||
|
// Login
|
||||||
if (!client.LoginRateLimit.request()) return;
|
if (!client.LoginRateLimit.request()) return;
|
||||||
if (msgArr.length !== 3) return;
|
if (msgArr.length !== 3) return;
|
||||||
var sha256 = createHash("sha256");
|
var sha256 = createHash("sha256");
|
||||||
@@ -285,7 +292,124 @@ export default class WSServer {
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
this.clients.forEach((c) => c.sendMsg(guacutils.encode("adduser", "1", client.username, client.rank)));
|
this.clients.forEach((c) => c.sendMsg(guacutils.encode("adduser", "1", client.username, client.rank)));
|
||||||
break;
|
break;
|
||||||
|
case "5":
|
||||||
|
// QEMU Monitor
|
||||||
|
if (client.rank !== Rank.Admin) return;
|
||||||
|
if (msgArr.length !== 4 || msgArr[2] !== this.Config.collabvm.node) return;
|
||||||
|
var output = await this.VM.qmpClient.runMonitorCmd(msgArr[3]);
|
||||||
|
client.sendMsg(guacutils.encode("admin", "2", String(output)));
|
||||||
|
break;
|
||||||
|
case "8":
|
||||||
|
// Restore
|
||||||
|
if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.restore)) return;
|
||||||
|
this.VM.Restore();
|
||||||
|
break;
|
||||||
|
case "10":
|
||||||
|
// Reboot
|
||||||
|
if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.reboot)) return;
|
||||||
|
if (msgArr.length !== 3 || msgArr[2] !== this.Config.collabvm.node) return;
|
||||||
|
this.VM.Reboot();
|
||||||
|
break;
|
||||||
|
case "12":
|
||||||
|
// Ban
|
||||||
|
if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.ban)) return;
|
||||||
|
var user = this.clients.find(c => c.username === msgArr[2]);
|
||||||
|
if (!user) return;
|
||||||
|
user.ban();
|
||||||
|
case "13":
|
||||||
|
// Force Vote
|
||||||
|
if (msgArr.length !== 3) return;
|
||||||
|
if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.forcevote)) return;
|
||||||
|
if (!this.voteInProgress) return;
|
||||||
|
switch (msgArr[2]) {
|
||||||
|
case "1":
|
||||||
|
this.endVote(true);
|
||||||
|
break;
|
||||||
|
case "0":
|
||||||
|
this.endVote(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "14":
|
||||||
|
// Mute
|
||||||
|
if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.mute)) return;
|
||||||
|
if (msgArr.length !== 4) return;
|
||||||
|
var user = this.clients.find(c => c.username === msgArr[2]);
|
||||||
|
if (!user) return;
|
||||||
|
var permamute;
|
||||||
|
switch (msgArr[3]) {
|
||||||
|
case "0":
|
||||||
|
permamute = false;
|
||||||
|
break;
|
||||||
|
case "1":
|
||||||
|
permamute = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
user.mute(permamute);
|
||||||
|
break;
|
||||||
|
case "15":
|
||||||
|
// Kick
|
||||||
|
if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.kick)) return;
|
||||||
|
var user = this.clients.find(c => c.username === msgArr[2]);
|
||||||
|
if (!user) return;
|
||||||
|
user.kick();
|
||||||
|
break;
|
||||||
|
case "16":
|
||||||
|
// End turn
|
||||||
|
if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.bypassturn)) return;
|
||||||
|
if (msgArr.length !== 3) return;
|
||||||
|
var user = this.clients.find(c => c.username === msgArr[2]);
|
||||||
|
if (!user) return;
|
||||||
|
this.endTurn(user);
|
||||||
|
break;
|
||||||
|
case "17":
|
||||||
|
// Clear turn queue
|
||||||
|
if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.bypassturn)) return;
|
||||||
|
if (msgArr.length !== 3 || msgArr[2] !== this.Config.collabvm.node) return;
|
||||||
|
this.clearTurns();
|
||||||
|
break;
|
||||||
|
case "18":
|
||||||
|
// Rename user
|
||||||
|
if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.rename)) return;
|
||||||
|
if (msgArr.length !== 4) return;
|
||||||
|
var user = this.clients.find(c => c.username === msgArr[2]);
|
||||||
|
if (!user) return;
|
||||||
|
this.renameUser(user, msgArr[3]);
|
||||||
|
break;
|
||||||
|
case "19":
|
||||||
|
// Get IP
|
||||||
|
if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.grabip)) return;
|
||||||
|
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));
|
||||||
|
break;
|
||||||
|
case "20":
|
||||||
|
// Steal turn
|
||||||
|
if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.bypassturn)) return;
|
||||||
|
this.bypassTurn(client);
|
||||||
|
break;
|
||||||
|
case "21":
|
||||||
|
// XSS
|
||||||
|
if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.xss)) return;
|
||||||
|
if (msgArr.length !== 3) return;
|
||||||
|
switch (client.rank) {
|
||||||
|
case Rank.Admin:
|
||||||
|
//@ts-ignore
|
||||||
|
this.clients.forEach(c => c.sendMsg(guacutils.encode("chat", client.username, msgArr[2])));
|
||||||
|
//@ts-ignore
|
||||||
|
this.ChatHistory.push({user: client.username, msg: msgArr[2]});
|
||||||
|
break;
|
||||||
|
case Rank.Moderator:
|
||||||
|
//@ts-ignore
|
||||||
|
this.clients.filter(c => c.rank !== Rank.Admin).forEach(c => c.sendMsg(guacutils.encode("chat", client.username, msgArr[2])));
|
||||||
|
//@ts-ignore
|
||||||
|
this.clients.filter(c => c.rank === Rank.Admin).forEach(c => c.sendMsg(guacutils.encode("chat", client.username, Utilities.HTMLSanitize(msgArr[2]))));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -298,6 +422,49 @@ export default class WSServer {
|
|||||||
this.clients.filter(c => c.username).forEach((c) => arr.push(c.username));
|
this.clients.filter(c => c.username).forEach((c) => arr.push(c.username));
|
||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renameUser(client : User, newName? : string) {
|
||||||
|
// This shouldn't need a ternary but it does for some reason
|
||||||
|
var hadName : boolean = client.username ? true : false;
|
||||||
|
var oldname : any;
|
||||||
|
if (hadName) oldname = client.username;
|
||||||
|
var status = "0";
|
||||||
|
if (!newName) {
|
||||||
|
client.assignGuestName(this.getUsernameList());
|
||||||
|
} else {
|
||||||
|
if (hadName && newName === oldname) {
|
||||||
|
//@ts-ignore
|
||||||
|
client.sendMsg(guacutils.encode("rename", "0", "0", client.username));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.getUsernameList().indexOf(newName) !== -1) {
|
||||||
|
client.assignGuestName(this.getUsernameList());
|
||||||
|
status = "1";
|
||||||
|
} else
|
||||||
|
if (!/^[a-zA-Z0-9\ \-\_\.]+$/.test(newName) || newName.length > 20 || newName.length < 3) {
|
||||||
|
client.assignGuestName(this.getUsernameList());
|
||||||
|
status = "2";
|
||||||
|
} else
|
||||||
|
if (this.Config.collabvm.usernameblacklist.indexOf(newName) !== -1) {
|
||||||
|
client.assignGuestName(this.getUsernameList());
|
||||||
|
status = "3";
|
||||||
|
} else client.username = newName;
|
||||||
|
}
|
||||||
|
//@ts-ignore
|
||||||
|
client.sendMsg(guacutils.encode("rename", "0", status, client.username));
|
||||||
|
if (hadName) {
|
||||||
|
console.log(`[RENAME] ${client.IP} 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}`);
|
||||||
|
this.clients.forEach((c) =>
|
||||||
|
//@ts-ignore
|
||||||
|
c.sendMsg(guacutils.encode("adduser", "1", client.username, client.rank)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getAdduserMsg() : string {
|
getAdduserMsg() : string {
|
||||||
var arr : string[] = ["adduser", this.clients.length.toString()];
|
var arr : string[] = ["adduser", this.clients.length.toString()];
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
@@ -309,12 +476,16 @@ export default class WSServer {
|
|||||||
this.ChatHistory.forEach(c => arr.push(c.user, c.msg));
|
this.ChatHistory.forEach(c => arr.push(c.user, c.msg));
|
||||||
return guacutils.encode(...arr);
|
return guacutils.encode(...arr);
|
||||||
}
|
}
|
||||||
private sendTurnUpdate() {
|
private sendTurnUpdate(client? : User) {
|
||||||
var turnQueueArr = this.TurnQueue.toArray();
|
var turnQueueArr = this.TurnQueue.toArray();
|
||||||
var arr = ["turn", (this.TurnTime * 1000).toString(), this.TurnQueue.size.toString()];
|
var arr = ["turn", (this.TurnTime * 1000).toString(), this.TurnQueue.size.toString()];
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.TurnQueue.forEach((c) => arr.push(c.username));
|
this.TurnQueue.forEach((c) => arr.push(c.username));
|
||||||
var currentTurningUser = this.TurnQueue.peek();
|
var currentTurningUser = this.TurnQueue.peek();
|
||||||
|
if (client) {
|
||||||
|
client.sendMsg(guacutils.encode(...arr));
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.clients.filter(c => (c !== currentTurningUser && c.connectedToNode)).forEach((c) => {
|
this.clients.filter(c => (c !== currentTurningUser && c.connectedToNode)).forEach((c) => {
|
||||||
if (turnQueueArr.indexOf(c) !== -1) {
|
if (turnQueueArr.indexOf(c) !== -1) {
|
||||||
var time = ((this.TurnTime * 1000) + ((turnQueueArr.indexOf(c) - 1) * this.Config.collabvm.turnTime * 1000));
|
var time = ((this.TurnTime * 1000) + ((turnQueueArr.indexOf(c) - 1) * this.Config.collabvm.turnTime * 1000));
|
||||||
@@ -336,6 +507,27 @@ export default class WSServer {
|
|||||||
}
|
}
|
||||||
this.sendTurnUpdate();
|
this.sendTurnUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearTurns() {
|
||||||
|
clearInterval(this.TurnInterval);
|
||||||
|
this.TurnIntervalRunning = false;
|
||||||
|
this.TurnQueue.clear();
|
||||||
|
this.sendTurnUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
bypassTurn(client : User) {
|
||||||
|
var a = this.TurnQueue.toArray().filter(c => c !== client);
|
||||||
|
this.TurnQueue = Queue.from([client, ...a]);
|
||||||
|
this.nextTurn();
|
||||||
|
}
|
||||||
|
|
||||||
|
endTurn(client : User) {
|
||||||
|
var hasTurn = (this.TurnQueue.peek() === client);
|
||||||
|
this.TurnQueue = Queue.from(this.TurnQueue.toArray().filter(c => c !== client));
|
||||||
|
if (hasTurn) this.nextTurn();
|
||||||
|
else this.sendTurnUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
private turnInterval() {
|
private turnInterval() {
|
||||||
this.TurnTime--;
|
this.TurnTime--;
|
||||||
if (this.TurnTime < 1) {
|
if (this.TurnTime < 1) {
|
||||||
@@ -364,4 +556,58 @@ export default class WSServer {
|
|||||||
res(jpg.toString("base64"));
|
res(jpg.toString("base64"));
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
startVote() {
|
||||||
|
if (this.voteInProgress) return;
|
||||||
|
this.voteInProgress = true;
|
||||||
|
this.clients.forEach(c => c.sendMsg(guacutils.encode("vote", "0")));
|
||||||
|
this.voteTime = this.Config.collabvm.voteTime;
|
||||||
|
this.voteInterval = setInterval(() => {
|
||||||
|
this.voteTime--;
|
||||||
|
if (this.voteTime < 1) {
|
||||||
|
this.endVote();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
endVote(result? : boolean) {
|
||||||
|
if (!this.voteInProgress) return;
|
||||||
|
this.voteInProgress = false;
|
||||||
|
clearInterval(this.voteInterval);
|
||||||
|
var count = this.getVoteCounts();
|
||||||
|
this.clients.forEach((c) => c.sendMsg(guacutils.encode("vote", "2")));
|
||||||
|
if (result === true || (result === undefined && count.yes >= count.no)) {
|
||||||
|
this.clients.forEach(c => c.sendMsg(guacutils.encode("chat", "", "The vote to reset the VM has won.")));
|
||||||
|
this.VM.Restore();
|
||||||
|
} 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);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendVoteUpdate(client? : User) {
|
||||||
|
if (!this.voteInProgress) return;
|
||||||
|
var count = this.getVoteCounts();
|
||||||
|
var msg = guacutils.encode("vote", "1", (this.voteTime * 1000).toString(), count.yes.toString(), count.no.toString());
|
||||||
|
if (client)
|
||||||
|
client.sendMsg(msg);
|
||||||
|
else
|
||||||
|
this.clients.forEach((c) => c.sendMsg(msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
getVoteCounts() : {yes:number,no:number} {
|
||||||
|
var yes = 0;
|
||||||
|
var no = 0;
|
||||||
|
this.clients.forEach((c) => {
|
||||||
|
if (c.vote === true) yes++;
|
||||||
|
if (c.vote === false) no++;
|
||||||
|
});
|
||||||
|
return {yes:yes,no:no};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user