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:
6
cvmts/src/BinRectsProtocol.ts
Normal file
6
cvmts/src/BinRectsProtocol.ts
Normal 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
|
||||||
@@ -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;
|
||||||
|
|||||||
282
cvmts/src/GuacamoleProtocol.ts
Normal file
282
cvmts/src/GuacamoleProtocol.ts
Normal 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
119
cvmts/src/Protocol.ts
Normal 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();
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user