From 1c062697b9ecdf7318294c283719af0e9fcec553 Mon Sep 17 00:00:00 2001 From: modeco80 Date: Wed, 21 Aug 2024 07:10:58 -0400 Subject: [PATCH 01/20] WIP: protocol abstraction Work on abstracting the CollabVMServer so it now calls into a interface for sending/recieving protocol messages. This will allow cleaner bringup of a fully binary protocol, and generally is just cleaner code. Mostly everything is parsd/running through this new layer, although there are some TODO items: - NetworkClient/... should just spit out a Buffer or something that eventually turns into or has one - TCP protocol will need to be revamped so we can support an actual binary protocol on top of it. The current thing is line based - More admin op stuff needs to be handled - The handlers are a bit jumbled around atm - There is still a good amount of code which assumes guacamole which needs to be rewritten dont use this branch fuckers --- cvmts/src/BinRectsProtocol.ts | 6 + cvmts/src/CollabVMServer.ts | 999 ++++++++++++++++----------------- cvmts/src/GuacamoleProtocol.ts | 282 ++++++++++ cvmts/src/Protocol.ts | 119 ++++ cvmts/src/TCP/TCPServer.ts | 3 + cvmts/src/User.ts | 22 +- cvmts/src/index.ts | 9 +- 7 files changed, 926 insertions(+), 514 deletions(-) create mode 100644 cvmts/src/BinRectsProtocol.ts create mode 100644 cvmts/src/GuacamoleProtocol.ts create mode 100644 cvmts/src/Protocol.ts diff --git a/cvmts/src/BinRectsProtocol.ts b/cvmts/src/BinRectsProtocol.ts new file mode 100644 index 0000000..f4a8d57 --- /dev/null +++ b/cvmts/src/BinRectsProtocol.ts @@ -0,0 +1,6 @@ +import * as msgpack from 'msgpackr'; +import { CollabVMProtocolMessage, CollabVMProtocolMessageType } from '@cvmts/collab-vm-1.2-binary-protocol'; + +// TODO: reimplement binrects protocol +// we can just create/proxy a GuacamoleProtocol manually, +// and for the rects do our own thing diff --git a/cvmts/src/CollabVMServer.ts b/cvmts/src/CollabVMServer.ts index d9ec4e8..1485ce7 100644 --- a/cvmts/src/CollabVMServer.ts +++ b/cvmts/src/CollabVMServer.ts @@ -14,12 +14,11 @@ import AuthManager from './AuthManager.js'; import { JPEGEncoder } from './JPEGEncoder.js'; import VM from './vm/interface.js'; import { ReaderModel } from '@maxmind/geoip2-node'; -import * as msgpack from 'msgpackr'; -import { CollabVMProtocolMessage, CollabVMProtocolMessageType } from '@cvmts/collab-vm-1.2-binary-protocol'; import { Size, Rect } from './Utilities.js'; import pino from 'pino'; import { BanManager } from './BanManager.js'; +import { IProtocolHandlers, ListEntry, ProtocolAddUser, TheProtocolManager } from './Protocol.js'; // Instead of strange hacks we can just use nodejs provided // import.meta properties, which have existed since LTS if not before @@ -39,7 +38,7 @@ type VoteTally = { no: number; }; -export default class CollabVMServer { +export default class CollabVMServer implements IProtocolHandlers { private Config: IConfig; private clients: User[]; @@ -76,8 +75,8 @@ export default class CollabVMServer { private screenHidden: boolean; // base64 image to show when the screen is hidden - private screenHiddenImg: string; - private screenHiddenThumb: string; + private screenHiddenImg: Buffer; + private screenHiddenThumb: Buffer; // Indefinite turn private indefiniteTurn: User | null; @@ -109,8 +108,8 @@ export default class CollabVMServer { 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.screenHiddenImg = readFileSync(path.join(kCVMTSAssetsRoot, 'screenhidden.jpeg')); + this.screenHiddenThumb = readFileSync(path.join(kCVMTSAssetsRoot, 'screenhiddenthumb.jpeg')); this.indefiniteTurn = null; this.ModPerms = Utilities.MakeModPerms(this.Config.collabvm.moderatorPermissions); @@ -158,7 +157,7 @@ export default class CollabVMServer { this.banmgr = banmgr; } - public addUser(user: User) { + public connectionOpened(user: User) { let sameip = this.clients.filter((c) => c.IP.address === user.IP.address); if (sameip.length >= this.Config.collabvm.maxConnections) { // Kick the oldest client @@ -166,6 +165,7 @@ export default class CollabVMServer { sameip[0].kick(); } this.clients.push(user); + if (this.Config.geoip.enabled) { try { user.countryCode = this.geoipReader!.country(user.IP.address).country!.isoCode; @@ -173,12 +173,28 @@ export default class CollabVMServer { this.logger.warn(`Failed to get country code for ${user.IP.address}: ${(error as Error).message}`); } } - user.socket.on('msg', (msg: string) => this.onMessage(user, msg)); + + // TODO: we should probably just make this a buffer arg lol.. + user.socket.on('msg', (msg: string) => { + let buf = Buffer.from(msg); + try { + user.protocol.processMessage(buf); + } catch (err) { + user.kick(); + } + }); + user.socket.on('disconnect', () => this.connectionClosed(user)); + + // Set ourselves as the handler + user.protocol.setHandler(this as IProtocolHandlers); + if (this.Config.auth.enabled) { - user.sendMsg(cvm.guacEncode('auth', this.Config.auth.apiEndpoint)); + user.protocol.sendAuth(this.Config.auth.apiEndpoint); } - user.sendMsg(this.getAdduserMsg()); + + // convert these to proto + user.protocol.sendAddUser(this.getAdduserMsg()); if (this.Config.geoip.enabled) user.sendMsg(this.getFlagMsg()); } @@ -203,440 +219,442 @@ export default class CollabVMServer { if (hadturn) this.nextTurn(); } - this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('remuser', '1', user.username!))); + this.clients.forEach((c) => c.protocol.sendRemUser([user.username!])); } - private async onMessage(client: User, message: string) { + // IProtocolHandlers + + // does auth check + private authCheck(user: User, guestPermission: boolean) { + if (!this.Config.auth.enabled) return true; + + if (user.rank === Rank.Unregistered && !guestPermission) { + user.protocol.sendChatMessage('', 'You need to login to do that.'); + return false; + } + + return true; + } + + onNop(user: User): void { + user.onNop(); + } + + async onLogin(user: User, token: string) { + if (!this.Config.auth.enabled) return; + + if (!user.connectedToNode) { + user.sendMsg(cvm.guacEncode('login', '0', 'You must connect to the VM before logging in.')); + return; + } + try { - var msgArr = cvm.guacDecode(message); - if (msgArr.length < 1) return; - switch (msgArr[0]) { - case 'login': - if (msgArr.length !== 2 || !this.Config.auth.enabled) return; - if (!client.connectedToNode) { - client.sendMsg(cvm.guacEncode('login', '0', 'You must connect to the VM before logging in.')); - return; - } - try { - let res = await this.auth!.Authenticate(msgArr[1], client); - if (res.clientSuccess) { - this.logger.info(`${client.IP.address} logged in as ${res.username}`); - client.sendMsg(cvm.guacEncode('login', '1')); - 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 - if (client.countryCode !== null && client.noFlag) { - // privacy - for (let cl of this.clients.filter((c) => c !== client)) { - cl.sendMsg(cvm.guacEncode('remuser', '1', client.username!)); - } - this.renameUser(client, res.username, false); - } else this.renameUser(client, res.username, true); - // Set rank - client.rank = res.rank; - if (client.rank === Rank.Admin) { - client.sendMsg(cvm.guacEncode('admin', '0', '1')); - } else if (client.rank === Rank.Moderator) { - client.sendMsg(cvm.guacEncode('admin', '0', '3', this.ModPerms.toString())); - } - this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('adduser', '1', client.username!, client.rank.toString()))); - } else { - client.sendMsg(cvm.guacEncode('login', '0', res.error!)); - if (res.error === 'You are banned') { - client.kick(); - } - } - } catch (err) { - this.logger.error(`Error authenticating client ${client.IP.address}: ${(err as Error).message}`); - // for now? - client.sendMsg(cvm.guacEncode('login', '0', 'There was an internal error while authenticating. Please let a staff member know as soon as possible')); - } - break; - case 'noflag': { - if (client.connectedToNode) - // too late - return; - client.noFlag = true; + let res = await this.auth!.Authenticate(token, user); + + if (res.clientSuccess) { + this.logger.info(`${user.IP.address} logged in as ${res.username}`); + user.protocol.sendLoginResponse(true, ''); + + 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(); } - case 'list': - 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())); + // Set username + if (user.countryCode !== null && user.noFlag) { + // privacy + for (let cl of this.clients.filter((c) => c !== user)) { + cl.sendMsg(cvm.guacEncode('remuser', '1', user.username!)); } - break; - case 'connect': - if (!client.username || msgArr.length !== 2 || msgArr[1] !== this.Config.collabvm.node) { - client.sendMsg(cvm.guacEncode('connect', '0')); - return; - } - - client.connectedToNode = true; - client.sendMsg(cvm.guacEncode('connect', '1', '1', this.VM.SnapshotsSupported() ? '1' : '0', '0')); - if (this.ChatHistory.size !== 0) client.sendMsg(this.getChatHistoryMsg()); - if (this.Config.collabvm.motd) client.sendMsg(cvm.guacEncode('chat', '', this.Config.collabvm.motd)); - if (this.screenHidden) { - client.sendMsg(cvm.guacEncode('size', '0', '1024', '768')); - client.sendMsg(cvm.guacEncode('png', '0', '0', '0', '0', this.screenHiddenImg)); - } else { - await this.SendFullScreenWithSize(client); - } - client.sendMsg(cvm.guacEncode('sync', Date.now().toString())); - if (this.voteInProgress) this.sendVoteUpdate(client); - this.sendTurnUpdate(client); - break; - case 'view': - if (client.connectedToNode) return; - if (client.username || msgArr.length !== 3 || msgArr[1] !== this.Config.collabvm.node) { - // The use of connect here is intentional. - client.sendMsg(cvm.guacEncode('connect', '0')); - return; - } - - switch (msgArr[2]) { - case '0': - client.viewMode = 0; - break; - case '1': - client.viewMode = 1; - break; - default: - client.sendMsg(cvm.guacEncode('connect', '0')); - return; - } - - client.sendMsg(cvm.guacEncode('connect', '1', '1', this.VM.SnapshotsSupported() ? '1' : '0', '0')); - if (this.ChatHistory.size !== 0) client.sendMsg(this.getChatHistoryMsg()); - if (this.Config.collabvm.motd) client.sendMsg(cvm.guacEncode('chat', '', this.Config.collabvm.motd)); - - if (client.viewMode == 1) { - if (this.screenHidden) { - client.sendMsg(cvm.guacEncode('size', '0', '1024', '768')); - client.sendMsg(cvm.guacEncode('png', '0', '0', '0', '0', this.screenHiddenImg)); - } else { - await this.SendFullScreenWithSize(client); - } - client.sendMsg(cvm.guacEncode('sync', Date.now().toString())); - } - - 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) { - client.sendMsg(cvm.guacEncode('chat', '', 'Go to your account settings to change your username.')); - 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 - if (client.username) client.sendMsg(cvm.guacEncode('chat', '', 'You need to log in to do that.')); - 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) { - client.sendMsg(cvm.guacEncode('chat', '', 'You need to login to do that.')); - 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; - - this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', client.username!, msg))); - 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.turnWhitelist) return; - if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.turn) { - client.sendMsg(cvm.guacEncode('chat', '', 'You need to login to do that.')); - 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]) { - case '0': - if (this.indefiniteTurn === client) { - this.indefiniteTurn = null; - } - takingTurn = false; - break; - case '1': - takingTurn = true; - break; - default: - return; - break; - } - 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; - } - 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; - this.VM.GetDisplay()?.MouseEvent(x, y, mask); - 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; - this.VM.GetDisplay()?.KeyboardEvent(keysym, down === 1 ? true : false); - break; - case 'vote': - if (!this.VM.SnapshotsSupported()) return; - if ((!this.turnsAllowed || this.Config.collabvm.turnwhitelist) && client.rank !== Rank.Admin && client.rank !== Rank.Moderator && !client.turnWhitelist) 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) { - client.sendMsg(cvm.guacEncode('chat', '', 'You need to login to do that.')); - return; - } - - if (this.voteCooldown !== 0) { - client.sendMsg(cvm.guacEncode('vote', '3', this.voteCooldown.toString())); - return; - } - this.startVote(); - this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', `${client.username} has started a vote to reset the VM.`))); - } - if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.vote) { - client.sendMsg(cvm.guacEncode('chat', '', 'You need to login to do that.')); - return; - } else if (client.IP.vote !== true) { - this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', `${client.username} has voted yes.`))); - } - 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) { - client.sendMsg(cvm.guacEncode('chat', '', 'You need to login to do that.')); - return; - } - if (client.IP.vote !== false) { - this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', `${client.username} has voted no.`))); - } - client.IP.vote = false; - break; - } - this.sendVoteUpdate(); - break; - case 'cap': { - if (msgArr.length < 2) return; - // Capabilities can only be announced before connecting to the VM - if (client.connectedToNode) return; - var caps = []; - 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; - } - } - client.sendMsg(cvm.guacEncode('cap', ...caps)); + 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.sendMsg(cvm.guacEncode('admin', '0', '1')); + } else if (user.rank === Rank.Moderator) { + user.sendMsg(cvm.guacEncode('admin', '0', '3', this.ModPerms.toString())); } - case 'admin': - if (msgArr.length < 2) return; - switch (msgArr[1]) { - case '2': - // Login + this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('adduser', '1', user.username!, user.rank.toString()))); + } else { + user.protocol.sendLoginResponse(false, res.error!); + if (res.error === 'You are banned') { + user.kick(); + } + } + } catch (err) { + this.logger.error(`Error authenticating client ${user.IP.address}: ${(err as Error).message}`); - 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(); + user.protocol.sendLoginResponse(false, 'There was an internal error while authenticating. Please let a staff member know as soon as possible'); + } + } - if (this.Config.collabvm.turnwhitelist && pwdHash === this.Config.collabvm.turnpass) { - client.turnWhitelist = true; - client.sendMsg(cvm.guacEncode('chat', '', 'You may now take turns.')); - return; - } + onNoFlag(user: User) { + // Too late + if (user.connectedToNode) return; + user.noFlag = true; + } - if (this.Config.auth.enabled) { - client.sendMsg(cvm.guacEncode('chat', '', 'This server does not support staff passwords. Please log in to become staff.')); - return; - } + onCapabilityUpgrade(user: User, capability: String[]): boolean { + if (user.connectedToNode) return false; - if (pwdHash === this.Config.collabvm.adminpass) { - client.rank = Rank.Admin; - client.sendMsg(cvm.guacEncode('admin', '0', '1')); - } else if (this.Config.collabvm.moderatorEnabled && pwdHash === this.Config.collabvm.modpass) { - client.rank = Rank.Moderator; - client.sendMsg(cvm.guacEncode('admin', '0', '3', this.ModPerms.toString())); - } else { - client.sendMsg(cvm.guacEncode('admin', '0', '0')); - return; - } - if (this.screenHidden) { - await this.SendFullScreenWithSize(client); + for (let cap of capability) { + switch (cap) { + // binary 1.0 (msgpack rects) + // TODO: re-enable once binary1.0 is enabled + case 'bin': + this.logger.info('Binary 1.0 protocol is currently disabled for refactoring'); + //user.Capabilities.bin = true; + //user.protocol = TheProtocolManager.createProtocol('binary1', user); + //user.protocol.setHandler(this as IProtocolHandlers); + break; + default: + break; + } + } + return true; + } - client.sendMsg(cvm.guacEncode('sync', Date.now().toString())); - } + onTurnRequest(user: User, forfeit: boolean): void { + if ((!this.turnsAllowed || this.Config.collabvm.turnwhitelist) && user.rank !== Rank.Admin && user.rank !== Rank.Moderator && !user.turnWhitelist) return; - this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('adduser', '1', client.username!, client.rank.toString()))); - break; - case '5': - // QEMU Monitor - if (client.rank !== Rank.Admin) return; - if (msgArr.length !== 4 || msgArr[2] !== this.Config.collabvm.node) return; - let output = await this.VM.MonitorCommand(msgArr[3]); - client.sendMsg(cvm.guacEncode('admin', '2', String(output))); - break; - case '8': - // Restore - if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.restore)) return; - this.VM.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; - this.logger.info(`Banning ${user.username!} (${user.IP.address}) by request of ${client.username!}`); - user.ban(this.banmgr); - 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) { - client.sendMsg(cvm.guacEncode('chat', '', 'Cannot rename users on a server that uses authentication.')); - } - if (msgArr.length !== 4) return; - var user = this.clients.find((c) => c.username === msgArr[2]); - if (!user) return; - this.renameUser(user, msgArr[3]); - break; - case '19': - // Get IP - if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.grabip)) return; - if (msgArr.length !== 3) return; - var user = this.clients.find((c) => c.username === msgArr[2]); - if (!user) return; - client.sendMsg(cvm.guacEncode('admin', '19', msgArr[2], user.IP.address)); - 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: - this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', client.username!, msgArr[2]))); + if (!this.authCheck(user, this.Config.auth.guestPermissions.turn)) return; - this.ChatHistory.push({ user: client.username!, msg: msgArr[2] }); - break; - case Rank.Moderator: - this.clients.filter((c) => c.rank !== Rank.Admin).forEach((c) => c.sendMsg(cvm.guacEncode('chat', client.username!, msgArr[2]))); + if (!user.TurnRateLimit.request()) return; + if (!user.connectedToNode) return; + + if (forfeit == false) { + var currentQueue = this.TurnQueue.toArray(); + // If the user is already in the turn queue, ignore the turn request. + if (currentQueue.indexOf(user) !== -1) return; + // If they're muted, also ignore the turn request. + // Send them the turn queue to prevent client glitches + if (user.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((otheruser) => otheruser.IP.address == user.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; + } + this.TurnQueue.enqueue(user); + if (this.TurnQueue.size === 1) this.nextTurn(); + } else { + var hadturn = this.TurnQueue.peek() === user; + this.TurnQueue = Queue.from(this.TurnQueue.toArray().filter((u) => u !== user)); + if (hadturn) this.nextTurn(); + } + this.sendTurnUpdate(); + } + + onVote(user: User, choice: number): void { + if (!this.VM.SnapshotsSupported()) return; + if ((!this.turnsAllowed || this.Config.collabvm.turnwhitelist) && user.rank !== Rank.Admin && user.rank !== Rank.Moderator && !user.turnWhitelist) return; + if (!user.connectedToNode) return; + if (!user.VoteRateLimit.request()) return; + switch (choice) { + case 1: + if (!this.voteInProgress) { + if (!this.authCheck(user, this.Config.auth.guestPermissions.callForReset)) return; + + if (this.voteCooldown !== 0) { + user.sendMsg(cvm.guacEncode('vote', '3', this.voteCooldown.toString())); + return; + } + this.startVote(); + this.clients.forEach((c) => c.protocol.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.`)); + } + user.IP.vote = true; + break; + case 0: + if (!this.voteInProgress) return; + + 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.`)); + } + user.IP.vote = false; + break; + default: + break; + } + this.sendVoteUpdate(); + } + + async onList(user: User) { + let listEntry: ListEntry = { + id: this.Config.collabvm.node, + name: this.Config.collabvm.displayname, + thumbnail: this.screenHidden ? this.screenHiddenThumb : await this.getThumbnail() + }; + + if (this.VM.GetState() == VMState.Started) { + user.protocol.sendListResponse([listEntry]); + } + } + + private async connectViewShared(user: User, node: string, viewMode: number | undefined) { + if (!user.username || node !== this.Config.collabvm.node) { + user.protocol.sendConnectFailResponse(); + return; + } + + user.connectedToNode = true; + + if (viewMode !== undefined) { + if (viewMode !== 0 && viewMode !== 1) { + user.protocol.sendConnectFailResponse(); + return; + } + + user.viewMode = viewMode; + } + + user.protocol.sendConnectOKResponse(this.VM.SnapshotsSupported()); + + if (this.ChatHistory.size !== 0) { + let history = this.ChatHistory.toArray() as ChatHistory[]; + user.protocol.sendChatHistoryMessage(history); + } + if (this.Config.collabvm.motd) user.protocol.sendChatMessage('', this.Config.collabvm.motd); + if (this.screenHidden) { + user?.protocol.sendScreenResize(1024, 768); + user?.protocol.sendScreenUpdate({ + x: 0, + y: 0, + data: this.screenHiddenImg + }); + } else { + await this.SendFullScreenWithSize(user); + } + + user.protocol.sendSync(Date.now()); + + if (this.voteInProgress) this.sendVoteUpdate(user); + this.sendTurnUpdate(user); + } + + async onConnect(user: User, node: string) { + return this.connectViewShared(user, node, undefined); + } + + async onView(user: User, node: string, viewMode: number) { + return this.connectViewShared(user, node, viewMode); + } + + onRename(user: User, newName: string | undefined): void { + 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.'); + 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.rank !== Rank.Unregistered) return; + this.renameUser(user, undefined); + return; + } + this.renameUser(user, newName!); + } + + onChat(user: User, message: string): void { + if (!user.username) return; + if (user.IP.muted) return; + if (!this.authCheck(user, this.Config.auth.guestPermissions.chat)) return; + + var msg = Utilities.HTMLSanitize(message); + // 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; + + this.clients.forEach((c) => c.protocol.sendChatMessage(user.username!, msg)); + this.ChatHistory.push({ user: user.username, msg: msg }); + user.onChatMsgSent(); + } + + onKey(user: User, keysym: number, pressed: boolean): void { + if (this.TurnQueue.peek() !== user && user.rank !== Rank.Admin) return; + this.VM.GetDisplay()?.KeyboardEvent(keysym, pressed); + } + + onMouse(user: User, x: number, y: number, buttonMask: number): void { + if (this.TurnQueue.peek() !== user && user.rank !== Rank.Admin) return; + this.VM.GetDisplay()?.MouseEvent(x, y, buttonMask); + } + + async onAdminLogin(user: User, password: string) { + if (!user.LoginRateLimit.request() || !user.username) return; + var sha256 = createHash('sha256'); + sha256.update(password, 'utf-8'); + var pwdHash = sha256.digest('hex'); + sha256.destroy(); + + if (this.Config.collabvm.turnwhitelist && pwdHash === this.Config.collabvm.turnpass) { + user.turnWhitelist = true; + user.protocol.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.'); + return; + } + + if (pwdHash === this.Config.collabvm.adminpass) { + user.rank = Rank.Admin; + user.sendMsg(cvm.guacEncode('admin', '0', '1')); + } else if (this.Config.collabvm.moderatorEnabled && pwdHash === this.Config.collabvm.modpass) { + user.rank = Rank.Moderator; + user.sendMsg(cvm.guacEncode('admin', '0', '3', this.ModPerms.toString())); + } else { + user.sendMsg(cvm.guacEncode('admin', '0', '0')); + return; + } + + if (this.screenHidden) { + await this.SendFullScreenWithSize(user); + } + + this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('adduser', '1', user.username!, user.rank.toString()))); + } + + async onAdminMonitor(user: User, node: string, command: string) { + if (user.rank !== Rank.Admin) return; + if (node !== this.Config.collabvm.node) return; + let output = await this.VM.MonitorCommand(command); + user.sendMsg(cvm.guacEncode('admin', '2', String(output))); + } + + private async onAdmin(user: User, msgArr: string[]) { + /* + switch (msgArr[0]) { + case '2': + // Login + + + break; + case '5': + // QEMU Monitor + + break; + case '8': + // Restore + if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.restore)) return; + this.VM.Reset(); + break; + case '10': + // Reboot + if (user.rank !== Rank.Admin && (user.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 (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.ban)) return; + var otherUser = this.clients.find((c) => c.username === msgArr[2]); + if (!otherUser) return; + this.logger.info(`Banning ${otherUser.username!} (${otherUser.IP.address}) by request of ${otherUser.username!}`); + user.ban(this.banmgr); + case '13': + // Force Vote + if (msgArr.length !== 3) return; + if (user.rank !== Rank.Admin && (user.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) { + client.protocol.sendChatMessage('', 'Cannot rename users on a server that uses authentication.'); + } + if (msgArr.length !== 4) return; + var user = this.clients.find((c) => c.username === msgArr[2]); + if (!user) return; + this.renameUser(user, msgArr[3]); + break; + case '19': + // Get IP + if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.grabip)) return; + if (msgArr.length !== 3) return; + var user = this.clients.find((c) => c.username === msgArr[2]); + if (!user) return; + client.sendMsg(cvm.guacEncode('admin', '19', msgArr[2], user.IP.address)); + 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: + this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', client.username!, msgArr[2]))); + + this.ChatHistory.push({ user: client.username!, msg: msgArr[2] }); + break; + case Rank.Moderator: + this.clients.filter((c) => c.rank !== Rank.Admin).forEach((c) => c.sendMsg(cvm.guacEncode('chat', client.username!, msgArr[2]))); this.clients.filter((c) => c.rank === Rank.Admin).forEach((c) => c.sendMsg(cvm.guacEncode('chat', client.username!, Utilities.HTMLSanitize(msgArr[2])))); break; @@ -680,34 +698,29 @@ export default class CollabVMServer { break; case '1': this.screenHidden = false; - let displaySize = this.VM.GetDisplay()?.Size(); - if (displaySize == undefined) return; + let displaySize = this.VM.GetDisplay().Size(); - let encoded = await this.MakeRectData({ - x: 0, - y: 0, - width: displaySize.width, - height: displaySize.height - }); + let encoded = await this.MakeRectData({ + x: 0, + y: 0, + width: displaySize.width, + height: displaySize.height + }); - this.clients.forEach(async (client) => this.SendFullScreenWithSize(client)); - break; - } - break; - case '25': - if (client.rank !== Rank.Admin || msgArr.length !== 3) return; - this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', msgArr[2]))); - break; - } - break; - } - } catch (err) { - // No - this.logger.error(`User ${user?.IP.address} ${user?.username ? `with username ${user?.username}` : ''} sent broken Guacamole: ${err as Error}`); - user?.kick(); + this.clients.forEach(async (client) => this.SendFullScreenWithSize(client)); + break; + } + break; + case '25': + if (client.rank !== Rank.Admin || msgArr.length !== 3) return; + this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', msgArr[2]))); + break; } + */ } + // end IProtocolHandlers + getUsernameList(): string[] { var arr: string[] = []; @@ -757,11 +770,15 @@ export default class CollabVMServer { } } - 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())); - return cvm.guacEncode(...arr); + getAdduserMsg(): ProtocolAddUser[] { + return this.clients + .filter((c) => c.username) + .map((c) => { + return { + username: c.username!, + rank: c.rank + }; + }); } getFlagMsg(): string { @@ -772,12 +789,6 @@ export default class CollabVMServer { return cvm.guacEncode(...arr); } - getChatHistoryMsg(): string { - var arr: string[] = ['chat']; - this.ChatHistory.forEach((c) => arr.push(c.user, c.msg)); - return cvm.guacEncode(...arr); - } - private sendTurnUpdate(client?: User) { var turnQueueArr = this.TurnQueue.toArray(); var turntime; @@ -852,7 +863,7 @@ export default class CollabVMServer { .filter((c) => c.connectedToNode || c.viewMode == 1) .forEach((c) => { if (this.screenHidden && c.rank == Rank.Unregistered) return; - c.sendMsg(cvm.guacEncode('size', '0', size.width.toString(), size.height.toString())); + c.protocol.sendScreenResize(size.width, size.height); }); } @@ -861,28 +872,17 @@ export default class CollabVMServer { let doRect = async (rect: Rect) => { let encoded = await this.MakeRectData(rect); - let encodedb64 = encoded.toString('base64'); - let bmsg: CollabVMProtocolMessage = { - type: CollabVMProtocolMessageType.rect, - rect: { - x: rect.x, - y: rect.y, - data: encoded - } - }; - - var encodedbin = msgpack.encode(bmsg); self.clients .filter((c) => c.connectedToNode || c.viewMode == 1) .forEach((c) => { if (self.screenHidden && c.rank == Rank.Unregistered) return; - 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())); - } + + c.protocol.sendScreenUpdate({ + x: rect.x, + y: rect.y, + data: encoded + }); }); }; @@ -908,21 +908,13 @@ export default class CollabVMServer { height: displaySize.height }); - client.sendMsg(cvm.guacEncode('size', '0', displaySize.width.toString(), displaySize.height.toString())); + client.protocol.sendScreenResize(displaySize.width, displaySize.height); - if (client.Capabilities.bin) { - let msg: CollabVMProtocolMessage = { - type: CollabVMProtocolMessageType.rect, - rect: { - x: 0, - y: 0, - data: encoded - } - }; - client.socket.sendBinary(msgpack.encode(msg)); - } else { - client.sendMsg(cvm.guacEncode('png', '0', '0', '0', '0', encoded.toString('base64'))); - } + client.protocol.sendScreenUpdate({ + x: 0, + y: 0, + data: encoded + }); } private async MakeRectData(rect: Rect) { @@ -937,14 +929,13 @@ export default class CollabVMServer { return encoded; } - async getThumbnail(): Promise { + async getThumbnail(): Promise { let display = this.VM.GetDisplay(); // oh well - if (!display?.Connected()) return ''; + if (!display?.Connected()) return Buffer.alloc(4); - let buf = await JPEGEncoder.EncodeThumbnail(display.Buffer(), display.Size()); - return buf.toString('base64'); + return JPEGEncoder.EncodeThumbnail(display.Buffer(), display.Size()); } startVote() { @@ -967,10 +958,10 @@ export default class CollabVMServer { var count = this.getVoteCounts(); this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('vote', '2'))); if (result === true || (result === undefined && count.yes >= count.no)) { - this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', 'The vote to reset the VM has won.'))); + this.clients.forEach((c) => c.protocol.sendChatMessage('', 'The vote to reset the VM has won.')); this.VM.Reset(); } else { - this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', 'The vote to reset the VM has lost.'))); + this.clients.forEach((c) => c.protocol.sendChatMessage('', 'The vote to reset the VM has lost.')); } this.clients.forEach((c) => { c.IP.vote = null; diff --git a/cvmts/src/GuacamoleProtocol.ts b/cvmts/src/GuacamoleProtocol.ts new file mode 100644 index 0000000..9fddc0a --- /dev/null +++ b/cvmts/src/GuacamoleProtocol.ts @@ -0,0 +1,282 @@ +import pino from 'pino'; +import { IProtocol, IProtocolHandlers, ListEntry, ProtocolAddUser, ProtocolChatHistory, ScreenRect } from './Protocol'; +import { User } from './User'; + +import * as cvm from '@cvmts/cvm-rs'; + +// CollabVM protocol implementation for Guacamole. +export class GuacamoleProtocol implements IProtocol { + private handlers: IProtocolHandlers | null = null; + private logger = pino({ + name: 'CVMTS.GuacamoleProtocol' + }); + + private user: User | null = null; + + init(u: User): void { + this.user = u; + } + + setHandler(handlers: IProtocolHandlers): void { + this.handlers = handlers; + } + + private __processMessage_admin(decodedElements: string[]): boolean { + switch (decodedElements[1]) { + case '2': + // Login + if (decodedElements.length !== 3) return false; + this.handlers?.onAdminLogin(this.user!, decodedElements[2]); + break; + case '5': + // QEMU Monitor + if (decodedElements.length !== 4) return false; + // [2] node + // [3] cmd + break; + case '8': + // Restore + break; + case '10': + // Reboot + if (decodedElements.length !== 3) return false; + // [2] - node + break; + case '12': + // Ban + + case '13': + // Force Vote + if (decodedElements.length !== 3) return false; + + break; + case '14': + // Mute + if (decodedElements.length !== 4) return false; + break; + case '15': + // Kick + case '16': + // End turn + if (decodedElements.length !== 3) return false; + break; + case '17': + // Clear turn queue + if (decodedElements.length !== 3) return false; + // [2] - node + break; + case '18': + // Rename user + if (decodedElements.length !== 4) return false; + + // [2] - username + // [3] - new username + break; + case '19': + // Get IP + if (decodedElements.length !== 3) return false; + break; + case '20': + // Steal turn + break; + case '21': + // XSS + if (decodedElements.length !== 3) return false; + // [2] message + break; + case '22': + // Toggle turns + if (decodedElements.length !== 3) return false; + // [2] 0 == disable 1 == enable + break; + case '23': + // Indefinite turn + break; + case '24': + // Hide screen + + if (decodedElements.length !== 3) return false; + // 0 - hide + // 1 - unhide + + break; + case '25': + if (decodedElements.length !== 3) return false; + // [2] + break; + } + return true; + } + + processMessage(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!); + break; + case 'cap': + if (decodedElements.length < 2) return false; + this.handlers?.onCapabilityUpgrade(this.user!, decodedElements.slice(1)); + break; + case 'login': + if (decodedElements.length !== 2) return false; + this.handlers?.onLogin(this.user!, decodedElements[1]); + break; + case 'noflag': + this.handlers?.onNoFlag(this.user!); + break; + case 'list': + this.handlers?.onList(this.user!); + break; + case 'connect': + if (decodedElements.length !== 2) return false; + this.handlers?.onConnect(this.user!, decodedElements[1]); + break; + case 'view': + { + if (decodedElements.length !== 3) return false; + let viewMode = parseInt(decodedElements[2]); + if (viewMode == undefined) return false; + + this.handlers?.onView(this.user!, decodedElements[1], viewMode); + } + break; + case 'rename': + this.handlers?.onRename(this.user!, decodedElements[1]); + break; + case 'chat': + if (decodedElements.length !== 2) return false; + this.handlers?.onChat(this.user!, decodedElements[1]); + break; + case 'turn': + let forfeit = false; + if (decodedElements.length > 2) return false; + if (decodedElements.length == 1) { + forfeit = false; + } else { + if (decodedElements[1] == '0') forfeit = true; + else if (decodedElements[1] == '1') forfeit = false; + } + + this.handlers?.onTurnRequest(this.user!, forfeit); + break; + case 'mouse': + if (decodedElements.length !== 4) return false; + + let x = parseInt(decodedElements[1]); + let y = parseInt(decodedElements[2]); + let mask = parseInt(decodedElements[3]); + if (x === undefined || y === undefined || mask === undefined) return false; + + this.handlers?.onMouse(this.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); + 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); + break; + + case 'admin': + if (decodedElements.length < 2) return false; + return this.__processMessage_admin(decodedElements); + } + + return true; + } + + // Senders + + sendAuth(authServer: string): void { + this.user?.sendMsg(cvm.guacEncode('auth', authServer)); + } + + sendNop(): void { + this.user?.sendMsg(cvm.guacEncode('nop')); + } + + sendSync(now: number): void { + this.user?.sendMsg(cvm.guacEncode('sync', now.toString())); + } + + sendConnectFailResponse(): void { + this.user?.sendMsg(cvm.guacEncode('connect', '0')); + } + + sendConnectOKResponse(votes: boolean): void { + this.user?.sendMsg(cvm.guacEncode('connect', '1', '1', votes ? '1' : '0', '0')); + } + + sendLoginResponse(ok: boolean, message: string | undefined): void { + if (ok) { + this.user?.sendMsg(cvm.guacEncode('login', '1')); + return; + } else { + this.user?.sendMsg(cvm.guacEncode('login', '0', message!)); + } + } + + sendChatMessage(username: string, message: string): void { + this.user?.sendMsg(cvm.guacEncode('chat', username, message)); + } + + sendChatHistoryMessage(history: ProtocolChatHistory[]): void { + let arr = ['chat']; + for (let a of history) { + arr.push(a.user); + arr.push(a.msg); + } + + this.user?.sendMsg(cvm.guacEncode(...arr)); + } + + sendAddUser(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)); + } + + sendRemUser(users: string[]): void { + let arr = ['remuser', users.length.toString()]; + + for (let user of users) { + arr.push(user); + } + + this.user?.sendMsg(cvm.guacEncode(...arr)); + } + + sendListResponse(list: ListEntry[]): void { + let arr = ['list']; + for (let node of list) { + arr.push(node.id); + arr.push(node.name); + arr.push(node.thumbnail.toString('base64')); + } + + this.user?.sendMsg(cvm.guacEncode(...arr)); + } + + sendScreenResize(width: number, height: number): void { + this.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()); + } +} diff --git a/cvmts/src/Protocol.ts b/cvmts/src/Protocol.ts new file mode 100644 index 0000000..10f5854 --- /dev/null +++ b/cvmts/src/Protocol.ts @@ -0,0 +1,119 @@ +import { Rank, User } from './User'; + +// We should probably put this in the binproto repository or something +enum UpgradeCapability { + Binary = 'bin' +} + +export interface ScreenRect { + x: number; + y: number; + data: Buffer; +} + +export interface ListEntry { + id: string; + name: string; + thumbnail: Buffer; +} + +export interface ProtocolChatHistory { + user: string; + msg: string; +} + +export interface ProtocolAddUser { + username: string; + rank: Rank; +} + +// Protocol handlers. This is implemented by a layer that wants to listen to CollabVM protocol messages. +export interface IProtocolHandlers { + onNop(user: User): void; + + onNoFlag(user: User): void; + + // Called when the client requests a capability upgrade + onCapabilityUpgrade(user: User, capability: Array): boolean; + + onLogin(user: User, token: string): void; + + // Called on turn request + onTurnRequest(user: User, forfeit: boolean): void; + + onVote(user: User, choice: number): void; + + onList(user: User): void; + onConnect(user: User, node: string): void; + onView(user: User, node: string, viewMode: number): void; + + onAdminLogin(user: User, password: string): void; + onAdminMonitor(user: User, node: string, command: string): void; + + onRename(user: User, newName: string|undefined): void; + onChat(user: User, message: string): void; + + onKey(user: User, keysym: number, pressed: boolean): void; + onMouse(user: User, x: number, y: number, buttonMask: number): void; +} + +// Abstracts away all of the CollabVM protocol details +export interface IProtocol { + init(u: User): void; + + // Sets handler object. + setHandler(handlers: IProtocolHandlers): void; + + // Parses a single CollabVM protocol message and fires the given handler. + // This function does not catch any thrown errors; it is the caller's responsibility + // to handle errors. It should, however, catch invalid parameters without failing. + processMessage(buffer: Buffer): boolean; + + // Senders + + sendNop(): void; + sendSync(now: number): void; + + sendAuth(authServer: string): void; + + sendConnectFailResponse(): void; + sendConnectOKResponse(votes: boolean): void; + + sendLoginResponse(ok: boolean, message: string | undefined): void; + + sendChatMessage(username: '' | string, message: string): void; + sendChatHistoryMessage(history: ProtocolChatHistory[]): void; + + sendAddUser(users: ProtocolAddUser[]): void; + sendRemUser(users: string[]): void; + + sendListResponse(list: ListEntry[]): void; + + sendScreenResize(width: number, height: number): void; + + // Sends a rectangle update to the user. + sendScreenUpdate(rect: ScreenRect): void; +} + +// Holds protocol factories. +export class ProtocolManager { + private protocols = new Map IProtocol>(); + + // Registers a protocol with the given name. + registerProtocol(name: string, protocolFactory: () => IProtocol) { + 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); + return proto; + } +} + +/// Global protocol manager +export let TheProtocolManager = new ProtocolManager(); diff --git a/cvmts/src/TCP/TCPServer.ts b/cvmts/src/TCP/TCPServer.ts index 8a8158b..04ec833 100644 --- a/cvmts/src/TCP/TCPServer.ts +++ b/cvmts/src/TCP/TCPServer.ts @@ -1,3 +1,6 @@ +// TODO: replace tcp protocol with smth like +// struct msg { beu32 len; char data[len] } +// (along with a length cap obviously) import EventEmitter from 'events'; import NetworkServer from '../NetworkServer.js'; import { Server, Socket } from 'net'; diff --git a/cvmts/src/User.ts b/cvmts/src/User.ts index 264ed1f..0804f68 100644 --- a/cvmts/src/User.ts +++ b/cvmts/src/User.ts @@ -8,6 +8,7 @@ import NetworkClient from './NetworkClient.js'; import { CollabVMCapabilities } from '@cvmts/collab-vm-1.2-binary-protocol'; import pino from 'pino'; import { BanManager } from './BanManager.js'; +import { IProtocol, TheProtocolManager } from './Protocol.js'; export class User { socket: NetworkClient; @@ -22,6 +23,7 @@ export class User { Config: IConfig; IP: IPData; Capabilities: CollabVMCapabilities; + protocol: IProtocol; turnWhitelist: boolean = false; // Hide flag. Only takes effect if the user is logged in. noFlag: boolean = false; @@ -44,6 +46,9 @@ export class User { this.msgsSent = 0; this.Capabilities = new CollabVMCapabilities(); + // All clients default to the Guacamole protocol. + this.protocol = TheProtocolManager.createProtocol('guacamole', this); + this.socket.on('disconnect', () => { // Unref the ip data for this connection this.IP.Unref(); @@ -52,11 +57,6 @@ export class User { clearInterval(this.msgRecieveInterval); }); - this.socket.on('msg', (e) => { - clearTimeout(this.nopRecieveTimeout); - clearInterval(this.msgRecieveInterval); - this.msgRecieveInterval = setInterval(() => this.onNoMsg(), 10000); - }); this.nopSendInterval = setInterval(() => this.sendNop(), 5000); this.msgRecieveInterval = setInterval(() => this.onNoMsg(), 10000); @@ -84,8 +84,14 @@ export class User { return username; } + onNop() { + clearTimeout(this.nopRecieveTimeout); + clearInterval(this.msgRecieveInterval); + this.msgRecieveInterval = setInterval(() => this.onNoMsg(), 10000); + } + sendNop() { - this.socket.send('3.nop;'); + this.protocol.sendNop(); } sendMsg(msg: string) { @@ -107,7 +113,7 @@ export class User { this.socket.close(); } - onMsgSent() { + onChatMsgSent() { if (!this.Config.collabvm.automute.enabled) return; // rate limit guest and unregistered chat messages, but not staff ones switch (this.rank) { @@ -153,5 +159,5 @@ export enum Rank { // After all these years Registered = 1, Admin = 2, - Moderator = 3, + Moderator = 3 } diff --git a/cvmts/src/index.ts b/cvmts/src/index.ts index 147bf1a..1769dd7 100644 --- a/cvmts/src/index.ts +++ b/cvmts/src/index.ts @@ -16,6 +16,8 @@ import pino from 'pino'; import { Database } from './Database.js'; import { BanManager } from './BanManager.js'; import { QemuVMShim } from './vm/qemu.js'; +import { TheProtocolManager } from './Protocol.js'; +import { GuacamoleProtocol } from './GuacamoleProtocol.js'; let logger = pino(); @@ -97,17 +99,20 @@ async function start() { process.on('SIGINT', async () => await stop()); process.on('SIGTERM', async () => await stop()); + // Register protocol(s) + TheProtocolManager.registerProtocol("guacamole", () => new GuacamoleProtocol); + await VM.Start(); // Start up the server var CVM = new CollabVMServer(Config, VM, banmgr, auth, geoipReader); var WS = new WSServer(Config, banmgr); - WS.on('connect', (client: User) => CVM.addUser(client)); + WS.on('connect', (client: User) => CVM.connectionOpened(client)); WS.start(); if (Config.tcp.enabled) { var TCP = new TCPServer(Config, banmgr); - TCP.on('connect', (client: User) => CVM.addUser(client)); + TCP.on('connect', (client: User) => CVM.connectionOpened(client)); TCP.start(); } } From a82388f82385dc4eb1e19acf6b450c06754e9ea1 Mon Sep 17 00:00:00 2001 From: modeco80 Date: Wed, 21 Aug 2024 20:15:14 -0400 Subject: [PATCH 02/20] handle admin messages TODO: Add senders for admin responses and flag and rename also verify I didn't fuck boolean conversion up --- cvmts/src/CollabVMServer.ts | 328 +++++++++++++++------------------ cvmts/src/GuacamoleProtocol.ts | 89 +++++---- cvmts/src/Protocol.ts | 19 +- 3 files changed, 219 insertions(+), 217 deletions(-) diff --git a/cvmts/src/CollabVMServer.ts b/cvmts/src/CollabVMServer.ts index 1485ce7..5367eda 100644 --- a/cvmts/src/CollabVMServer.ts +++ b/cvmts/src/CollabVMServer.ts @@ -341,9 +341,8 @@ export default class CollabVMServer implements IProtocolHandlers { this.TurnQueue.enqueue(user); if (this.TurnQueue.size === 1) this.nextTurn(); } else { - var hadturn = this.TurnQueue.peek() === user; - this.TurnQueue = Queue.from(this.TurnQueue.toArray().filter((u) => u !== user)); - if (hadturn) this.nextTurn(); + // Not sure why this wasn't using this before + this.endTurn(user); } this.sendTurnUpdate(); } @@ -492,6 +491,8 @@ export default class CollabVMServer implements IProtocolHandlers { this.VM.GetDisplay()?.MouseEvent(x, y, buttonMask); } + // TODO: make senders for admin things + async onAdminLogin(user: User, password: string) { if (!user.LoginRateLimit.request() || !user.username) return; var sha256 = createHash('sha256'); @@ -535,188 +536,159 @@ export default class CollabVMServer implements IProtocolHandlers { user.sendMsg(cvm.guacEncode('admin', '2', String(output))); } - private async onAdmin(user: User, msgArr: string[]) { - /* - switch (msgArr[0]) { - case '2': - // Login + onAdminRestore(user: User, node: string): void { + if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.restore)) return; + this.VM.Reset(); + } - - break; - case '5': - // QEMU Monitor - - break; - case '8': - // Restore - if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.restore)) return; - this.VM.Reset(); - break; - case '10': - // Reboot - if (user.rank !== Rank.Admin && (user.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 (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.ban)) return; - var otherUser = this.clients.find((c) => c.username === msgArr[2]); - if (!otherUser) return; - this.logger.info(`Banning ${otherUser.username!} (${otherUser.IP.address}) by request of ${otherUser.username!}`); - user.ban(this.banmgr); - case '13': - // Force Vote - if (msgArr.length !== 3) return; - if (user.rank !== Rank.Admin && (user.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) { - client.protocol.sendChatMessage('', 'Cannot rename users on a server that uses authentication.'); - } - if (msgArr.length !== 4) return; - var user = this.clients.find((c) => c.username === msgArr[2]); - if (!user) return; - this.renameUser(user, msgArr[3]); - break; - case '19': - // Get IP - if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.grabip)) return; - if (msgArr.length !== 3) return; - var user = this.clients.find((c) => c.username === msgArr[2]); - if (!user) return; - client.sendMsg(cvm.guacEncode('admin', '19', msgArr[2], user.IP.address)); - 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: - this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', client.username!, msgArr[2]))); + async onAdminReboot(user: User, node: string) { + if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.reboot)) return; + if (node !== this.Config.collabvm.node) return; + await this.VM.Reboot(); + } - this.ChatHistory.push({ user: client.username!, msg: msgArr[2] }); - break; - case Rank.Moderator: - this.clients.filter((c) => c.rank !== Rank.Admin).forEach((c) => c.sendMsg(cvm.guacEncode('chat', client.username!, msgArr[2]))); + onAdminBanUser(user: User, username: string): void { + // Ban + if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.ban)) return; + let otherUser = this.clients.find((c) => c.username === username); + if (!otherUser) return; + this.logger.info(`Banning ${otherUser.username!} (${otherUser.IP.address}) by request of ${otherUser.username!}`); + user.ban(this.banmgr); + } - this.clients.filter((c) => c.rank === Rank.Admin).forEach((c) => c.sendMsg(cvm.guacEncode('chat', client.username!, Utilities.HTMLSanitize(msgArr[2])))); - 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) => { - 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())); - }); - break; - case '1': - this.screenHidden = false; - let displaySize = this.VM.GetDisplay().Size(); + onAdminForceVote(user: User, choice: number): void { + if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.forcevote)) return; + if (!this.voteInProgress) return; + this.endVote(choice == 1); + } - let encoded = await this.MakeRectData({ - x: 0, - y: 0, - width: displaySize.width, - height: displaySize.height - }); + onAdminMuteUser(user: User, username: string, temporary: boolean): void { + if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.mute)) return; - this.clients.forEach(async (client) => this.SendFullScreenWithSize(client)); - break; - } + let target = this.clients.find((c) => c.username === username); + if (!target) return; + target.mute(!temporary); + } + + onAdminKickUser(user: User, username: string): void { + if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.kick)) return; + var target = this.clients.find((c) => c.username === username); + if (!target) return; + target.kick(); + } + + onAdminEndTurn(user: User, username: string): void { + if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.bypassturn)) return; + + var target = this.clients.find((c) => c.username === username); + if (!target) return; + + // don't let a mod end a infinite turn + if(user.rank == Rank.Moderator) + if(this.indefiniteTurn == target) + return; + + this.endTurn(target); + } + + onAdminClearQueue(user: User, node: string): void { + if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.bypassturn)) return; + if (node !== this.Config.collabvm.node) return; + this.clearTurns(); + } + + 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.'); + } + var targetUser = this.clients.find((c) => c.username === target); + if (!targetUser) return; + this.renameUser(targetUser, newName); + } + + onAdminGetIP(user: User, username: string): void { + 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.sendMsg(cvm.guacEncode('admin', '19', username, target.IP.address)); + } + + onAdminBypassTurn(user: User): void { + if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.bypassturn)) return; + this.bypassTurn(user); + } + + onAdminRawMessage(user: User, message: string): void { + 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.ChatHistory.push({ user: user.username!, msg: message }); break; - case '25': - if (client.rank !== Rank.Admin || msgArr.length !== 3) return; - this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', msgArr[2]))); + 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.protocol.sendChatMessage(user.username!, Utilities.HTMLSanitize(message))); break; } - */ + } + + onAdminToggleTurns(user: User, enabled: boolean): void { + if (user.rank !== Rank.Admin) return; + if (enabled) { + this.turnsAllowed = true; + } else { + this.turnsAllowed = false; + this.clearTurns(); + } + } + + onAdminIndefiniteTurn(user: User): void { + if (user.rank !== Rank.Admin) return; + this.indefiniteTurn = user; + this.TurnQueue = Queue.from([user, ...this.TurnQueue.toArray().filter((c) => c !== user)]); + this.sendTurnUpdate(); + } + + async onAdminHideScreen(user: User, show: boolean) { + if (user.rank !== Rank.Admin) return; + if (show) { + // if(!this.screenHidden) return; ? + + this.screenHidden = false; + let displaySize = this.VM.GetDisplay()?.Size(); + + if(displaySize == undefined) + return; + + let encoded = await this.MakeRectData({ + x: 0, + y: 0, + width: displaySize.width, + height: displaySize.height + }); + + this.clients.forEach(async (client) => this.SendFullScreenWithSize(client)); + } else { + this.screenHidden = true; + this.clients + .filter((c) => c.rank == Rank.Unregistered) + .forEach((client) => { + client.protocol.sendScreenResize(1024, 768); + client.protocol.sendScreenUpdate({ + x: 0, + y: 0, + data: this.screenHiddenImg + }); + }); + } + } + + onAdminSystemMessage(user: User, message: string): void { + if (user.rank !== Rank.Admin) return; + this.clients.forEach((c) => c.protocol.sendChatMessage('', message)); } // end IProtocolHandlers @@ -839,6 +811,8 @@ export default class CollabVMServer implements IProtocolHandlers { } endTurn(client: User) { + // I must have somehow accidentally removed this while scalpaling everything out + if (this.indefiniteTurn === client) this.indefiniteTurn = null; var hasTurn = this.TurnQueue.peek() === client; this.TurnQueue = Queue.from(this.TurnQueue.toArray().filter((c) => c !== client)); if (hasTurn) this.nextTurn(); diff --git a/cvmts/src/GuacamoleProtocol.ts b/cvmts/src/GuacamoleProtocol.ts index 9fddc0a..258c863 100644 --- a/cvmts/src/GuacamoleProtocol.ts +++ b/cvmts/src/GuacamoleProtocol.ts @@ -24,85 +24,96 @@ export class GuacamoleProtocol implements IProtocol { private __processMessage_admin(decodedElements: string[]): boolean { switch (decodedElements[1]) { case '2': - // Login if (decodedElements.length !== 3) return false; this.handlers?.onAdminLogin(this.user!, decodedElements[2]); break; case '5': - // QEMU Monitor if (decodedElements.length !== 4) return false; - // [2] node - // [3] cmd + this.handlers?.onAdminMonitor(this.user!, decodedElements[2], decodedElements[3]); break; case '8': - // Restore + if (decodedElements.length !== 3) return false; + this.handlers?.onAdminRestore(this.user!, decodedElements[2]); break; case '10': - // Reboot if (decodedElements.length !== 3) return false; - // [2] - node + this.handlers?.onAdminReboot(this.user!, decodedElements[2]); break; case '12': - // Ban - + if (decodedElements.length < 3) return false; + this.handlers?.onAdminBanUser(this.user!, decodedElements[2]); case '13': - // Force Vote - if (decodedElements.length !== 3) return false; - + { + if (decodedElements.length !== 3) return false; + let choice = parseInt(decodedElements[2]); + if (choice == undefined) return false; + this.handlers?.onAdminForceVote(this.user!, choice); + } break; case '14': - // Mute - if (decodedElements.length !== 4) return false; + { + if (decodedElements.length !== 4) return false; + let temporary = true; + if (decodedElements[3] == '0') temporary = true; + else if (decodedElements[3] == '1') temporary = false; + else return false; + this.handlers?.onAdminMuteUser(this.user!, decodedElements[2], temporary); + } break; case '15': - // Kick - case '16': - // End turn if (decodedElements.length !== 3) return false; + this.handlers?.onAdminKickUser(this.user!, decodedElements[2]); + break; + case '16': + if (decodedElements.length !== 3) return false; + this.handlers?.onAdminEndTurn(this.user!, decodedElements[2]); break; case '17': - // Clear turn queue if (decodedElements.length !== 3) return false; - // [2] - node + this.handlers?.onAdminClearQueue(this.user!, decodedElements[2]); break; case '18': - // Rename user if (decodedElements.length !== 4) return false; - - // [2] - username - // [3] - new username + this.handlers?.onAdminRename(this.user!, decodedElements[2], decodedElements[3]); break; case '19': - // Get IP if (decodedElements.length !== 3) return false; + this.handlers?.onAdminGetIP(this.user!, decodedElements[2]); break; case '20': - // Steal turn + this.handlers?.onAdminBypassTurn(this.user!); break; case '21': - // XSS if (decodedElements.length !== 3) return false; - // [2] message + this.handlers?.onAdminRawMessage(this.user!, decodedElements[2]); break; case '22': - // Toggle turns - if (decodedElements.length !== 3) return false; - // [2] 0 == disable 1 == enable + { + // Toggle turns + if (decodedElements.length !== 3) return false; + let enabled = true; + if (decodedElements[2] == '0') enabled = false; + else if (decodedElements[2] == '1') enabled = true; + else return false; + this.handlers?.onAdminToggleTurns(this.user!, enabled); + } break; case '23': - // Indefinite turn + this.handlers?.onAdminIndefiniteTurn(this.user!); break; case '24': - // Hide screen - - if (decodedElements.length !== 3) return false; - // 0 - hide - // 1 - unhide - + { + if (decodedElements.length !== 3) return false; + let show = true; + if (decodedElements[2] == '0') show = false; + else if (decodedElements[2] == '1') show = true; + else return false; + this.handlers?.onAdminHideScreen(this.user!, show); + } break; case '25': if (decodedElements.length !== 3) return false; - // [2] + this.handlers?.onAdminSystemMessage(this.user!, decodedElements[2]); break; } return true; @@ -155,7 +166,7 @@ export class GuacamoleProtocol implements IProtocol { let forfeit = false; if (decodedElements.length > 2) return false; if (decodedElements.length == 1) { - forfeit = false; + forfeit = false; } else { if (decodedElements[1] == '0') forfeit = true; else if (decodedElements[1] == '1') forfeit = false; diff --git a/cvmts/src/Protocol.ts b/cvmts/src/Protocol.ts index 10f5854..1883772 100644 --- a/cvmts/src/Protocol.ts +++ b/cvmts/src/Protocol.ts @@ -47,10 +47,27 @@ export interface IProtocolHandlers { onConnect(user: User, node: string): void; onView(user: User, node: string, viewMode: number): void; + // Admin handlers onAdminLogin(user: User, password: string): void; onAdminMonitor(user: User, node: string, command: string): void; + onAdminRestore(user: User, node: string): void; + onAdminReboot(user: User, node: string): void; + onAdminBanUser(user: User, username: string): void; + onAdminForceVote(user: User, choice: number): void; + onAdminMuteUser(user: User, username: string, temporary: boolean): void; + onAdminKickUser(user: User, username: string): void; + onAdminEndTurn(user: User, username: string): void; + onAdminClearQueue(user: User, node: string): void; + onAdminRename(user: User, target: string, newName: string): void; + onAdminGetIP(user: User, username: string): void; + onAdminBypassTurn(user: User): void; + onAdminRawMessage(user: User, message: string): void; + onAdminToggleTurns(user: User, enabled: boolean): void; + onAdminIndefiniteTurn(user: User): void; + onAdminHideScreen(user: User, show: boolean): void; + onAdminSystemMessage(user: User, message: string): void; - onRename(user: User, newName: string|undefined): void; + onRename(user: User, newName: string | undefined): void; onChat(user: User, message: string): void; onKey(user: User, keysym: number, pressed: boolean): void; From 6e1a075194a9735c10426edc101dfe513495041a Mon Sep 17 00:00:00 2001 From: modeco80 Date: Wed, 21 Aug 2024 21:45:13 -0400 Subject: [PATCH 03/20] oops thought that was one of the admin only opcodes --- cvmts/src/CollabVMServer.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cvmts/src/CollabVMServer.ts b/cvmts/src/CollabVMServer.ts index 5367eda..d0b238e 100644 --- a/cvmts/src/CollabVMServer.ts +++ b/cvmts/src/CollabVMServer.ts @@ -582,12 +582,6 @@ export default class CollabVMServer implements IProtocolHandlers { var target = this.clients.find((c) => c.username === username); if (!target) return; - - // don't let a mod end a infinite turn - if(user.rank == Rank.Moderator) - if(this.indefiniteTurn == target) - return; - this.endTurn(target); } From 4583531fce0695e4c86db06145f8a247a5e17603 Mon Sep 17 00:00:00 2001 From: modeco80 Date: Wed, 21 Aug 2024 22:18:11 -0400 Subject: [PATCH 04/20] re-implement binrect protocol pretty easy since we can just subclass guac --- cvmts/src/BinRectsProtocol.ts | 16 +++++++++++++--- cvmts/src/CollabVMServer.ts | 35 +++++++++++++++++++++------------- cvmts/src/GuacamoleProtocol.ts | 27 +++++++++++++++++++++++++- cvmts/src/Protocol.ts | 5 +++++ cvmts/src/index.ts | 4 +++- 5 files changed, 69 insertions(+), 18 deletions(-) diff --git a/cvmts/src/BinRectsProtocol.ts b/cvmts/src/BinRectsProtocol.ts index f4a8d57..21bb76a 100644 --- a/cvmts/src/BinRectsProtocol.ts +++ b/cvmts/src/BinRectsProtocol.ts @@ -1,6 +1,16 @@ import * as msgpack from 'msgpackr'; import { CollabVMProtocolMessage, CollabVMProtocolMessageType } from '@cvmts/collab-vm-1.2-binary-protocol'; +import { GuacamoleProtocol } from './GuacamoleProtocol.js'; -// TODO: reimplement binrects protocol -// we can just create/proxy a GuacamoleProtocol manually, -// and for the rects do our own thing +import { ScreenRect } from './Protocol'; + +export class BinRectsProtocol extends GuacamoleProtocol { + sendScreenUpdate(rect: ScreenRect): void { + let bmsg: CollabVMProtocolMessage = { + type: CollabVMProtocolMessageType.rect, + rect: rect + }; + + this.user?.socket.sendBinary(msgpack.encode(bmsg)); + } +} diff --git a/cvmts/src/CollabVMServer.ts b/cvmts/src/CollabVMServer.ts index d0b238e..7d9bea5 100644 --- a/cvmts/src/CollabVMServer.ts +++ b/cvmts/src/CollabVMServer.ts @@ -211,6 +211,8 @@ export default class CollabVMServer implements IProtocolHandlers { 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) { @@ -273,9 +275,9 @@ export default class CollabVMServer implements IProtocolHandlers { // Set rank user.rank = res.rank; if (user.rank === Rank.Admin) { - user.sendMsg(cvm.guacEncode('admin', '0', '1')); + user.protocol.sendAdminLoginResponse(true, undefined); } else if (user.rank === Rank.Moderator) { - user.sendMsg(cvm.guacEncode('admin', '0', '3', this.ModPerms.toString())); + user.protocol.sendAdminLoginResponse(true, this.ModPerms); } this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('adduser', '1', user.username!, user.rank.toString()))); } else { @@ -303,12 +305,11 @@ export default class CollabVMServer implements IProtocolHandlers { for (let cap of capability) { switch (cap) { // binary 1.0 (msgpack rects) - // TODO: re-enable once binary1.0 is enabled case 'bin': - this.logger.info('Binary 1.0 protocol is currently disabled for refactoring'); - //user.Capabilities.bin = true; - //user.protocol = TheProtocolManager.createProtocol('binary1', user); - //user.protocol.setHandler(this as IProtocolHandlers); + user.Capabilities.bin = true; + user.protocol.dispose(); + user.protocol = TheProtocolManager.createProtocol('binary1', user); + user.protocol.setHandler(this as IProtocolHandlers); break; default: break; @@ -513,12 +514,12 @@ export default class CollabVMServer implements IProtocolHandlers { if (pwdHash === this.Config.collabvm.adminpass) { user.rank = Rank.Admin; - user.sendMsg(cvm.guacEncode('admin', '0', '1')); + user.protocol.sendAdminLoginResponse(true, undefined); } else if (this.Config.collabvm.moderatorEnabled && pwdHash === this.Config.collabvm.modpass) { user.rank = Rank.Moderator; - user.sendMsg(cvm.guacEncode('admin', '0', '3', this.ModPerms.toString())); + user.protocol.sendAdminLoginResponse(true, this.ModPerms); } else { - user.sendMsg(cvm.guacEncode('admin', '0', '0')); + user.protocol.sendAdminLoginResponse(false, undefined); return; } @@ -526,14 +527,22 @@ export default class CollabVMServer implements IProtocolHandlers { await this.SendFullScreenWithSize(user); } - this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('adduser', '1', user.username!, user.rank.toString()))); + // Update rank + this.clients.forEach((c) => + c.protocol.sendAddUser([ + { + username: user.username!, + rank: user.rank + } + ]) + ); } async onAdminMonitor(user: User, node: string, command: string) { if (user.rank !== Rank.Admin) return; if (node !== this.Config.collabvm.node) return; let output = await this.VM.MonitorCommand(command); - user.sendMsg(cvm.guacEncode('admin', '2', String(output))); + user.protocol.sendAdminMonitorResponse(String(output)); } onAdminRestore(user: User, node: string): void { @@ -605,7 +614,7 @@ export default class CollabVMServer implements IProtocolHandlers { 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.sendMsg(cvm.guacEncode('admin', '19', username, target.IP.address)); + user.protocol.sendAdminIPResponse(username, target.IP.address); } onAdminBypassTurn(user: User): void { diff --git a/cvmts/src/GuacamoleProtocol.ts b/cvmts/src/GuacamoleProtocol.ts index 258c863..f928455 100644 --- a/cvmts/src/GuacamoleProtocol.ts +++ b/cvmts/src/GuacamoleProtocol.ts @@ -11,12 +11,17 @@ export class GuacamoleProtocol implements IProtocol { name: 'CVMTS.GuacamoleProtocol' }); - private user: User | null = null; + protected user: User | null = null; init(u: User): void { this.user = u; } + dispose(): void { + this.user = null; + this.handlers = null; + } + setHandler(handlers: IProtocolHandlers): void { this.handlers = handlers; } @@ -237,6 +242,26 @@ export class GuacamoleProtocol implements IProtocol { } } + sendAdminLoginResponse(ok: boolean, modPerms: number | undefined): void { + if (ok) { + if (modPerms == undefined) { + this.user?.sendMsg(cvm.guacEncode('admin', '0', '1')); + } else { + this.user?.sendMsg(cvm.guacEncode('admin', '0', '3', modPerms.toString())); + } + } else { + this.user?.sendMsg(cvm.guacEncode('admin', '0', '0')); + } + } + + sendAdminMonitorResponse(output: string): void { + this.user?.sendMsg(cvm.guacEncode('admin', '2', output)); + } + + sendAdminIPResponse(username: string, ip: string): void { + this.user?.sendMsg(cvm.guacEncode('admin', '19', username, ip)); + } + sendChatMessage(username: string, message: string): void { this.user?.sendMsg(cvm.guacEncode('chat', username, message)); } diff --git a/cvmts/src/Protocol.ts b/cvmts/src/Protocol.ts index 1883772..ab5bc1f 100644 --- a/cvmts/src/Protocol.ts +++ b/cvmts/src/Protocol.ts @@ -77,6 +77,7 @@ export interface IProtocolHandlers { // Abstracts away all of the CollabVM protocol details export interface IProtocol { init(u: User): void; + dispose(): void; // Sets handler object. setHandler(handlers: IProtocolHandlers): void; @@ -98,6 +99,10 @@ export interface IProtocol { sendLoginResponse(ok: boolean, message: string | undefined): void; + sendAdminLoginResponse(ok: boolean, modPerms: number | undefined): void; + sendAdminMonitorResponse(output: string): void; + sendAdminIPResponse(username: string, ip: string): void; + sendChatMessage(username: '' | string, message: string): void; sendChatHistoryMessage(history: ProtocolChatHistory[]): void; diff --git a/cvmts/src/index.ts b/cvmts/src/index.ts index 1769dd7..d24541a 100644 --- a/cvmts/src/index.ts +++ b/cvmts/src/index.ts @@ -18,6 +18,7 @@ import { BanManager } from './BanManager.js'; import { QemuVMShim } from './vm/qemu.js'; import { TheProtocolManager } from './Protocol.js'; import { GuacamoleProtocol } from './GuacamoleProtocol.js'; +import { BinRectsProtocol } from './BinRectsProtocol.js'; let logger = pino(); @@ -99,8 +100,9 @@ async function start() { process.on('SIGINT', async () => await stop()); process.on('SIGTERM', async () => await stop()); - // Register protocol(s) + // Register protocol(s) that the server supports TheProtocolManager.registerProtocol("guacamole", () => new GuacamoleProtocol); + TheProtocolManager.registerProtocol("binary1", () => new BinRectsProtocol); await VM.Start(); // Start up the server From 1673f0abd74484566d6cb864bb498af927db13ec Mon Sep 17 00:00:00 2001 From: modeco80 Date: Wed, 21 Aug 2024 22:26:36 -0400 Subject: [PATCH 05/20] move some shared stuff into a new baseclass for protocols to implement will make greenfield (i.e: brand new non-Guac or whatever) protocol implementation a bit less boilerplatey --- cvmts/src/GuacamoleProtocol.ts | 20 ++------------------ cvmts/src/Protocol.ts | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/cvmts/src/GuacamoleProtocol.ts b/cvmts/src/GuacamoleProtocol.ts index f928455..069ec05 100644 --- a/cvmts/src/GuacamoleProtocol.ts +++ b/cvmts/src/GuacamoleProtocol.ts @@ -1,31 +1,15 @@ import pino from 'pino'; -import { IProtocol, IProtocolHandlers, ListEntry, ProtocolAddUser, ProtocolChatHistory, ScreenRect } from './Protocol'; +import { IProtocol, IProtocolHandlers, ListEntry, ProtocolAddUser, ProtocolBase, ProtocolChatHistory, ScreenRect } from './Protocol'; import { User } from './User'; import * as cvm from '@cvmts/cvm-rs'; // CollabVM protocol implementation for Guacamole. -export class GuacamoleProtocol implements IProtocol { - private handlers: IProtocolHandlers | null = null; +export class GuacamoleProtocol extends ProtocolBase implements IProtocol { private logger = pino({ name: 'CVMTS.GuacamoleProtocol' }); - protected user: User | null = null; - - init(u: User): void { - this.user = u; - } - - dispose(): void { - this.user = null; - this.handlers = null; - } - - setHandler(handlers: IProtocolHandlers): void { - this.handlers = handlers; - } - private __processMessage_admin(decodedElements: string[]): boolean { switch (decodedElements[1]) { case '2': diff --git a/cvmts/src/Protocol.ts b/cvmts/src/Protocol.ts index ab5bc1f..8a04aa6 100644 --- a/cvmts/src/Protocol.ts +++ b/cvmts/src/Protocol.ts @@ -76,12 +76,15 @@ export interface IProtocolHandlers { // Abstracts away all of the CollabVM protocol details export interface IProtocol { + // don't implement this yourself, extend from ProtocolBase init(u: User): void; dispose(): void; // Sets handler object. setHandler(handlers: IProtocolHandlers): void; + // Protocol implementation stuff + // Parses a single CollabVM protocol message and fires the given handler. // This function does not catch any thrown errors; it is the caller's responsibility // to handle errors. It should, however, catch invalid parameters without failing. @@ -117,6 +120,25 @@ export interface IProtocol { sendScreenUpdate(rect: ScreenRect): void; } +// base mixin for all protocols to use +export class ProtocolBase { + protected handlers: IProtocolHandlers | null = null; + protected user: User | null = null; + + init(u: User): void { + this.user = u; + } + + dispose(): void { + this.user = null; + this.handlers = null; + } + + setHandler(handlers: IProtocolHandlers): void { + this.handlers = handlers; + } +} + // Holds protocol factories. export class ProtocolManager { private protocols = new Map IProtocol>(); From 0010a8f30047ab7529a9e2d57d4447ea5b75c0e2 Mon Sep 17 00:00:00 2001 From: modeco80 Date: Wed, 21 Aug 2024 22:36:22 -0400 Subject: [PATCH 06/20] move 'vote' to Protocol --- cvmts/src/CollabVMServer.ts | 13 +++++++------ cvmts/src/GuacamoleProtocol.ts | 16 ++++++++++++++++ cvmts/src/Protocol.ts | 5 +++++ 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/cvmts/src/CollabVMServer.ts b/cvmts/src/CollabVMServer.ts index 7d9bea5..bd98660 100644 --- a/cvmts/src/CollabVMServer.ts +++ b/cvmts/src/CollabVMServer.ts @@ -359,9 +359,10 @@ export default class CollabVMServer implements IProtocolHandlers { if (!this.authCheck(user, this.Config.auth.guestPermissions.callForReset)) return; if (this.voteCooldown !== 0) { - user.sendMsg(cvm.guacEncode('vote', '3', this.voteCooldown.toString())); + user.protocol.sendVoteCooldown(this.voteCooldown); return; } + this.startVote(); this.clients.forEach((c) => c.protocol.sendChatMessage('', `${user.username} has started a vote to reset the VM.`)); } @@ -918,7 +919,7 @@ export default class CollabVMServer implements IProtocolHandlers { startVote() { if (this.voteInProgress) return; this.voteInProgress = true; - this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('vote', '0'))); + this.clients.forEach((c) => c.protocol.sendVoteStarted()); this.voteTime = this.Config.collabvm.voteTime; this.voteInterval = setInterval(() => { this.voteTime--; @@ -933,7 +934,7 @@ export default class CollabVMServer implements IProtocolHandlers { this.voteInProgress = false; clearInterval(this.voteInterval); var count = this.getVoteCounts(); - this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('vote', '2'))); + this.clients.forEach((c) => c.protocol.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.VM.Reset(); @@ -953,9 +954,9 @@ export default class CollabVMServer implements IProtocolHandlers { sendVoteUpdate(client?: User) { if (!this.voteInProgress) return; var count = this.getVoteCounts(); - var msg = cvm.guacEncode('vote', '1', (this.voteTime * 1000).toString(), count.yes.toString(), count.no.toString()); - if (client) client.sendMsg(msg); - else this.clients.forEach((c) => c.sendMsg(msg)); + + 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)); } getVoteCounts(): VoteTally { diff --git a/cvmts/src/GuacamoleProtocol.ts b/cvmts/src/GuacamoleProtocol.ts index 069ec05..5668eea 100644 --- a/cvmts/src/GuacamoleProtocol.ts +++ b/cvmts/src/GuacamoleProtocol.ts @@ -290,6 +290,22 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol { this.user?.sendMsg(cvm.guacEncode(...arr)); } + + sendVoteStarted(): void { + this.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())); + } + + sendVoteEnded(): void { + this.user?.sendMsg(cvm.guacEncode('vote', '2')); + } + + sendVoteCooldown(ms: number): void { + this.user?.sendMsg(cvm.guacEncode('vote', '3', ms.toString())); + } sendScreenResize(width: number, height: number): void { this.user?.sendMsg(cvm.guacEncode('size', '0', width.toString(), height.toString())); diff --git a/cvmts/src/Protocol.ts b/cvmts/src/Protocol.ts index 8a04aa6..ca281bb 100644 --- a/cvmts/src/Protocol.ts +++ b/cvmts/src/Protocol.ts @@ -114,6 +114,11 @@ export interface IProtocol { sendListResponse(list: ListEntry[]): void; + sendVoteStarted(): void; + sendVoteStats(msLeft: number, nrYes: number, nrNo: number): void; + sendVoteEnded(): void; + sendVoteCooldown(ms: number): void; + sendScreenResize(width: number, height: number): void; // Sends a rectangle update to the user. From c9edb174f14bc59409f122c5318a993d7d748160 Mon Sep 17 00:00:00 2001 From: modeco80 Date: Wed, 21 Aug 2024 22:39:58 -0400 Subject: [PATCH 07/20] keep forgeting javascript tooling has about the same usability as a 80s ford --- cvmts/src/GuacamoleProtocol.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cvmts/src/GuacamoleProtocol.ts b/cvmts/src/GuacamoleProtocol.ts index 5668eea..e790ffb 100644 --- a/cvmts/src/GuacamoleProtocol.ts +++ b/cvmts/src/GuacamoleProtocol.ts @@ -1,5 +1,5 @@ import pino from 'pino'; -import { IProtocol, IProtocolHandlers, ListEntry, ProtocolAddUser, ProtocolBase, ProtocolChatHistory, ScreenRect } from './Protocol'; +import { IProtocol, IProtocolHandlers, ListEntry, ProtocolAddUser, ProtocolBase, ProtocolChatHistory, ScreenRect } from './Protocol.js'; import { User } from './User'; import * as cvm from '@cvmts/cvm-rs'; @@ -290,7 +290,7 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol { this.user?.sendMsg(cvm.guacEncode(...arr)); } - + sendVoteStarted(): void { this.user?.sendMsg(cvm.guacEncode('vote', '0')); } From 5dc53116b2fc1993716c3a3877ca3fe3de59d6bc Mon Sep 17 00:00:00 2001 From: modeco80 Date: Thu, 22 Aug 2024 03:50:04 -0400 Subject: [PATCH 08/20] move flag and rename to protocol layer This means that 'turn' is now the only thing not sent by the protocol layer. --- cvmts/src/CollabVMServer.ts | 82 ++++++++++++++++++++++++---------- cvmts/src/GuacamoleProtocol.ts | 27 +++++++++-- cvmts/src/Protocol.ts | 36 ++++++++++++--- 3 files changed, 111 insertions(+), 34 deletions(-) diff --git a/cvmts/src/CollabVMServer.ts b/cvmts/src/CollabVMServer.ts index bd98660..4bc7c08 100644 --- a/cvmts/src/CollabVMServer.ts +++ b/cvmts/src/CollabVMServer.ts @@ -18,7 +18,7 @@ import { ReaderModel } from '@maxmind/geoip2-node'; import { Size, Rect } from './Utilities.js'; import pino from 'pino'; import { BanManager } from './BanManager.js'; -import { IProtocolHandlers, ListEntry, ProtocolAddUser, TheProtocolManager } from './Protocol.js'; +import { IProtocolHandlers, ListEntry, ProtocolAddUser, ProtocolFlag, ProtocolRenameStatus, ProtocolUpgradeCapability, TheProtocolManager } from './Protocol.js'; // Instead of strange hacks we can just use nodejs provided // import.meta properties, which have existed since LTS if not before @@ -193,9 +193,11 @@ export default class CollabVMServer implements IProtocolHandlers { user.protocol.sendAuth(this.Config.auth.apiEndpoint); } - // convert these to proto - user.protocol.sendAddUser(this.getAdduserMsg()); - if (this.Config.geoip.enabled) user.sendMsg(this.getFlagMsg()); + user.protocol.sendAddUser(this.getAddUser()); + if (this.Config.geoip.enabled) { + let flags = this.getFlags(); + user.protocol.sendFlag(flags); + } } private connectionClosed(user: User) { @@ -246,7 +248,7 @@ export default class CollabVMServer implements IProtocolHandlers { if (!this.Config.auth.enabled) return; if (!user.connectedToNode) { - user.sendMsg(cvm.guacEncode('login', '0', 'You must connect to the VM before logging in.')); + user.protocol.sendLoginResponse(false, 'You must connect to the VM before logging in.'); return; } @@ -268,7 +270,7 @@ export default class CollabVMServer implements IProtocolHandlers { if (user.countryCode !== null && user.noFlag) { // privacy for (let cl of this.clients.filter((c) => c !== user)) { - cl.sendMsg(cvm.guacEncode('remuser', '1', user.username!)); + cl.protocol.sendRemUser([user.username!]); } this.renameUser(user, res.username, false); } else this.renameUser(user, res.username, true); @@ -279,7 +281,14 @@ export default class CollabVMServer implements IProtocolHandlers { } else if (user.rank === Rank.Moderator) { user.protocol.sendAdminLoginResponse(true, this.ModPerms); } - this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('adduser', '1', user.username!, user.rank.toString()))); + this.clients.forEach((c) => + c.protocol.sendAddUser([ + { + username: user.username!, + rank: user.rank + } + ]) + ); } else { user.protocol.sendLoginResponse(false, res.error!); if (res.error === 'You are banned') { @@ -302,10 +311,13 @@ export default class CollabVMServer implements IProtocolHandlers { onCapabilityUpgrade(user: User, capability: String[]): boolean { if (user.connectedToNode) return false; + let enabledCaps = []; + for (let cap of capability) { switch (cap) { // binary 1.0 (msgpack rects) - case 'bin': + case ProtocolUpgradeCapability.BinRects: + enabledCaps.push(cap as ProtocolUpgradeCapability); user.Capabilities.bin = true; user.protocol.dispose(); user.protocol = TheProtocolManager.createProtocol('binary1', user); @@ -315,6 +327,8 @@ export default class CollabVMServer implements IProtocolHandlers { break; } } + + user.protocol.sendCapabilities(enabledCaps); return true; } @@ -706,47 +720,64 @@ export default class CollabVMServer implements IProtocolHandlers { renameUser(client: User, newName?: string, announce: boolean = true) { // This shouldn't need a ternary but it does for some reason - var hadName: boolean = client.username ? true : false; - var oldname: any; + let hadName = client.username ? true : false; + let oldname: any; if (hadName) oldname = client.username; - var status = '0'; + if (!newName) { client.assignGuestName(this.getUsernameList()); } else { newName = newName.trim(); if (hadName && newName === oldname) { - client.sendMsg(cvm.guacEncode('rename', '0', '0', client.username!, client.rank.toString())); + client.protocol.sendSelfRename(ProtocolRenameStatus.Ok, client.username!, client.rank); return; } + + let status = ProtocolRenameStatus.Ok; + if (this.getUsernameList().indexOf(newName) !== -1) { client.assignGuestName(this.getUsernameList()); if (client.connectedToNode) { - status = '1'; + status = ProtocolRenameStatus.UsernameTaken; } } else if (!/^[a-zA-Z0-9\ \-\_\.]+$/.test(newName) || newName.length > 20 || newName.length < 3) { client.assignGuestName(this.getUsernameList()); - status = '2'; + status = ProtocolRenameStatus.UsernameInvalid; } else if (this.Config.collabvm.usernameblacklist.indexOf(newName) !== -1) { client.assignGuestName(this.getUsernameList()); - status = '3'; + status = ProtocolRenameStatus.UsernameNotAllowed; } else client.username = newName; + + client.protocol.sendSelfRename(status, client.username!, client.rank); } - client.sendMsg(cvm.guacEncode('rename', '0', status, client.username!, client.rank.toString())); if (hadName) { this.logger.info(`Rename ${client.IP.address} from ${oldname} to ${client.username}`); - if (announce) this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('rename', '1', oldname, client.username!, client.rank.toString()))); + if (announce) this.clients.forEach((c) => c.protocol.sendRename(oldname, client.username!, client.rank)); } else { this.logger.info(`Rename ${client.IP.address} to ${client.username}`); 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)); + c.protocol.sendAddUser([ + { + username: client.username!, + rank: client.rank + } + ]); + + if (client.countryCode !== null) { + c.protocol.sendFlag([ + { + username: client.username!, + countryCode: client.countryCode + } + ]); + } }); } } - getAdduserMsg(): ProtocolAddUser[] { + private getAddUser(): ProtocolAddUser[] { return this.clients .filter((c) => c.username) .map((c) => { @@ -757,12 +788,15 @@ export default class CollabVMServer implements IProtocolHandlers { }); } - getFlagMsg(): string { - var arr = ['flag']; + private getFlags(): ProtocolFlag[] { + let arr = []; for (let c of this.clients.filter((cl) => cl.countryCode !== null && cl.username && (!cl.noFlag || cl.rank === Rank.Unregistered))) { - arr.push(c.username!, c.countryCode!); + arr.push({ + username: c.username!, + countryCode: c.countryCode! + }); } - return cvm.guacEncode(...arr); + return arr; } private sendTurnUpdate(client?: User) { diff --git a/cvmts/src/GuacamoleProtocol.ts b/cvmts/src/GuacamoleProtocol.ts index e790ffb..38c9d3e 100644 --- a/cvmts/src/GuacamoleProtocol.ts +++ b/cvmts/src/GuacamoleProtocol.ts @@ -1,6 +1,6 @@ import pino from 'pino'; -import { IProtocol, IProtocolHandlers, ListEntry, ProtocolAddUser, ProtocolBase, ProtocolChatHistory, ScreenRect } from './Protocol.js'; -import { User } from './User'; +import { IProtocol, IProtocolHandlers, ListEntry, ProtocolAddUser, ProtocolBase, ProtocolChatHistory, ProtocolFlag, ProtocolRenameStatus, ProtocolUpgradeCapability, ScreenRect } from './Protocol.js'; +import { Rank, User } from './User'; import * as cvm from '@cvmts/cvm-rs'; @@ -209,6 +209,11 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol { this.user?.sendMsg(cvm.guacEncode('sync', now.toString())); } + sendCapabilities(caps: ProtocolUpgradeCapability[]): void { + let arr = ['cap', ...caps]; + this?.user?.sendMsg(cvm.guacEncode(...arr)); + } + sendConnectFailResponse(): void { this.user?.sendMsg(cvm.guacEncode('connect', '0')); } @@ -253,8 +258,7 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol { sendChatHistoryMessage(history: ProtocolChatHistory[]): void { let arr = ['chat']; for (let a of history) { - arr.push(a.user); - arr.push(a.msg); + arr.push(a.user, a.msg); } this.user?.sendMsg(cvm.guacEncode(...arr)); @@ -280,6 +284,21 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol { this.user?.sendMsg(cvm.guacEncode(...arr)); } + sendFlag(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)); + } + + sendSelfRename(status: ProtocolRenameStatus, newUsername: string, rank: Rank): void { + this.user?.sendMsg(cvm.guacEncode('rename', '0', status.toString(), newUsername, rank.toString())); + } + + sendRename(oldUsername: string, newUsername: string, rank: Rank): void { + this.user?.sendMsg(cvm.guacEncode('rename', '1', oldUsername, newUsername, rank.toString())); + } + sendListResponse(list: ListEntry[]): void { let arr = ['list']; for (let node of list) { diff --git a/cvmts/src/Protocol.ts b/cvmts/src/Protocol.ts index ca281bb..d4185ce 100644 --- a/cvmts/src/Protocol.ts +++ b/cvmts/src/Protocol.ts @@ -1,8 +1,15 @@ import { Rank, User } from './User'; // We should probably put this in the binproto repository or something -enum UpgradeCapability { - Binary = 'bin' +export enum ProtocolUpgradeCapability { + BinRects = 'bin' +} + +export enum ProtocolRenameStatus { + Ok = 0, + UsernameTaken = 1, + UsernameInvalid = 2, + UsernameNotAllowed = 3 } export interface ScreenRect { @@ -27,6 +34,11 @@ export interface ProtocolAddUser { rank: Rank; } +export interface ProtocolFlag { + username: string; + countryCode: string; +} + // Protocol handlers. This is implemented by a layer that wants to listen to CollabVM protocol messages. export interface IProtocolHandlers { onNop(user: User): void; @@ -74,7 +86,9 @@ export interface IProtocolHandlers { onMouse(user: User, x: number, y: number, buttonMask: number): void; } -// Abstracts away all of the CollabVM protocol details +// Abstracts away all of the protocol details from the CollabVM server, +// 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; @@ -85,9 +99,11 @@ export interface IProtocol { // Protocol implementation stuff - // Parses a single CollabVM protocol message and fires the given handler. + // Parses a single message and fires the given handler with deserialized arguments. // This function does not catch any thrown errors; it is the caller's responsibility // 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; // Senders @@ -97,6 +113,8 @@ export interface IProtocol { sendAuth(authServer: string): void; + sendCapabilities(caps: ProtocolUpgradeCapability[]): void; + sendConnectFailResponse(): void; sendConnectOKResponse(votes: boolean): void; @@ -111,7 +129,11 @@ export interface IProtocol { sendAddUser(users: ProtocolAddUser[]): void; sendRemUser(users: string[]): void; + sendFlag(flag: ProtocolFlag[]): void; + sendSelfRename(status: ProtocolRenameStatus, newUsername: string, rank: Rank): void; + sendRename(oldUsername: string, newUsername: string, rank: Rank): void; + sendListResponse(list: ListEntry[]): void; sendVoteStarted(): void; @@ -125,7 +147,7 @@ export interface IProtocol { sendScreenUpdate(rect: ScreenRect): void; } -// base mixin for all protocols to use +// Base mixin for all concrete protocols to use. Inherit from this! export class ProtocolBase { protected handlers: IProtocolHandlers | null = null; protected user: User | null = null; @@ -144,7 +166,9 @@ export class ProtocolBase { } } -// Holds protocol factories. +// 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. export class ProtocolManager { private protocols = new Map IProtocol>(); From 8f48092c5cfe2d391170792ef2f07fed92e63f17 Mon Sep 17 00:00:00 2001 From: modeco80 Date: Thu, 22 Aug 2024 04:04:12 -0400 Subject: [PATCH 09/20] move turn to protocol everything now uses the protocol layer! woohoo. --- cvmts/src/CollabVMServer.ts | 20 +++++++++++--------- cvmts/src/GuacamoleProtocol.ts | 14 ++++++++++++++ cvmts/src/Protocol.ts | 3 +++ 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/cvmts/src/CollabVMServer.ts b/cvmts/src/CollabVMServer.ts index 4bc7c08..61cbe8c 100644 --- a/cvmts/src/CollabVMServer.ts +++ b/cvmts/src/CollabVMServer.ts @@ -1,7 +1,6 @@ import IConfig from './IConfig.js'; import * as Utilities from './Utilities.js'; import { User, Rank } from './User.js'; -import * as cvm from '@cvmts/cvm-rs'; // I hate that you have to do it like this import CircularBuffer from 'mnemonist/circular-buffer.js'; import Queue from 'mnemonist/queue.js'; @@ -801,17 +800,20 @@ export default class CollabVMServer implements IProtocolHandlers { private sendTurnUpdate(client?: User) { var turnQueueArr = this.TurnQueue.toArray(); - var turntime; + var turntime: number; 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 users: string[] = []; + + this.TurnQueue.forEach((c) => users.push(c.username!)); + var currentTurningUser = this.TurnQueue.peek(); + if (client) { - client.sendMsg(cvm.guacEncode(...arr)); + client.protocol.sendTurnQueue(turntime, users); return; } + this.clients .filter((c) => c !== currentTurningUser && c.connectedToNode) .forEach((c) => { @@ -819,12 +821,12 @@ export default class CollabVMServer implements IProtocolHandlers { var time; if (this.indefiniteTurn === null) time = this.TurnTime * 1000 + (turnQueueArr.indexOf(c) - 1) * this.Config.collabvm.turnTime * 1000; else time = 9999999999; - c.sendMsg(cvm.guacEncode(...arr, time.toString())); + c.protocol.sendTurnQueueWaiting(turntime, users, time); } else { - c.sendMsg(cvm.guacEncode(...arr)); + c.protocol.sendTurnQueue(turntime, users); } }); - if (currentTurningUser) currentTurningUser.sendMsg(cvm.guacEncode(...arr)); + if (currentTurningUser) currentTurningUser.protocol.sendTurnQueue(turntime, users); } private nextTurn() { clearInterval(this.TurnInterval); diff --git a/cvmts/src/GuacamoleProtocol.ts b/cvmts/src/GuacamoleProtocol.ts index 38c9d3e..be41f01 100644 --- a/cvmts/src/GuacamoleProtocol.ts +++ b/cvmts/src/GuacamoleProtocol.ts @@ -326,6 +326,20 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol { this.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))); + } + + sendTurnQueueWaiting(turnTime: number, users: string[], waitTime: number): void { + let queue = this.getTurnQueueBase(turnTime, users); + queue.push(waitTime.toString()); + this.user?.sendMsg(cvm.guacEncode(...queue)); + } + sendScreenResize(width: number, height: number): void { this.user?.sendMsg(cvm.guacEncode('size', '0', width.toString(), height.toString())); } diff --git a/cvmts/src/Protocol.ts b/cvmts/src/Protocol.ts index d4185ce..1a368d8 100644 --- a/cvmts/src/Protocol.ts +++ b/cvmts/src/Protocol.ts @@ -136,6 +136,9 @@ export interface IProtocol { sendListResponse(list: ListEntry[]): void; + sendTurnQueue(turnTime: number, users: string[]): void; + sendTurnQueueWaiting(turnTime: number, users: string[], waitTime: number): void + sendVoteStarted(): void; sendVoteStats(msLeft: number, nrYes: number, nrNo: number): void; sendVoteEnded(): void; From 53b5b6d87419bb60a8ceb61af93f82ff2bcc3f67 Mon Sep 17 00:00:00 2001 From: modeco80 Date: Thu, 22 Aug 2024 04:08:13 -0400 Subject: [PATCH 10/20] oops --- cvmts/src/Protocol.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cvmts/src/Protocol.ts b/cvmts/src/Protocol.ts index 1a368d8..0f45fcb 100644 --- a/cvmts/src/Protocol.ts +++ b/cvmts/src/Protocol.ts @@ -133,11 +133,11 @@ export interface IProtocol { sendSelfRename(status: ProtocolRenameStatus, newUsername: string, rank: Rank): void; sendRename(oldUsername: string, newUsername: string, rank: Rank): void; - + sendListResponse(list: ListEntry[]): void; sendTurnQueue(turnTime: number, users: string[]): void; - sendTurnQueueWaiting(turnTime: number, users: string[], waitTime: number): void + sendTurnQueueWaiting(turnTime: number, users: string[], waitTime: number): void; sendVoteStarted(): void; sendVoteStats(msLeft: number, nrYes: number, nrNo: number): void; From f7fb30ba20f93f7cebee7918b1aa37f4e8e1b64f Mon Sep 17 00:00:00 2001 From: modeco80 Date: Thu, 22 Aug 2024 04:20:26 -0400 Subject: [PATCH 11/20] make network abstraction pass bare buffer this bitrots tcp a bit. once the tcp protocol is replaced with a message based one it shouild be fine --- cvmts/src/CollabVMServer.ts | 4 +--- cvmts/src/NetworkClient.ts | 11 ++++++++--- cvmts/src/NetworkServer.ts | 11 ++++++++--- cvmts/src/TCP/TCPClient.ts | 2 +- cvmts/src/TCP/TCPServer.ts | 2 +- cvmts/src/WebSocket/WSClient.ts | 10 ++++++---- cvmts/src/WebSocket/WSServer.ts | 2 +- 7 files changed, 26 insertions(+), 16 deletions(-) diff --git a/cvmts/src/CollabVMServer.ts b/cvmts/src/CollabVMServer.ts index 61cbe8c..a1b4c17 100644 --- a/cvmts/src/CollabVMServer.ts +++ b/cvmts/src/CollabVMServer.ts @@ -173,9 +173,7 @@ export default class CollabVMServer implements IProtocolHandlers { } } - // TODO: we should probably just make this a buffer arg lol.. - user.socket.on('msg', (msg: string) => { - let buf = Buffer.from(msg); + user.socket.on('msg', (buf: Buffer, binary: boolean) => { try { user.protocol.processMessage(buf); } catch (err) { diff --git a/cvmts/src/NetworkClient.ts b/cvmts/src/NetworkClient.ts index 600356f..5bfd4e4 100644 --- a/cvmts/src/NetworkClient.ts +++ b/cvmts/src/NetworkClient.ts @@ -1,9 +1,14 @@ -export default interface NetworkClient { +import { EventEmitter } from "stream"; + +interface NetworkClientEvents extends EventEmitter { + on(event: 'msg', listener: (buf: Buffer, binary: boolean) => void): this; + on(event: 'disconnect', listener: () => void): this; +} + +export interface NetworkClient extends NetworkClientEvents { getIP(): string; send(msg: string): Promise; sendBinary(msg: Uint8Array): Promise; close(): void; - on(event: string, listener: (...args: any[]) => void): void; - off(event: string, listener: (...args: any[]) => void): void; isOpen(): boolean; } diff --git a/cvmts/src/NetworkServer.ts b/cvmts/src/NetworkServer.ts index 5fce6f6..1719354 100644 --- a/cvmts/src/NetworkServer.ts +++ b/cvmts/src/NetworkServer.ts @@ -1,6 +1,11 @@ -export default interface NetworkServer { +import { EventEmitter } from "stream"; +import { User } from "./User"; + +interface NetworkServerEvents extends EventEmitter { + on(event: 'connect', listener: (user: User) => void): this; +} + +export interface NetworkServer extends NetworkServerEvents { start(): void; stop(): void; - on(event: string, listener: (...args: any[]) => void): void; - off(event: string, listener: (...args: any[]) => void): void; } diff --git a/cvmts/src/TCP/TCPClient.ts b/cvmts/src/TCP/TCPClient.ts index 7d3ff9c..8275596 100644 --- a/cvmts/src/TCP/TCPClient.ts +++ b/cvmts/src/TCP/TCPClient.ts @@ -1,5 +1,5 @@ import EventEmitter from 'events'; -import NetworkClient from '../NetworkClient.js'; +import { NetworkClient } from '../NetworkClient.js'; import { Socket } from 'net'; const TextHeader = 0; diff --git a/cvmts/src/TCP/TCPServer.ts b/cvmts/src/TCP/TCPServer.ts index 04ec833..0277269 100644 --- a/cvmts/src/TCP/TCPServer.ts +++ b/cvmts/src/TCP/TCPServer.ts @@ -2,7 +2,7 @@ // struct msg { beu32 len; char data[len] } // (along with a length cap obviously) import EventEmitter from 'events'; -import NetworkServer from '../NetworkServer.js'; +import { NetworkServer } from '../NetworkServer.js'; import { Server, Socket } from 'net'; import IConfig from '../IConfig.js'; import TCPClient from './TCPClient.js'; diff --git a/cvmts/src/WebSocket/WSClient.ts b/cvmts/src/WebSocket/WSClient.ts index c2b0cbf..d0c0f04 100644 --- a/cvmts/src/WebSocket/WSClient.ts +++ b/cvmts/src/WebSocket/WSClient.ts @@ -1,23 +1,25 @@ import { WebSocket } from 'ws'; -import NetworkClient from '../NetworkClient.js'; +import { NetworkClient } from '../NetworkClient.js'; import EventEmitter from 'events'; export default class WSClient extends EventEmitter implements NetworkClient { socket: WebSocket; ip: string; + enforceTextOnly = true constructor(ws: WebSocket, ip: string) { super(); this.socket = ws; this.ip = ip; this.socket.on('message', (buf: Buffer, isBinary: boolean) => { - // Close the user's connection if they send a non-string message - if (isBinary) { + // Close the user's connection if they send a binary message + // when we are not expecting them yet. + if (isBinary && this.enforceTextOnly) { this.close(); return; } - this.emit('msg', buf.toString('utf-8')); + this.emit('msg', buf, isBinary); }); this.socket.on('close', () => { diff --git a/cvmts/src/WebSocket/WSServer.ts b/cvmts/src/WebSocket/WSServer.ts index 82e7a24..b549eb9 100644 --- a/cvmts/src/WebSocket/WSServer.ts +++ b/cvmts/src/WebSocket/WSServer.ts @@ -1,5 +1,5 @@ import * as http from 'http'; -import NetworkServer from '../NetworkServer.js'; +import { NetworkServer } from '../NetworkServer.js'; import EventEmitter from 'events'; import { WebSocketServer, WebSocket } from 'ws'; import internal from 'stream'; From 74d7b17d8b6a5e1f766d4bc2fee1f7b9c4f582e3 Mon Sep 17 00:00:00 2001 From: modeco80 Date: Thu, 22 Aug 2024 04:26:17 -0400 Subject: [PATCH 12/20] re-org source tree slightly network layer is net/ protocol is protocol/ --- cvmts/src/CollabVMServer.ts | 2 +- cvmts/src/User.ts | 4 ++-- cvmts/src/index.ts | 10 +++++----- cvmts/src/{ => net}/NetworkClient.ts | 0 cvmts/src/{ => net}/NetworkServer.ts | 2 +- cvmts/src/{TCP => net/tcp}/TCPClient.ts | 0 cvmts/src/{TCP => net/tcp}/TCPServer.ts | 8 ++++---- cvmts/src/{WebSocket => net/ws}/WSClient.ts | 0 cvmts/src/{WebSocket => net/ws}/WSServer.ts | 8 ++++---- cvmts/src/{ => protocol}/BinRectsProtocol.ts | 0 cvmts/src/{ => protocol}/GuacamoleProtocol.ts | 2 +- cvmts/src/{ => protocol}/Protocol.ts | 2 +- 12 files changed, 19 insertions(+), 19 deletions(-) rename cvmts/src/{ => net}/NetworkClient.ts (100%) rename cvmts/src/{ => net}/NetworkServer.ts (88%) rename cvmts/src/{TCP => net/tcp}/TCPClient.ts (100%) rename cvmts/src/{TCP => net/tcp}/TCPServer.ts (88%) rename cvmts/src/{WebSocket => net/ws}/WSClient.ts (100%) rename cvmts/src/{WebSocket => net/ws}/WSServer.ts (96%) rename cvmts/src/{ => protocol}/BinRectsProtocol.ts (100%) rename cvmts/src/{ => protocol}/GuacamoleProtocol.ts (99%) rename cvmts/src/{ => protocol}/Protocol.ts (99%) diff --git a/cvmts/src/CollabVMServer.ts b/cvmts/src/CollabVMServer.ts index a1b4c17..019e333 100644 --- a/cvmts/src/CollabVMServer.ts +++ b/cvmts/src/CollabVMServer.ts @@ -17,7 +17,7 @@ import { ReaderModel } from '@maxmind/geoip2-node'; import { Size, Rect } from './Utilities.js'; import pino from 'pino'; import { BanManager } from './BanManager.js'; -import { IProtocolHandlers, ListEntry, ProtocolAddUser, ProtocolFlag, ProtocolRenameStatus, ProtocolUpgradeCapability, TheProtocolManager } from './Protocol.js'; +import { IProtocolHandlers, ListEntry, ProtocolAddUser, ProtocolFlag, ProtocolRenameStatus, ProtocolUpgradeCapability, TheProtocolManager } from './protocol/Protocol.js'; // Instead of strange hacks we can just use nodejs provided // import.meta properties, which have existed since LTS if not before diff --git a/cvmts/src/User.ts b/cvmts/src/User.ts index 0804f68..8a539a0 100644 --- a/cvmts/src/User.ts +++ b/cvmts/src/User.ts @@ -4,11 +4,11 @@ import { IPData } from './IPData.js'; import IConfig from './IConfig.js'; import RateLimiter from './RateLimiter.js'; import { execa, execaCommand, ExecaSyncError } from 'execa'; -import NetworkClient from './NetworkClient.js'; +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, TheProtocolManager } from './Protocol.js'; +import { IProtocol, TheProtocolManager } from './protocol/Protocol.js'; export class User { socket: NetworkClient; diff --git a/cvmts/src/index.ts b/cvmts/src/index.ts index d24541a..5cad003 100644 --- a/cvmts/src/index.ts +++ b/cvmts/src/index.ts @@ -6,9 +6,9 @@ import CollabVMServer from './CollabVMServer.js'; import { QemuVmDefinition } from '@computernewb/superqemu'; import AuthManager from './AuthManager.js'; -import WSServer from './WebSocket/WSServer.js'; +import WSServer from './net/ws/WSServer.js'; import { User } from './User.js'; -import TCPServer from './TCP/TCPServer.js'; +import TCPServer from './net/tcp/TCPServer.js'; import VM from './vm/interface.js'; import VNCVM from './vm/vnc/VNCVM.js'; import GeoIPDownloader from './GeoIPDownloader.js'; @@ -16,9 +16,9 @@ import pino from 'pino'; import { Database } from './Database.js'; import { BanManager } from './BanManager.js'; import { QemuVMShim } from './vm/qemu.js'; -import { TheProtocolManager } from './Protocol.js'; -import { GuacamoleProtocol } from './GuacamoleProtocol.js'; -import { BinRectsProtocol } from './BinRectsProtocol.js'; +import { TheProtocolManager } from './protocol/Protocol.js'; +import { GuacamoleProtocol } from './protocol/GuacamoleProtocol.js'; +import { BinRectsProtocol } from './protocol/BinRectsProtocol.js'; let logger = pino(); diff --git a/cvmts/src/NetworkClient.ts b/cvmts/src/net/NetworkClient.ts similarity index 100% rename from cvmts/src/NetworkClient.ts rename to cvmts/src/net/NetworkClient.ts diff --git a/cvmts/src/NetworkServer.ts b/cvmts/src/net/NetworkServer.ts similarity index 88% rename from cvmts/src/NetworkServer.ts rename to cvmts/src/net/NetworkServer.ts index 1719354..2c767d3 100644 --- a/cvmts/src/NetworkServer.ts +++ b/cvmts/src/net/NetworkServer.ts @@ -1,5 +1,5 @@ import { EventEmitter } from "stream"; -import { User } from "./User"; +import { User } from "../User"; interface NetworkServerEvents extends EventEmitter { on(event: 'connect', listener: (user: User) => void): this; diff --git a/cvmts/src/TCP/TCPClient.ts b/cvmts/src/net/tcp/TCPClient.ts similarity index 100% rename from cvmts/src/TCP/TCPClient.ts rename to cvmts/src/net/tcp/TCPClient.ts diff --git a/cvmts/src/TCP/TCPServer.ts b/cvmts/src/net/tcp/TCPServer.ts similarity index 88% rename from cvmts/src/TCP/TCPServer.ts rename to cvmts/src/net/tcp/TCPServer.ts index 0277269..58a12be 100644 --- a/cvmts/src/TCP/TCPServer.ts +++ b/cvmts/src/net/tcp/TCPServer.ts @@ -4,12 +4,12 @@ import EventEmitter from 'events'; import { NetworkServer } from '../NetworkServer.js'; import { Server, Socket } from 'net'; -import IConfig from '../IConfig.js'; +import IConfig from '../../IConfig.js'; import TCPClient from './TCPClient.js'; -import { IPDataManager } from '../IPData.js'; -import { User } from '../User.js'; +import { IPDataManager } from '../../IPData.js'; +import { User } from '../../User.js'; import pino from 'pino'; -import { BanManager } from '../BanManager.js'; +import { BanManager } from '../../BanManager.js'; export default class TCPServer extends EventEmitter implements NetworkServer { listener: Server; diff --git a/cvmts/src/WebSocket/WSClient.ts b/cvmts/src/net/ws/WSClient.ts similarity index 100% rename from cvmts/src/WebSocket/WSClient.ts rename to cvmts/src/net/ws/WSClient.ts diff --git a/cvmts/src/WebSocket/WSServer.ts b/cvmts/src/net/ws/WSServer.ts similarity index 96% rename from cvmts/src/WebSocket/WSServer.ts rename to cvmts/src/net/ws/WSServer.ts index b549eb9..7660424 100644 --- a/cvmts/src/WebSocket/WSServer.ts +++ b/cvmts/src/net/ws/WSServer.ts @@ -3,13 +3,13 @@ import { NetworkServer } from '../NetworkServer.js'; import EventEmitter from 'events'; import { WebSocketServer, WebSocket } from 'ws'; import internal from 'stream'; -import IConfig from '../IConfig.js'; +import IConfig from '../../IConfig.js'; import { isIP } from 'net'; -import { IPDataManager } from '../IPData.js'; +import { IPDataManager } from '../../IPData.js'; import WSClient from './WSClient.js'; -import { User } from '../User.js'; +import { User } from '../../User.js'; import pino from 'pino'; -import { BanManager } from '../BanManager.js'; +import { BanManager } from '../../BanManager.js'; export default class WSServer extends EventEmitter implements NetworkServer { private httpServer: http.Server; diff --git a/cvmts/src/BinRectsProtocol.ts b/cvmts/src/protocol/BinRectsProtocol.ts similarity index 100% rename from cvmts/src/BinRectsProtocol.ts rename to cvmts/src/protocol/BinRectsProtocol.ts diff --git a/cvmts/src/GuacamoleProtocol.ts b/cvmts/src/protocol/GuacamoleProtocol.ts similarity index 99% rename from cvmts/src/GuacamoleProtocol.ts rename to cvmts/src/protocol/GuacamoleProtocol.ts index be41f01..7dfb551 100644 --- a/cvmts/src/GuacamoleProtocol.ts +++ b/cvmts/src/protocol/GuacamoleProtocol.ts @@ -1,6 +1,6 @@ import pino from 'pino'; import { IProtocol, IProtocolHandlers, ListEntry, ProtocolAddUser, ProtocolBase, ProtocolChatHistory, ProtocolFlag, ProtocolRenameStatus, ProtocolUpgradeCapability, ScreenRect } from './Protocol.js'; -import { Rank, User } from './User'; +import { Rank, User } from '../User.js'; import * as cvm from '@cvmts/cvm-rs'; diff --git a/cvmts/src/Protocol.ts b/cvmts/src/protocol/Protocol.ts similarity index 99% rename from cvmts/src/Protocol.ts rename to cvmts/src/protocol/Protocol.ts index 0f45fcb..c7da3f8 100644 --- a/cvmts/src/Protocol.ts +++ b/cvmts/src/protocol/Protocol.ts @@ -1,4 +1,4 @@ -import { Rank, User } from './User'; +import { Rank, User } from '../User'; // We should probably put this in the binproto repository or something export enum ProtocolUpgradeCapability { From a8d32f05550ebeca941711ddb8e93d534140195c Mon Sep 17 00:00:00 2001 From: modeco80 Date: Thu, 22 Aug 2024 04:29:17 -0400 Subject: [PATCH 13/20] move ProtocolManager into its own unit --- cvmts/src/CollabVMServer.ts | 14 +++++------ cvmts/src/User.ts | 3 ++- cvmts/src/index.ts | 2 +- cvmts/src/protocol/GuacamoleProtocol.ts | 2 +- cvmts/src/protocol/Manager.ts | 27 ++++++++++++++++++++ cvmts/src/protocol/Protocol.ts | 33 +++---------------------- 6 files changed, 42 insertions(+), 39 deletions(-) create mode 100644 cvmts/src/protocol/Manager.ts diff --git a/cvmts/src/CollabVMServer.ts b/cvmts/src/CollabVMServer.ts index 019e333..9e67275 100644 --- a/cvmts/src/CollabVMServer.ts +++ b/cvmts/src/CollabVMServer.ts @@ -17,7 +17,8 @@ import { ReaderModel } from '@maxmind/geoip2-node'; import { Size, Rect } from './Utilities.js'; import pino from 'pino'; import { BanManager } from './BanManager.js'; -import { IProtocolHandlers, ListEntry, ProtocolAddUser, ProtocolFlag, ProtocolRenameStatus, ProtocolUpgradeCapability, TheProtocolManager } from './protocol/Protocol.js'; +import { IProtocolMessageHandler, ListEntry, ProtocolAddUser, ProtocolFlag, ProtocolRenameStatus, ProtocolUpgradeCapability } from './protocol/Protocol.js'; +import { TheProtocolManager } from './protocol/Manager.js'; // Instead of strange hacks we can just use nodejs provided // import.meta properties, which have existed since LTS if not before @@ -37,7 +38,7 @@ type VoteTally = { no: number; }; -export default class CollabVMServer implements IProtocolHandlers { +export default class CollabVMServer implements IProtocolMessageHandler { private Config: IConfig; private clients: User[]; @@ -184,7 +185,7 @@ export default class CollabVMServer implements IProtocolHandlers { user.socket.on('disconnect', () => this.connectionClosed(user)); // Set ourselves as the handler - user.protocol.setHandler(this as IProtocolHandlers); + user.protocol.setHandler(this as IProtocolMessageHandler); if (this.Config.auth.enabled) { user.protocol.sendAuth(this.Config.auth.apiEndpoint); @@ -223,7 +224,7 @@ export default class CollabVMServer implements IProtocolHandlers { this.clients.forEach((c) => c.protocol.sendRemUser([user.username!])); } - // IProtocolHandlers + // Protocol message handlers // does auth check private authCheck(user: User, guestPermission: boolean) { @@ -318,7 +319,7 @@ export default class CollabVMServer implements IProtocolHandlers { user.Capabilities.bin = true; user.protocol.dispose(); user.protocol = TheProtocolManager.createProtocol('binary1', user); - user.protocol.setHandler(this as IProtocolHandlers); + user.protocol.setHandler(this as IProtocolMessageHandler); break; default: break; @@ -706,11 +707,10 @@ export default class CollabVMServer implements IProtocolHandlers { this.clients.forEach((c) => c.protocol.sendChatMessage('', message)); } - // end IProtocolHandlers + // end protocol message handlers getUsernameList(): string[] { var arr: string[] = []; - this.clients.filter((c) => c.username).forEach((c) => arr.push(c.username!)); return arr; } diff --git a/cvmts/src/User.ts b/cvmts/src/User.ts index 8a539a0..4efe2aa 100644 --- a/cvmts/src/User.ts +++ b/cvmts/src/User.ts @@ -8,7 +8,8 @@ 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, TheProtocolManager } from './protocol/Protocol.js'; +import { IProtocol } from './protocol/Protocol.js'; +import { TheProtocolManager } from './protocol/Manager.js'; export class User { socket: NetworkClient; diff --git a/cvmts/src/index.ts b/cvmts/src/index.ts index 5cad003..26d89ed 100644 --- a/cvmts/src/index.ts +++ b/cvmts/src/index.ts @@ -16,7 +16,7 @@ import pino from 'pino'; import { Database } from './Database.js'; import { BanManager } from './BanManager.js'; import { QemuVMShim } from './vm/qemu.js'; -import { TheProtocolManager } from './protocol/Protocol.js'; +import { TheProtocolManager } from './protocol/Manager.js'; import { GuacamoleProtocol } from './protocol/GuacamoleProtocol.js'; import { BinRectsProtocol } from './protocol/BinRectsProtocol.js'; diff --git a/cvmts/src/protocol/GuacamoleProtocol.ts b/cvmts/src/protocol/GuacamoleProtocol.ts index 7dfb551..e5c1998 100644 --- a/cvmts/src/protocol/GuacamoleProtocol.ts +++ b/cvmts/src/protocol/GuacamoleProtocol.ts @@ -1,5 +1,5 @@ import pino from 'pino'; -import { IProtocol, IProtocolHandlers, ListEntry, ProtocolAddUser, ProtocolBase, ProtocolChatHistory, ProtocolFlag, ProtocolRenameStatus, ProtocolUpgradeCapability, ScreenRect } from './Protocol.js'; +import { IProtocol, IProtocolMessageHandler, ListEntry, ProtocolAddUser, ProtocolBase, ProtocolChatHistory, ProtocolFlag, ProtocolRenameStatus, ProtocolUpgradeCapability, ScreenRect } from './Protocol.js'; import { Rank, User } from '../User.js'; import * as cvm from '@cvmts/cvm-rs'; diff --git a/cvmts/src/protocol/Manager.ts b/cvmts/src/protocol/Manager.ts new file mode 100644 index 0000000..d61b003 --- /dev/null +++ b/cvmts/src/protocol/Manager.ts @@ -0,0 +1,27 @@ +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. +export class ProtocolManager { + private protocols = new Map IProtocol>(); + + // Registers a protocol with the given name. + registerProtocol(name: string, protocolFactory: () => IProtocol) { + 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); + return proto; + } +} + +/// Global protocol manager +export let TheProtocolManager = new ProtocolManager(); diff --git a/cvmts/src/protocol/Protocol.ts b/cvmts/src/protocol/Protocol.ts index c7da3f8..e6fe4ef 100644 --- a/cvmts/src/protocol/Protocol.ts +++ b/cvmts/src/protocol/Protocol.ts @@ -40,7 +40,7 @@ export interface ProtocolFlag { } // Protocol handlers. This is implemented by a layer that wants to listen to CollabVM protocol messages. -export interface IProtocolHandlers { +export interface IProtocolMessageHandler { onNop(user: User): void; onNoFlag(user: User): void; @@ -95,7 +95,7 @@ export interface IProtocol { dispose(): void; // Sets handler object. - setHandler(handlers: IProtocolHandlers): void; + setHandler(handlers: IProtocolMessageHandler): void; // Protocol implementation stuff @@ -152,7 +152,7 @@ export interface IProtocol { // Base mixin for all concrete protocols to use. Inherit from this! export class ProtocolBase { - protected handlers: IProtocolHandlers | null = null; + protected handlers: IProtocolMessageHandler | null = null; protected user: User | null = null; init(u: User): void { @@ -164,32 +164,7 @@ export class ProtocolBase { this.handlers = null; } - setHandler(handlers: IProtocolHandlers): void { + setHandler(handlers: IProtocolMessageHandler): void { this.handlers = handlers; } } - -// 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. -export class ProtocolManager { - private protocols = new Map IProtocol>(); - - // Registers a protocol with the given name. - registerProtocol(name: string, protocolFactory: () => IProtocol) { - 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); - return proto; - } -} - -/// Global protocol manager -export let TheProtocolManager = new ProtocolManager(); From 48409a469a130a498d8cbb903fcf1e02040a9046 Mon Sep 17 00:00:00 2001 From: modeco80 Date: Thu, 22 Aug 2024 04:32:04 -0400 Subject: [PATCH 14/20] remove dead import --- cvmts/src/User.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/cvmts/src/User.ts b/cvmts/src/User.ts index 4efe2aa..45111a6 100644 --- a/cvmts/src/User.ts +++ b/cvmts/src/User.ts @@ -3,7 +3,6 @@ import * as cvm from '@cvmts/cvm-rs'; import { IPData } from './IPData.js'; import IConfig from './IConfig.js'; import RateLimiter from './RateLimiter.js'; -import { execa, execaCommand, ExecaSyncError } from 'execa'; import { NetworkClient } from './net/NetworkClient.js'; import { CollabVMCapabilities } from '@cvmts/collab-vm-1.2-binary-protocol'; import pino from 'pino'; From 4c085619ff4b72546aa46b21b88a39e07e068ef8 Mon Sep 17 00:00:00 2001 From: modeco80 Date: Fri, 23 Aug 2024 04:37:43 -0400 Subject: [PATCH 15/20] remove todo that was done already --- cvmts/src/CollabVMServer.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/cvmts/src/CollabVMServer.ts b/cvmts/src/CollabVMServer.ts index 9e67275..ab209bf 100644 --- a/cvmts/src/CollabVMServer.ts +++ b/cvmts/src/CollabVMServer.ts @@ -505,8 +505,6 @@ export default class CollabVMServer implements IProtocolMessageHandler { this.VM.GetDisplay()?.MouseEvent(x, y, buttonMask); } - // TODO: make senders for admin things - async onAdminLogin(user: User, password: string) { if (!user.LoginRateLimit.request() || !user.username) return; var sha256 = createHash('sha256'); From ab634200752d5d174958486d60ca4498520ae27d Mon Sep 17 00:00:00 2001 From: Elijah R Date: Fri, 21 Mar 2025 19:13:15 -0400 Subject: [PATCH 16/20] cvmts: rip out raw tcp transport it's really just a tech debt that nothing uses, these is pretty much no point keeping it --- config.example.toml | 7 ---- cvmts/src/IConfig.ts | 5 --- cvmts/src/index.ts | 7 ---- cvmts/src/net/tcp/TCPClient.ts | 72 ---------------------------------- cvmts/src/net/tcp/TCPServer.ts | 50 ----------------------- 5 files changed, 141 deletions(-) delete mode 100644 cvmts/src/net/tcp/TCPClient.ts delete mode 100644 cvmts/src/net/tcp/TCPServer.ts diff --git a/config.example.toml b/config.example.toml index 2506247..2698ec7 100644 --- a/config.example.toml +++ b/config.example.toml @@ -20,13 +20,6 @@ directory = "geoip/" accountID = "" licenseKey = "" -[tcp] -# Enabled the raw TCP socket server -# You usually want to leave this disabled -enabled = false -host = "0.0.0.0" -port = 6014 - [auth] # Enables the CollabVM account authentication system # Requires an authentication server (https://git.computernewb.com/collabvm/CollabVMAuthServer) diff --git a/cvmts/src/IConfig.ts b/cvmts/src/IConfig.ts index 667c509..5d0f6f5 100644 --- a/cvmts/src/IConfig.ts +++ b/cvmts/src/IConfig.ts @@ -16,11 +16,6 @@ export default interface IConfig { accountID: string; licenseKey: string; }; - tcp: { - enabled: boolean; - host: string; - port: number; - }; auth: { enabled: boolean; apiEndpoint: string; diff --git a/cvmts/src/index.ts b/cvmts/src/index.ts index eae77f6..9018c11 100644 --- a/cvmts/src/index.ts +++ b/cvmts/src/index.ts @@ -8,7 +8,6 @@ import { QemuVmDefinition } from '@computernewb/superqemu'; import AuthManager from './AuthManager.js'; import WSServer from './net/ws/WSServer.js'; import { User } from './User.js'; -import TCPServer from './net/tcp/TCPServer.js'; import VM from './vm/interface.js'; import VNCVM from './vm/vnc/VNCVM.js'; import GeoIPDownloader from './GeoIPDownloader.js'; @@ -111,11 +110,5 @@ async function start() { var WS = new WSServer(Config, banmgr); WS.on('connect', (client: User) => CVM.connectionOpened(client)); WS.start(); - - if (Config.tcp.enabled) { - var TCP = new TCPServer(Config, banmgr); - TCP.on('connect', (client: User) => CVM.connectionOpened(client)); - TCP.start(); - } } start(); diff --git a/cvmts/src/net/tcp/TCPClient.ts b/cvmts/src/net/tcp/TCPClient.ts deleted file mode 100644 index 8275596..0000000 --- a/cvmts/src/net/tcp/TCPClient.ts +++ /dev/null @@ -1,72 +0,0 @@ -import EventEmitter from 'events'; -import { NetworkClient } from '../NetworkClient.js'; -import { Socket } from 'net'; - -const TextHeader = 0; -const BinaryHeader = 1; - -export default class TCPClient extends EventEmitter implements NetworkClient { - private socket: Socket; - private cache: string; - - constructor(socket: Socket) { - super(); - this.socket = socket; - this.cache = ''; - this.socket.on('end', () => { - this.emit('disconnect'); - }); - this.socket.on('data', (data) => { - var msg = data.toString('utf-8'); - if (msg[msg.length - 1] === '\n') msg = msg.slice(0, -1); - this.cache += msg; - this.readCache(); - }); - } - - private readCache() { - for (var index = this.cache.indexOf(';'); index !== -1; index = this.cache.indexOf(';')) { - this.emit('msg', this.cache.slice(0, index + 1)); - this.cache = this.cache.slice(index + 1); - } - } - - getIP(): string { - return this.socket.remoteAddress!; - } - - send(msg: string): Promise { - return new Promise((res, rej) => { - let _msg = new Uint32Array([TextHeader, ...Buffer.from(msg, 'utf-8')]); - this.socket.write(Buffer.from(_msg), (err) => { - if (err) { - rej(err); - return; - } - res(); - }); - }); - } - - sendBinary(msg: Uint8Array): Promise { - return new Promise((res, rej) => { - let _msg = new Uint32Array([BinaryHeader, msg.length, ...msg]); - this.socket.write(Buffer.from(_msg), (err) => { - if (err) { - rej(err); - return; - } - res(); - }); - }); - } - - close(): void { - this.emit('disconnect'); - this.socket.end(); - } - - isOpen(): boolean { - return this.socket.writable; - } -} diff --git a/cvmts/src/net/tcp/TCPServer.ts b/cvmts/src/net/tcp/TCPServer.ts deleted file mode 100644 index 58a12be..0000000 --- a/cvmts/src/net/tcp/TCPServer.ts +++ /dev/null @@ -1,50 +0,0 @@ -// TODO: replace tcp protocol with smth like -// struct msg { beu32 len; char data[len] } -// (along with a length cap obviously) -import EventEmitter from 'events'; -import { NetworkServer } from '../NetworkServer.js'; -import { Server, Socket } from 'net'; -import IConfig from '../../IConfig.js'; -import TCPClient from './TCPClient.js'; -import { IPDataManager } from '../../IPData.js'; -import { User } from '../../User.js'; -import pino from 'pino'; -import { BanManager } from '../../BanManager.js'; - -export default class TCPServer extends EventEmitter implements NetworkServer { - listener: Server; - Config: IConfig; - logger = pino({ name: 'CVMTS.TCPServer' }); - clients: TCPClient[]; - private banmgr: BanManager; - - constructor(config: IConfig, banmgr: BanManager) { - super(); - this.Config = config; - this.listener = new Server(); - this.clients = []; - this.listener.on('connection', (socket) => this.onConnection(socket)); - this.banmgr = banmgr; - } - - private async onConnection(socket: Socket) { - this.logger.info(`New TCP connection from ${socket.remoteAddress}`); - if (await this.banmgr.isIPBanned(socket.remoteAddress!)) { - socket.write('6.banned;'); - socket.destroy(); - return; - } - var client = new TCPClient(socket); - this.clients.push(client); - this.emit('connect', new User(client, IPDataManager.GetIPData(client.getIP()), this.Config)); - } - - start(): void { - this.listener.listen(this.Config.tcp.port, this.Config.tcp.host, () => { - this.logger.info(`TCP server listening on ${this.Config.tcp.host}:${this.Config.tcp.port}`); - }); - } - stop(): void { - this.listener.close(); - } -} From 10dd3c2489c5759d479fc9b963bb9d0e285e1fab Mon Sep 17 00:00:00 2001 From: Elijah R Date: Fri, 21 Mar 2025 20:29:16 -0400 Subject: [PATCH 17/20] cvmts: Move initial protocol selection to transport layer --- cvmts/src/User.ts | 4 ++-- cvmts/src/net/ws/WSServer.ts | 14 ++++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/cvmts/src/User.ts b/cvmts/src/User.ts index 45111a6..e3d2054 100644 --- a/cvmts/src/User.ts +++ b/cvmts/src/User.ts @@ -37,7 +37,7 @@ export class User { private logger = pino({ name: 'CVMTS.User' }); - constructor(socket: NetworkClient, ip: IPData, config: IConfig, username?: string, node?: string) { + constructor(socket: NetworkClient, protocol: string, ip: IPData, config: IConfig, username?: string, node?: string) { this.IP = ip; this.connectedToNode = false; this.viewMode = -1; @@ -47,7 +47,7 @@ export class User { this.Capabilities = new CollabVMCapabilities(); // All clients default to the Guacamole protocol. - this.protocol = TheProtocolManager.createProtocol('guacamole', this); + this.protocol = TheProtocolManager.createProtocol(protocol, this); this.socket.on('disconnect', () => { // Unref the ip data for this connection diff --git a/cvmts/src/net/ws/WSServer.ts b/cvmts/src/net/ws/WSServer.ts index 8436e63..3fd7b67 100644 --- a/cvmts/src/net/ws/WSServer.ts +++ b/cvmts/src/net/ws/WSServer.ts @@ -11,6 +11,10 @@ import { User } from '../../User.js'; import pino from 'pino'; import { BanManager } from '../../BanManager.js'; +const kAllowedProtocols = [ + "guacamole" // Regular ol' collabvm1 protocol +] + export default class WSServer extends EventEmitter implements NetworkServer { private httpServer: http.Server; private wsServer: WebSocketServer; @@ -50,7 +54,9 @@ export default class WSServer extends EventEmitter implements NetworkServer { socket.destroy(); }; - if (req.headers['sec-websocket-protocol'] !== 'guacamole') { + let protocol = req.headers['sec-websocket-protocol']; + + if (!protocol || kAllowedProtocols.indexOf(protocol) === -1) { killConnection(); return; } @@ -131,14 +137,14 @@ export default class WSServer extends EventEmitter implements NetworkServer { this.wsServer.handleUpgrade(req, socket, head, (ws: WebSocket) => { this.wsServer.emit('connection', ws, req); - this.onConnection(ws, req, ip); + this.onConnection(ws, req, ip, protocol); }); } - private onConnection(ws: WebSocket, req: http.IncomingMessage, ip: string) { + private onConnection(ws: WebSocket, req: http.IncomingMessage, ip: string, protocol: string) { let client = new WSClient(ws, ip); this.clients.push(client); - let user = new User(client, IPDataManager.GetIPData(ip), this.Config); + let user = new User(client, protocol, IPDataManager.GetIPData(ip), this.Config); this.emit('connect', user); From 5705971be6fc6b0c7eeec6a9ccb211ab75aa1093 Mon Sep 17 00:00:00 2001 From: Elijah R Date: Fri, 21 Mar 2025 21:28:13 -0400 Subject: [PATCH 18/20] cvmts: fix rename oddities --- cvmts/src/CollabVMServer.ts | 10 +++++----- cvmts/src/protocol/GuacamoleProtocol.ts | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cvmts/src/CollabVMServer.ts b/cvmts/src/CollabVMServer.ts index 8806a62..e1cf409 100644 --- a/cvmts/src/CollabVMServer.ts +++ b/cvmts/src/CollabVMServer.ts @@ -477,7 +477,7 @@ export default class CollabVMServer implements IProtocolMessageHandler { this.renameUser(user, undefined); return; } - this.renameUser(user, newName!); + this.renameUser(user, newName); } onChat(user: User, message: string): void { @@ -719,6 +719,8 @@ export default class CollabVMServer implements IProtocolMessageHandler { let oldname: any; if (hadName) oldname = client.username; + let status = ProtocolRenameStatus.Ok; + if (!newName) { client.assignGuestName(this.getUsernameList()); } else { @@ -728,8 +730,6 @@ export default class CollabVMServer implements IProtocolMessageHandler { return; } - let status = ProtocolRenameStatus.Ok; - if (this.getUsernameList().indexOf(newName) !== -1) { client.assignGuestName(this.getUsernameList()); if (client.connectedToNode) { @@ -742,10 +742,10 @@ export default class CollabVMServer implements IProtocolMessageHandler { client.assignGuestName(this.getUsernameList()); status = ProtocolRenameStatus.UsernameNotAllowed; } else client.username = newName; - - client.protocol.sendSelfRename(status, client.username!, client.rank); } + client.protocol.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)); diff --git a/cvmts/src/protocol/GuacamoleProtocol.ts b/cvmts/src/protocol/GuacamoleProtocol.ts index e5c1998..4735a37 100644 --- a/cvmts/src/protocol/GuacamoleProtocol.ts +++ b/cvmts/src/protocol/GuacamoleProtocol.ts @@ -292,11 +292,11 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol { } sendSelfRename(status: ProtocolRenameStatus, newUsername: string, rank: Rank): void { - this.user?.sendMsg(cvm.guacEncode('rename', '0', status.toString(), newUsername, rank.toString())); + this.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, rank.toString())); + this.user?.sendMsg(cvm.guacEncode('rename', '1', oldUsername, newUsername)); } sendListResponse(list: ListEntry[]): void { From db4ddea2e753f64ef2487d31f6c4845499689173 Mon Sep 17 00:00:00 2001 From: Elijah R Date: Fri, 21 Mar 2025 21:36:02 -0400 Subject: [PATCH 19/20] cvmts: fix ban, re-add audit log --- cvmts/src/CollabVMServer.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/cvmts/src/CollabVMServer.ts b/cvmts/src/CollabVMServer.ts index e1cf409..e412771 100644 --- a/cvmts/src/CollabVMServer.ts +++ b/cvmts/src/CollabVMServer.ts @@ -17,6 +17,7 @@ import { ReaderModel } from '@maxmind/geoip2-node'; import { Size, Rect } from './Utilities.js'; import pino from 'pino'; import { BanManager } from './BanManager.js'; +import { TheAuditLog } from './AuditLog.js'; import { IProtocolMessageHandler, ListEntry, ProtocolAddUser, ProtocolFlag, ProtocolRenameStatus, ProtocolUpgradeCapability } from './protocol/Protocol.js'; import { TheProtocolManager } from './protocol/Manager.js'; @@ -552,28 +553,31 @@ export default class CollabVMServer implements IProtocolMessageHandler { async onAdminMonitor(user: User, node: string, command: string) { if (user.rank !== Rank.Admin) return; if (node !== this.Config.collabvm.node) return; + TheAuditLog.onMonitorCommand(user, command); let output = await this.VM.MonitorCommand(command); user.protocol.sendAdminMonitorResponse(String(output)); } onAdminRestore(user: User, node: string): void { if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.restore)) return; + TheAuditLog.onReset(user); this.VM.Reset(); } async onAdminReboot(user: User, node: string) { if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.reboot)) return; if (node !== this.Config.collabvm.node) return; + TheAuditLog.onReboot(user); await this.VM.Reboot(); } - onAdminBanUser(user: User, username: string): void { + async onAdminBanUser(user: User, username: string) { // Ban if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.ban)) return; - let otherUser = this.clients.find((c) => c.username === username); - if (!otherUser) return; - this.logger.info(`Banning ${otherUser.username!} (${otherUser.IP.address}) by request of ${otherUser.username!}`); - user.ban(this.banmgr); + let target = this.clients.find((c) => c.username === username); + if (!target) return; + TheAuditLog.onBan(user, target); + await target.ban(this.banmgr); } onAdminForceVote(user: User, choice: number): void { @@ -594,6 +598,7 @@ export default class CollabVMServer implements IProtocolMessageHandler { if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.kick)) return; var target = this.clients.find((c) => c.username === username); if (!target) return; + TheAuditLog.onKick(user, target); target.kick(); } From 1c088cf81491937420bea79a7f72745d22721db9 Mon Sep 17 00:00:00 2001 From: Elijah R Date: Fri, 21 Mar 2025 21:45:04 -0400 Subject: [PATCH 20/20] cvmts: protocol error logging --- cvmts/src/CollabVMServer.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cvmts/src/CollabVMServer.ts b/cvmts/src/CollabVMServer.ts index e412771..15f4ca2 100644 --- a/cvmts/src/CollabVMServer.ts +++ b/cvmts/src/CollabVMServer.ts @@ -179,6 +179,11 @@ export default class CollabVMServer implements IProtocolMessageHandler { try { user.protocol.processMessage(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); user.kick(); } });