Merge pull request #29 from computernewb/dev/proto_capability_rework
merge proto rework branch
This commit is contained in:
@@ -20,13 +20,6 @@ directory = "geoip/"
|
|||||||
accountID = ""
|
accountID = ""
|
||||||
licenseKey = ""
|
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]
|
[auth]
|
||||||
# Enables the CollabVM account authentication system
|
# Enables the CollabVM account authentication system
|
||||||
# Requires an authentication server (https://git.computernewb.com/collabvm/CollabVMAuthServer)
|
# Requires an authentication server (https://git.computernewb.com/collabvm/CollabVMAuthServer)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -16,11 +16,6 @@ export default interface IConfig {
|
|||||||
accountID: string;
|
accountID: string;
|
||||||
licenseKey: string;
|
licenseKey: string;
|
||||||
};
|
};
|
||||||
tcp: {
|
|
||||||
enabled: boolean;
|
|
||||||
host: string;
|
|
||||||
port: number;
|
|
||||||
};
|
|
||||||
auth: {
|
auth: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
apiEndpoint: string;
|
apiEndpoint: string;
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
export default interface NetworkClient {
|
|
||||||
getIP(): string;
|
|
||||||
send(msg: string): Promise<void>;
|
|
||||||
sendBinary(msg: Uint8Array): Promise<void>;
|
|
||||||
close(): void;
|
|
||||||
on(event: string, listener: (...args: any[]) => void): void;
|
|
||||||
off(event: string, listener: (...args: any[]) => void): void;
|
|
||||||
isOpen(): boolean;
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export default interface NetworkServer {
|
|
||||||
start(): void;
|
|
||||||
stop(): void;
|
|
||||||
on(event: string, listener: (...args: any[]) => void): void;
|
|
||||||
off(event: string, listener: (...args: any[]) => void): void;
|
|
||||||
}
|
|
||||||
@@ -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<void> {
|
|
||||||
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<void> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,11 +3,12 @@ import * as cvm from '@cvmts/cvm-rs';
|
|||||||
import { IPData } from './IPData.js';
|
import { IPData } from './IPData.js';
|
||||||
import IConfig from './IConfig.js';
|
import IConfig from './IConfig.js';
|
||||||
import RateLimiter from './RateLimiter.js';
|
import RateLimiter from './RateLimiter.js';
|
||||||
import { execa, execaCommand, ExecaSyncError } from 'execa';
|
import { NetworkClient } from './net/NetworkClient.js';
|
||||||
import NetworkClient from './NetworkClient.js';
|
|
||||||
import { CollabVMCapabilities } from '@cvmts/collab-vm-1.2-binary-protocol';
|
import { CollabVMCapabilities } from '@cvmts/collab-vm-1.2-binary-protocol';
|
||||||
import pino from 'pino';
|
import pino from 'pino';
|
||||||
import { BanManager } from './BanManager.js';
|
import { BanManager } from './BanManager.js';
|
||||||
|
import { IProtocol } from './protocol/Protocol.js';
|
||||||
|
import { TheProtocolManager } from './protocol/Manager.js';
|
||||||
|
|
||||||
export class User {
|
export class User {
|
||||||
socket: NetworkClient;
|
socket: NetworkClient;
|
||||||
@@ -22,6 +23,7 @@ export class User {
|
|||||||
Config: IConfig;
|
Config: IConfig;
|
||||||
IP: IPData;
|
IP: IPData;
|
||||||
Capabilities: CollabVMCapabilities;
|
Capabilities: CollabVMCapabilities;
|
||||||
|
protocol: IProtocol;
|
||||||
turnWhitelist: boolean = false;
|
turnWhitelist: boolean = false;
|
||||||
// Hide flag. Only takes effect if the user is logged in.
|
// Hide flag. Only takes effect if the user is logged in.
|
||||||
noFlag: boolean = false;
|
noFlag: boolean = false;
|
||||||
@@ -35,7 +37,7 @@ export class User {
|
|||||||
|
|
||||||
private logger = pino({ name: 'CVMTS.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.IP = ip;
|
||||||
this.connectedToNode = false;
|
this.connectedToNode = false;
|
||||||
this.viewMode = -1;
|
this.viewMode = -1;
|
||||||
@@ -44,6 +46,9 @@ export class User {
|
|||||||
this.msgsSent = 0;
|
this.msgsSent = 0;
|
||||||
this.Capabilities = new CollabVMCapabilities();
|
this.Capabilities = new CollabVMCapabilities();
|
||||||
|
|
||||||
|
// All clients default to the Guacamole protocol.
|
||||||
|
this.protocol = TheProtocolManager.createProtocol(protocol, this);
|
||||||
|
|
||||||
this.socket.on('disconnect', () => {
|
this.socket.on('disconnect', () => {
|
||||||
// Unref the ip data for this connection
|
// Unref the ip data for this connection
|
||||||
this.IP.Unref();
|
this.IP.Unref();
|
||||||
@@ -52,11 +57,6 @@ export class User {
|
|||||||
clearInterval(this.msgRecieveInterval);
|
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.nopSendInterval = setInterval(() => this.sendNop(), 5000);
|
||||||
this.msgRecieveInterval = setInterval(() => this.onNoMsg(), 10000);
|
this.msgRecieveInterval = setInterval(() => this.onNoMsg(), 10000);
|
||||||
@@ -84,8 +84,14 @@ export class User {
|
|||||||
return username;
|
return username;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onNop() {
|
||||||
|
clearTimeout(this.nopRecieveTimeout);
|
||||||
|
clearInterval(this.msgRecieveInterval);
|
||||||
|
this.msgRecieveInterval = setInterval(() => this.onNoMsg(), 10000);
|
||||||
|
}
|
||||||
|
|
||||||
sendNop() {
|
sendNop() {
|
||||||
this.socket.send('3.nop;');
|
this.protocol.sendNop();
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMsg(msg: string) {
|
sendMsg(msg: string) {
|
||||||
@@ -107,7 +113,7 @@ export class User {
|
|||||||
this.socket.close();
|
this.socket.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
onMsgSent() {
|
onChatMsgSent() {
|
||||||
if (!this.Config.collabvm.automute.enabled) return;
|
if (!this.Config.collabvm.automute.enabled) return;
|
||||||
// rate limit guest and unregistered chat messages, but not staff ones
|
// rate limit guest and unregistered chat messages, but not staff ones
|
||||||
switch (this.rank) {
|
switch (this.rank) {
|
||||||
@@ -153,5 +159,5 @@ export enum Rank {
|
|||||||
// After all these years
|
// After all these years
|
||||||
Registered = 1,
|
Registered = 1,
|
||||||
Admin = 2,
|
Admin = 2,
|
||||||
Moderator = 3,
|
Moderator = 3
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ import CollabVMServer from './CollabVMServer.js';
|
|||||||
import { QemuVmDefinition } from '@computernewb/superqemu';
|
import { QemuVmDefinition } from '@computernewb/superqemu';
|
||||||
|
|
||||||
import AuthManager from './AuthManager.js';
|
import AuthManager from './AuthManager.js';
|
||||||
import WSServer from './WebSocket/WSServer.js';
|
import WSServer from './net/ws/WSServer.js';
|
||||||
import { User } from './User.js';
|
import { User } from './User.js';
|
||||||
import TCPServer from './TCP/TCPServer.js';
|
|
||||||
import VM from './vm/interface.js';
|
import VM from './vm/interface.js';
|
||||||
import VNCVM from './vm/vnc/VNCVM.js';
|
import VNCVM from './vm/vnc/VNCVM.js';
|
||||||
import GeoIPDownloader from './GeoIPDownloader.js';
|
import GeoIPDownloader from './GeoIPDownloader.js';
|
||||||
@@ -16,6 +15,9 @@ import pino from 'pino';
|
|||||||
import { Database } from './Database.js';
|
import { Database } from './Database.js';
|
||||||
import { BanManager } from './BanManager.js';
|
import { BanManager } from './BanManager.js';
|
||||||
import { QemuVMShim } from './vm/qemu.js';
|
import { QemuVMShim } from './vm/qemu.js';
|
||||||
|
import { TheProtocolManager } from './protocol/Manager.js';
|
||||||
|
import { GuacamoleProtocol } from './protocol/GuacamoleProtocol.js';
|
||||||
|
import { BinRectsProtocol } from './protocol/BinRectsProtocol.js';
|
||||||
|
|
||||||
let logger = pino();
|
let logger = pino();
|
||||||
|
|
||||||
@@ -97,18 +99,16 @@ async function start() {
|
|||||||
process.on('SIGINT', async () => await stop());
|
process.on('SIGINT', async () => await stop());
|
||||||
process.on('SIGTERM', async () => await stop());
|
process.on('SIGTERM', async () => await stop());
|
||||||
|
|
||||||
|
// Register protocol(s) that the server supports
|
||||||
|
TheProtocolManager.registerProtocol("guacamole", () => new GuacamoleProtocol);
|
||||||
|
TheProtocolManager.registerProtocol("binary1", () => new BinRectsProtocol);
|
||||||
|
|
||||||
await VM.Start();
|
await VM.Start();
|
||||||
// Start up the server
|
// Start up the server
|
||||||
var CVM = new CollabVMServer(Config, VM, banmgr, auth, geoipReader);
|
var CVM = new CollabVMServer(Config, VM, banmgr, auth, geoipReader);
|
||||||
|
|
||||||
var WS = new WSServer(Config, banmgr);
|
var WS = new WSServer(Config, banmgr);
|
||||||
WS.on('connect', (client: User) => CVM.addUser(client));
|
WS.on('connect', (client: User) => CVM.connectionOpened(client));
|
||||||
WS.start();
|
WS.start();
|
||||||
|
|
||||||
if (Config.tcp.enabled) {
|
|
||||||
var TCP = new TCPServer(Config, banmgr);
|
|
||||||
TCP.on('connect', (client: User) => CVM.addUser(client));
|
|
||||||
TCP.start();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
start();
|
start();
|
||||||
|
|||||||
14
cvmts/src/net/NetworkClient.ts
Normal file
14
cvmts/src/net/NetworkClient.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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<void>;
|
||||||
|
sendBinary(msg: Uint8Array): Promise<void>;
|
||||||
|
close(): void;
|
||||||
|
isOpen(): boolean;
|
||||||
|
}
|
||||||
11
cvmts/src/net/NetworkServer.ts
Normal file
11
cvmts/src/net/NetworkServer.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { WebSocket } from 'ws';
|
import { WebSocket } from 'ws';
|
||||||
import NetworkClient from '../NetworkClient.js';
|
import { NetworkClient } from '../NetworkClient.js';
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import pino from 'pino';
|
import pino from 'pino';
|
||||||
|
|
||||||
export default class WSClient extends EventEmitter implements NetworkClient {
|
export default class WSClient extends EventEmitter implements NetworkClient {
|
||||||
socket: WebSocket;
|
socket: WebSocket;
|
||||||
ip: string;
|
ip: string;
|
||||||
|
enforceTextOnly = true
|
||||||
private logger = pino({ name: "CVMTS.WebsocketClient" });
|
private logger = pino({ name: "CVMTS.WebsocketClient" });
|
||||||
|
|
||||||
constructor(ws: WebSocket, ip: string) {
|
constructor(ws: WebSocket, ip: string) {
|
||||||
@@ -13,13 +14,14 @@ export default class WSClient extends EventEmitter implements NetworkClient {
|
|||||||
this.socket = ws;
|
this.socket = ws;
|
||||||
this.ip = ip;
|
this.ip = ip;
|
||||||
this.socket.on('message', (buf: Buffer, isBinary: boolean) => {
|
this.socket.on('message', (buf: Buffer, isBinary: boolean) => {
|
||||||
// Close the user's connection if they send a non-string message
|
// Close the user's connection if they send a binary message
|
||||||
if (isBinary) {
|
// when we are not expecting them yet.
|
||||||
|
if (isBinary && this.enforceTextOnly) {
|
||||||
this.close();
|
this.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emit('msg', buf.toString('utf-8'));
|
this.emit('msg', buf, isBinary);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.socket.on('error', (err: Error) => {
|
this.socket.on('error', (err: Error) => {
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import NetworkServer from '../NetworkServer.js';
|
import { NetworkServer } from '../NetworkServer.js';
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import { WebSocketServer, WebSocket } from 'ws';
|
import { WebSocketServer, WebSocket } from 'ws';
|
||||||
import internal from 'stream';
|
import internal from 'stream';
|
||||||
import IConfig from '../IConfig.js';
|
import IConfig from '../../IConfig.js';
|
||||||
import { isIP } from 'net';
|
import { isIP } from 'net';
|
||||||
import { IPDataManager } from '../IPData.js';
|
import { IPDataManager } from '../../IPData.js';
|
||||||
import WSClient from './WSClient.js';
|
import WSClient from './WSClient.js';
|
||||||
import { User } from '../User.js';
|
import { User } from '../../User.js';
|
||||||
import pino from 'pino';
|
import pino from 'pino';
|
||||||
import { BanManager } from '../BanManager.js';
|
import { BanManager } from '../../BanManager.js';
|
||||||
|
|
||||||
|
const kAllowedProtocols = [
|
||||||
|
"guacamole" // Regular ol' collabvm1 protocol
|
||||||
|
]
|
||||||
|
|
||||||
export default class WSServer extends EventEmitter implements NetworkServer {
|
export default class WSServer extends EventEmitter implements NetworkServer {
|
||||||
private httpServer: http.Server;
|
private httpServer: http.Server;
|
||||||
@@ -50,7 +54,9 @@ export default class WSServer extends EventEmitter implements NetworkServer {
|
|||||||
socket.destroy();
|
socket.destroy();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (req.headers['sec-websocket-protocol'] !== 'guacamole') {
|
let protocol = req.headers['sec-websocket-protocol'];
|
||||||
|
|
||||||
|
if (!protocol || kAllowedProtocols.indexOf(protocol) === -1) {
|
||||||
killConnection();
|
killConnection();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -131,14 +137,14 @@ export default class WSServer extends EventEmitter implements NetworkServer {
|
|||||||
|
|
||||||
this.wsServer.handleUpgrade(req, socket, head, (ws: WebSocket) => {
|
this.wsServer.handleUpgrade(req, socket, head, (ws: WebSocket) => {
|
||||||
this.wsServer.emit('connection', ws, req);
|
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);
|
let client = new WSClient(ws, ip);
|
||||||
this.clients.push(client);
|
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);
|
this.emit('connect', user);
|
||||||
|
|
||||||
16
cvmts/src/protocol/BinRectsProtocol.ts
Normal file
16
cvmts/src/protocol/BinRectsProtocol.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import * as msgpack from 'msgpackr';
|
||||||
|
import { CollabVMProtocolMessage, CollabVMProtocolMessageType } from '@cvmts/collab-vm-1.2-binary-protocol';
|
||||||
|
import { GuacamoleProtocol } from './GuacamoleProtocol.js';
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
351
cvmts/src/protocol/GuacamoleProtocol.ts
Normal file
351
cvmts/src/protocol/GuacamoleProtocol.ts
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
import pino from 'pino';
|
||||||
|
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';
|
||||||
|
|
||||||
|
// CollabVM protocol implementation for Guacamole.
|
||||||
|
export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
|
||||||
|
private logger = pino({
|
||||||
|
name: 'CVMTS.GuacamoleProtocol'
|
||||||
|
});
|
||||||
|
|
||||||
|
private __processMessage_admin(decodedElements: string[]): boolean {
|
||||||
|
switch (decodedElements[1]) {
|
||||||
|
case '2':
|
||||||
|
if (decodedElements.length !== 3) return false;
|
||||||
|
this.handlers?.onAdminLogin(this.user!, decodedElements[2]);
|
||||||
|
break;
|
||||||
|
case '5':
|
||||||
|
if (decodedElements.length !== 4) return false;
|
||||||
|
this.handlers?.onAdminMonitor(this.user!, decodedElements[2], decodedElements[3]);
|
||||||
|
break;
|
||||||
|
case '8':
|
||||||
|
if (decodedElements.length !== 3) return false;
|
||||||
|
this.handlers?.onAdminRestore(this.user!, decodedElements[2]);
|
||||||
|
break;
|
||||||
|
case '10':
|
||||||
|
if (decodedElements.length !== 3) return false;
|
||||||
|
this.handlers?.onAdminReboot(this.user!, decodedElements[2]);
|
||||||
|
break;
|
||||||
|
case '12':
|
||||||
|
if (decodedElements.length < 3) return false;
|
||||||
|
this.handlers?.onAdminBanUser(this.user!, decodedElements[2]);
|
||||||
|
case '13':
|
||||||
|
{
|
||||||
|
if (decodedElements.length !== 3) return false;
|
||||||
|
let choice = parseInt(decodedElements[2]);
|
||||||
|
if (choice == undefined) return false;
|
||||||
|
this.handlers?.onAdminForceVote(this.user!, choice);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '14':
|
||||||
|
{
|
||||||
|
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':
|
||||||
|
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':
|
||||||
|
if (decodedElements.length !== 3) return false;
|
||||||
|
this.handlers?.onAdminClearQueue(this.user!, decodedElements[2]);
|
||||||
|
break;
|
||||||
|
case '18':
|
||||||
|
if (decodedElements.length !== 4) return false;
|
||||||
|
this.handlers?.onAdminRename(this.user!, decodedElements[2], decodedElements[3]);
|
||||||
|
break;
|
||||||
|
case '19':
|
||||||
|
if (decodedElements.length !== 3) return false;
|
||||||
|
this.handlers?.onAdminGetIP(this.user!, decodedElements[2]);
|
||||||
|
break;
|
||||||
|
case '20':
|
||||||
|
this.handlers?.onAdminBypassTurn(this.user!);
|
||||||
|
break;
|
||||||
|
case '21':
|
||||||
|
if (decodedElements.length !== 3) return false;
|
||||||
|
this.handlers?.onAdminRawMessage(this.user!, decodedElements[2]);
|
||||||
|
break;
|
||||||
|
case '22':
|
||||||
|
{
|
||||||
|
// 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':
|
||||||
|
this.handlers?.onAdminIndefiniteTurn(this.user!);
|
||||||
|
break;
|
||||||
|
case '24':
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
this.handlers?.onAdminSystemMessage(this.user!, decodedElements[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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
sendCapabilities(caps: ProtocolUpgradeCapability[]): void {
|
||||||
|
let arr = ['cap', ...caps];
|
||||||
|
this?.user?.sendMsg(cvm.guacEncode(...arr));
|
||||||
|
}
|
||||||
|
|
||||||
|
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!));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
sendChatHistoryMessage(history: ProtocolChatHistory[]): void {
|
||||||
|
let arr = ['chat'];
|
||||||
|
for (let a of history) {
|
||||||
|
arr.push(a.user, 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
sendRename(oldUsername: string, newUsername: string, rank: Rank): void {
|
||||||
|
this.user?.sendMsg(cvm.guacEncode('rename', '1', oldUsername, newUsername));
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
27
cvmts/src/protocol/Manager.ts
Normal file
27
cvmts/src/protocol/Manager.ts
Normal file
@@ -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<String, () => 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();
|
||||||
170
cvmts/src/protocol/Protocol.ts
Normal file
170
cvmts/src/protocol/Protocol.ts
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { Rank, User } from '../User';
|
||||||
|
|
||||||
|
// We should probably put this in the binproto repository or something
|
||||||
|
export enum ProtocolUpgradeCapability {
|
||||||
|
BinRects = 'bin'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ProtocolRenameStatus {
|
||||||
|
Ok = 0,
|
||||||
|
UsernameTaken = 1,
|
||||||
|
UsernameInvalid = 2,
|
||||||
|
UsernameNotAllowed = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 IProtocolMessageHandler {
|
||||||
|
onNop(user: User): void;
|
||||||
|
|
||||||
|
onNoFlag(user: User): void;
|
||||||
|
|
||||||
|
// Called when the client requests a capability upgrade
|
||||||
|
onCapabilityUpgrade(user: User, capability: Array<String>): 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;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
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 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;
|
||||||
|
dispose(): void;
|
||||||
|
|
||||||
|
// Sets handler object.
|
||||||
|
setHandler(handlers: IProtocolMessageHandler): void;
|
||||||
|
|
||||||
|
// Protocol implementation stuff
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
sendNop(): void;
|
||||||
|
sendSync(now: number): void;
|
||||||
|
|
||||||
|
sendAuth(authServer: string): void;
|
||||||
|
|
||||||
|
sendCapabilities(caps: ProtocolUpgradeCapability[]): void;
|
||||||
|
|
||||||
|
sendConnectFailResponse(): void;
|
||||||
|
sendConnectOKResponse(votes: boolean): void;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
sendVoteCooldown(ms: number): void;
|
||||||
|
|
||||||
|
sendScreenResize(width: number, height: number): void;
|
||||||
|
|
||||||
|
// Sends a rectangle update to the user.
|
||||||
|
sendScreenUpdate(rect: ScreenRect): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base mixin for all concrete protocols to use. Inherit from this!
|
||||||
|
export class ProtocolBase {
|
||||||
|
protected handlers: IProtocolMessageHandler | null = null;
|
||||||
|
protected user: User | null = null;
|
||||||
|
|
||||||
|
init(u: User): void {
|
||||||
|
this.user = u;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
this.user = null;
|
||||||
|
this.handlers = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setHandler(handlers: IProtocolMessageHandler): void {
|
||||||
|
this.handlers = handlers;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user