From 5dc53116b2fc1993716c3a3877ca3fe3de59d6bc Mon Sep 17 00:00:00 2001 From: modeco80 Date: Thu, 22 Aug 2024 03:50:04 -0400 Subject: [PATCH] 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>();