2023-01-31 22:00:30 -05:00
|
|
|
import {WebSocketServer, WebSocket} from 'ws';
|
|
|
|
|
import * as http from 'http';
|
2023-02-02 21:19:55 -05:00
|
|
|
import IConfig from './IConfig.js';
|
2023-01-31 22:00:30 -05:00
|
|
|
import internal from 'stream';
|
2023-02-02 21:19:55 -05:00
|
|
|
import * as Utilities from './Utilities.js';
|
|
|
|
|
import { User, Rank } from './User.js';
|
|
|
|
|
import * as guacutils from './guacutils.js';
|
|
|
|
|
// I hate that you have to do it like this
|
|
|
|
|
import CircularBuffer from 'mnemonist/circular-buffer.js';
|
|
|
|
|
import Queue from 'mnemonist/queue.js';
|
2023-01-31 22:00:30 -05:00
|
|
|
import { createHash } from 'crypto';
|
2024-04-23 09:57:02 -04:00
|
|
|
import { isIP } from 'node:net';
|
|
|
|
|
import { QemuVM, QemuVmDefinition } from '@cvmts/qemu';
|
|
|
|
|
import { IPData, IPDataManager } from './IPData.js';
|
|
|
|
|
import { readFileSync } from 'node:fs';
|
2024-04-23 19:43:23 -04:00
|
|
|
import path from 'node:path';
|
2024-04-05 09:10:47 -04:00
|
|
|
import AuthManager from './AuthManager.js';
|
2024-04-23 09:57:02 -04:00
|
|
|
import { Size, Rect, Logger } from '@cvmts/shared';
|
|
|
|
|
|
|
|
|
|
import sharp from 'sharp';
|
2024-04-23 19:43:23 -04:00
|
|
|
import Piscina from 'piscina';
|
2024-04-23 09:57:02 -04:00
|
|
|
|
2024-04-23 19:43:23 -04:00
|
|
|
// Instead of strange hacks we can just use nodejs provided
|
|
|
|
|
// import.meta properties, which have existed since LTS if not before
|
|
|
|
|
const __filename = import.meta.filename;
|
|
|
|
|
const __dirname = import.meta.dirname;
|
2024-04-23 10:42:36 -04:00
|
|
|
|
2024-04-23 19:43:23 -04:00
|
|
|
const kCVMTSAssetsRoot = path.resolve(__dirname, '../../assets');
|
2024-04-23 10:42:36 -04:00
|
|
|
|
2024-04-23 09:57:02 -04:00
|
|
|
type ChatHistory = {
|
|
|
|
|
user: string,
|
|
|
|
|
msg: string
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// A good balance. TODO: Configurable?
|
|
|
|
|
const kJpegQuality = 35;
|
|
|
|
|
|
|
|
|
|
// this returns appropiate Sharp options to deal with the framebuffer
|
|
|
|
|
function GetRawSharpOptions(size: Size): sharp.CreateRaw {
|
|
|
|
|
return {
|
|
|
|
|
width: size.width,
|
|
|
|
|
height: size.height,
|
|
|
|
|
channels: 4
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-23 19:43:23 -04:00
|
|
|
const kJpegPool = new Piscina({
|
|
|
|
|
filename: path.join(import.meta.dirname + '/JPEGEncoderWorker.js'),
|
|
|
|
|
minThreads: 4,
|
|
|
|
|
maxThreads: 4
|
|
|
|
|
});
|
|
|
|
|
|
2024-04-23 09:57:02 -04:00
|
|
|
async function EncodeJpeg(canvas: Buffer, displaySize: Size, rect: Rect): Promise<Buffer> {
|
|
|
|
|
let offset = (rect.y * displaySize.width + rect.x) * 4;
|
|
|
|
|
|
2024-04-23 19:43:23 -04:00
|
|
|
let res = await kJpegPool.run({
|
|
|
|
|
buffer: canvas.subarray(offset),
|
|
|
|
|
width: rect.width,
|
|
|
|
|
height: rect.height,
|
|
|
|
|
stride: displaySize.width,
|
|
|
|
|
quality: kJpegQuality
|
|
|
|
|
});
|
2024-04-23 09:57:02 -04:00
|
|
|
|
2024-04-23 19:43:23 -04:00
|
|
|
|
|
|
|
|
// have to manually turn it back into a buffer because
|
|
|
|
|
// Piscina for some reason turns it into a Uint8Array
|
|
|
|
|
return Buffer.from(res);
|
2024-04-23 09:57:02 -04:00
|
|
|
}
|
2023-01-31 22:00:30 -05:00
|
|
|
|
|
|
|
|
export default class WSServer {
|
|
|
|
|
private Config : IConfig;
|
2024-04-23 09:57:02 -04:00
|
|
|
|
|
|
|
|
private httpServer : http.Server;
|
|
|
|
|
private wsServer : WebSocketServer;
|
|
|
|
|
|
2023-01-31 22:00:30 -05:00
|
|
|
private clients : User[];
|
2024-04-23 09:57:02 -04:00
|
|
|
|
|
|
|
|
private ChatHistory : CircularBuffer<ChatHistory>
|
|
|
|
|
|
2023-01-31 22:00:30 -05:00
|
|
|
private TurnQueue : Queue<User>;
|
2024-04-23 09:57:02 -04:00
|
|
|
|
2023-02-07 12:29:33 -05:00
|
|
|
// Time remaining on the current turn
|
2023-01-31 22:00:30 -05:00
|
|
|
private TurnTime : number;
|
2024-04-23 09:57:02 -04:00
|
|
|
|
2023-02-07 12:29:33 -05:00
|
|
|
// Interval to keep track of the current turn time
|
2023-09-12 00:25:57 +02:00
|
|
|
private TurnInterval? : NodeJS.Timeout;
|
2024-04-23 09:57:02 -04:00
|
|
|
|
2023-02-07 12:29:33 -05:00
|
|
|
// If a reset vote is in progress
|
|
|
|
|
private voteInProgress : boolean;
|
2024-04-23 09:57:02 -04:00
|
|
|
|
2023-02-07 12:29:33 -05:00
|
|
|
// Interval to keep track of vote resets
|
2023-09-12 00:25:57 +02:00
|
|
|
private voteInterval? : NodeJS.Timeout;
|
2024-04-23 09:57:02 -04:00
|
|
|
|
2023-02-07 12:29:33 -05:00
|
|
|
// How much time is left on the vote
|
|
|
|
|
private voteTime : number;
|
2024-04-23 09:57:02 -04:00
|
|
|
|
2023-02-07 12:29:33 -05:00
|
|
|
// How much time until another reset vote can be cast
|
2023-02-11 15:58:20 +01:00
|
|
|
private voteCooldown : number;
|
2024-04-23 09:57:02 -04:00
|
|
|
|
2023-02-07 12:29:33 -05:00
|
|
|
// Interval to keep track
|
2023-09-12 00:25:57 +02:00
|
|
|
private voteCooldownInterval? : NodeJS.Timeout;
|
2024-04-23 09:57:02 -04:00
|
|
|
|
2023-02-09 20:08:32 -05:00
|
|
|
// Completely disable turns
|
|
|
|
|
private turnsAllowed : boolean;
|
2024-04-23 09:57:02 -04:00
|
|
|
|
2023-09-02 17:54:44 -04:00
|
|
|
// Hide the screen
|
|
|
|
|
private screenHidden : boolean;
|
2024-04-23 09:57:02 -04:00
|
|
|
|
2023-09-02 17:54:44 -04:00
|
|
|
// base64 image to show when the screen is hidden
|
|
|
|
|
private screenHiddenImg : string;
|
|
|
|
|
private screenHiddenThumb : string;
|
2024-04-23 09:57:02 -04:00
|
|
|
|
2023-02-09 20:49:27 -05:00
|
|
|
// Indefinite turn
|
|
|
|
|
private indefiniteTurn : User | null;
|
2023-02-07 20:51:25 -05:00
|
|
|
private ModPerms : number;
|
2024-04-23 09:57:02 -04:00
|
|
|
private VM : QemuVM;
|
2024-04-05 09:10:47 -04:00
|
|
|
|
|
|
|
|
// Authentication manager
|
|
|
|
|
private auth : AuthManager | null;
|
|
|
|
|
|
2024-04-23 09:57:02 -04:00
|
|
|
private logger = new Logger("CVMTS.Server");
|
|
|
|
|
|
|
|
|
|
constructor(config : IConfig, vm : QemuVM, auth : AuthManager | null) {
|
2023-05-01 15:03:14 +01:00
|
|
|
this.Config = config;
|
2024-04-23 09:57:02 -04:00
|
|
|
this.ChatHistory = new CircularBuffer<ChatHistory>(Array, this.Config.collabvm.maxChatHistoryLength);
|
2023-01-31 22:00:30 -05:00
|
|
|
this.TurnQueue = new Queue<User>();
|
|
|
|
|
this.TurnTime = 0;
|
|
|
|
|
this.clients = [];
|
2023-02-07 12:29:33 -05:00
|
|
|
this.voteInProgress = false;
|
|
|
|
|
this.voteTime = 0;
|
2023-02-11 15:58:20 +01:00
|
|
|
this.voteCooldown = 0;
|
2023-02-09 20:08:32 -05:00
|
|
|
this.turnsAllowed = true;
|
2023-09-02 17:54:44 -04:00
|
|
|
this.screenHidden = false;
|
2024-04-23 19:43:23 -04:00
|
|
|
this.screenHiddenImg = readFileSync(path.join(kCVMTSAssetsRoot, "screenhidden.jpeg")).toString("base64");
|
|
|
|
|
this.screenHiddenThumb = readFileSync(path.join(kCVMTSAssetsRoot, "screenhiddenthumb.jpeg")).toString("base64");
|
2023-09-02 17:54:44 -04:00
|
|
|
|
2023-02-09 20:49:27 -05:00
|
|
|
this.indefiniteTurn = null;
|
2023-01-31 22:00:30 -05:00
|
|
|
this.ModPerms = Utilities.MakeModPerms(this.Config.collabvm.moderatorPermissions);
|
2024-04-23 09:57:02 -04:00
|
|
|
this.httpServer = http.createServer();
|
|
|
|
|
this.wsServer = new WebSocketServer({noServer: true});
|
|
|
|
|
this.httpServer.on('upgrade', (req : http.IncomingMessage, socket : internal.Duplex, head : Buffer) => this.httpOnUpgrade(req, socket, head));
|
|
|
|
|
this.httpServer.on('request', (req, res) => {
|
2023-02-07 12:52:03 -05:00
|
|
|
res.writeHead(426);
|
|
|
|
|
res.write("This server only accepts WebSocket connections.");
|
|
|
|
|
res.end();
|
|
|
|
|
});
|
2024-04-23 09:57:02 -04:00
|
|
|
|
|
|
|
|
let initSize = vm.GetDisplay().Size() || {
|
|
|
|
|
width: 0,
|
|
|
|
|
height: 0
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.OnDisplayResized(initSize);
|
|
|
|
|
|
|
|
|
|
vm.GetDisplay().on('resize', (size: Size) => this.OnDisplayResized(size));
|
|
|
|
|
vm.GetDisplay().on('rect', (rect: Rect) => this.OnDisplayRectangle(rect));
|
|
|
|
|
|
2023-02-02 21:19:55 -05:00
|
|
|
this.VM = vm;
|
2024-04-23 09:57:02 -04:00
|
|
|
|
2024-04-05 09:10:47 -04:00
|
|
|
// authentication manager
|
|
|
|
|
this.auth = auth;
|
2023-01-31 22:00:30 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
listen() {
|
2024-04-23 09:57:02 -04:00
|
|
|
this.httpServer.listen(this.Config.http.port, this.Config.http.host);
|
2023-01-31 22:00:30 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private httpOnUpgrade(req : http.IncomingMessage, socket : internal.Duplex, head : Buffer) {
|
|
|
|
|
var killConnection = () => {
|
|
|
|
|
socket.write("HTTP/1.1 400 Bad Request\n\n400 Bad Request");
|
|
|
|
|
socket.destroy();
|
|
|
|
|
}
|
2023-05-25 15:59:16 +02:00
|
|
|
|
|
|
|
|
if (req.headers['sec-websocket-protocol'] !== "guacamole") {
|
2023-01-31 22:00:30 -05:00
|
|
|
killConnection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-05-25 15:59:16 +02:00
|
|
|
|
|
|
|
|
if (this.Config.http.origin) {
|
|
|
|
|
// If the client is not sending an Origin header, kill the connection.
|
|
|
|
|
if(!req.headers.origin) {
|
|
|
|
|
killConnection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try to parse the Origin header sent by the client, if it fails, kill the connection.
|
2024-03-31 13:36:55 -04:00
|
|
|
var _uri;
|
2023-05-25 15:59:16 +02:00
|
|
|
var _host;
|
|
|
|
|
try {
|
2024-03-31 13:36:55 -04:00
|
|
|
_uri = new URL(req.headers.origin.toLowerCase());
|
|
|
|
|
_host = _uri.host;
|
2023-05-25 15:59:16 +02:00
|
|
|
} catch {
|
|
|
|
|
killConnection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-31 13:36:55 -04:00
|
|
|
// detect fake origin headers
|
|
|
|
|
if (_uri.pathname !== "/" || _uri.search !== "") {
|
|
|
|
|
killConnection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-25 15:59:16 +02:00
|
|
|
// If the domain name is not in the list of allowed origins, kill the connection.
|
|
|
|
|
if(!this.Config.http.originAllowedDomains.includes(_host)) {
|
|
|
|
|
killConnection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-31 13:36:28 -04:00
|
|
|
let ip: string;
|
|
|
|
|
if (this.Config.http.proxying) {
|
2023-01-31 22:00:30 -05:00
|
|
|
// If the requesting IP isn't allowed to proxy, kill it
|
2024-03-31 13:36:28 -04:00
|
|
|
if (this.Config.http.proxyAllowedIps.indexOf(req.socket.remoteAddress!) === -1) {
|
|
|
|
|
killConnection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Make sure x-forwarded-for is set
|
|
|
|
|
if (req.headers["x-forwarded-for"] === undefined) {
|
2023-01-31 22:00:30 -05:00
|
|
|
killConnection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
// Get the first IP from the X-Forwarded-For variable
|
2024-03-31 13:36:28 -04:00
|
|
|
ip = req.headers["x-forwarded-for"]?.toString().replace(/\ /g, "").split(",")[0];
|
2023-01-31 22:00:30 -05:00
|
|
|
} catch {
|
2023-09-12 00:25:57 +02:00
|
|
|
// If we can't get the IP, kill the connection
|
2023-01-31 22:00:30 -05:00
|
|
|
killConnection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// If for some reason the IP isn't defined, kill it
|
2024-03-31 13:36:28 -04:00
|
|
|
if (!ip) {
|
2023-01-31 22:00:30 -05:00
|
|
|
killConnection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-09-12 00:25:57 +02:00
|
|
|
// Make sure the IP is valid. If not, kill the connection.
|
2024-03-31 13:36:28 -04:00
|
|
|
if (!isIP(ip)) {
|
2023-01-31 22:00:30 -05:00
|
|
|
killConnection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (!req.socket.remoteAddress) return;
|
|
|
|
|
ip = req.socket.remoteAddress;
|
|
|
|
|
}
|
2023-02-11 15:58:20 +01:00
|
|
|
|
2023-09-12 00:25:57 +02:00
|
|
|
// 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();
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-23 09:57:02 -04:00
|
|
|
this.wsServer.handleUpgrade(req, socket, head, (ws: WebSocket) => {
|
|
|
|
|
this.wsServer.emit('connection', ws, req);
|
2024-03-31 13:36:28 -04:00
|
|
|
this.onConnection(ws, req, ip);
|
|
|
|
|
});
|
2023-09-12 00:25:57 +02:00
|
|
|
}
|
|
|
|
|
|
2024-03-31 13:36:28 -04:00
|
|
|
private onConnection(ws : WebSocket, req: http.IncomingMessage, ip : string) {
|
2024-04-23 09:57:02 -04:00
|
|
|
let user = new User(ws, IPDataManager.GetIPData(ip), this.Config);
|
2023-01-31 22:00:30 -05:00
|
|
|
this.clients.push(user);
|
2024-04-23 09:57:02 -04:00
|
|
|
|
|
|
|
|
ws.on('error', (e) => {
|
|
|
|
|
this.logger.Error(`${e} (caused by connection ${ip})`);
|
2023-09-12 00:25:57 +02:00
|
|
|
ws.close();
|
|
|
|
|
});
|
2024-04-23 09:57:02 -04:00
|
|
|
|
2023-01-31 22:00:30 -05:00
|
|
|
ws.on('close', () => this.connectionClosed(user));
|
2024-04-23 09:57:02 -04:00
|
|
|
|
|
|
|
|
ws.on('message', (buf: Buffer, isBinary: boolean) => {
|
2023-01-31 22:00:30 -05:00
|
|
|
var msg;
|
2024-04-23 09:57:02 -04:00
|
|
|
|
|
|
|
|
// Close the user's connection if they send a non-string message
|
|
|
|
|
if(isBinary) {
|
2023-01-31 22:00:30 -05:00
|
|
|
user.closeConnection();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-04-23 09:57:02 -04:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
this.onMessage(user, buf.toString());
|
|
|
|
|
} catch {
|
|
|
|
|
}
|
2023-01-31 22:00:30 -05:00
|
|
|
});
|
2024-04-23 09:57:02 -04:00
|
|
|
|
2024-04-07 14:33:43 -04:00
|
|
|
if (this.Config.auth.enabled) {
|
|
|
|
|
user.sendMsg(guacutils.encode("auth", this.Config.auth.apiEndpoint));
|
|
|
|
|
}
|
2023-01-31 22:00:30 -05:00
|
|
|
user.sendMsg(this.getAdduserMsg());
|
2024-04-23 09:57:02 -04:00
|
|
|
this.logger.Info(`Connect from ${user.IP.address}`);
|
2023-01-31 22:00:30 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private connectionClosed(user : User) {
|
2024-04-23 09:57:02 -04:00
|
|
|
let clientIndex = this.clients.indexOf(user)
|
|
|
|
|
if (clientIndex === -1) return;
|
|
|
|
|
|
2023-09-12 00:25:57 +02:00
|
|
|
if(user.IP.vote != null) {
|
|
|
|
|
user.IP.vote = null;
|
|
|
|
|
this.sendVoteUpdate();
|
2024-04-23 09:57:02 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Unreference the IP data.
|
|
|
|
|
user.IP.Unref();
|
|
|
|
|
|
2023-02-23 16:45:57 -05:00
|
|
|
if (this.indefiniteTurn === user) this.indefiniteTurn = null;
|
2024-04-23 09:57:02 -04:00
|
|
|
|
|
|
|
|
this.clients.splice(clientIndex, 1);
|
|
|
|
|
|
|
|
|
|
this.logger.Info(`Disconnect From ${user.IP.address}${user.username ? ` with username ${user.username}` : ""}`);
|
2023-01-31 22:00:30 -05:00
|
|
|
if (!user.username) return;
|
|
|
|
|
if (this.TurnQueue.toArray().indexOf(user) !== -1) {
|
|
|
|
|
var hadturn = (this.TurnQueue.peek() === user);
|
|
|
|
|
this.TurnQueue = Queue.from(this.TurnQueue.toArray().filter(u => u !== user));
|
|
|
|
|
if (hadturn) this.nextTurn();
|
|
|
|
|
}
|
2024-03-31 13:36:28 -04:00
|
|
|
|
|
|
|
|
this.clients.forEach((c) => c.sendMsg(guacutils.encode("remuser", "1", user.username!)));
|
2023-01-31 22:00:30 -05:00
|
|
|
}
|
2024-04-23 09:57:02 -04:00
|
|
|
|
|
|
|
|
|
2023-02-02 21:19:55 -05:00
|
|
|
private async onMessage(client : User, message : string) {
|
2023-01-31 22:00:30 -05:00
|
|
|
var msgArr = guacutils.decode(message);
|
|
|
|
|
if (msgArr.length < 1) return;
|
|
|
|
|
switch (msgArr[0]) {
|
2024-04-05 09:10:47 -04:00
|
|
|
case "login":
|
|
|
|
|
if (msgArr.length !== 2 || !this.Config.auth.enabled) return;
|
2024-04-07 15:15:20 -04:00
|
|
|
if (!client.connectedToNode) {
|
|
|
|
|
client.sendMsg(guacutils.encode("login", "0", "You must connect to the VM before logging in."));
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-04-05 09:10:47 -04:00
|
|
|
var res = await this.auth!.Authenticate(msgArr[1], client);
|
|
|
|
|
if (res.clientSuccess) {
|
2024-04-23 09:57:02 -04:00
|
|
|
this.logger.Info(`${client.IP.address} logged in as ${res.username}`);
|
2024-04-05 09:10:47 -04:00
|
|
|
client.sendMsg(guacutils.encode("login", "1"));
|
|
|
|
|
var old = this.clients.find(c=>c.username === res.username);
|
|
|
|
|
if (old) {
|
|
|
|
|
// kick() doesnt wait until the user is actually removed from the list and itd be anal to make it do that
|
|
|
|
|
// so we call connectionClosed manually here. When it gets called on kick(), it will return because the user isn't in the list
|
|
|
|
|
this.connectionClosed(old);
|
|
|
|
|
await old.kick();
|
|
|
|
|
}
|
|
|
|
|
// Set username
|
|
|
|
|
this.renameUser(client, res.username);
|
|
|
|
|
// Set rank
|
|
|
|
|
client.rank = res.rank;
|
|
|
|
|
if (client.rank === Rank.Admin) {
|
|
|
|
|
client.sendMsg(guacutils.encode("admin", "0", "1"));
|
|
|
|
|
} else if (client.rank === Rank.Moderator) {
|
|
|
|
|
client.sendMsg(guacutils.encode("admin", "0", "3", this.ModPerms.toString()));
|
|
|
|
|
}
|
|
|
|
|
this.clients.forEach((c) => c.sendMsg(guacutils.encode("adduser", "1", client.username!, client.rank.toString())));
|
|
|
|
|
} else {
|
|
|
|
|
client.sendMsg(guacutils.encode("login", "0", res.error!));
|
|
|
|
|
if (res.error === "You are banned") {
|
|
|
|
|
client.kick();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
2023-01-31 22:00:30 -05:00
|
|
|
case "list":
|
2023-09-02 17:54:44 -04:00
|
|
|
client.sendMsg(guacutils.encode("list", this.Config.collabvm.node, this.Config.collabvm.displayname, this.screenHidden ? this.screenHiddenThumb : await this.getThumbnail()));
|
2023-01-31 22:00:30 -05:00
|
|
|
break;
|
|
|
|
|
case "connect":
|
2023-02-11 14:29:06 +00:00
|
|
|
if (!client.username || msgArr.length !== 2 || msgArr[1] !== this.Config.collabvm.node) {
|
2023-01-31 22:00:30 -05:00
|
|
|
client.sendMsg(guacutils.encode("connect", "0"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
client.connectedToNode = true;
|
2023-02-25 00:28:39 -05:00
|
|
|
client.sendMsg(guacutils.encode("connect", "1", "1", this.Config.vm.snapshots ? "1" : "0", "0"));
|
2023-01-31 22:00:30 -05:00
|
|
|
if (this.ChatHistory.size !== 0) client.sendMsg(this.getChatHistoryMsg());
|
2023-02-25 00:18:14 -05:00
|
|
|
if (this.Config.collabvm.motd) client.sendMsg(guacutils.encode("chat", "", this.Config.collabvm.motd));
|
2023-09-02 17:54:44 -04:00
|
|
|
if (this.screenHidden) {
|
|
|
|
|
client.sendMsg(guacutils.encode("size", "0", "1024", "768"));
|
|
|
|
|
client.sendMsg(guacutils.encode("png", "0", "0", "0", "0", this.screenHiddenImg));
|
|
|
|
|
} else {
|
2024-04-23 09:57:02 -04:00
|
|
|
await this.SendFullScreenWithSize(client);
|
2023-09-02 17:54:44 -04:00
|
|
|
}
|
2023-02-08 10:11:31 -05:00
|
|
|
client.sendMsg(guacutils.encode("sync", Date.now().toString()));
|
2023-03-28 18:13:43 +02:00
|
|
|
if (this.voteInProgress) this.sendVoteUpdate(client);
|
|
|
|
|
this.sendTurnUpdate(client);
|
|
|
|
|
break;
|
|
|
|
|
case "view":
|
|
|
|
|
if(client.connectedToNode) return;
|
|
|
|
|
if(client.username || msgArr.length !== 3 || msgArr[1] !== this.Config.collabvm.node) {
|
|
|
|
|
// The use of connect here is intentional.
|
|
|
|
|
client.sendMsg(guacutils.encode("connect", "0"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch(msgArr[2]) {
|
|
|
|
|
case "0":
|
|
|
|
|
client.viewMode = 0;
|
|
|
|
|
break;
|
|
|
|
|
case "1":
|
|
|
|
|
client.viewMode = 1;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
client.sendMsg(guacutils.encode("connect", "0"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
client.sendMsg(guacutils.encode("connect", "1", "1", this.Config.vm.snapshots ? "1" : "0", "0"));
|
|
|
|
|
if (this.ChatHistory.size !== 0) client.sendMsg(this.getChatHistoryMsg());
|
|
|
|
|
if (this.Config.collabvm.motd) client.sendMsg(guacutils.encode("chat", "", this.Config.collabvm.motd));
|
|
|
|
|
|
|
|
|
|
if(client.viewMode == 1) {
|
2023-09-02 17:54:44 -04:00
|
|
|
if (this.screenHidden) {
|
|
|
|
|
client.sendMsg(guacutils.encode("size", "0", "1024", "768"));
|
|
|
|
|
client.sendMsg(guacutils.encode("png", "0", "0", "0", "0", this.screenHiddenImg));
|
|
|
|
|
} else {
|
2024-04-23 09:57:02 -04:00
|
|
|
await this.SendFullScreenWithSize(client);
|
2023-09-02 17:54:44 -04:00
|
|
|
}
|
|
|
|
|
client.sendMsg(guacutils.encode("sync", Date.now().toString()));
|
2023-03-28 18:13:43 +02:00
|
|
|
}
|
|
|
|
|
|
2023-02-07 12:29:33 -05:00
|
|
|
if (this.voteInProgress) this.sendVoteUpdate(client);
|
|
|
|
|
this.sendTurnUpdate(client);
|
2023-01-31 22:00:30 -05:00
|
|
|
break;
|
|
|
|
|
case "rename":
|
|
|
|
|
if (!client.RenameRateLimit.request()) return;
|
2023-03-30 20:30:07 +02:00
|
|
|
if (client.connectedToNode && client.IP.muted) return;
|
2024-04-05 09:10:47 -04:00
|
|
|
if (this.Config.auth.enabled && client.rank !== Rank.Unregistered) {
|
|
|
|
|
client.sendMsg(guacutils.encode("chat", "", "Go to your account settings to change your username."));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (this.Config.auth.enabled && msgArr[1] !== undefined) {
|
2024-04-07 14:33:43 -04:00
|
|
|
// Don't send system message to a user without a username since it was likely an automated attempt by the webapp
|
|
|
|
|
if (client.username) client.sendMsg(guacutils.encode("chat", "", "You need to log in to do that."));
|
2024-04-05 09:10:47 -04:00
|
|
|
if (client.rank !== Rank.Unregistered) return;
|
|
|
|
|
this.renameUser(client, undefined);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-02-07 12:29:33 -05:00
|
|
|
this.renameUser(client, msgArr[1]);
|
2023-01-31 22:00:30 -05:00
|
|
|
break;
|
|
|
|
|
case "chat":
|
|
|
|
|
if (!client.username) return;
|
2023-02-11 15:58:20 +01:00
|
|
|
if (client.IP.muted) return;
|
2023-01-31 22:00:30 -05:00
|
|
|
if (msgArr.length !== 2) return;
|
2024-04-05 09:10:47 -04:00
|
|
|
if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.chat) {
|
|
|
|
|
client.sendMsg(guacutils.encode("chat", "", "You need to login to do that."));
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-01-31 22:00:30 -05:00
|
|
|
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
|
|
|
|
|
if (msg.length > this.Config.collabvm.maxChatLength) msg = msg.substring(0, this.Config.collabvm.maxChatLength);
|
2023-03-27 01:05:18 +02:00
|
|
|
if (msg.trim().length < 1) return;
|
2024-03-31 13:36:28 -04:00
|
|
|
|
|
|
|
|
this.clients.forEach(c => c.sendMsg(guacutils.encode("chat", client.username!, msg)));
|
2023-01-31 22:00:30 -05:00
|
|
|
this.ChatHistory.push({user: client.username, msg: msg});
|
|
|
|
|
client.onMsgSent();
|
|
|
|
|
break;
|
|
|
|
|
case "turn":
|
2023-06-05 21:59:37 -04:00
|
|
|
if ((!this.turnsAllowed || this.Config.collabvm.turnwhitelist) && client.rank !== Rank.Admin && client.rank !== Rank.Moderator && client.rank !== Rank.Turn) return;
|
2024-04-05 09:10:47 -04:00
|
|
|
if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.turn) {
|
|
|
|
|
client.sendMsg(guacutils.encode("chat", "", "You need to login to do that."));
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-01-31 22:00:30 -05:00
|
|
|
if (!client.TurnRateLimit.request()) return;
|
|
|
|
|
if (!client.connectedToNode) return;
|
|
|
|
|
if (msgArr.length > 2) return;
|
|
|
|
|
var takingTurn : boolean;
|
|
|
|
|
if (msgArr.length === 1) takingTurn = true;
|
|
|
|
|
else switch (msgArr[1]) {
|
|
|
|
|
case "0":
|
2023-02-09 20:49:27 -05:00
|
|
|
if (this.indefiniteTurn === client) {
|
|
|
|
|
this.indefiniteTurn = null;
|
|
|
|
|
}
|
2023-01-31 22:00:30 -05:00
|
|
|
takingTurn = false;
|
|
|
|
|
break;
|
|
|
|
|
case "1":
|
|
|
|
|
takingTurn = true;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
return;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (takingTurn) {
|
2023-04-05 17:04:21 +02:00
|
|
|
var currentQueue = this.TurnQueue.toArray();
|
2023-09-12 00:25:57 +02:00
|
|
|
// If the user is already in the turn queue, ignore the turn request.
|
2023-04-05 17:04:21 +02:00
|
|
|
if (currentQueue.indexOf(client) !== -1) return;
|
2023-09-12 00:25:57 +02:00
|
|
|
// If they're muted, also ignore the turn request.
|
2023-01-31 22:00:30 -05:00
|
|
|
// Send them the turn queue to prevent client glitches
|
2023-02-11 15:58:20 +01:00
|
|
|
if (client.IP.muted) return;
|
2023-09-12 00:25:57 +02:00
|
|
|
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;
|
|
|
|
|
}
|
2023-01-31 22:00:30 -05:00
|
|
|
this.TurnQueue.enqueue(client);
|
|
|
|
|
if (this.TurnQueue.size === 1) this.nextTurn();
|
|
|
|
|
} else {
|
|
|
|
|
var hadturn = (this.TurnQueue.peek() === client);
|
|
|
|
|
this.TurnQueue = Queue.from(this.TurnQueue.toArray().filter(u => u !== client));
|
|
|
|
|
if (hadturn) this.nextTurn();
|
|
|
|
|
}
|
|
|
|
|
this.sendTurnUpdate();
|
|
|
|
|
break;
|
2023-02-02 21:19:55 -05:00
|
|
|
case "mouse":
|
|
|
|
|
if (this.TurnQueue.peek() !== client && client.rank !== Rank.Admin) return;
|
|
|
|
|
var x = parseInt(msgArr[1]);
|
|
|
|
|
var y = parseInt(msgArr[2]);
|
|
|
|
|
var mask = parseInt(msgArr[3]);
|
|
|
|
|
if (x === undefined || y === undefined || mask === undefined) return;
|
2024-04-23 09:57:02 -04:00
|
|
|
this.VM.GetDisplay()!.MouseEvent(x, y, mask);
|
2023-02-02 21:19:55 -05:00
|
|
|
break;
|
|
|
|
|
case "key":
|
|
|
|
|
if (this.TurnQueue.peek() !== client && client.rank !== Rank.Admin) return;
|
|
|
|
|
var keysym = parseInt(msgArr[1]);
|
|
|
|
|
var down = parseInt(msgArr[2]);
|
|
|
|
|
if (keysym === undefined || (down !== 0 && down !== 1)) return;
|
2024-04-23 09:57:02 -04:00
|
|
|
this.VM.GetDisplay()!.KeyboardEvent(keysym, down === 1 ? true : false);
|
2023-02-02 21:19:55 -05:00
|
|
|
break;
|
2023-02-07 12:29:33 -05:00
|
|
|
case "vote":
|
2023-02-25 00:28:39 -05:00
|
|
|
if (!this.Config.vm.snapshots) return;
|
2023-06-05 21:59:37 -04:00
|
|
|
if ((!this.turnsAllowed || this.Config.collabvm.turnwhitelist) && client.rank !== Rank.Admin && client.rank !== Rank.Moderator && client.rank !== Rank.Turn) return;
|
2023-02-07 12:29:33 -05:00
|
|
|
if (!client.connectedToNode) return;
|
|
|
|
|
if (msgArr.length !== 2) return;
|
2023-02-12 18:15:12 -05:00
|
|
|
if (!client.VoteRateLimit.request()) return;
|
2023-02-07 12:29:33 -05:00
|
|
|
switch (msgArr[1]) {
|
|
|
|
|
case "1":
|
|
|
|
|
if (!this.voteInProgress) {
|
2023-02-11 15:58:20 +01:00
|
|
|
if (this.voteCooldown !== 0) {
|
|
|
|
|
client.sendMsg(guacutils.encode("vote", "3", this.voteCooldown.toString()));
|
2023-02-07 12:29:33 -05:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.startVote();
|
|
|
|
|
this.clients.forEach(c => c.sendMsg(guacutils.encode("chat", "", `${client.username} has started a vote to reset the VM.`)));
|
|
|
|
|
}
|
2023-02-11 15:58:20 +01:00
|
|
|
else if (client.IP.vote !== true)
|
2023-02-07 12:29:33 -05:00
|
|
|
this.clients.forEach(c => c.sendMsg(guacutils.encode("chat", "", `${client.username} has voted yes.`)));
|
2023-02-11 15:58:20 +01:00
|
|
|
client.IP.vote = true;
|
2023-02-07 12:29:33 -05:00
|
|
|
break;
|
|
|
|
|
case "0":
|
|
|
|
|
if (!this.voteInProgress) return;
|
2023-02-11 15:58:20 +01:00
|
|
|
if (client.IP.vote !== false)
|
2023-02-07 12:29:33 -05:00
|
|
|
this.clients.forEach(c => c.sendMsg(guacutils.encode("chat", "", `${client.username} has voted no.`)));
|
2023-02-11 15:58:20 +01:00
|
|
|
client.IP.vote = false;
|
2023-02-07 12:29:33 -05:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
this.sendVoteUpdate();
|
|
|
|
|
break;
|
2023-01-31 22:00:30 -05:00
|
|
|
case "admin":
|
|
|
|
|
if (msgArr.length < 2) return;
|
|
|
|
|
switch (msgArr[1]) {
|
|
|
|
|
case "2":
|
2023-02-07 12:29:33 -05:00
|
|
|
// Login
|
2024-04-05 09:10:47 -04:00
|
|
|
if (this.Config.auth.enabled) {
|
|
|
|
|
client.sendMsg(guacutils.encode("chat", "", "This server does not support staff passwords. Please log in to become staff."));
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-02-07 10:50:58 -05:00
|
|
|
if (!client.LoginRateLimit.request() || !client.username) return;
|
2023-01-31 22:00:30 -05:00
|
|
|
if (msgArr.length !== 3) return;
|
|
|
|
|
var sha256 = createHash("sha256");
|
|
|
|
|
sha256.update(msgArr[2]);
|
|
|
|
|
var pwdHash = sha256.digest('hex');
|
|
|
|
|
sha256.destroy();
|
|
|
|
|
if (pwdHash === this.Config.collabvm.adminpass) {
|
|
|
|
|
client.rank = Rank.Admin;
|
|
|
|
|
client.sendMsg(guacutils.encode("admin", "0", "1"));
|
|
|
|
|
} else if (this.Config.collabvm.moderatorEnabled && pwdHash === this.Config.collabvm.modpass) {
|
|
|
|
|
client.rank = Rank.Moderator;
|
|
|
|
|
client.sendMsg(guacutils.encode("admin", "0", "3", this.ModPerms.toString()));
|
2023-06-05 21:59:37 -04:00
|
|
|
} else if (this.Config.collabvm.turnwhitelist && pwdHash === this.Config.collabvm.turnpass) {
|
|
|
|
|
client.rank = Rank.Turn;
|
|
|
|
|
client.sendMsg(guacutils.encode("chat", "", "You may now take turns."));
|
2023-01-31 22:00:30 -05:00
|
|
|
} else {
|
|
|
|
|
client.sendMsg(guacutils.encode("admin", "0", "0"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-09-02 17:54:44 -04:00
|
|
|
if (this.screenHidden) {
|
2024-04-23 09:57:02 -04:00
|
|
|
await this.SendFullScreenWithSize(client);
|
|
|
|
|
|
2023-09-02 17:54:44 -04:00
|
|
|
client.sendMsg(guacutils.encode("sync", Date.now().toString()));
|
|
|
|
|
}
|
2024-03-31 13:36:28 -04:00
|
|
|
|
|
|
|
|
this.clients.forEach((c) => c.sendMsg(guacutils.encode("adduser", "1", client.username!, client.rank.toString())));
|
2023-01-31 22:00:30 -05:00
|
|
|
break;
|
2023-02-07 12:29:33 -05:00
|
|
|
case "5":
|
|
|
|
|
// QEMU Monitor
|
|
|
|
|
if (client.rank !== Rank.Admin) return;
|
2024-04-23 09:57:02 -04:00
|
|
|
/* Surely there could be rudimentary processing to convert some qemu monitor syntax to [XYZ hypervisor] if possible
|
2023-02-25 15:34:59 -05:00
|
|
|
if (!(this.VM instanceof QEMUVM)) {
|
|
|
|
|
client.sendMsg(guacutils.encode("admin", "2", "This is not a QEMU VM and therefore QEMU monitor commands cannot be run."));
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-04-23 09:57:02 -04:00
|
|
|
*/
|
2023-02-07 12:29:33 -05:00
|
|
|
if (msgArr.length !== 4 || msgArr[2] !== this.Config.collabvm.node) return;
|
2024-04-23 09:57:02 -04:00
|
|
|
var output = await this.VM.MonitorCommand(msgArr[3]);
|
2023-02-07 12:29:33 -05:00
|
|
|
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;
|
2024-04-23 09:57:02 -04:00
|
|
|
this.VM.Reset();
|
2023-02-07 12:29:33 -05:00
|
|
|
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;
|
2024-04-23 09:57:02 -04:00
|
|
|
this.VM.MonitorCommand("system_reset");
|
2023-02-07 12:29:33 -05:00
|
|
|
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;
|
2024-04-05 09:10:47 -04:00
|
|
|
if (this.Config.auth.enabled) {
|
|
|
|
|
client.sendMsg(guacutils.encode("chat", "", "Cannot rename users on a server that uses authentication."));
|
|
|
|
|
}
|
2023-02-07 12:29:33 -05:00
|
|
|
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;
|
2023-02-11 15:58:20 +01:00
|
|
|
client.sendMsg(guacutils.encode("admin", "19", msgArr[2], user.IP.address));
|
2023-02-07 12:29:33 -05:00
|
|
|
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:
|
2024-03-31 13:36:28 -04:00
|
|
|
|
|
|
|
|
this.clients.forEach(c => c.sendMsg(guacutils.encode("chat", client.username!, msgArr[2])));
|
|
|
|
|
|
|
|
|
|
this.ChatHistory.push({user: client.username!, msg: msgArr[2]});
|
2023-02-07 12:29:33 -05:00
|
|
|
break;
|
|
|
|
|
case Rank.Moderator:
|
2024-03-31 13:36:28 -04:00
|
|
|
|
|
|
|
|
this.clients.filter(c => c.rank !== Rank.Admin).forEach(c => c.sendMsg(guacutils.encode("chat", client.username!, msgArr[2])));
|
|
|
|
|
|
|
|
|
|
this.clients.filter(c => c.rank === Rank.Admin).forEach(c => c.sendMsg(guacutils.encode("chat", client.username!, Utilities.HTMLSanitize(msgArr[2]))));
|
2023-02-07 12:29:33 -05:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
break;
|
2023-02-09 20:08:32 -05:00
|
|
|
case "22":
|
2023-02-09 20:49:27 -05:00
|
|
|
// Toggle turns
|
2023-02-09 20:08:32 -05:00
|
|
|
if (client.rank !== Rank.Admin) return;
|
|
|
|
|
if (msgArr.length !== 3) return;
|
|
|
|
|
switch (msgArr[2]) {
|
|
|
|
|
case "0":
|
|
|
|
|
this.clearTurns();
|
|
|
|
|
this.turnsAllowed = false;
|
|
|
|
|
break;
|
|
|
|
|
case "1":
|
|
|
|
|
this.turnsAllowed = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
break;
|
2023-02-09 20:49:27 -05:00
|
|
|
case "23":
|
|
|
|
|
// Indefinite turn
|
|
|
|
|
if (client.rank !== Rank.Admin) return;
|
|
|
|
|
this.indefiniteTurn = client;
|
|
|
|
|
this.TurnQueue = Queue.from([client, ...this.TurnQueue.toArray().filter(c=>c!==client)]);
|
|
|
|
|
this.sendTurnUpdate();
|
|
|
|
|
break;
|
2023-09-02 17:54:44 -04:00
|
|
|
case "24":
|
|
|
|
|
// Hide screen
|
|
|
|
|
if (client.rank !== Rank.Admin) return;
|
|
|
|
|
if (msgArr.length !== 3) return;
|
|
|
|
|
switch (msgArr[2]) {
|
|
|
|
|
case "0":
|
|
|
|
|
this.screenHidden = true;
|
|
|
|
|
this.clients.filter(c => c.rank == Rank.Unregistered).forEach(client => {
|
|
|
|
|
client.sendMsg(guacutils.encode("size", "0", "1024", "768"));
|
|
|
|
|
client.sendMsg(guacutils.encode("png", "0", "0", "0", "0", this.screenHiddenImg));
|
|
|
|
|
client.sendMsg(guacutils.encode("sync", Date.now().toString()));
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
case "1":
|
|
|
|
|
this.screenHidden = false;
|
2024-04-23 09:57:02 -04:00
|
|
|
let displaySize = this.VM.GetDisplay().Size();
|
|
|
|
|
|
|
|
|
|
let encoded = await this.MakeRectData({
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 0,
|
|
|
|
|
width: displaySize.width,
|
|
|
|
|
height: displaySize.height
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.clients.forEach(async client => {
|
|
|
|
|
client.sendMsg(guacutils.encode("size", "0", displaySize.width.toString(), displaySize.height.toString()));
|
|
|
|
|
client.sendMsg(guacutils.encode("png", "0", "0", "0", "0", encoded));
|
2023-09-02 17:54:44 -04:00
|
|
|
client.sendMsg(guacutils.encode("sync", Date.now().toString()));
|
|
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
break;
|
2024-04-11 20:40:04 +03:00
|
|
|
case "25":
|
|
|
|
|
if (client.rank !== Rank.Admin || msgArr.length !== 3)
|
|
|
|
|
return;
|
|
|
|
|
this.clients.forEach(c => c.sendMsg(guacutils.encode("chat", "", msgArr[2])));
|
|
|
|
|
break;
|
|
|
|
|
|
2023-01-31 22:00:30 -05:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getUsernameList() : string[] {
|
|
|
|
|
var arr : string[] = [];
|
2024-03-31 13:36:28 -04:00
|
|
|
|
|
|
|
|
this.clients.filter(c => c.username).forEach((c) => arr.push(c.username!));
|
2023-01-31 22:00:30 -05:00
|
|
|
return arr;
|
|
|
|
|
}
|
2023-02-07 12:29:33 -05:00
|
|
|
|
|
|
|
|
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 {
|
2023-05-06 15:21:18 +01:00
|
|
|
newName = newName.trim();
|
2023-02-07 12:29:33 -05:00
|
|
|
if (hadName && newName === oldname) {
|
2024-03-31 13:36:28 -04:00
|
|
|
|
|
|
|
|
client.sendMsg(guacutils.encode("rename", "0", "0", client.username!, client.rank.toString()));
|
2023-02-07 12:29:33 -05:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (this.getUsernameList().indexOf(newName) !== -1) {
|
|
|
|
|
client.assignGuestName(this.getUsernameList());
|
2023-02-11 15:58:20 +01:00
|
|
|
if(client.connectedToNode) {
|
|
|
|
|
status = "1";
|
|
|
|
|
}
|
2023-02-07 12:29:33 -05:00
|
|
|
} else
|
2023-05-06 16:50:25 +02:00
|
|
|
if (!/^[a-zA-Z0-9\ \-\_\.]+$/.test(newName) || newName.length > 20 || newName.length < 3) {
|
2023-02-07 12:29:33 -05:00
|
|
|
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;
|
|
|
|
|
}
|
2024-03-31 13:36:28 -04:00
|
|
|
|
|
|
|
|
client.sendMsg(guacutils.encode("rename", "0", status, client.username!, client.rank.toString()));
|
2023-02-07 12:29:33 -05:00
|
|
|
if (hadName) {
|
2024-04-23 09:57:02 -04:00
|
|
|
this.logger.Info(`Rename ${client.IP.address} from ${oldname} to ${client.username}`);
|
2023-03-28 16:29:32 +02:00
|
|
|
this.clients.forEach((c) =>
|
2024-03-31 13:36:28 -04:00
|
|
|
|
|
|
|
|
c.sendMsg(guacutils.encode("rename", "1", oldname, client.username!, client.rank.toString())));
|
2023-02-07 12:29:33 -05:00
|
|
|
} else {
|
2024-04-23 09:57:02 -04:00
|
|
|
this.logger.Info(`Rename ${client.IP.address} to ${client.username}`);
|
2023-02-07 12:29:33 -05:00
|
|
|
this.clients.forEach((c) =>
|
2024-03-31 13:36:28 -04:00
|
|
|
|
|
|
|
|
c.sendMsg(guacutils.encode("adduser", "1", client.username!, client.rank.toString())));
|
2023-02-07 12:29:33 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-31 22:00:30 -05:00
|
|
|
getAdduserMsg() : string {
|
2023-02-07 13:54:18 -05:00
|
|
|
var arr : string[] = ["adduser", this.clients.filter(c=>c.username).length.toString()];
|
2024-03-31 13:36:28 -04:00
|
|
|
|
|
|
|
|
this.clients.filter(c=>c.username).forEach((c) => arr.push(c.username!, c.rank.toString()));
|
2023-01-31 22:00:30 -05:00
|
|
|
return guacutils.encode(...arr);
|
|
|
|
|
}
|
|
|
|
|
getChatHistoryMsg() : string {
|
|
|
|
|
var arr : string[] = ["chat"];
|
|
|
|
|
this.ChatHistory.forEach(c => arr.push(c.user, c.msg));
|
|
|
|
|
return guacutils.encode(...arr);
|
|
|
|
|
}
|
2023-02-07 12:29:33 -05:00
|
|
|
private sendTurnUpdate(client? : User) {
|
2023-01-31 22:00:30 -05:00
|
|
|
var turnQueueArr = this.TurnQueue.toArray();
|
2023-02-09 20:49:27 -05:00
|
|
|
var turntime;
|
|
|
|
|
if (this.indefiniteTurn === null) turntime = (this.TurnTime * 1000);
|
|
|
|
|
else turntime = 9999999999;
|
|
|
|
|
var arr = ["turn", turntime.toString(), this.TurnQueue.size.toString()];
|
2023-01-31 22:00:30 -05:00
|
|
|
// @ts-ignore
|
|
|
|
|
this.TurnQueue.forEach((c) => arr.push(c.username));
|
|
|
|
|
var currentTurningUser = this.TurnQueue.peek();
|
2023-02-07 12:29:33 -05:00
|
|
|
if (client) {
|
|
|
|
|
client.sendMsg(guacutils.encode(...arr));
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-01-31 22:00:30 -05:00
|
|
|
this.clients.filter(c => (c !== currentTurningUser && c.connectedToNode)).forEach((c) => {
|
|
|
|
|
if (turnQueueArr.indexOf(c) !== -1) {
|
2023-02-09 20:49:27 -05:00
|
|
|
var time;
|
|
|
|
|
if (this.indefiniteTurn === null) time = ((this.TurnTime * 1000) + ((turnQueueArr.indexOf(c) - 1) * this.Config.collabvm.turnTime * 1000));
|
|
|
|
|
else time = 9999999999;
|
2023-01-31 22:00:30 -05:00
|
|
|
c.sendMsg(guacutils.encode(...arr, time.toString()));
|
|
|
|
|
} else {
|
|
|
|
|
c.sendMsg(guacutils.encode(...arr));
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
if (currentTurningUser)
|
|
|
|
|
currentTurningUser.sendMsg(guacutils.encode(...arr));
|
|
|
|
|
}
|
|
|
|
|
private nextTurn() {
|
|
|
|
|
clearInterval(this.TurnInterval);
|
|
|
|
|
if (this.TurnQueue.size === 0) {
|
|
|
|
|
} else {
|
|
|
|
|
this.TurnTime = this.Config.collabvm.turnTime;
|
|
|
|
|
this.TurnInterval = setInterval(() => this.turnInterval(), 1000);
|
|
|
|
|
}
|
|
|
|
|
this.sendTurnUpdate();
|
|
|
|
|
}
|
2023-02-07 12:29:33 -05:00
|
|
|
|
|
|
|
|
clearTurns() {
|
|
|
|
|
clearInterval(this.TurnInterval);
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-31 22:00:30 -05:00
|
|
|
private turnInterval() {
|
2023-02-09 20:49:27 -05:00
|
|
|
if (this.indefiniteTurn !== null) return;
|
2023-01-31 22:00:30 -05:00
|
|
|
this.TurnTime--;
|
|
|
|
|
if (this.TurnTime < 1) {
|
|
|
|
|
this.TurnQueue.dequeue();
|
|
|
|
|
this.nextTurn();
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-02-02 21:19:55 -05:00
|
|
|
|
2024-04-23 09:57:02 -04:00
|
|
|
private async OnDisplayRectangle(rect: Rect) {
|
|
|
|
|
let encodedb64 = await this.MakeRectData(rect);
|
|
|
|
|
|
2023-03-28 18:13:43 +02:00
|
|
|
this.clients.filter(c => c.connectedToNode || c.viewMode == 1).forEach(c => {
|
2023-09-02 17:54:44 -04:00
|
|
|
if (this.screenHidden && c.rank == Rank.Unregistered) return;
|
2024-04-23 09:57:02 -04:00
|
|
|
c.sendMsg(guacutils.encode("png", "0", "0", rect.x.toString(), rect.y.toString(), encodedb64));
|
2023-02-08 10:11:31 -05:00
|
|
|
c.sendMsg(guacutils.encode("sync", Date.now().toString()));
|
2023-02-07 14:00:22 -05:00
|
|
|
});
|
2023-02-02 21:19:55 -05:00
|
|
|
}
|
|
|
|
|
|
2024-04-23 09:57:02 -04:00
|
|
|
private OnDisplayResized(size : Size) {
|
2023-09-02 18:08:41 -04:00
|
|
|
this.clients.filter(c => c.connectedToNode || c.viewMode == 1).forEach(c => {
|
|
|
|
|
if (this.screenHidden && c.rank == Rank.Unregistered) return;
|
|
|
|
|
c.sendMsg(guacutils.encode("size", "0", size.width.toString(), size.height.toString()))
|
|
|
|
|
});
|
2023-02-02 21:19:55 -05:00
|
|
|
}
|
|
|
|
|
|
2024-04-23 09:57:02 -04:00
|
|
|
private async SendFullScreenWithSize(client: User) {
|
|
|
|
|
let display = this.VM.GetDisplay();
|
|
|
|
|
let displaySize = display.Size();
|
|
|
|
|
|
|
|
|
|
let encoded = await this.MakeRectData({
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 0,
|
|
|
|
|
width: displaySize.width,
|
|
|
|
|
height: displaySize.height
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
client.sendMsg(guacutils.encode("size", "0", displaySize.width.toString(), displaySize.height.toString()));
|
|
|
|
|
client.sendMsg(guacutils.encode("png", "0", "0", "0", "0", encoded));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async MakeRectData(rect: Rect) {
|
|
|
|
|
let display = this.VM.GetDisplay();
|
|
|
|
|
let displaySize = display.Size();
|
|
|
|
|
|
|
|
|
|
let encoded = await EncodeJpeg(display.Buffer(), displaySize, rect);
|
|
|
|
|
|
|
|
|
|
return encoded.toString('base64');
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-02 21:19:55 -05:00
|
|
|
getThumbnail() : Promise<string> {
|
|
|
|
|
return new Promise(async (res, rej) => {
|
2024-04-23 09:57:02 -04:00
|
|
|
let display = this.VM.GetDisplay();
|
|
|
|
|
if(display == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// TODO: pass custom options to Sharp.resize() probably
|
|
|
|
|
let out = await sharp(display.Buffer(), {raw: GetRawSharpOptions(display.Size())})
|
2024-04-23 19:43:23 -04:00
|
|
|
.resize(400, 300, { fit: 'fill' })
|
2024-04-23 09:57:02 -04:00
|
|
|
.toFormat('jpeg')
|
|
|
|
|
.toBuffer();
|
|
|
|
|
|
|
|
|
|
res(out.toString('base64'));
|
|
|
|
|
});
|
2023-02-02 21:19:55 -05:00
|
|
|
}
|
2023-02-07 12:29:33 -05:00
|
|
|
|
|
|
|
|
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.")));
|
2024-04-23 09:57:02 -04:00
|
|
|
this.VM.Reset();
|
2023-02-07 12:29:33 -05:00
|
|
|
} else {
|
|
|
|
|
this.clients.forEach(c => c.sendMsg(guacutils.encode("chat", "", "The vote to reset the VM has lost.")));
|
|
|
|
|
}
|
2023-02-11 15:58:20 +01:00
|
|
|
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);
|
2023-02-07 12:29:33 -05:00
|
|
|
}, 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;
|
2024-04-23 09:57:02 -04:00
|
|
|
IPDataManager.ForEachIPData((c) => {
|
2023-02-07 12:29:33 -05:00
|
|
|
if (c.vote === true) yes++;
|
|
|
|
|
if (c.vote === false) no++;
|
|
|
|
|
});
|
|
|
|
|
return {yes:yes,no:no};
|
|
|
|
|
}
|
2024-04-23 19:43:23 -04:00
|
|
|
}
|