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()); } }