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
This commit is contained in:
modeco80
2024-08-21 07:10:58 -04:00
parent 3c4ddb72b8
commit 1c062697b9
7 changed files with 926 additions and 514 deletions

View File

@@ -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

View File

@@ -14,12 +14,11 @@ import AuthManager from './AuthManager.js';
import { JPEGEncoder } from './JPEGEncoder.js'; import { JPEGEncoder } from './JPEGEncoder.js';
import VM from './vm/interface.js'; import VM from './vm/interface.js';
import { ReaderModel } from '@maxmind/geoip2-node'; 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 { Size, Rect } from './Utilities.js';
import pino from 'pino'; import pino from 'pino';
import { BanManager } from './BanManager.js'; import { BanManager } from './BanManager.js';
import { IProtocolHandlers, ListEntry, ProtocolAddUser, TheProtocolManager } from './Protocol.js';
// Instead of strange hacks we can just use nodejs provided // Instead of strange hacks we can just use nodejs provided
// import.meta properties, which have existed since LTS if not before // import.meta properties, which have existed since LTS if not before
@@ -39,7 +38,7 @@ type VoteTally = {
no: number; no: number;
}; };
export default class CollabVMServer { export default class CollabVMServer implements IProtocolHandlers {
private Config: IConfig; private Config: IConfig;
private clients: User[]; private clients: User[];
@@ -76,8 +75,8 @@ export default class CollabVMServer {
private screenHidden: boolean; private screenHidden: boolean;
// base64 image to show when the screen is hidden // base64 image to show when the screen is hidden
private screenHiddenImg: string; private screenHiddenImg: Buffer;
private screenHiddenThumb: string; private screenHiddenThumb: Buffer;
// Indefinite turn // Indefinite turn
private indefiniteTurn: User | null; private indefiniteTurn: User | null;
@@ -109,8 +108,8 @@ export default class CollabVMServer {
this.voteCooldown = 0; this.voteCooldown = 0;
this.turnsAllowed = true; this.turnsAllowed = true;
this.screenHidden = false; this.screenHidden = false;
this.screenHiddenImg = readFileSync(path.join(kCVMTSAssetsRoot, 'screenhidden.jpeg')).toString('base64'); this.screenHiddenImg = readFileSync(path.join(kCVMTSAssetsRoot, 'screenhidden.jpeg'));
this.screenHiddenThumb = readFileSync(path.join(kCVMTSAssetsRoot, 'screenhiddenthumb.jpeg')).toString('base64'); this.screenHiddenThumb = readFileSync(path.join(kCVMTSAssetsRoot, 'screenhiddenthumb.jpeg'));
this.indefiniteTurn = null; this.indefiniteTurn = null;
this.ModPerms = Utilities.MakeModPerms(this.Config.collabvm.moderatorPermissions); this.ModPerms = Utilities.MakeModPerms(this.Config.collabvm.moderatorPermissions);
@@ -158,7 +157,7 @@ export default class CollabVMServer {
this.banmgr = banmgr; this.banmgr = banmgr;
} }
public addUser(user: User) { public connectionOpened(user: User) {
let sameip = this.clients.filter((c) => c.IP.address === user.IP.address); let sameip = this.clients.filter((c) => c.IP.address === user.IP.address);
if (sameip.length >= this.Config.collabvm.maxConnections) { if (sameip.length >= this.Config.collabvm.maxConnections) {
// Kick the oldest client // Kick the oldest client
@@ -166,6 +165,7 @@ export default class CollabVMServer {
sameip[0].kick(); sameip[0].kick();
} }
this.clients.push(user); this.clients.push(user);
if (this.Config.geoip.enabled) { if (this.Config.geoip.enabled) {
try { try {
user.countryCode = this.geoipReader!.country(user.IP.address).country!.isoCode; 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}`); 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));
user.socket.on('disconnect', () => this.connectionClosed(user)); // TODO: we should probably just make this a buffer arg lol..
if (this.Config.auth.enabled) { user.socket.on('msg', (msg: string) => {
user.sendMsg(cvm.guacEncode('auth', this.Config.auth.apiEndpoint)); let buf = Buffer.from(msg);
try {
user.protocol.processMessage(buf);
} catch (err) {
user.kick();
} }
user.sendMsg(this.getAdduserMsg()); });
user.socket.on('disconnect', () => this.connectionClosed(user));
// Set ourselves as the handler
user.protocol.setHandler(this as IProtocolHandlers);
if (this.Config.auth.enabled) {
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()); if (this.Config.geoip.enabled) user.sendMsg(this.getFlagMsg());
} }
@@ -203,25 +219,42 @@ export default class CollabVMServer {
if (hadturn) this.nextTurn(); 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
try {
var msgArr = cvm.guacDecode(message); // does auth check
if (msgArr.length < 1) return; private authCheck(user: User, guestPermission: boolean) {
switch (msgArr[0]) { if (!this.Config.auth.enabled) return true;
case 'login':
if (msgArr.length !== 2 || !this.Config.auth.enabled) return; if (user.rank === Rank.Unregistered && !guestPermission) {
if (!client.connectedToNode) { user.protocol.sendChatMessage('', 'You need to login to do that.');
client.sendMsg(cvm.guacEncode('login', '0', 'You must connect to the VM before logging in.')); 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; return;
} }
try { try {
let res = await this.auth!.Authenticate(msgArr[1], client); let res = await this.auth!.Authenticate(token, user);
if (res.clientSuccess) { if (res.clientSuccess) {
this.logger.info(`${client.IP.address} logged in as ${res.username}`); this.logger.info(`${user.IP.address} logged in as ${res.username}`);
client.sendMsg(cvm.guacEncode('login', '1')); user.protocol.sendLoginResponse(true, '');
let old = this.clients.find((c) => c.username === res.username); let old = this.clients.find((c) => c.username === res.username);
if (old) { if (old) {
// kick() doesnt wait until the user is actually removed from the list and itd be anal to make it do that // kick() doesnt wait until the user is actually removed from the list and itd be anal to make it do that
@@ -230,327 +263,312 @@ export default class CollabVMServer {
await old.kick(); await old.kick();
} }
// Set username // Set username
if (client.countryCode !== null && client.noFlag) { if (user.countryCode !== null && user.noFlag) {
// privacy // privacy
for (let cl of this.clients.filter((c) => c !== client)) { for (let cl of this.clients.filter((c) => c !== user)) {
cl.sendMsg(cvm.guacEncode('remuser', '1', client.username!)); cl.sendMsg(cvm.guacEncode('remuser', '1', user.username!));
} }
this.renameUser(client, res.username, false); this.renameUser(user, res.username, false);
} else this.renameUser(client, res.username, true); } else this.renameUser(user, res.username, true);
// Set rank // Set rank
client.rank = res.rank; user.rank = res.rank;
if (client.rank === Rank.Admin) { if (user.rank === Rank.Admin) {
client.sendMsg(cvm.guacEncode('admin', '0', '1')); user.sendMsg(cvm.guacEncode('admin', '0', '1'));
} else if (client.rank === Rank.Moderator) { } else if (user.rank === Rank.Moderator) {
client.sendMsg(cvm.guacEncode('admin', '0', '3', this.ModPerms.toString())); user.sendMsg(cvm.guacEncode('admin', '0', '3', this.ModPerms.toString()));
} }
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('adduser', '1', client.username!, client.rank.toString()))); this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('adduser', '1', user.username!, user.rank.toString())));
} else { } else {
client.sendMsg(cvm.guacEncode('login', '0', res.error!)); user.protocol.sendLoginResponse(false, res.error!);
if (res.error === 'You are banned') { if (res.error === 'You are banned') {
client.kick(); user.kick();
} }
} }
} catch (err) { } catch (err) {
this.logger.error(`Error authenticating client ${client.IP.address}: ${(err as Error).message}`); this.logger.error(`Error authenticating client ${user.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')); user.protocol.sendLoginResponse(false, '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;
}
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()));
}
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; onNoFlag(user: User) {
client.sendMsg(cvm.guacEncode('connect', '1', '1', this.VM.SnapshotsSupported() ? '1' : '0', '0')); // Too late
if (this.ChatHistory.size !== 0) client.sendMsg(this.getChatHistoryMsg()); if (user.connectedToNode) return;
if (this.Config.collabvm.motd) client.sendMsg(cvm.guacEncode('chat', '', this.Config.collabvm.motd)); user.noFlag = true;
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]) { onCapabilityUpgrade(user: User, capability: String[]): boolean {
case '0': if (user.connectedToNode) return false;
client.viewMode = 0;
break; for (let cap of capability) {
case '1': switch (cap) {
client.viewMode = 1; // 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; break;
default: default:
client.sendMsg(cvm.guacEncode('connect', '0')); break;
return; }
}
return true;
} }
client.sendMsg(cvm.guacEncode('connect', '1', '1', this.VM.SnapshotsSupported() ? '1' : '0', '0')); onTurnRequest(user: User, forfeit: boolean): void {
if (this.ChatHistory.size !== 0) client.sendMsg(this.getChatHistoryMsg()); if ((!this.turnsAllowed || this.Config.collabvm.turnwhitelist) && user.rank !== Rank.Admin && user.rank !== Rank.Moderator && !user.turnWhitelist) return;
if (this.Config.collabvm.motd) client.sendMsg(cvm.guacEncode('chat', '', this.Config.collabvm.motd));
if (client.viewMode == 1) { if (!this.authCheck(user, this.Config.auth.guestPermissions.turn)) return;
if (this.screenHidden) {
client.sendMsg(cvm.guacEncode('size', '0', '1024', '768')); if (!user.TurnRateLimit.request()) return;
client.sendMsg(cvm.guacEncode('png', '0', '0', '0', '0', this.screenHiddenImg)); 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 { } else {
await this.SendFullScreenWithSize(client); var hadturn = this.TurnQueue.peek() === user;
this.TurnQueue = Queue.from(this.TurnQueue.toArray().filter((u) => u !== user));
if (hadturn) this.nextTurn();
} }
client.sendMsg(cvm.guacEncode('sync', Date.now().toString())); this.sendTurnUpdate();
} }
if (this.voteInProgress) this.sendVoteUpdate(client); onVote(user: User, choice: number): void {
this.sendTurnUpdate(client); if (!this.VM.SnapshotsSupported()) return;
break; if ((!this.turnsAllowed || this.Config.collabvm.turnwhitelist) && user.rank !== Rank.Admin && user.rank !== Rank.Moderator && !user.turnWhitelist) return;
case 'rename': if (!user.connectedToNode) return;
if (!client.RenameRateLimit.request()) return; if (!user.VoteRateLimit.request()) return;
if (client.connectedToNode && client.IP.muted) return; switch (choice) {
if (this.Config.auth.enabled && client.rank !== Rank.Unregistered) { case 1:
client.sendMsg(cvm.guacEncode('chat', '', 'Go to your account settings to change your username.')); 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; return;
} }
if (this.Config.auth.enabled && msgArr[1] !== undefined) { 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 // 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 (user.username) user.protocol.sendChatMessage('', 'You need to log in to do that.');
if (client.rank !== Rank.Unregistered) return; if (user.rank !== Rank.Unregistered) return;
this.renameUser(client, undefined); this.renameUser(user, undefined);
return; return;
} }
this.renameUser(client, msgArr[1]); this.renameUser(user, newName!);
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]);
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 // 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.length > this.Config.collabvm.maxChatLength) msg = msg.substring(0, this.Config.collabvm.maxChatLength);
if (msg.trim().length < 1) return; if (msg.trim().length < 1) return;
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', client.username!, msg))); this.clients.forEach((c) => c.protocol.sendChatMessage(user.username!, msg));
this.ChatHistory.push({ user: client.username, msg: msg }); this.ChatHistory.push({ user: user.username, msg: msg });
client.onMsgSent(); user.onChatMsgSent();
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) { onKey(user: User, keysym: number, pressed: boolean): void {
client.sendMsg(cvm.guacEncode('vote', '3', this.voteCooldown.toString())); if (this.TurnQueue.peek() !== user && user.rank !== Rank.Admin) return;
return; this.VM.GetDisplay()?.KeyboardEvent(keysym, pressed);
} }
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));
}
case 'admin':
if (msgArr.length < 2) return;
switch (msgArr[1]) {
case '2':
// Login
if (!client.LoginRateLimit.request() || !client.username) return; onMouse(user: User, x: number, y: number, buttonMask: number): void {
if (msgArr.length !== 3) return; 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'); var sha256 = createHash('sha256');
sha256.update(msgArr[2]); sha256.update(password, 'utf-8');
var pwdHash = sha256.digest('hex'); var pwdHash = sha256.digest('hex');
sha256.destroy(); sha256.destroy();
if (this.Config.collabvm.turnwhitelist && pwdHash === this.Config.collabvm.turnpass) { if (this.Config.collabvm.turnwhitelist && pwdHash === this.Config.collabvm.turnpass) {
client.turnWhitelist = true; user.turnWhitelist = true;
client.sendMsg(cvm.guacEncode('chat', '', 'You may now take turns.')); user.protocol.sendChatMessage('', 'You may now take turns.');
return; return;
} }
if (this.Config.auth.enabled) { if (this.Config.auth.enabled) {
client.sendMsg(cvm.guacEncode('chat', '', 'This server does not support staff passwords. Please log in to become staff.')); user.protocol.sendChatMessage('', 'This server does not support staff passwords. Please log in to become staff.');
return; return;
} }
if (pwdHash === this.Config.collabvm.adminpass) { if (pwdHash === this.Config.collabvm.adminpass) {
client.rank = Rank.Admin; user.rank = Rank.Admin;
client.sendMsg(cvm.guacEncode('admin', '0', '1')); user.sendMsg(cvm.guacEncode('admin', '0', '1'));
} else if (this.Config.collabvm.moderatorEnabled && pwdHash === this.Config.collabvm.modpass) { } else if (this.Config.collabvm.moderatorEnabled && pwdHash === this.Config.collabvm.modpass) {
client.rank = Rank.Moderator; user.rank = Rank.Moderator;
client.sendMsg(cvm.guacEncode('admin', '0', '3', this.ModPerms.toString())); user.sendMsg(cvm.guacEncode('admin', '0', '3', this.ModPerms.toString()));
} else { } else {
client.sendMsg(cvm.guacEncode('admin', '0', '0')); user.sendMsg(cvm.guacEncode('admin', '0', '0'));
return; return;
} }
if (this.screenHidden) {
await this.SendFullScreenWithSize(client);
client.sendMsg(cvm.guacEncode('sync', Date.now().toString())); if (this.screenHidden) {
await this.SendFullScreenWithSize(user);
} }
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('adduser', '1', client.username!, client.rank.toString()))); 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; break;
case '5': case '5':
// QEMU Monitor // 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; break;
case '8': case '8':
// Restore // Restore
if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.restore)) return; if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.restore)) return;
this.VM.Reset(); this.VM.Reset();
break; break;
case '10': case '10':
// Reboot // Reboot
if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.reboot)) return; 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; if (msgArr.length !== 3 || msgArr[2] !== this.Config.collabvm.node) return;
await this.VM.Reboot(); await this.VM.Reboot();
break; break;
case '12': case '12':
// Ban // Ban
if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.ban)) return; if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.ban)) return;
var user = this.clients.find((c) => c.username === msgArr[2]); var otherUser = this.clients.find((c) => c.username === msgArr[2]);
if (!user) return; if (!otherUser) return;
this.logger.info(`Banning ${user.username!} (${user.IP.address}) by request of ${client.username!}`); this.logger.info(`Banning ${otherUser.username!} (${otherUser.IP.address}) by request of ${otherUser.username!}`);
user.ban(this.banmgr); user.ban(this.banmgr);
case '13': case '13':
// Force Vote // Force Vote
if (msgArr.length !== 3) return; if (msgArr.length !== 3) return;
if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.forcevote)) return; if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.forcevote)) return;
if (!this.voteInProgress) return; if (!this.voteInProgress) return;
switch (msgArr[2]) { switch (msgArr[2]) {
case '1': case '1':
@@ -605,7 +623,7 @@ export default class CollabVMServer {
// Rename user // Rename user
if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.rename)) return; if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.rename)) return;
if (this.Config.auth.enabled) { if (this.Config.auth.enabled) {
client.sendMsg(cvm.guacEncode('chat', '', 'Cannot rename users on a server that uses authentication.')); client.protocol.sendChatMessage('', 'Cannot rename users on a server that uses authentication.');
} }
if (msgArr.length !== 4) return; if (msgArr.length !== 4) return;
var user = this.clients.find((c) => c.username === msgArr[2]); var user = this.clients.find((c) => c.username === msgArr[2]);
@@ -680,8 +698,7 @@ export default class CollabVMServer {
break; break;
case '1': case '1':
this.screenHidden = false; this.screenHidden = false;
let displaySize = this.VM.GetDisplay()?.Size(); let displaySize = this.VM.GetDisplay().Size();
if (displaySize == undefined) return;
let encoded = await this.MakeRectData({ let encoded = await this.MakeRectData({
x: 0, x: 0,
@@ -699,15 +716,11 @@ export default class CollabVMServer {
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', msgArr[2]))); this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('chat', '', msgArr[2])));
break; 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();
}
} }
// end IProtocolHandlers
getUsernameList(): string[] { getUsernameList(): string[] {
var arr: string[] = []; var arr: string[] = [];
@@ -757,11 +770,15 @@ export default class CollabVMServer {
} }
} }
getAdduserMsg(): string { getAdduserMsg(): ProtocolAddUser[] {
var arr: string[] = ['adduser', this.clients.filter((c) => c.username).length.toString()]; return this.clients
.filter((c) => c.username)
this.clients.filter((c) => c.username).forEach((c) => arr.push(c.username!, c.rank.toString())); .map((c) => {
return cvm.guacEncode(...arr); return {
username: c.username!,
rank: c.rank
};
});
} }
getFlagMsg(): string { getFlagMsg(): string {
@@ -772,12 +789,6 @@ export default class CollabVMServer {
return cvm.guacEncode(...arr); 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) { private sendTurnUpdate(client?: User) {
var turnQueueArr = this.TurnQueue.toArray(); var turnQueueArr = this.TurnQueue.toArray();
var turntime; var turntime;
@@ -852,7 +863,7 @@ export default class CollabVMServer {
.filter((c) => c.connectedToNode || c.viewMode == 1) .filter((c) => c.connectedToNode || c.viewMode == 1)
.forEach((c) => { .forEach((c) => {
if (this.screenHidden && c.rank == Rank.Unregistered) return; 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 doRect = async (rect: Rect) => {
let encoded = await this.MakeRectData(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 self.clients
.filter((c) => c.connectedToNode || c.viewMode == 1) .filter((c) => c.connectedToNode || c.viewMode == 1)
.forEach((c) => { .forEach((c) => {
if (self.screenHidden && c.rank == Rank.Unregistered) return; if (self.screenHidden && c.rank == Rank.Unregistered) return;
if (c.Capabilities.bin) {
c.socket.sendBinary(encodedbin); c.protocol.sendScreenUpdate({
} else { x: rect.x,
c.sendMsg(cvm.guacEncode('png', '0', '0', rect.x.toString(), rect.y.toString(), encodedb64)); y: rect.y,
c.sendMsg(cvm.guacEncode('sync', Date.now().toString())); data: encoded
} });
}); });
}; };
@@ -908,21 +908,13 @@ export default class CollabVMServer {
height: displaySize.height 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) { client.protocol.sendScreenUpdate({
let msg: CollabVMProtocolMessage = {
type: CollabVMProtocolMessageType.rect,
rect: {
x: 0, x: 0,
y: 0, y: 0,
data: encoded data: encoded
} });
};
client.socket.sendBinary(msgpack.encode(msg));
} else {
client.sendMsg(cvm.guacEncode('png', '0', '0', '0', '0', encoded.toString('base64')));
}
} }
private async MakeRectData(rect: Rect) { private async MakeRectData(rect: Rect) {
@@ -937,14 +929,13 @@ export default class CollabVMServer {
return encoded; return encoded;
} }
async getThumbnail(): Promise<string> { async getThumbnail(): Promise<Buffer> {
let display = this.VM.GetDisplay(); let display = this.VM.GetDisplay();
// oh well // oh well
if (!display?.Connected()) return ''; if (!display?.Connected()) return Buffer.alloc(4);
let buf = await JPEGEncoder.EncodeThumbnail(display.Buffer(), display.Size()); return JPEGEncoder.EncodeThumbnail(display.Buffer(), display.Size());
return buf.toString('base64');
} }
startVote() { startVote() {
@@ -967,10 +958,10 @@ export default class CollabVMServer {
var count = this.getVoteCounts(); var count = this.getVoteCounts();
this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('vote', '2'))); this.clients.forEach((c) => c.sendMsg(cvm.guacEncode('vote', '2')));
if (result === true || (result === undefined && count.yes >= count.no)) { 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(); this.VM.Reset();
} else { } 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) => { this.clients.forEach((c) => {
c.IP.vote = null; c.IP.vote = null;

View File

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

119
cvmts/src/Protocol.ts Normal file
View File

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

View File

@@ -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 EventEmitter from 'events';
import NetworkServer from '../NetworkServer.js'; import NetworkServer from '../NetworkServer.js';
import { Server, Socket } from 'net'; import { Server, Socket } from 'net';

View File

@@ -8,6 +8,7 @@ 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, TheProtocolManager } from './Protocol.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;
@@ -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('guacamole', 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
} }

View File

@@ -16,6 +16,8 @@ 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.js';
import { GuacamoleProtocol } from './GuacamoleProtocol.js';
let logger = pino(); let logger = pino();
@@ -97,17 +99,20 @@ 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)
TheProtocolManager.registerProtocol("guacamole", () => new GuacamoleProtocol);
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) { if (Config.tcp.enabled) {
var TCP = new TCPServer(Config, banmgr); var TCP = new TCPServer(Config, banmgr);
TCP.on('connect', (client: User) => CVM.addUser(client)); TCP.on('connect', (client: User) => CVM.connectionOpened(client));
TCP.start(); TCP.start();
} }
} }