2023-02-02 21:19:55 -05:00
|
|
|
import IConfig from './IConfig.js';
|
|
|
|
|
import * as Utilities from './Utilities.js';
|
|
|
|
|
import { User, Rank } from './User.js';
|
2024-06-22 21:14:05 -04:00
|
|
|
import * as cvm from '@cvmts/cvm-rs';
|
2023-02-02 21:19:55 -05:00
|
|
|
// 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-07-16 08:29:52 -04:00
|
|
|
import { VMState, QemuVM, QemuVmDefinition } from '@computernewb/superqemu';
|
2024-06-11 13:46:24 -04:00
|
|
|
import { IPDataManager } from './IPData.js';
|
2024-04-23 09:57:02 -04:00
|
|
|
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-24 04:18:05 -04:00
|
|
|
import { JPEGEncoder } from './JPEGEncoder.js';
|
2024-06-11 13:46:24 -04:00
|
|
|
import VM from './VM.js';
|
2024-06-23 02:23:59 -04:00
|
|
|
import { ReaderModel } from '@maxmind/geoip2-node';
|
2024-06-25 20:09:34 -04:00
|
|
|
import * as msgpack from 'msgpackr';
|
2024-06-25 21:24:08 -04:00
|
|
|
import { CollabVMProtocolMessage, CollabVMProtocolMessageType } from '@cvmts/collab-vm-1.2-binary-protocol';
|
2024-04-23 09:57:02 -04:00
|
|
|
|
2024-07-16 08:29:52 -04:00
|
|
|
import { Size, Rect } from './VMDisplay.js';
|
|
|
|
|
import pino from 'pino';
|
2024-07-31 16:34:42 -04:00
|
|
|
import { BanManager } from './BanManager.js';
|
2024-07-16 08:29:52 -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 __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-06-19 23:30:29 -04:00
|
|
|
const kRestartTimeout = 5000;
|
|
|
|
|
|
2024-04-23 09:57:02 -04:00
|
|
|
type ChatHistory = {
|
2024-04-24 03:50:17 -04:00
|
|
|
user: string;
|
|
|
|
|
msg: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type VoteTally = {
|
|
|
|
|
yes: number;
|
|
|
|
|
no: number;
|
2024-04-23 09:57:02 -04:00
|
|
|
};
|
|
|
|
|
|
2024-07-16 08:29:52 -04:00
|
|
|
|
2024-05-26 23:19:55 -04:00
|
|
|
export default class CollabVMServer {
|
2024-04-24 03:50:17 -04:00
|
|
|
private Config: IConfig;
|
2024-04-23 09:57:02 -04:00
|
|
|
|
2024-04-24 03:50:17 -04:00
|
|
|
private clients: User[];
|
|
|
|
|
|
|
|
|
|
private ChatHistory: CircularBuffer<ChatHistory>;
|
|
|
|
|
|
|
|
|
|
private TurnQueue: Queue<User>;
|
|
|
|
|
|
|
|
|
|
// Time remaining on the current turn
|
|
|
|
|
private TurnTime: number;
|
|
|
|
|
|
|
|
|
|
// Interval to keep track of the current turn time
|
|
|
|
|
private TurnInterval?: NodeJS.Timeout;
|
|
|
|
|
|
|
|
|
|
// If a reset vote is in progress
|
|
|
|
|
private voteInProgress: boolean;
|
|
|
|
|
|
|
|
|
|
// Interval to keep track of vote resets
|
|
|
|
|
private voteInterval?: NodeJS.Timeout;
|
|
|
|
|
|
|
|
|
|
// How much time is left on the vote
|
|
|
|
|
private voteTime: number;
|
|
|
|
|
|
|
|
|
|
// How much time until another reset vote can be cast
|
|
|
|
|
private voteCooldown: number;
|
|
|
|
|
|
|
|
|
|
// Interval to keep track
|
|
|
|
|
private voteCooldownInterval?: NodeJS.Timeout;
|
|
|
|
|
|
|
|
|
|
// Completely disable turns
|
|
|
|
|
private turnsAllowed: boolean;
|
|
|
|
|
|
|
|
|
|
// Hide the screen
|
|
|
|
|
private screenHidden: boolean;
|
|
|
|
|
|
|
|
|
|
// base64 image to show when the screen is hidden
|
|
|
|
|
private screenHiddenImg: string;
|
|
|
|
|
private screenHiddenThumb: string;
|
|
|
|
|
|
|
|
|
|
// Indefinite turn
|
|
|
|
|
private indefiniteTurn: User | null;
|
|
|
|
|
private ModPerms: number;
|
2024-06-11 13:46:24 -04:00
|
|
|
private VM: VM;
|
2024-04-24 03:50:17 -04:00
|
|
|
|
|
|
|
|
// Authentication manager
|
|
|
|
|
private auth: AuthManager | null;
|
|
|
|
|
|
2024-06-23 02:23:59 -04:00
|
|
|
// Geoip
|
|
|
|
|
private geoipReader: ReaderModel | null;
|
|
|
|
|
|
2024-07-31 16:34:42 -04:00
|
|
|
// Ban manager
|
|
|
|
|
private banmgr: BanManager;
|
|
|
|
|
|
2024-07-16 08:29:52 -04:00
|
|
|
private logger = pino({ name: 'CVMTS.Server' });
|
2024-04-24 03:50:17 -04:00
|
|
|
|
2024-07-31 16:34:42 -04:00
|
|
|
constructor(config: IConfig, vm: VM, banmgr: BanManager, auth: AuthManager | null, geoipReader: ReaderModel | null) {
|
2024-04-24 03:50:17 -04:00
|
|
|
this.Config = config;
|
|
|
|
|
this.ChatHistory = new CircularBuffer<ChatHistory>(Array, this.Config.collabvm.maxChatHistoryLength);
|
|
|
|
|
this.TurnQueue = new Queue<User>();
|
|
|
|
|
this.TurnTime = 0;
|
|
|
|
|
this.clients = [];
|
|
|
|
|
this.voteInProgress = false;
|
|
|
|
|
this.voteTime = 0;
|
|
|
|
|
this.voteCooldown = 0;
|
|
|
|
|
this.turnsAllowed = true;
|
|
|
|
|
this.screenHidden = false;
|
|
|
|
|
this.screenHiddenImg = readFileSync(path.join(kCVMTSAssetsRoot, 'screenhidden.jpeg')).toString('base64');
|
|
|
|
|
this.screenHiddenThumb = readFileSync(path.join(kCVMTSAssetsRoot, 'screenhiddenthumb.jpeg')).toString('base64');
|
|
|
|
|
|
|
|
|
|
this.indefiniteTurn = null;
|
|
|
|
|
this.ModPerms = Utilities.MakeModPerms(this.Config.collabvm.moderatorPermissions);
|
|
|
|
|
|
2024-07-11 20:49:49 -04:00
|
|
|
// No size initially, since there usually won't be a display connected at all during initalization
|
2024-07-11 20:33:50 -04:00
|
|
|
this.OnDisplayResized({
|
2024-04-24 03:50:17 -04:00
|
|
|
width: 0,
|
|
|
|
|
height: 0
|
2024-07-11 20:33:50 -04:00
|
|
|
});
|
2024-04-24 03:50:17 -04:00
|
|
|
|
|
|
|
|
this.VM = vm;
|
|
|
|
|
|
2024-07-11 20:49:49 -04:00
|
|
|
// this probably should be made general at some point,
|
|
|
|
|
// and the VM interface allowed to return a nullable display
|
|
|
|
|
// but i cba
|
2024-07-10 22:20:12 -04:00
|
|
|
let self = this;
|
2024-06-22 21:26:49 -04:00
|
|
|
if (config.vm.type == 'qemu') {
|
2024-06-19 23:30:29 -04:00
|
|
|
(vm as QemuVM).on('statechange', (newState: VMState) => {
|
2024-07-11 20:33:50 -04:00
|
|
|
if (newState == VMState.Started) {
|
2024-07-16 08:29:52 -04:00
|
|
|
self.logger.info('VM started');
|
2024-07-10 18:43:35 -04:00
|
|
|
// well aware this sucks but whatever
|
2024-07-10 22:20:12 -04:00
|
|
|
self.VM.GetDisplay().on('resize', (size: Size) => self.OnDisplayResized(size));
|
|
|
|
|
self.VM.GetDisplay().on('rect', (rect: Rect) => self.OnDisplayRectangle(rect));
|
2024-07-10 18:43:35 -04:00
|
|
|
}
|
|
|
|
|
|
2024-06-22 21:26:49 -04:00
|
|
|
if (newState == VMState.Stopped) {
|
2024-06-19 23:30:29 -04:00
|
|
|
setTimeout(async () => {
|
2024-07-16 08:29:52 -04:00
|
|
|
self.logger.info('restarting VM');
|
2024-07-10 22:20:12 -04:00
|
|
|
await self.VM.Start();
|
2024-06-22 21:26:49 -04:00
|
|
|
}, kRestartTimeout);
|
2024-06-19 23:30:29 -04:00
|
|
|
}
|
|
|
|
|
});
|
2024-08-01 18:13:06 -04:00
|
|
|
} else {
|
|
|
|
|
// this sucks too fix this
|
|
|
|
|
self.VM.GetDisplay().on('resize', (size: Size) => self.OnDisplayResized(size));
|
|
|
|
|
self.VM.GetDisplay().on('rect', (rect: Rect) => self.OnDisplayRectangle(rect));
|
2024-06-19 23:30:29 -04:00
|
|
|
}
|
|
|
|
|
|
2024-04-24 03:50:17 -04:00
|
|
|
// authentication manager
|
|
|
|
|
this.auth = auth;
|
2024-06-23 02:23:59 -04:00
|
|
|
|
|
|
|
|
this.geoipReader = geoipReader;
|
2024-07-31 16:34:42 -04:00
|
|
|
|
|
|
|
|
this.banmgr = banmgr;
|
2024-04-24 03:50:17 -04:00
|
|
|
}
|
|
|
|
|
|
2024-05-26 23:19:55 -04:00
|
|
|
public addUser(user: User) {
|
2024-06-19 01:36:07 -04:00
|
|
|
let sameip = this.clients.filter((c) => c.IP.address === user.IP.address);
|
2024-05-27 00:06:05 -04:00
|
|
|
if (sameip.length >= this.Config.collabvm.maxConnections) {
|
|
|
|
|
// Kick the oldest client
|
|
|
|
|
// I think this is a better solution than just rejecting the connection
|
|
|
|
|
sameip[0].kick();
|
|
|
|
|
}
|
2024-04-24 03:50:17 -04:00
|
|
|
this.clients.push(user);
|
2024-06-23 02:23:59 -04:00
|
|
|
if (this.Config.geoip.enabled) {
|
|
|
|
|
try {
|
|
|
|
|
user.countryCode = this.geoipReader!.country(user.IP.address).country!.isoCode;
|
|
|
|
|
} catch (error) {
|
2024-07-16 08:29:52 -04:00
|
|
|
this.logger.warn(`Failed to get country code for ${user.IP.address}: ${(error as Error).message}`);
|
2024-06-23 02:23:59 -04:00
|
|
|
}
|
|
|
|
|
}
|
2024-05-26 23:19:55 -04:00
|
|
|
user.socket.on('msg', (msg: string) => this.onMessage(user, msg));
|
|
|
|
|
user.socket.on('disconnect', () => this.connectionClosed(user));
|
2024-04-24 03:50:17 -04:00
|
|
|
if (this.Config.auth.enabled) {
|
2024-06-22 21:14:05 -04:00
|
|
|
user.sendMsg(cvm.guacEncode('auth', this.Config.auth.apiEndpoint));
|
2024-04-24 03:50:17 -04:00
|
|
|
}
|
|
|
|
|
user.sendMsg(this.getAdduserMsg());
|
2024-06-23 02:23:59 -04:00
|
|
|
if (this.Config.geoip.enabled) user.sendMsg(this.getFlagMsg());
|
2024-04-24 03:50:17 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private connectionClosed(user: User) {
|
|
|
|
|
let clientIndex = this.clients.indexOf(user);
|
|
|
|
|
if (clientIndex === -1) return;
|
|
|
|
|
|
|
|
|
|
if (user.IP.vote != null) {
|
|
|
|
|
user.IP.vote = null;
|
|
|
|
|
this.sendVoteUpdate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.indefiniteTurn === user) this.indefiniteTurn = null;
|
|
|
|
|
|
|
|
|
|
this.clients.splice(clientIndex, 1);
|
|
|
|
|
|
2024-07-16 08:29:52 -04:00
|
|
|
this.logger.info(`Disconnect From ${user.IP.address}${user.username ? ` with username ${user.username}` : ''}`);
|
2024-04-24 03:50:17 -04: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-06-22 21:14:05 -04:00
|
|
|
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('remuser', '1', user.username!)));
|
2024-04-24 03:50:17 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async onMessage(client: User, message: string) {
|
2024-06-19 01:36:07 -04:00
|
|
|
try {
|
2024-06-22 21:14:05 -04:00
|
|
|
var msgArr = cvm.guacDecode(message);
|
2024-06-19 01:36:07 -04:00
|
|
|
if (msgArr.length < 1) return;
|
|
|
|
|
switch (msgArr[0]) {
|
|
|
|
|
case 'login':
|
|
|
|
|
if (msgArr.length !== 2 || !this.Config.auth.enabled) return;
|
|
|
|
|
if (!client.connectedToNode) {
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('login', '0', 'You must connect to the VM before logging in.'));
|
2024-06-19 01:36:07 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
let res = await this.auth!.Authenticate(msgArr[1], client);
|
|
|
|
|
if (res.clientSuccess) {
|
2024-07-16 08:29:52 -04:00
|
|
|
this.logger.info(`${client.IP.address} logged in as ${res.username}`);
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('login', '1'));
|
2024-06-19 01:36:07 -04:00
|
|
|
let 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
|
2024-06-23 02:23:59 -04:00
|
|
|
if (client.countryCode !== null && client.noFlag) {
|
|
|
|
|
// privacy
|
2024-07-11 20:33:50 -04:00
|
|
|
for (let cl of this.clients.filter((c) => c !== client)) {
|
2024-06-23 02:23:59 -04:00
|
|
|
cl.sendMsg(cvm.guacEncode('remuser', '1', client.username!));
|
|
|
|
|
}
|
|
|
|
|
this.renameUser(client, res.username, false);
|
2024-07-11 20:33:50 -04:00
|
|
|
} else this.renameUser(client, res.username, true);
|
2024-06-19 01:36:07 -04:00
|
|
|
// Set rank
|
|
|
|
|
client.rank = res.rank;
|
|
|
|
|
if (client.rank === Rank.Admin) {
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('admin', '0', '1'));
|
2024-06-19 01:36:07 -04:00
|
|
|
} else if (client.rank === Rank.Moderator) {
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('admin', '0', '3', this.ModPerms.toString()));
|
2024-06-19 01:36:07 -04:00
|
|
|
}
|
2024-06-22 21:14:05 -04:00
|
|
|
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('adduser', '1', client.username!, client.rank.toString())));
|
2024-06-19 01:36:07 -04:00
|
|
|
} else {
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('login', '0', res.error!));
|
2024-06-19 01:36:07 -04:00
|
|
|
if (res.error === 'You are banned') {
|
|
|
|
|
client.kick();
|
|
|
|
|
}
|
2024-05-26 16:33:35 -04:00
|
|
|
}
|
2024-06-19 01:36:07 -04:00
|
|
|
} catch (err) {
|
2024-07-16 08:29:52 -04:00
|
|
|
this.logger.error(`Error authenticating client ${client.IP.address}: ${(err as Error).message}`);
|
2024-06-19 01:36:07 -04:00
|
|
|
// for now?
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('login', '0', 'There was an internal error while authenticating. Please let a staff member know as soon as possible'));
|
2024-04-24 03:50:17 -04:00
|
|
|
}
|
2024-06-19 01:36:07 -04:00
|
|
|
break;
|
2024-06-23 02:23:59 -04:00
|
|
|
case 'noflag': {
|
2024-07-11 20:33:50 -04:00
|
|
|
if (client.connectedToNode)
|
|
|
|
|
// too late
|
2024-06-23 02:23:59 -04:00
|
|
|
return;
|
|
|
|
|
client.noFlag = true;
|
|
|
|
|
}
|
2024-06-19 01:36:07 -04:00
|
|
|
case 'list':
|
2024-07-11 20:33:50 -04:00
|
|
|
if (this.VM.GetState() == VMState.Started) {
|
|
|
|
|
client.sendMsg(cvm.guacEncode('list', this.Config.collabvm.node, this.Config.collabvm.displayname, this.screenHidden ? this.screenHiddenThumb : await this.getThumbnail()));
|
|
|
|
|
}
|
2024-06-19 01:36:07 -04:00
|
|
|
break;
|
|
|
|
|
case 'connect':
|
|
|
|
|
if (!client.username || msgArr.length !== 2 || msgArr[1] !== this.Config.collabvm.node) {
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('connect', '0'));
|
2024-04-24 03:50:17 -04:00
|
|
|
return;
|
2024-06-19 01:36:07 -04:00
|
|
|
}
|
2024-07-11 20:33:50 -04:00
|
|
|
|
2024-06-19 01:36:07 -04:00
|
|
|
client.connectedToNode = true;
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('connect', '1', '1', this.VM.SnapshotsSupported() ? '1' : '0', '0'));
|
2024-06-19 01:36:07 -04:00
|
|
|
if (this.ChatHistory.size !== 0) client.sendMsg(this.getChatHistoryMsg());
|
2024-06-22 21:14:05 -04:00
|
|
|
if (this.Config.collabvm.motd) client.sendMsg(cvm.guacEncode('chat', '', this.Config.collabvm.motd));
|
2024-04-24 03:50:17 -04:00
|
|
|
if (this.screenHidden) {
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('size', '0', '1024', '768'));
|
|
|
|
|
client.sendMsg(cvm.guacEncode('png', '0', '0', '0', '0', this.screenHiddenImg));
|
2024-04-24 03:50:17 -04:00
|
|
|
} else {
|
|
|
|
|
await this.SendFullScreenWithSize(client);
|
|
|
|
|
}
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('sync', Date.now().toString()));
|
2024-06-19 01:36:07 -04: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.
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('connect', '0'));
|
2024-06-19 01:36:07 -04:00
|
|
|
return;
|
|
|
|
|
}
|
2024-04-24 03:50:17 -04:00
|
|
|
|
2024-06-19 01:36:07 -04:00
|
|
|
switch (msgArr[2]) {
|
2024-04-24 03:50:17 -04:00
|
|
|
case '0':
|
2024-06-19 01:36:07 -04:00
|
|
|
client.viewMode = 0;
|
2024-04-24 03:50:17 -04:00
|
|
|
break;
|
|
|
|
|
case '1':
|
2024-06-19 01:36:07 -04:00
|
|
|
client.viewMode = 1;
|
2024-04-24 03:50:17 -04:00
|
|
|
break;
|
|
|
|
|
default:
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('connect', '0'));
|
2024-04-24 03:50:17 -04:00
|
|
|
return;
|
|
|
|
|
}
|
2024-05-01 08:08:43 -04:00
|
|
|
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('connect', '1', '1', this.VM.SnapshotsSupported() ? '1' : '0', '0'));
|
2024-06-19 01:36:07 -04:00
|
|
|
if (this.ChatHistory.size !== 0) client.sendMsg(this.getChatHistoryMsg());
|
2024-06-22 21:14:05 -04:00
|
|
|
if (this.Config.collabvm.motd) client.sendMsg(cvm.guacEncode('chat', '', this.Config.collabvm.motd));
|
2024-06-19 01:36:07 -04:00
|
|
|
|
|
|
|
|
if (client.viewMode == 1) {
|
2024-04-24 03:50:17 -04:00
|
|
|
if (this.screenHidden) {
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('size', '0', '1024', '768'));
|
|
|
|
|
client.sendMsg(cvm.guacEncode('png', '0', '0', '0', '0', this.screenHiddenImg));
|
2024-06-19 01:36:07 -04:00
|
|
|
} else {
|
2024-04-24 03:50:17 -04:00
|
|
|
await this.SendFullScreenWithSize(client);
|
|
|
|
|
}
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('sync', Date.now().toString()));
|
2024-06-19 01:36:07 -04:00
|
|
|
}
|
2024-04-24 03:50:17 -04:00
|
|
|
|
2024-06-19 01:36:07 -04:00
|
|
|
if (this.voteInProgress) this.sendVoteUpdate(client);
|
|
|
|
|
this.sendTurnUpdate(client);
|
|
|
|
|
break;
|
|
|
|
|
case 'rename':
|
|
|
|
|
if (!client.RenameRateLimit.request()) return;
|
|
|
|
|
if (client.connectedToNode && client.IP.muted) return;
|
|
|
|
|
if (this.Config.auth.enabled && client.rank !== Rank.Unregistered) {
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('chat', '', 'Go to your account settings to change your username.'));
|
2024-06-19 01:36:07 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (this.Config.auth.enabled && msgArr[1] !== undefined) {
|
|
|
|
|
// Don't send system message to a user without a username since it was likely an automated attempt by the webapp
|
2024-06-22 21:14:05 -04:00
|
|
|
if (client.username) client.sendMsg(cvm.guacEncode('chat', '', 'You need to log in to do that.'));
|
2024-06-19 01:36:07 -04:00
|
|
|
if (client.rank !== Rank.Unregistered) return;
|
|
|
|
|
this.renameUser(client, undefined);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.renameUser(client, msgArr[1]);
|
|
|
|
|
break;
|
|
|
|
|
case 'chat':
|
|
|
|
|
if (!client.username) return;
|
|
|
|
|
if (client.IP.muted) return;
|
|
|
|
|
if (msgArr.length !== 2) return;
|
|
|
|
|
if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.chat) {
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('chat', '', 'You need to login to do that.'));
|
2024-06-19 01:36:07 -04:00
|
|
|
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
|
|
|
|
|
if (msg.length > this.Config.collabvm.maxChatLength) msg = msg.substring(0, this.Config.collabvm.maxChatLength);
|
|
|
|
|
if (msg.trim().length < 1) return;
|
|
|
|
|
|
2024-06-22 21:14:05 -04:00
|
|
|
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', client.username!, msg)));
|
2024-06-19 01:36:07 -04:00
|
|
|
this.ChatHistory.push({ user: client.username, msg: msg });
|
|
|
|
|
client.onMsgSent();
|
|
|
|
|
break;
|
|
|
|
|
case 'turn':
|
|
|
|
|
if ((!this.turnsAllowed || this.Config.collabvm.turnwhitelist) && client.rank !== Rank.Admin && client.rank !== Rank.Moderator && client.rank !== Rank.Turn) return;
|
|
|
|
|
if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.turn) {
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('chat', '', 'You need to login to do that.'));
|
2024-06-19 01:36:07 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
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]) {
|
2024-04-24 03:50:17 -04:00
|
|
|
case '0':
|
2024-06-19 01:36:07 -04:00
|
|
|
if (this.indefiniteTurn === client) {
|
|
|
|
|
this.indefiniteTurn = null;
|
|
|
|
|
}
|
|
|
|
|
takingTurn = false;
|
2024-04-24 03:50:17 -04:00
|
|
|
break;
|
|
|
|
|
case '1':
|
2024-06-19 01:36:07 -04:00
|
|
|
takingTurn = true;
|
2024-04-24 03:50:17 -04:00
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
return;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2024-06-19 01:36:07 -04:00
|
|
|
if (takingTurn) {
|
|
|
|
|
var currentQueue = this.TurnQueue.toArray();
|
|
|
|
|
// If the user is already in the turn queue, ignore the turn request.
|
|
|
|
|
if (currentQueue.indexOf(client) !== -1) return;
|
|
|
|
|
// If they're muted, also ignore the turn request.
|
|
|
|
|
// Send them the turn queue to prevent client glitches
|
|
|
|
|
if (client.IP.muted) return;
|
|
|
|
|
if (this.Config.collabvm.turnlimit.enabled) {
|
|
|
|
|
// Get the amount of users in the turn queue with the same IP as the user requesting a turn.
|
|
|
|
|
let turns = currentQueue.filter((user) => user.IP.address == client.IP.address);
|
|
|
|
|
// If it exceeds the limit set in the config, ignore the turn request.
|
|
|
|
|
if (turns.length + 1 > this.Config.collabvm.turnlimit.maximum) return;
|
2024-04-24 03:50:17 -04:00
|
|
|
}
|
2024-06-19 01:36:07 -04: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;
|
|
|
|
|
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-07-11 03:24:22 -04:00
|
|
|
this.VM.GetDisplay()?.MouseEvent(x, y, mask);
|
2024-06-19 01:36:07 -04: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-07-11 03:24:22 -04:00
|
|
|
this.VM.GetDisplay()?.KeyboardEvent(keysym, down === 1 ? true : false);
|
2024-06-19 01:36:07 -04:00
|
|
|
break;
|
|
|
|
|
case 'vote':
|
|
|
|
|
if (!this.VM.SnapshotsSupported()) return;
|
|
|
|
|
if ((!this.turnsAllowed || this.Config.collabvm.turnwhitelist) && client.rank !== Rank.Admin && client.rank !== Rank.Moderator && client.rank !== Rank.Turn) return;
|
|
|
|
|
if (!client.connectedToNode) return;
|
|
|
|
|
if (msgArr.length !== 2) return;
|
|
|
|
|
if (!client.VoteRateLimit.request()) return;
|
|
|
|
|
switch (msgArr[1]) {
|
|
|
|
|
case '1':
|
|
|
|
|
if (!this.voteInProgress) {
|
|
|
|
|
if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.callForReset) {
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('chat', '', 'You need to login to do that.'));
|
2024-06-19 01:36:07 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.voteCooldown !== 0) {
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('vote', '3', this.voteCooldown.toString()));
|
2024-06-19 01:36:07 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.startVote();
|
2024-06-22 21:14:05 -04:00
|
|
|
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', `${client.username} has started a vote to reset the VM.`)));
|
2024-06-19 01:36:07 -04:00
|
|
|
}
|
|
|
|
|
if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.vote) {
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('chat', '', 'You need to login to do that.'));
|
2024-06-19 01:36:07 -04:00
|
|
|
return;
|
|
|
|
|
} else if (client.IP.vote !== true) {
|
2024-06-22 21:14:05 -04:00
|
|
|
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', `${client.username} has voted yes.`)));
|
2024-06-19 01:36:07 -04:00
|
|
|
}
|
|
|
|
|
client.IP.vote = true;
|
|
|
|
|
break;
|
|
|
|
|
case '0':
|
|
|
|
|
if (!this.voteInProgress) return;
|
|
|
|
|
if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.vote) {
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('chat', '', 'You need to login to do that.'));
|
2024-06-19 01:36:07 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (client.IP.vote !== false) {
|
2024-06-22 21:14:05 -04:00
|
|
|
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', `${client.username} has voted no.`)));
|
2024-06-19 01:36:07 -04:00
|
|
|
}
|
|
|
|
|
client.IP.vote = false;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
this.sendVoteUpdate();
|
|
|
|
|
break;
|
2024-07-11 20:33:50 -04:00
|
|
|
case 'cap': {
|
2024-06-25 19:56:28 -04:00
|
|
|
if (msgArr.length < 2) return;
|
|
|
|
|
// Capabilities can only be announced before connecting to the VM
|
|
|
|
|
if (client.connectedToNode) return;
|
|
|
|
|
var caps = [];
|
2024-07-11 20:33:50 -04:00
|
|
|
for (const cap of msgArr.slice(1))
|
|
|
|
|
switch (cap) {
|
|
|
|
|
case 'bin': {
|
|
|
|
|
if (caps.indexOf('bin') !== -1) break;
|
|
|
|
|
client.Capabilities.bin = true;
|
|
|
|
|
caps.push('bin');
|
|
|
|
|
break;
|
|
|
|
|
}
|
2024-06-25 19:56:28 -04:00
|
|
|
}
|
2024-07-11 20:33:50 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('cap', ...caps));
|
2024-06-25 19:56:28 -04:00
|
|
|
}
|
2024-06-19 01:36:07 -04:00
|
|
|
case 'admin':
|
|
|
|
|
if (msgArr.length < 2) return;
|
|
|
|
|
switch (msgArr[1]) {
|
|
|
|
|
case '2':
|
|
|
|
|
// Login
|
|
|
|
|
if (this.Config.auth.enabled) {
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('chat', '', 'This server does not support staff passwords. Please log in to become staff.'));
|
2024-06-19 01:36:07 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!client.LoginRateLimit.request() || !client.username) return;
|
|
|
|
|
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;
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('admin', '0', '1'));
|
2024-06-19 01:36:07 -04:00
|
|
|
} else if (this.Config.collabvm.moderatorEnabled && pwdHash === this.Config.collabvm.modpass) {
|
|
|
|
|
client.rank = Rank.Moderator;
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('admin', '0', '3', this.ModPerms.toString()));
|
2024-06-19 01:36:07 -04:00
|
|
|
} else if (this.Config.collabvm.turnwhitelist && pwdHash === this.Config.collabvm.turnpass) {
|
|
|
|
|
client.rank = Rank.Turn;
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('chat', '', 'You may now take turns.'));
|
2024-06-19 01:36:07 -04:00
|
|
|
} else {
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('admin', '0', '0'));
|
2024-06-19 01:36:07 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (this.screenHidden) {
|
|
|
|
|
await this.SendFullScreenWithSize(client);
|
|
|
|
|
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('sync', Date.now().toString()));
|
2024-06-19 01:36:07 -04:00
|
|
|
}
|
|
|
|
|
|
2024-06-22 21:14:05 -04:00
|
|
|
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('adduser', '1', client.username!, client.rank.toString())));
|
2024-06-19 01:36:07 -04:00
|
|
|
break;
|
|
|
|
|
case '5':
|
|
|
|
|
// QEMU Monitor
|
|
|
|
|
if (client.rank !== Rank.Admin) return;
|
|
|
|
|
if (msgArr.length !== 4 || msgArr[2] !== this.Config.collabvm.node) return;
|
2024-07-11 03:24:22 -04:00
|
|
|
let output = await this.VM.MonitorCommand(msgArr[3]);
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('admin', '2', String(output)));
|
2024-06-19 01:36:07 -04:00
|
|
|
break;
|
|
|
|
|
case '8':
|
|
|
|
|
// Restore
|
|
|
|
|
if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.restore)) return;
|
|
|
|
|
this.VM.Reset();
|
|
|
|
|
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;
|
|
|
|
|
await 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;
|
2024-07-31 16:34:42 -04:00
|
|
|
this.logger.info(`Banning ${user.username!} (${user.IP.address}) by request of ${client.username!}`);
|
|
|
|
|
user.ban(this.banmgr);
|
2024-06-19 01:36:07 -04:00
|
|
|
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 (this.Config.auth.enabled) {
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('chat', '', 'Cannot rename users on a server that uses authentication.'));
|
2024-06-19 01:36:07 -04: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;
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('admin', '19', msgArr[2], user.IP.address));
|
2024-06-19 01:36:07 -04: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-06-22 21:14:05 -04:00
|
|
|
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', client.username!, msgArr[2])));
|
2024-06-19 01:36:07 -04:00
|
|
|
|
|
|
|
|
this.ChatHistory.push({ user: client.username!, msg: msgArr[2] });
|
|
|
|
|
break;
|
|
|
|
|
case Rank.Moderator:
|
2024-06-22 21:14:05 -04:00
|
|
|
this.clients.filter((c) => c.rank !== Rank.Admin).forEach((c) => c.sendMsg(cvm.guacEncode('chat', client.username!, msgArr[2])));
|
2024-06-19 01:36:07 -04:00
|
|
|
|
2024-06-22 21:14:05 -04:00
|
|
|
this.clients.filter((c) => c.rank === Rank.Admin).forEach((c) => c.sendMsg(cvm.guacEncode('chat', client.username!, Utilities.HTMLSanitize(msgArr[2]))));
|
2024-06-19 01:36:07 -04:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case '22':
|
|
|
|
|
// Toggle turns
|
|
|
|
|
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;
|
|
|
|
|
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;
|
|
|
|
|
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) => {
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('size', '0', '1024', '768'));
|
|
|
|
|
client.sendMsg(cvm.guacEncode('png', '0', '0', '0', '0', this.screenHiddenImg));
|
|
|
|
|
client.sendMsg(cvm.guacEncode('sync', Date.now().toString()));
|
2024-06-19 01:36:07 -04:00
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
case '1':
|
|
|
|
|
this.screenHidden = false;
|
|
|
|
|
let displaySize = this.VM.GetDisplay().Size();
|
|
|
|
|
|
|
|
|
|
let encoded = await this.MakeRectData({
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 0,
|
|
|
|
|
width: displaySize.width,
|
|
|
|
|
height: displaySize.height
|
2024-04-24 03:50:17 -04:00
|
|
|
});
|
2024-06-19 01:36:07 -04:00
|
|
|
|
2024-06-25 19:56:28 -04:00
|
|
|
this.clients.forEach(async (client) => this.SendFullScreenWithSize(client));
|
2024-06-19 01:36:07 -04:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case '25':
|
|
|
|
|
if (client.rank !== Rank.Admin || msgArr.length !== 3) return;
|
2024-06-22 21:14:05 -04:00
|
|
|
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', msgArr[2])));
|
2024-06-19 01:36:07 -04:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
// No
|
2024-07-16 08:29:52 -04:00
|
|
|
this.logger.error(`User ${user?.IP.address} ${user?.username ? `with username ${user?.username}` : ''} sent broken Guacamole: ${err as Error}`);
|
2024-06-19 01:36:07 -04:00
|
|
|
user?.kick();
|
2024-04-24 03:50:17 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getUsernameList(): string[] {
|
|
|
|
|
var arr: string[] = [];
|
|
|
|
|
|
|
|
|
|
this.clients.filter((c) => c.username).forEach((c) => arr.push(c.username!));
|
|
|
|
|
return arr;
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-23 02:23:59 -04:00
|
|
|
renameUser(client: User, newName?: string, announce: boolean = true) {
|
2024-04-24 03:50:17 -04:00
|
|
|
// 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 {
|
|
|
|
|
newName = newName.trim();
|
|
|
|
|
if (hadName && newName === oldname) {
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('rename', '0', '0', client.username!, client.rank.toString()));
|
2024-04-24 03:50:17 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (this.getUsernameList().indexOf(newName) !== -1) {
|
|
|
|
|
client.assignGuestName(this.getUsernameList());
|
|
|
|
|
if (client.connectedToNode) {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('rename', '0', status, client.username!, client.rank.toString()));
|
2024-04-24 03:50:17 -04:00
|
|
|
if (hadName) {
|
2024-07-16 08:29:52 -04:00
|
|
|
this.logger.info(`Rename ${client.IP.address} from ${oldname} to ${client.username}`);
|
2024-06-23 02:23:59 -04:00
|
|
|
if (announce) this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('rename', '1', oldname, client.username!, client.rank.toString())));
|
2024-04-24 03:50:17 -04:00
|
|
|
} else {
|
2024-07-16 08:29:52 -04:00
|
|
|
this.logger.info(`Rename ${client.IP.address} to ${client.username}`);
|
2024-07-11 20:33:50 -04:00
|
|
|
if (announce)
|
|
|
|
|
this.clients.forEach((c) => {
|
|
|
|
|
c.sendMsg(cvm.guacEncode('adduser', '1', client.username!, client.rank.toString()));
|
|
|
|
|
if (client.countryCode !== null) c.sendMsg(cvm.guacEncode('flag', client.username!, client.countryCode));
|
|
|
|
|
});
|
2024-04-24 03:50:17 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getAdduserMsg(): string {
|
|
|
|
|
var arr: string[] = ['adduser', this.clients.filter((c) => c.username).length.toString()];
|
|
|
|
|
|
|
|
|
|
this.clients.filter((c) => c.username).forEach((c) => arr.push(c.username!, c.rank.toString()));
|
2024-06-22 21:14:05 -04:00
|
|
|
return cvm.guacEncode(...arr);
|
2024-04-24 03:50:17 -04:00
|
|
|
}
|
|
|
|
|
|
2024-07-11 20:33:50 -04:00
|
|
|
getFlagMsg(): string {
|
2024-06-23 02:23:59 -04:00
|
|
|
var arr = ['flag'];
|
2024-07-11 20:33:50 -04:00
|
|
|
for (let c of this.clients.filter((cl) => cl.countryCode !== null && cl.username && (!cl.noFlag || cl.rank === Rank.Unregistered))) {
|
2024-06-23 02:23:59 -04:00
|
|
|
arr.push(c.username!, c.countryCode!);
|
|
|
|
|
}
|
|
|
|
|
return cvm.guacEncode(...arr);
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-24 03:50:17 -04:00
|
|
|
getChatHistoryMsg(): string {
|
|
|
|
|
var arr: string[] = ['chat'];
|
|
|
|
|
this.ChatHistory.forEach((c) => arr.push(c.user, c.msg));
|
2024-06-22 21:14:05 -04:00
|
|
|
return cvm.guacEncode(...arr);
|
2024-04-24 03:50:17 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private sendTurnUpdate(client?: User) {
|
|
|
|
|
var turnQueueArr = this.TurnQueue.toArray();
|
|
|
|
|
var turntime;
|
|
|
|
|
if (this.indefiniteTurn === null) turntime = this.TurnTime * 1000;
|
|
|
|
|
else turntime = 9999999999;
|
|
|
|
|
var arr = ['turn', turntime.toString(), this.TurnQueue.size.toString()];
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
this.TurnQueue.forEach((c) => arr.push(c.username));
|
|
|
|
|
var currentTurningUser = this.TurnQueue.peek();
|
|
|
|
|
if (client) {
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode(...arr));
|
2024-04-24 03:50:17 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.clients
|
|
|
|
|
.filter((c) => c !== currentTurningUser && c.connectedToNode)
|
|
|
|
|
.forEach((c) => {
|
|
|
|
|
if (turnQueueArr.indexOf(c) !== -1) {
|
|
|
|
|
var time;
|
|
|
|
|
if (this.indefiniteTurn === null) time = this.TurnTime * 1000 + (turnQueueArr.indexOf(c) - 1) * this.Config.collabvm.turnTime * 1000;
|
|
|
|
|
else time = 9999999999;
|
2024-06-22 21:14:05 -04:00
|
|
|
c.sendMsg(cvm.guacEncode(...arr, time.toString()));
|
2024-04-24 03:50:17 -04:00
|
|
|
} else {
|
2024-06-22 21:14:05 -04:00
|
|
|
c.sendMsg(cvm.guacEncode(...arr));
|
2024-04-24 03:50:17 -04:00
|
|
|
}
|
|
|
|
|
});
|
2024-06-22 21:14:05 -04:00
|
|
|
if (currentTurningUser) currentTurningUser.sendMsg(cvm.guacEncode(...arr));
|
2024-04-24 03:50:17 -04:00
|
|
|
}
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private turnInterval() {
|
|
|
|
|
if (this.indefiniteTurn !== null) return;
|
|
|
|
|
this.TurnTime--;
|
|
|
|
|
if (this.TurnTime < 1) {
|
|
|
|
|
this.TurnQueue.dequeue();
|
|
|
|
|
this.nextTurn();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async OnDisplayRectangle(rect: Rect) {
|
2024-06-25 19:56:28 -04:00
|
|
|
let encoded = await this.MakeRectData(rect);
|
2024-07-11 20:33:50 -04:00
|
|
|
let encodedb64 = encoded.toString('base64');
|
|
|
|
|
let bmsg: CollabVMProtocolMessage = {
|
2024-06-25 19:56:28 -04:00
|
|
|
type: CollabVMProtocolMessageType.rect,
|
|
|
|
|
rect: {
|
|
|
|
|
x: rect.x,
|
|
|
|
|
y: rect.y,
|
|
|
|
|
data: encoded
|
2024-07-11 20:33:50 -04:00
|
|
|
}
|
2024-06-25 19:56:28 -04:00
|
|
|
};
|
|
|
|
|
var encodedbin = msgpack.encode(bmsg);
|
2024-04-24 03:50:17 -04:00
|
|
|
this.clients
|
|
|
|
|
.filter((c) => c.connectedToNode || c.viewMode == 1)
|
|
|
|
|
.forEach((c) => {
|
|
|
|
|
if (this.screenHidden && c.rank == Rank.Unregistered) return;
|
2024-06-25 19:56:28 -04:00
|
|
|
if (c.Capabilities.bin) {
|
|
|
|
|
c.socket.sendBinary(encodedbin);
|
|
|
|
|
} else {
|
|
|
|
|
c.sendMsg(cvm.guacEncode('png', '0', '0', rect.x.toString(), rect.y.toString(), encodedb64));
|
|
|
|
|
c.sendMsg(cvm.guacEncode('sync', Date.now().toString()));
|
|
|
|
|
}
|
2024-04-24 03:50:17 -04:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private OnDisplayResized(size: Size) {
|
|
|
|
|
this.clients
|
|
|
|
|
.filter((c) => c.connectedToNode || c.viewMode == 1)
|
|
|
|
|
.forEach((c) => {
|
|
|
|
|
if (this.screenHidden && c.rank == Rank.Unregistered) return;
|
2024-06-22 21:14:05 -04:00
|
|
|
c.sendMsg(cvm.guacEncode('size', '0', size.width.toString(), size.height.toString()));
|
2024-04-24 03:50:17 -04:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async SendFullScreenWithSize(client: User) {
|
|
|
|
|
let display = this.VM.GetDisplay();
|
2024-07-11 20:49:49 -04:00
|
|
|
if (display == null) return;
|
|
|
|
|
|
2024-04-24 03:50:17 -04:00
|
|
|
let displaySize = display.Size();
|
|
|
|
|
|
|
|
|
|
let encoded = await this.MakeRectData({
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 0,
|
|
|
|
|
width: displaySize.width,
|
|
|
|
|
height: displaySize.height
|
|
|
|
|
});
|
|
|
|
|
|
2024-06-22 21:14:05 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('size', '0', displaySize.width.toString(), displaySize.height.toString()));
|
2024-06-25 19:56:28 -04:00
|
|
|
|
|
|
|
|
if (client.Capabilities.bin) {
|
2024-07-11 20:33:50 -04:00
|
|
|
let msg: CollabVMProtocolMessage = {
|
2024-06-25 19:56:28 -04:00
|
|
|
type: CollabVMProtocolMessageType.rect,
|
|
|
|
|
rect: {
|
|
|
|
|
x: 0,
|
|
|
|
|
y: 0,
|
|
|
|
|
data: encoded
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
client.socket.sendBinary(msgpack.encode(msg));
|
|
|
|
|
} else {
|
2024-07-11 20:33:50 -04:00
|
|
|
client.sendMsg(cvm.guacEncode('png', '0', '0', '0', '0', encoded.toString('base64')));
|
2024-06-25 19:56:28 -04:00
|
|
|
}
|
2024-04-24 03:50:17 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async MakeRectData(rect: Rect) {
|
|
|
|
|
let display = this.VM.GetDisplay();
|
|
|
|
|
|
2024-06-23 02:55:05 -04:00
|
|
|
// TODO: actually throw an error here
|
2024-07-11 20:49:49 -04:00
|
|
|
if (display == null) return Buffer.from('no');
|
2024-06-23 02:55:05 -04:00
|
|
|
|
2024-07-11 20:49:49 -04:00
|
|
|
let displaySize = display.Size();
|
2024-06-20 03:20:56 -04:00
|
|
|
let encoded = await JPEGEncoder.Encode(display.Buffer(), displaySize, rect);
|
2024-04-24 03:50:17 -04:00
|
|
|
|
2024-06-25 19:56:28 -04:00
|
|
|
return encoded;
|
2024-04-24 03:50:17 -04:00
|
|
|
}
|
|
|
|
|
|
2024-04-24 04:18:05 -04:00
|
|
|
async getThumbnail(): Promise<string> {
|
|
|
|
|
let display = this.VM.GetDisplay();
|
2024-05-26 16:33:35 -04:00
|
|
|
|
|
|
|
|
// oh well
|
2024-06-19 01:36:07 -04:00
|
|
|
if (!display.Connected()) return '';
|
2024-04-24 03:50:17 -04:00
|
|
|
|
2024-04-24 04:18:05 -04:00
|
|
|
let buf = await JPEGEncoder.EncodeThumbnail(display.Buffer(), display.Size());
|
|
|
|
|
return buf.toString('base64');
|
2024-04-24 03:50:17 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
startVote() {
|
|
|
|
|
if (this.voteInProgress) return;
|
|
|
|
|
this.voteInProgress = true;
|
2024-06-22 21:14:05 -04:00
|
|
|
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('vote', '0')));
|
2024-04-24 03:50:17 -04:00
|
|
|
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();
|
2024-06-22 21:14:05 -04:00
|
|
|
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('vote', '2')));
|
2024-04-24 03:50:17 -04:00
|
|
|
if (result === true || (result === undefined && count.yes >= count.no)) {
|
2024-06-22 21:14:05 -04:00
|
|
|
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', 'The vote to reset the VM has won.')));
|
2024-04-24 03:50:17 -04:00
|
|
|
this.VM.Reset();
|
|
|
|
|
} else {
|
2024-06-22 21:14:05 -04:00
|
|
|
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', 'The vote to reset the VM has lost.')));
|
2024-04-24 03:50:17 -04: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);
|
|
|
|
|
}, 1000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sendVoteUpdate(client?: User) {
|
|
|
|
|
if (!this.voteInProgress) return;
|
|
|
|
|
var count = this.getVoteCounts();
|
2024-06-22 21:14:05 -04:00
|
|
|
var msg = cvm.guacEncode('vote', '1', (this.voteTime * 1000).toString(), count.yes.toString(), count.no.toString());
|
2024-04-24 03:50:17 -04:00
|
|
|
if (client) client.sendMsg(msg);
|
|
|
|
|
else this.clients.forEach((c) => c.sendMsg(msg));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getVoteCounts(): VoteTally {
|
|
|
|
|
let yes = 0;
|
|
|
|
|
let no = 0;
|
|
|
|
|
IPDataManager.ForEachIPData((c) => {
|
|
|
|
|
if (c.vote === true) yes++;
|
|
|
|
|
if (c.vote === false) no++;
|
|
|
|
|
});
|
|
|
|
|
return { yes: yes, no: no };
|
|
|
|
|
}
|
2024-04-24 03:41:32 -04:00
|
|
|
}
|