cvmts/protocol: Make protocols stateless

Instead of creating an instance of a protocol per user and storing state there, we just have the protocol implementations take in all of the state they should need in processMessage(), and store them all globally (with some wrappers to make it easier to handle this). This makes things slightly cleaner (and probably also helps memory usage, since we now don't need to create protocol instances as soon as a user connects/swaps, and they don't need to be garbage collected since they are held in the manager.)
This commit is contained in:
modeco80
2025-06-15 15:03:13 -04:00
parent bce2a0172a
commit 4211941560
6 changed files with 317 additions and 247 deletions

View File

@@ -177,30 +177,27 @@ export default class CollabVMServer implements IProtocolMessageHandler {
user.socket.on('msg', (buf: Buffer, binary: boolean) => {
try {
user.protocol.processMessage(buf);
user.processMessage(this, buf);
} catch (err) {
this.logger.error({
ip: user.IP.address,
username: user.username,
error_message: (err as Error).message
}, 'Error in %s#processMessage.', Object.getPrototypeOf(user.protocol).constructor?.name);
}, 'Error in %s#processMessage.', Object.getPrototypeOf(user).constructor?.name);
user.kick();
}
});
user.socket.on('disconnect', () => this.connectionClosed(user));
// Set ourselves as the handler
user.protocol.setHandler(this as IProtocolMessageHandler);
if (this.Config.auth.enabled) {
user.protocol.sendAuth(this.Config.auth.apiEndpoint);
user.sendAuth(this.Config.auth.apiEndpoint);
}
user.protocol.sendAddUser(this.getAddUser());
user.sendAddUser(this.getAddUser());
if (this.Config.geoip.enabled) {
let flags = this.getFlags();
user.protocol.sendFlag(flags);
user.sendFlag(flags);
}
}
@@ -217,8 +214,6 @@ export default class CollabVMServer implements IProtocolMessageHandler {
this.clients.splice(clientIndex, 1);
user.protocol.dispose();
this.logger.info(`Disconnect From ${user.IP.address}${user.username ? ` with username ${user.username}` : ''}`);
if (!user.username) return;
if (this.TurnQueue.toArray().indexOf(user) !== -1) {
@@ -227,7 +222,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (hadturn) this.nextTurn();
}
this.clients.forEach((c) => c.protocol.sendRemUser([user.username!]));
this.clients.forEach((c) => c.sendRemUser([user.username!]));
}
// Protocol message handlers
@@ -237,7 +232,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (!this.Config.auth.enabled) return true;
if (user.rank === Rank.Unregistered && !guestPermission) {
user.protocol.sendChatMessage('', 'You need to login to do that.');
user.sendChatMessage('', 'You need to login to do that.');
return false;
}
@@ -252,7 +247,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (!this.Config.auth.enabled) return;
if (!user.connectedToNode) {
user.protocol.sendLoginResponse(false, 'You must connect to the VM before logging in.');
user.sendLoginResponse(false, 'You must connect to the VM before logging in.');
return;
}
@@ -261,7 +256,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (res.clientSuccess) {
this.logger.info(`${user.IP.address} logged in as ${res.username}`);
user.protocol.sendLoginResponse(true, '');
user.sendLoginResponse(true, '');
let old = this.clients.find((c) => c.username === res.username);
if (old) {
@@ -274,19 +269,19 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (user.countryCode !== null && user.noFlag) {
// privacy
for (let cl of this.clients.filter((c) => c !== user)) {
cl.protocol.sendRemUser([user.username!]);
cl.sendRemUser([user.username!]);
}
this.renameUser(user, res.username, false);
} else this.renameUser(user, res.username, true);
// Set rank
user.rank = res.rank;
if (user.rank === Rank.Admin) {
user.protocol.sendAdminLoginResponse(true, undefined);
user.sendAdminLoginResponse(true, undefined);
} else if (user.rank === Rank.Moderator) {
user.protocol.sendAdminLoginResponse(true, this.ModPerms);
user.sendAdminLoginResponse(true, this.ModPerms);
}
this.clients.forEach((c) =>
c.protocol.sendAddUser([
c.sendAddUser([
{
username: user.username!,
rank: user.rank
@@ -294,7 +289,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
])
);
} else {
user.protocol.sendLoginResponse(false, res.error!);
user.sendLoginResponse(false, res.error!);
if (res.error === 'You are banned') {
user.kick();
}
@@ -302,7 +297,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
} catch (err) {
this.logger.error(`Error authenticating client ${user.IP.address}: ${(err as Error).message}`);
user.protocol.sendLoginResponse(false, 'There was an internal error while authenticating. Please let a staff member know as soon as possible');
user.sendLoginResponse(false, 'There was an internal error while authenticating. Please let a staff member know as soon as possible');
}
}
@@ -323,16 +318,14 @@ export default class CollabVMServer implements IProtocolMessageHandler {
case ProtocolUpgradeCapability.BinRects:
enabledCaps.push(cap as ProtocolUpgradeCapability);
user.Capabilities.bin = true;
user.protocol.dispose();
user.protocol = TheProtocolManager.createProtocol('binary1', user);
user.protocol.setHandler(this as IProtocolMessageHandler);
user.protocol = TheProtocolManager.getProtocol('binary1');
break;
default:
break;
}
}
user.protocol.sendCapabilities(enabledCaps);
user.sendCapabilities(enabledCaps);
return true;
}
@@ -377,18 +370,18 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (!this.authCheck(user, this.Config.auth.guestPermissions.callForReset)) return;
if (this.voteCooldown !== 0) {
user.protocol.sendVoteCooldown(this.voteCooldown);
user.sendVoteCooldown(this.voteCooldown);
return;
}
this.startVote();
this.clients.forEach((c) => c.protocol.sendChatMessage('', `${user.username} has started a vote to reset the VM.`));
this.clients.forEach((c) => c.sendChatMessage('', `${user.username} has started a vote to reset the VM.`));
}
if (!this.authCheck(user, this.Config.auth.guestPermissions.vote)) return;
if (user.IP.vote !== true) {
this.clients.forEach((c) => c.protocol.sendChatMessage('', `${user.username} has voted yes.`));
this.clients.forEach((c) => c.sendChatMessage('', `${user.username} has voted yes.`));
}
user.IP.vote = true;
break;
@@ -398,7 +391,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (!this.authCheck(user, this.Config.auth.guestPermissions.vote)) return;
if (user.IP.vote !== false) {
this.clients.forEach((c) => c.protocol.sendChatMessage('', `${user.username} has voted no.`));
this.clients.forEach((c) => c.sendChatMessage('', `${user.username} has voted no.`));
}
user.IP.vote = false;
break;
@@ -416,13 +409,13 @@ export default class CollabVMServer implements IProtocolMessageHandler {
};
if (this.VM.GetState() == VMState.Started) {
user.protocol.sendListResponse([listEntry]);
user.sendListResponse([listEntry]);
}
}
private async connectViewShared(user: User, node: string, viewMode: number | undefined) {
if (!user.username || node !== this.Config.collabvm.node) {
user.protocol.sendConnectFailResponse();
user.sendConnectFailResponse();
return;
}
@@ -430,23 +423,23 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (viewMode !== undefined) {
if (viewMode !== 0 && viewMode !== 1) {
user.protocol.sendConnectFailResponse();
user.sendConnectFailResponse();
return;
}
user.viewMode = viewMode;
}
user.protocol.sendConnectOKResponse(this.VM.SnapshotsSupported());
user.sendConnectOKResponse(this.VM.SnapshotsSupported());
if (this.ChatHistory.size !== 0) {
let history = this.ChatHistory.toArray() as ChatHistory[];
user.protocol.sendChatHistoryMessage(history);
user.sendChatHistoryMessage(history);
}
if (this.Config.collabvm.motd) user.protocol.sendChatMessage('', this.Config.collabvm.motd);
if (this.Config.collabvm.motd) user.sendChatMessage('', this.Config.collabvm.motd);
if (this.screenHidden) {
user?.protocol.sendScreenResize(1024, 768);
user?.protocol.sendScreenUpdate({
user?.sendScreenResize(1024, 768);
user?.sendScreenUpdate({
x: 0,
y: 0,
data: this.screenHiddenImg
@@ -455,7 +448,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
await this.SendFullScreenWithSize(user);
}
user.protocol.sendSync(Date.now());
user.sendSync(Date.now());
if (this.voteInProgress) this.sendVoteUpdate(user);
this.sendTurnUpdate(user);
@@ -473,12 +466,12 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (!user.RenameRateLimit.request()) return;
if (user.connectedToNode && user.IP.muted) return;
if (this.Config.auth.enabled && user.rank !== Rank.Unregistered) {
user.protocol.sendChatMessage('', 'Go to your account settings to change your username.');
user.sendChatMessage('', 'Go to your account settings to change your username.');
return;
}
if (this.Config.auth.enabled && newName !== undefined) {
// Don't send system message to a user without a username since it was likely an automated attempt by the webapp
if (user.username) user.protocol.sendChatMessage('', 'You need to log in to do that.');
if (user.username) user.sendChatMessage('', 'You need to log in to do that.');
if (user.rank !== Rank.Unregistered) return;
this.renameUser(user, undefined);
return;
@@ -496,7 +489,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (msg.length > this.Config.collabvm.maxChatLength) msg = msg.substring(0, this.Config.collabvm.maxChatLength);
if (msg.trim().length < 1) return;
this.clients.forEach((c) => c.protocol.sendChatMessage(user.username!, msg));
this.clients.forEach((c) => c.sendChatMessage(user.username!, msg));
this.ChatHistory.push({ user: user.username, msg: msg });
user.onChatMsgSent();
}
@@ -520,23 +513,23 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (this.Config.collabvm.turnwhitelist && pwdHash === this.Config.collabvm.turnpass) {
user.turnWhitelist = true;
user.protocol.sendChatMessage('', 'You may now take turns.');
user.sendChatMessage('', 'You may now take turns.');
return;
}
if (this.Config.auth.enabled) {
user.protocol.sendChatMessage('', 'This server does not support staff passwords. Please log in to become staff.');
user.sendChatMessage('', 'This server does not support staff passwords. Please log in to become staff.');
return;
}
if (pwdHash === this.Config.collabvm.adminpass) {
user.rank = Rank.Admin;
user.protocol.sendAdminLoginResponse(true, undefined);
user.sendAdminLoginResponse(true, undefined);
} else if (this.Config.collabvm.moderatorEnabled && pwdHash === this.Config.collabvm.modpass) {
user.rank = Rank.Moderator;
user.protocol.sendAdminLoginResponse(true, this.ModPerms);
user.sendAdminLoginResponse(true, this.ModPerms);
} else {
user.protocol.sendAdminLoginResponse(false, undefined);
user.sendAdminLoginResponse(false, undefined);
return;
}
@@ -546,7 +539,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
// Update rank
this.clients.forEach((c) =>
c.protocol.sendAddUser([
c.sendAddUser([
{
username: user.username!,
rank: user.rank
@@ -560,7 +553,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (node !== this.Config.collabvm.node) return;
TheAuditLog.onMonitorCommand(user, command);
let output = await this.VM.MonitorCommand(command);
user.protocol.sendAdminMonitorResponse(String(output));
user.sendAdminMonitorResponse(String(output));
}
onAdminRestore(user: User, node: string): void {
@@ -624,7 +617,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
onAdminRename(user: User, target: string, newName: string): void {
if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.rename)) return;
if (this.Config.auth.enabled) {
user.protocol.sendChatMessage('', 'Cannot rename users on a server that uses authentication.');
user.sendChatMessage('', 'Cannot rename users on a server that uses authentication.');
}
var targetUser = this.clients.find((c) => c.username === target);
if (!targetUser) return;
@@ -635,7 +628,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.grabip)) return;
let target = this.clients.find((c) => c.username === username);
if (!target) return;
user.protocol.sendAdminIPResponse(username, target.IP.address);
user.sendAdminIPResponse(username, target.IP.address);
}
onAdminBypassTurn(user: User): void {
@@ -647,14 +640,14 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.xss)) return;
switch (user.rank) {
case Rank.Admin:
this.clients.forEach((c) => c.protocol.sendChatMessage(user.username!, message));
this.clients.forEach((c) => c.sendChatMessage(user.username!, message));
this.ChatHistory.push({ user: user.username!, msg: message });
break;
case Rank.Moderator:
this.clients.filter((c) => c.rank !== Rank.Admin).forEach((c) => c.protocol.sendChatMessage(user.username!, message));
this.clients.filter((c) => c.rank !== Rank.Admin).forEach((c) => c.sendChatMessage(user.username!, message));
this.clients.filter((c) => c.rank === Rank.Admin).forEach((c) => c.protocol.sendChatMessage(user.username!, Utilities.HTMLSanitize(message)));
this.clients.filter((c) => c.rank === Rank.Admin).forEach((c) => c.sendChatMessage(user.username!, Utilities.HTMLSanitize(message)));
break;
}
}
@@ -700,8 +693,8 @@ export default class CollabVMServer implements IProtocolMessageHandler {
this.clients
.filter((c) => c.rank == Rank.Unregistered)
.forEach((client) => {
client.protocol.sendScreenResize(1024, 768);
client.protocol.sendScreenUpdate({
client.sendScreenResize(1024, 768);
client.sendScreenUpdate({
x: 0,
y: 0,
data: this.screenHiddenImg
@@ -712,7 +705,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
onAdminSystemMessage(user: User, message: string): void {
if (user.rank !== Rank.Admin) return;
this.clients.forEach((c) => c.protocol.sendChatMessage('', message));
this.clients.forEach((c) => c.sendChatMessage('', message));
}
// end protocol message handlers
@@ -736,7 +729,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
} else {
newName = newName.trim();
if (hadName && newName === oldname) {
client.protocol.sendSelfRename(ProtocolRenameStatus.Ok, client.username!, client.rank);
client.sendSelfRename(ProtocolRenameStatus.Ok, client.username!, client.rank);
return;
}
@@ -754,16 +747,16 @@ export default class CollabVMServer implements IProtocolMessageHandler {
} else client.username = newName;
}
client.protocol.sendSelfRename(status, client.username!, client.rank);
client.sendSelfRename(status, client.username!, client.rank);
if (hadName) {
this.logger.info(`Rename ${client.IP.address} from ${oldname} to ${client.username}`);
if (announce) this.clients.forEach((c) => c.protocol.sendRename(oldname, client.username!, client.rank));
if (announce) this.clients.forEach((c) => c.sendRename(oldname, client.username!, client.rank));
} else {
this.logger.info(`Rename ${client.IP.address} to ${client.username}`);
if (announce)
this.clients.forEach((c) => {
c.protocol.sendAddUser([
c.sendAddUser([
{
username: client.username!,
rank: client.rank
@@ -771,7 +764,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
]);
if (client.countryCode !== null) {
c.protocol.sendFlag([
c.sendFlag([
{
username: client.username!,
countryCode: client.countryCode
@@ -816,7 +809,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
var currentTurningUser = this.TurnQueue.peek();
if (client) {
client.protocol.sendTurnQueue(turntime, users);
client.sendTurnQueue(turntime, users);
return;
}
@@ -827,12 +820,12 @@ export default class CollabVMServer implements IProtocolMessageHandler {
var time;
if (this.indefiniteTurn === null) time = this.TurnTime * 1000 + (turnQueueArr.indexOf(c) - 1) * this.Config.collabvm.turnTime * 1000;
else time = 9999999999;
c.protocol.sendTurnQueueWaiting(turntime, users, time);
c.sendTurnQueueWaiting(turntime, users, time);
} else {
c.protocol.sendTurnQueue(turntime, users);
c.sendTurnQueue(turntime, users);
}
});
if (currentTurningUser) currentTurningUser.protocol.sendTurnQueue(turntime, users);
if (currentTurningUser) currentTurningUser.sendTurnQueue(turntime, users);
}
private nextTurn() {
clearInterval(this.TurnInterval);
@@ -883,7 +876,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
.filter((c) => c.connectedToNode || c.viewMode == 1)
.forEach((c) => {
if (this.screenHidden && c.rank == Rank.Unregistered) return;
c.protocol.sendScreenResize(size.width, size.height);
c.sendScreenResize(size.width, size.height);
});
}
@@ -898,7 +891,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
.forEach((c) => {
if (self.screenHidden && c.rank == Rank.Unregistered) return;
c.protocol.sendScreenUpdate({
c.sendScreenUpdate({
x: rect.x,
y: rect.y,
data: encoded
@@ -930,9 +923,9 @@ export default class CollabVMServer implements IProtocolMessageHandler {
height: displaySize.height
});
client.protocol.sendScreenResize(displaySize.width, displaySize.height);
client.sendScreenResize(displaySize.width, displaySize.height);
client.protocol.sendScreenUpdate({
client.sendScreenUpdate({
x: 0,
y: 0,
data: encoded
@@ -963,7 +956,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
startVote() {
if (this.voteInProgress) return;
this.voteInProgress = true;
this.clients.forEach((c) => c.protocol.sendVoteStarted());
this.clients.forEach((c) => c.sendVoteStarted());
this.voteTime = this.Config.collabvm.voteTime;
this.voteInterval = setInterval(() => {
this.voteTime--;
@@ -978,12 +971,12 @@ export default class CollabVMServer implements IProtocolMessageHandler {
this.voteInProgress = false;
clearInterval(this.voteInterval);
var count = this.getVoteCounts();
this.clients.forEach((c) => c.protocol.sendVoteEnded());
this.clients.forEach((c) => c.sendVoteEnded());
if (result === true || (result === undefined && count.yes >= count.no)) {
this.clients.forEach((c) => c.protocol.sendChatMessage('', 'The vote to reset the VM has won.'));
this.clients.forEach((c) => c.sendChatMessage('', 'The vote to reset the VM has won.'));
this.VM.Reset();
} else {
this.clients.forEach((c) => c.protocol.sendChatMessage('', 'The vote to reset the VM has lost.'));
this.clients.forEach((c) => c.sendChatMessage('', 'The vote to reset the VM has lost.'));
}
this.clients.forEach((c) => {
c.IP.vote = null;
@@ -999,8 +992,8 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (!this.voteInProgress) return;
var count = this.getVoteCounts();
if (client) client.protocol.sendVoteStats(this.voteTime * 1000, count.yes, count.no);
else this.clients.forEach((c) => c.protocol.sendVoteStats(this.voteTime * 1000, count.yes, count.no));
if (client) client.sendVoteStats(this.voteTime * 1000, count.yes, count.no);
else this.clients.forEach((c) => c.sendVoteStats(this.voteTime * 1000, count.yes, count.no));
}
getVoteCounts(): VoteTally {

View File

@@ -7,7 +7,7 @@ import { NetworkClient } from './net/NetworkClient.js';
import { CollabVMCapabilities } from '@cvmts/collab-vm-1.2-binary-protocol';
import pino from 'pino';
import { BanManager } from './BanManager.js';
import { IProtocol } from './protocol/Protocol.js';
import { IProtocol, IProtocolMessageHandler, ListEntry, ProtocolAddUser, ProtocolChatHistory, ProtocolFlag, ProtocolRenameStatus, ProtocolUpgradeCapability, ScreenRect } from './protocol/Protocol.js';
import { TheProtocolManager } from './protocol/Manager.js';
export class User {
@@ -47,7 +47,7 @@ export class User {
this.Capabilities = new CollabVMCapabilities();
// All clients default to the Guacamole protocol.
this.protocol = TheProtocolManager.createProtocol(protocol, this);
this.protocol = TheProtocolManager.getProtocol(protocol);
this.socket.on('disconnect', () => {
// Unref the ip data for this connection
@@ -90,10 +90,6 @@ export class User {
this.msgRecieveInterval = setInterval(() => this.onNoMsg(), 10000);
}
sendNop() {
this.protocol.sendNop();
}
sendMsg(msg: string) {
if (!this.socket.isOpen()) return;
clearInterval(this.nopSendInterval);
@@ -152,6 +148,118 @@ export class User {
this.sendMsg('10.disconnect;');
this.socket.close();
}
// These wrap the currently set IProtocol instance to feed state to them.
// This is probably grody, but /shrug. It works, and feels less awful than
// manually wrapping state (and probably prevents mixup bugs too.)
processMessage(handler: IProtocolMessageHandler, buffer: Buffer) {
this.protocol.processMessage(this, handler, buffer);
}
sendNop(): void {
this.protocol.sendNop(this);
}
sendSync(now: number): void {
this.protocol.sendSync(this, now);
}
sendAuth(authServer: string): void {
this.protocol.sendAuth(this, authServer);
}
sendCapabilities(caps: ProtocolUpgradeCapability[]): void {
this.protocol.sendCapabilities(this, caps);
}
sendConnectFailResponse(): void {
this.protocol.sendConnectFailResponse(this);
}
sendConnectOKResponse(votes: boolean): void {
this.protocol.sendConnectOKResponse(this, votes);
}
sendLoginResponse(ok: boolean, message: string | undefined): void {
this.protocol.sendLoginResponse(this, ok, message);
}
sendAdminLoginResponse(ok: boolean, modPerms: number | undefined): void {
this.protocol.sendAdminLoginResponse(this, ok, modPerms);
}
sendAdminMonitorResponse(output: string): void {
this.protocol.sendAdminMonitorResponse(this, output);
}
sendAdminIPResponse(username: string, ip: string): void {
this.protocol.sendAdminIPResponse(this, username, ip);
}
sendChatMessage(username: '' | string, message: string): void {
this.protocol.sendChatMessage(this, username, message);
}
sendChatHistoryMessage(history: ProtocolChatHistory[]): void {
this.protocol.sendChatHistoryMessage(this, history);
}
sendAddUser(users: ProtocolAddUser[]): void {
this.protocol.sendAddUser(this, users);
}
sendRemUser(users: string[]): void {
this.protocol.sendRemUser(this, users);
}
sendFlag(flag: ProtocolFlag[]): void {
this.protocol.sendFlag(this, flag);
}
sendSelfRename(status: ProtocolRenameStatus, newUsername: string, rank: Rank): void {
this.protocol.sendSelfRename(this, status, newUsername, rank);
}
sendRename(oldUsername: string, newUsername: string, rank: Rank): void {
this.protocol.sendRename(this, oldUsername, newUsername, rank);
}
sendListResponse(list: ListEntry[]): void {
this.protocol.sendListResponse(this, list);
}
sendTurnQueue(turnTime: number, users: string[]): void {
this.protocol.sendTurnQueue(this, turnTime, users);
}
sendTurnQueueWaiting(turnTime: number, users: string[], waitTime: number): void {
this.protocol.sendTurnQueueWaiting(this, turnTime, users, waitTime);
}
sendVoteStarted(): void {
this.protocol.sendVoteStarted(this);
}
sendVoteStats(msLeft: number, nrYes: number, nrNo: number): void {
this.protocol.sendVoteStats(this, msLeft, nrYes, nrNo);
}
sendVoteEnded(): void {
this.protocol.sendVoteEnded(this);
}
sendVoteCooldown(ms: number): void {
this.protocol.sendVoteCooldown(this, ms);
}
sendScreenResize(width: number, height: number): void {
this.protocol.sendScreenResize(this, width, height);
}
sendScreenUpdate(rect: ScreenRect): void {
this.protocol.sendScreenUpdate(this, rect);
}
}
export enum Rank {

View File

@@ -3,14 +3,15 @@ import { CollabVMProtocolMessage, CollabVMProtocolMessageType } from '@cvmts/col
import { GuacamoleProtocol } from './GuacamoleProtocol.js';
import { ScreenRect } from './Protocol';
import { User } from '../User.js';
export class BinRectsProtocol extends GuacamoleProtocol {
sendScreenUpdate(rect: ScreenRect): void {
sendScreenUpdate(user: User, rect: ScreenRect): void {
let bmsg: CollabVMProtocolMessage = {
type: CollabVMProtocolMessageType.rect,
rect: rect
};
this.user?.socket.sendBinary(msgpack.encode(bmsg));
user.socket.sendBinary(msgpack.encode(bmsg));
}
}

View File

@@ -1,42 +1,37 @@
import pino from 'pino';
import { IProtocol, IProtocolMessageHandler, ListEntry, ProtocolAddUser, ProtocolBase, ProtocolChatHistory, ProtocolFlag, ProtocolRenameStatus, ProtocolUpgradeCapability, ScreenRect } from './Protocol.js';
import { IProtocol, IProtocolMessageHandler, ListEntry, ProtocolAddUser, ProtocolChatHistory, ProtocolFlag, ProtocolRenameStatus, ProtocolUpgradeCapability, ScreenRect } from './Protocol.js';
import { Rank, User } from '../User.js';
import * as cvm from '@cvmts/cvm-rs';
// CollabVM protocol implementation for Guacamole.
export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
private logger = pino({
name: 'CVMTS.GuacamoleProtocol'
});
private __processMessage_admin(decodedElements: string[]): boolean {
export class GuacamoleProtocol implements IProtocol {
private __processMessage_admin(user: User, handler: IProtocolMessageHandler, decodedElements: string[]): boolean {
switch (decodedElements[1]) {
case '2':
if (decodedElements.length !== 3) return false;
this.handlers?.onAdminLogin(this.user!, decodedElements[2]);
handler.onAdminLogin(user, decodedElements[2]);
break;
case '5':
if (decodedElements.length !== 4) return false;
this.handlers?.onAdminMonitor(this.user!, decodedElements[2], decodedElements[3]);
handler.onAdminMonitor(user, decodedElements[2], decodedElements[3]);
break;
case '8':
if (decodedElements.length !== 3) return false;
this.handlers?.onAdminRestore(this.user!, decodedElements[2]);
handler.onAdminRestore(user, decodedElements[2]);
break;
case '10':
if (decodedElements.length !== 3) return false;
this.handlers?.onAdminReboot(this.user!, decodedElements[2]);
handler.onAdminReboot(user, decodedElements[2]);
break;
case '12':
if (decodedElements.length < 3) return false;
this.handlers?.onAdminBanUser(this.user!, decodedElements[2]);
handler.onAdminBanUser(user, decodedElements[2]);
case '13':
{
if (decodedElements.length !== 3) return false;
let choice = parseInt(decodedElements[2]);
if (choice == undefined) return false;
this.handlers?.onAdminForceVote(this.user!, choice);
handler.onAdminForceVote(user, choice);
}
break;
case '14':
@@ -46,35 +41,35 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
if (decodedElements[3] == '0') temporary = true;
else if (decodedElements[3] == '1') temporary = false;
else return false;
this.handlers?.onAdminMuteUser(this.user!, decodedElements[2], temporary);
handler.onAdminMuteUser(user, decodedElements[2], temporary);
}
break;
case '15':
if (decodedElements.length !== 3) return false;
this.handlers?.onAdminKickUser(this.user!, decodedElements[2]);
handler.onAdminKickUser(user, decodedElements[2]);
break;
case '16':
if (decodedElements.length !== 3) return false;
this.handlers?.onAdminEndTurn(this.user!, decodedElements[2]);
handler.onAdminEndTurn(user, decodedElements[2]);
break;
case '17':
if (decodedElements.length !== 3) return false;
this.handlers?.onAdminClearQueue(this.user!, decodedElements[2]);
handler.onAdminClearQueue(user, decodedElements[2]);
break;
case '18':
if (decodedElements.length !== 4) return false;
this.handlers?.onAdminRename(this.user!, decodedElements[2], decodedElements[3]);
handler.onAdminRename(user, decodedElements[2], decodedElements[3]);
break;
case '19':
if (decodedElements.length !== 3) return false;
this.handlers?.onAdminGetIP(this.user!, decodedElements[2]);
handler.onAdminGetIP(user, decodedElements[2]);
break;
case '20':
this.handlers?.onAdminBypassTurn(this.user!);
handler.onAdminBypassTurn(user);
break;
case '21':
if (decodedElements.length !== 3) return false;
this.handlers?.onAdminRawMessage(this.user!, decodedElements[2]);
handler.onAdminRawMessage(user, decodedElements[2]);
break;
case '22':
{
@@ -84,11 +79,11 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
if (decodedElements[2] == '0') enabled = false;
else if (decodedElements[2] == '1') enabled = true;
else return false;
this.handlers?.onAdminToggleTurns(this.user!, enabled);
handler.onAdminToggleTurns(user, enabled);
}
break;
case '23':
this.handlers?.onAdminIndefiniteTurn(this.user!);
handler.onAdminIndefiniteTurn(user);
break;
case '24':
{
@@ -97,43 +92,43 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
if (decodedElements[2] == '0') show = false;
else if (decodedElements[2] == '1') show = true;
else return false;
this.handlers?.onAdminHideScreen(this.user!, show);
handler.onAdminHideScreen(user, show);
}
break;
case '25':
if (decodedElements.length !== 3) return false;
this.handlers?.onAdminSystemMessage(this.user!, decodedElements[2]);
handler.onAdminSystemMessage(user, decodedElements[2]);
break;
}
return true;
}
processMessage(buffer: Buffer): boolean {
processMessage(user: User, handler: IProtocolMessageHandler, buffer: Buffer): boolean {
let decodedElements = cvm.guacDecode(buffer.toString('utf-8'));
if (decodedElements.length < 1) return false;
// The first element is the "opcode".
switch (decodedElements[0]) {
case 'nop':
this.handlers?.onNop(this.user!);
handler.onNop(user);
break;
case 'cap':
if (decodedElements.length < 2) return false;
this.handlers?.onCapabilityUpgrade(this.user!, decodedElements.slice(1));
handler.onCapabilityUpgrade(user, decodedElements.slice(1));
break;
case 'login':
if (decodedElements.length !== 2) return false;
this.handlers?.onLogin(this.user!, decodedElements[1]);
handler.onLogin(user, decodedElements[1]);
break;
case 'noflag':
this.handlers?.onNoFlag(this.user!);
handler.onNoFlag(user);
break;
case 'list':
this.handlers?.onList(this.user!);
handler.onList(user);
break;
case 'connect':
if (decodedElements.length !== 2) return false;
this.handlers?.onConnect(this.user!, decodedElements[1]);
handler.onConnect(user, decodedElements[1]);
break;
case 'view':
{
@@ -141,15 +136,15 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
let viewMode = parseInt(decodedElements[2]);
if (viewMode == undefined) return false;
this.handlers?.onView(this.user!, decodedElements[1], viewMode);
handler.onView(user, decodedElements[1], viewMode);
}
break;
case 'rename':
this.handlers?.onRename(this.user!, decodedElements[1]);
handler.onRename(user, decodedElements[1]);
break;
case 'chat':
if (decodedElements.length !== 2) return false;
this.handlers?.onChat(this.user!, decodedElements[1]);
handler.onChat(user, decodedElements[1]);
break;
case 'turn':
let forfeit = false;
@@ -161,7 +156,7 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
else if (decodedElements[1] == '1') forfeit = false;
}
this.handlers?.onTurnRequest(this.user!, forfeit);
handler.onTurnRequest(user, forfeit);
break;
case 'mouse':
if (decodedElements.length !== 4) return false;
@@ -171,25 +166,25 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
let mask = parseInt(decodedElements[3]);
if (x === undefined || y === undefined || mask === undefined) return false;
this.handlers?.onMouse(this.user!, x, y, mask);
handler.onMouse(user, x, y, mask);
break;
case 'key':
if (decodedElements.length !== 3) return false;
var keysym = parseInt(decodedElements[1]);
var down = parseInt(decodedElements[2]);
if (keysym === undefined || (down !== 0 && down !== 1)) return false;
this.handlers?.onKey(this.user!, keysym, down === 1);
handler.onKey(user, keysym, down === 1);
break;
case 'vote':
if (decodedElements.length !== 2) return false;
let choice = parseInt(decodedElements[1]);
if (choice == undefined) return false;
this.handlers?.onVote(this.user!, choice);
handler.onVote(user, choice);
break;
case 'admin':
if (decodedElements.length < 2) return false;
return this.__processMessage_admin(decodedElements);
return this.__processMessage_admin(user, handler, decodedElements);
}
return true;
@@ -197,109 +192,109 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
// Senders
sendAuth(authServer: string): void {
this.user?.sendMsg(cvm.guacEncode('auth', authServer));
sendAuth(user: User, authServer: string): void {
user.sendMsg(cvm.guacEncode('auth', authServer));
}
sendNop(): void {
this.user?.sendMsg(cvm.guacEncode('nop'));
sendNop(user: User): void {
user.sendMsg(cvm.guacEncode('nop'));
}
sendSync(now: number): void {
this.user?.sendMsg(cvm.guacEncode('sync', now.toString()));
sendSync(user: User, now: number): void {
user.sendMsg(cvm.guacEncode('sync', now.toString()));
}
sendCapabilities(caps: ProtocolUpgradeCapability[]): void {
sendCapabilities(user: User, caps: ProtocolUpgradeCapability[]): void {
let arr = ['cap', ...caps];
this?.user?.sendMsg(cvm.guacEncode(...arr));
user.sendMsg(cvm.guacEncode(...arr));
}
sendConnectFailResponse(): void {
this.user?.sendMsg(cvm.guacEncode('connect', '0'));
sendConnectFailResponse(user: User): void {
user.sendMsg(cvm.guacEncode('connect', '0'));
}
sendConnectOKResponse(votes: boolean): void {
this.user?.sendMsg(cvm.guacEncode('connect', '1', '1', votes ? '1' : '0', '0'));
sendConnectOKResponse(user: User, votes: boolean): void {
user.sendMsg(cvm.guacEncode('connect', '1', '1', votes ? '1' : '0', '0'));
}
sendLoginResponse(ok: boolean, message: string | undefined): void {
sendLoginResponse(user: User, ok: boolean, message: string | undefined): void {
if (ok) {
this.user?.sendMsg(cvm.guacEncode('login', '1'));
user.sendMsg(cvm.guacEncode('login', '1'));
return;
} else {
this.user?.sendMsg(cvm.guacEncode('login', '0', message!));
user.sendMsg(cvm.guacEncode('login', '0', message!));
}
}
sendAdminLoginResponse(ok: boolean, modPerms: number | undefined): void {
sendAdminLoginResponse(user: User, ok: boolean, modPerms: number | undefined): void {
if (ok) {
if (modPerms == undefined) {
this.user?.sendMsg(cvm.guacEncode('admin', '0', '1'));
user.sendMsg(cvm.guacEncode('admin', '0', '1'));
} else {
this.user?.sendMsg(cvm.guacEncode('admin', '0', '3', modPerms.toString()));
user.sendMsg(cvm.guacEncode('admin', '0', '3', modPerms.toString()));
}
} else {
this.user?.sendMsg(cvm.guacEncode('admin', '0', '0'));
user.sendMsg(cvm.guacEncode('admin', '0', '0'));
}
}
sendAdminMonitorResponse(output: string): void {
this.user?.sendMsg(cvm.guacEncode('admin', '2', output));
sendAdminMonitorResponse(user: User, output: string): void {
user.sendMsg(cvm.guacEncode('admin', '2', output));
}
sendAdminIPResponse(username: string, ip: string): void {
this.user?.sendMsg(cvm.guacEncode('admin', '19', username, ip));
sendAdminIPResponse(user: User, username: string, ip: string): void {
user.sendMsg(cvm.guacEncode('admin', '19', username, ip));
}
sendChatMessage(username: string, message: string): void {
this.user?.sendMsg(cvm.guacEncode('chat', username, message));
sendChatMessage(user: User, username: string, message: string): void {
user.sendMsg(cvm.guacEncode('chat', username, message));
}
sendChatHistoryMessage(history: ProtocolChatHistory[]): void {
sendChatHistoryMessage(user: User, history: ProtocolChatHistory[]): void {
let arr = ['chat'];
for (let a of history) {
arr.push(a.user, a.msg);
}
this.user?.sendMsg(cvm.guacEncode(...arr));
user.sendMsg(cvm.guacEncode(...arr));
}
sendAddUser(users: ProtocolAddUser[]): void {
sendAddUser(user: User, users: ProtocolAddUser[]): void {
let arr = ['adduser', users.length.toString()];
for (let user of users) {
arr.push(user.username);
arr.push(user.rank.toString());
}
this.user?.sendMsg(cvm.guacEncode(...arr));
user.sendMsg(cvm.guacEncode(...arr));
}
sendRemUser(users: string[]): void {
sendRemUser(user: User, users: string[]): void {
let arr = ['remuser', users.length.toString()];
for (let user of users) {
arr.push(user);
}
this.user?.sendMsg(cvm.guacEncode(...arr));
user.sendMsg(cvm.guacEncode(...arr));
}
sendFlag(flag: ProtocolFlag[]): void {
sendFlag(user: User, flag: ProtocolFlag[]): void {
// Basically this does the same as the above manual for of things
// but in one line of code
let arr = ['flag', ...flag.flatMap((flag) => [flag.username, flag.countryCode])];
this.user?.sendMsg(cvm.guacEncode(...arr));
user.sendMsg(cvm.guacEncode(...arr));
}
sendSelfRename(status: ProtocolRenameStatus, newUsername: string, rank: Rank): void {
this.user?.sendMsg(cvm.guacEncode('rename', '0', status.toString(), newUsername));
sendSelfRename(user: User, status: ProtocolRenameStatus, newUsername: string, rank: Rank): void {
user.sendMsg(cvm.guacEncode('rename', '0', status.toString(), newUsername));
}
sendRename(oldUsername: string, newUsername: string, rank: Rank): void {
this.user?.sendMsg(cvm.guacEncode('rename', '1', oldUsername, newUsername));
sendRename(user: User, oldUsername: string, newUsername: string, rank: Rank): void {
user.sendMsg(cvm.guacEncode('rename', '1', oldUsername, newUsername));
}
sendListResponse(list: ListEntry[]): void {
sendListResponse(user: User, list: ListEntry[]): void {
let arr = ['list'];
for (let node of list) {
arr.push(node.id);
@@ -307,45 +302,45 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
arr.push(node.thumbnail.toString('base64'));
}
this.user?.sendMsg(cvm.guacEncode(...arr));
user.sendMsg(cvm.guacEncode(...arr));
}
sendVoteStarted(): void {
this.user?.sendMsg(cvm.guacEncode('vote', '0'));
sendVoteStarted(user: User): void {
user.sendMsg(cvm.guacEncode('vote', '0'));
}
sendVoteStats(msLeft: number, nrYes: number, nrNo: number): void {
this.user?.sendMsg(cvm.guacEncode('vote', '1', msLeft.toString(), nrYes.toString(), nrNo.toString()));
sendVoteStats(user: User, msLeft: number, nrYes: number, nrNo: number): void {
user.sendMsg(cvm.guacEncode('vote', '1', msLeft.toString(), nrYes.toString(), nrNo.toString()));
}
sendVoteEnded(): void {
this.user?.sendMsg(cvm.guacEncode('vote', '2'));
sendVoteEnded(user: User): void {
user.sendMsg(cvm.guacEncode('vote', '2'));
}
sendVoteCooldown(ms: number): void {
this.user?.sendMsg(cvm.guacEncode('vote', '3', ms.toString()));
sendVoteCooldown(user: User, ms: number): void {
user.sendMsg(cvm.guacEncode('vote', '3', ms.toString()));
}
private getTurnQueueBase(turnTime: number, users: string[]): string[] {
return ['turn', turnTime.toString(), users.length.toString(), ...users];
}
sendTurnQueue(turnTime: number, users: string[]): void {
this.user?.sendMsg(cvm.guacEncode(...this.getTurnQueueBase(turnTime, users)));
sendTurnQueue(user: User, turnTime: number, users: string[]): void {
user.sendMsg(cvm.guacEncode(...this.getTurnQueueBase(turnTime, users)));
}
sendTurnQueueWaiting(turnTime: number, users: string[], waitTime: number): void {
sendTurnQueueWaiting(user: User, turnTime: number, users: string[], waitTime: number): void {
let queue = this.getTurnQueueBase(turnTime, users);
queue.push(waitTime.toString());
this.user?.sendMsg(cvm.guacEncode(...queue));
user.sendMsg(cvm.guacEncode(...queue));
}
sendScreenResize(width: number, height: number): void {
this.user?.sendMsg(cvm.guacEncode('size', '0', width.toString(), height.toString()));
sendScreenResize(user: User, width: number, height: number): void {
user.sendMsg(cvm.guacEncode('size', '0', width.toString(), height.toString()));
}
sendScreenUpdate(rect: ScreenRect): void {
this.user?.sendMsg(cvm.guacEncode('png', '0', '0', rect.x.toString(), rect.y.toString(), rect.data.toString('base64')));
this.sendSync(Date.now());
sendScreenUpdate(user: User, rect: ScreenRect): void {
user.sendMsg(cvm.guacEncode('png', '0', '0', rect.x.toString(), rect.y.toString(), rect.data.toString('base64')));
this.sendSync(user, Date.now());
}
}

View File

@@ -1,24 +1,23 @@
import { IProtocol } from "./Protocol";
import { User } from "../User";
import { IProtocol } from './Protocol';
import { User } from '../User';
// The protocol manager. Holds protocol factories, and provides the ability
// to create a protocol by name. Avoids direct dependency on a given list of protocols,
// and allows (relatively simple) expansion.
// The protocol manager.
// Holds protocols, and provides the ability to obtain them by name.
//
// Avoids direct dependency on a given list of protocols,
// and allows (relatively simple) expansion of the supported protocols.
export class ProtocolManager {
private protocols = new Map<String, () => IProtocol>();
private protocols = new Map<String, IProtocol>();
// Registers a protocol with the given name.
// Registers a protocol with the given name, creates it, and stores it for later use.
registerProtocol(name: string, protocolFactory: () => IProtocol) {
if (!this.protocols.has(name)) this.protocols.set(name, protocolFactory);
if (!this.protocols.has(name)) this.protocols.set(name, protocolFactory());
}
// Creates an instance of a given protocol for a user.
createProtocol(name: string, user: User): IProtocol {
if (!this.protocols.has(name)) throw new Error(`ProtocolManager does not have protocol \"${name}\"`);
let factory = this.protocols.get(name)!;
let proto = factory();
proto.init(user);
// Gets an instance of a protocol.
getProtocol(name: string): IProtocol {
let proto = this.protocols.get(name);
if (proto == undefined) throw new Error(`ProtocolManager does not have protocol \"${name}\"`);
return proto;
}
}

View File

@@ -90,13 +90,6 @@ export interface IProtocolMessageHandler {
// allowing it to be protocol-independent (as long as the client and server
// are able to speak the same protocol.)
export interface IProtocol {
// don't implement this yourself, extend from ProtocolBase
init(u: User): void;
dispose(): void;
// Sets handler object.
setHandler(handlers: IProtocolMessageHandler): void;
// Protocol implementation stuff
// Parses a single message and fires the given handler with deserialized arguments.
@@ -104,67 +97,48 @@ export interface IProtocol {
// to handle errors. It should, however, catch invalid parameters without failing.
//
// This function will perform conversion to text if it is required.
processMessage(buffer: Buffer): boolean;
processMessage(user: User, handler: IProtocolMessageHandler, buffer: Buffer): boolean;
// Senders
sendNop(): void;
sendSync(now: number): void;
sendNop(user: User): void;
sendSync(user: User, now: number): void;
sendAuth(authServer: string): void;
sendAuth(user: User, authServer: string): void;
sendCapabilities(caps: ProtocolUpgradeCapability[]): void;
sendCapabilities(user: User, caps: ProtocolUpgradeCapability[]): void;
sendConnectFailResponse(): void;
sendConnectOKResponse(votes: boolean): void;
sendConnectFailResponse(user: User): void;
sendConnectOKResponse(user: User, votes: boolean): void;
sendLoginResponse(ok: boolean, message: string | undefined): void;
sendLoginResponse(user: User, ok: boolean, message: string | undefined): void;
sendAdminLoginResponse(ok: boolean, modPerms: number | undefined): void;
sendAdminMonitorResponse(output: string): void;
sendAdminIPResponse(username: string, ip: string): void;
sendAdminLoginResponse(user: User, ok: boolean, modPerms: number | undefined): void;
sendAdminMonitorResponse(user: User, output: string): void;
sendAdminIPResponse(user: User, username: string, ip: string): void;
sendChatMessage(username: '' | string, message: string): void;
sendChatHistoryMessage(history: ProtocolChatHistory[]): void;
sendChatMessage(user: User, username: '' | string, message: string): void;
sendChatHistoryMessage(user: User, history: ProtocolChatHistory[]): void;
sendAddUser(users: ProtocolAddUser[]): void;
sendRemUser(users: string[]): void;
sendFlag(flag: ProtocolFlag[]): void;
sendAddUser(user: User, users: ProtocolAddUser[]): void;
sendRemUser(user: User, users: string[]): void;
sendFlag(user: User, flag: ProtocolFlag[]): void;
sendSelfRename(status: ProtocolRenameStatus, newUsername: string, rank: Rank): void;
sendRename(oldUsername: string, newUsername: string, rank: Rank): void;
sendSelfRename(user: User, status: ProtocolRenameStatus, newUsername: string, rank: Rank): void;
sendRename(user: User, oldUsername: string, newUsername: string, rank: Rank): void;
sendListResponse(list: ListEntry[]): void;
sendListResponse(user: User, list: ListEntry[]): void;
sendTurnQueue(turnTime: number, users: string[]): void;
sendTurnQueueWaiting(turnTime: number, users: string[], waitTime: number): void;
sendTurnQueue(user: User, turnTime: number, users: string[]): void;
sendTurnQueueWaiting(user: User, turnTime: number, users: string[], waitTime: number): void;
sendVoteStarted(): void;
sendVoteStats(msLeft: number, nrYes: number, nrNo: number): void;
sendVoteEnded(): void;
sendVoteCooldown(ms: number): void;
sendVoteStarted(user: User): void;
sendVoteStats(user: User, msLeft: number, nrYes: number, nrNo: number): void;
sendVoteEnded(user: User): void;
sendVoteCooldown(user: User, ms: number): void;
sendScreenResize(width: number, height: number): void;
sendScreenResize(user: User, width: number, height: number): void;
// Sends a rectangle update to the user.
sendScreenUpdate(rect: ScreenRect): void;
}
// Base mixin for all concrete protocols to use. Inherit from this!
export class ProtocolBase {
protected handlers: IProtocolMessageHandler | null = null;
protected user: User | null = null;
init(u: User): void {
this.user = u;
}
dispose(): void {
this.user = null;
this.handlers = null;
}
setHandler(handlers: IProtocolMessageHandler): void {
this.handlers = handlers;
}
sendScreenUpdate(user: User, rect: ScreenRect): void;
}