diff --git a/cvmts/src/CollabVMServer.ts b/cvmts/src/CollabVMServer.ts index 15f4ca2..b789709 100644 --- a/cvmts/src/CollabVMServer.ts +++ b/cvmts/src/CollabVMServer.ts @@ -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 { diff --git a/cvmts/src/User.ts b/cvmts/src/User.ts index e3d2054..44a3cab 100644 --- a/cvmts/src/User.ts +++ b/cvmts/src/User.ts @@ -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 { diff --git a/cvmts/src/protocol/BinRectsProtocol.ts b/cvmts/src/protocol/BinRectsProtocol.ts index 21bb76a..4979792 100644 --- a/cvmts/src/protocol/BinRectsProtocol.ts +++ b/cvmts/src/protocol/BinRectsProtocol.ts @@ -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)); } } diff --git a/cvmts/src/protocol/GuacamoleProtocol.ts b/cvmts/src/protocol/GuacamoleProtocol.ts index 4735a37..be480a0 100644 --- a/cvmts/src/protocol/GuacamoleProtocol.ts +++ b/cvmts/src/protocol/GuacamoleProtocol.ts @@ -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()); } } diff --git a/cvmts/src/protocol/Manager.ts b/cvmts/src/protocol/Manager.ts index d61b003..1c00955 100644 --- a/cvmts/src/protocol/Manager.ts +++ b/cvmts/src/protocol/Manager.ts @@ -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 IProtocol>(); + private protocols = new Map(); - // 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; } } diff --git a/cvmts/src/protocol/Protocol.ts b/cvmts/src/protocol/Protocol.ts index e6fe4ef..4e36084 100644 --- a/cvmts/src/protocol/Protocol.ts +++ b/cvmts/src/protocol/Protocol.ts @@ -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; }