cvmts: replace guacamole decoder with a node native module written in rust

This commit is contained in:
modeco80
2024-06-19 01:36:07 -04:00
parent a4247bbcc3
commit 4e50106585
14 changed files with 1011 additions and 501 deletions

4
.gitignore vendored
View File

@@ -8,3 +8,7 @@ cvmts/attic
/dist
**/dist/
# Guac-rs
guac-rs/target
guac-rs/index.node

View File

@@ -12,6 +12,7 @@
"license": "GPL-3.0",
"dependencies": {
"@computernewb/jpeg-turbo": "*",
"@cvmts/guac-rs": "*",
"@cvmts/qemu": "*",
"execa": "^8.0.1",
"mnemonist": "^0.39.5",

View File

@@ -1,7 +1,7 @@
import IConfig from './IConfig.js';
import * as Utilities from './Utilities.js';
import { User, Rank } from './User.js';
import * as guacutils from './guacutils.js';
import * as guac from '@cvmts/guac-rs';
// I hate that you have to do it like this
import CircularBuffer from 'mnemonist/circular-buffer.js';
import Queue from 'mnemonist/queue.js';
@@ -115,7 +115,7 @@ export default class CollabVMServer {
}
public addUser(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) {
// Kick the oldest client
// I think this is a better solution than just rejecting the connection
@@ -125,7 +125,7 @@ export default class CollabVMServer {
user.socket.on('msg', (msg: string) => this.onMessage(user, msg));
user.socket.on('disconnect', () => this.connectionClosed(user));
if (this.Config.auth.enabled) {
user.sendMsg(guacutils.encode('auth', this.Config.auth.apiEndpoint));
user.sendMsg(guac.guacEncode('auth', this.Config.auth.apiEndpoint));
}
user.sendMsg(this.getAdduserMsg());
}
@@ -154,24 +154,25 @@ export default class CollabVMServer {
if (hadturn) this.nextTurn();
}
this.clients.forEach((c) => c.sendMsg(guacutils.encode('remuser', '1', user.username!)));
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('remuser', '1', user.username!)));
}
private async onMessage(client: User, message: string) {
var msgArr = guacutils.decode(message);
try {
var msgArr = guac.guacDecode(message);
if (msgArr.length < 1) return;
switch (msgArr[0]) {
case 'login':
if (msgArr.length !== 2 || !this.Config.auth.enabled) return;
if (!client.connectedToNode) {
client.sendMsg(guacutils.encode('login', '0', 'You must connect to the VM before logging in.'));
client.sendMsg(guac.guacEncode('login', '0', 'You must connect to the VM before logging in.'));
return;
}
try {
let res = await this.auth!.Authenticate(msgArr[1], client);
if (res.clientSuccess) {
this.logger.Info(`${client.IP.address} logged in as ${res.username}`);
client.sendMsg(guacutils.encode('login', '1'));
client.sendMsg(guac.guacEncode('login', '1'));
let old = this.clients.find((c) => c.username === res.username);
if (old) {
// kick() doesnt wait until the user is actually removed from the list and itd be anal to make it do that
@@ -184,13 +185,13 @@ export default class CollabVMServer {
// Set rank
client.rank = res.rank;
if (client.rank === Rank.Admin) {
client.sendMsg(guacutils.encode('admin', '0', '1'));
client.sendMsg(guac.guacEncode('admin', '0', '1'));
} else if (client.rank === Rank.Moderator) {
client.sendMsg(guacutils.encode('admin', '0', '3', this.ModPerms.toString()));
client.sendMsg(guac.guacEncode('admin', '0', '3', this.ModPerms.toString()));
}
this.clients.forEach((c) => c.sendMsg(guacutils.encode('adduser', '1', client.username!, client.rank.toString())));
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('adduser', '1', client.username!, client.rank.toString())));
} else {
client.sendMsg(guacutils.encode('login', '0', res.error!));
client.sendMsg(guac.guacEncode('login', '0', res.error!));
if (res.error === 'You are banned') {
client.kick();
}
@@ -198,28 +199,28 @@ export default class CollabVMServer {
} catch (err) {
this.logger.Error(`Error authenticating client ${client.IP.address}: ${(err as Error).message}`);
// for now?
client.sendMsg(guacutils.encode('login', '0', 'There was an internal error while authenticating. Please let a staff member know as soon as possible'))
client.sendMsg(guac.guacEncode('login', '0', 'There was an internal error while authenticating. Please let a staff member know as soon as possible'));
}
break;
case 'list':
client.sendMsg(guacutils.encode('list', this.Config.collabvm.node, this.Config.collabvm.displayname, this.screenHidden ? this.screenHiddenThumb : await this.getThumbnail()));
client.sendMsg(guac.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(guacutils.encode('connect', '0'));
client.sendMsg(guac.guacEncode('connect', '0'));
return;
}
client.connectedToNode = true;
client.sendMsg(guacutils.encode('connect', '1', '1', this.VM.SnapshotsSupported() ? '1' : '0', '0'));
client.sendMsg(guac.guacEncode('connect', '1', '1', this.VM.SnapshotsSupported() ? '1' : '0', '0'));
if (this.ChatHistory.size !== 0) client.sendMsg(this.getChatHistoryMsg());
if (this.Config.collabvm.motd) client.sendMsg(guacutils.encode('chat', '', this.Config.collabvm.motd));
if (this.Config.collabvm.motd) client.sendMsg(guac.guacEncode('chat', '', this.Config.collabvm.motd));
if (this.screenHidden) {
client.sendMsg(guacutils.encode('size', '0', '1024', '768'));
client.sendMsg(guacutils.encode('png', '0', '0', '0', '0', this.screenHiddenImg));
client.sendMsg(guac.guacEncode('size', '0', '1024', '768'));
client.sendMsg(guac.guacEncode('png', '0', '0', '0', '0', this.screenHiddenImg));
} else {
await this.SendFullScreenWithSize(client);
}
client.sendMsg(guacutils.encode('sync', Date.now().toString()));
client.sendMsg(guac.guacEncode('sync', Date.now().toString()));
if (this.voteInProgress) this.sendVoteUpdate(client);
this.sendTurnUpdate(client);
break;
@@ -227,7 +228,7 @@ export default class CollabVMServer {
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(guacutils.encode('connect', '0'));
client.sendMsg(guac.guacEncode('connect', '0'));
return;
}
@@ -239,22 +240,22 @@ export default class CollabVMServer {
client.viewMode = 1;
break;
default:
client.sendMsg(guacutils.encode('connect', '0'));
client.sendMsg(guac.guacEncode('connect', '0'));
return;
}
client.sendMsg(guacutils.encode('connect', '1', '1', this.VM.SnapshotsSupported() ? '1' : '0', '0'));
client.sendMsg(guac.guacEncode('connect', '1', '1', this.VM.SnapshotsSupported() ? '1' : '0', '0'));
if (this.ChatHistory.size !== 0) client.sendMsg(this.getChatHistoryMsg());
if (this.Config.collabvm.motd) client.sendMsg(guacutils.encode('chat', '', this.Config.collabvm.motd));
if (this.Config.collabvm.motd) client.sendMsg(guac.guacEncode('chat', '', this.Config.collabvm.motd));
if (client.viewMode == 1) {
if (this.screenHidden) {
client.sendMsg(guacutils.encode('size', '0', '1024', '768'));
client.sendMsg(guacutils.encode('png', '0', '0', '0', '0', this.screenHiddenImg));
client.sendMsg(guac.guacEncode('size', '0', '1024', '768'));
client.sendMsg(guac.guacEncode('png', '0', '0', '0', '0', this.screenHiddenImg));
} else {
await this.SendFullScreenWithSize(client);
}
client.sendMsg(guacutils.encode('sync', Date.now().toString()));
client.sendMsg(guac.guacEncode('sync', Date.now().toString()));
}
if (this.voteInProgress) this.sendVoteUpdate(client);
@@ -264,12 +265,12 @@ export default class CollabVMServer {
if (!client.RenameRateLimit.request()) return;
if (client.connectedToNode && client.IP.muted) return;
if (this.Config.auth.enabled && client.rank !== Rank.Unregistered) {
client.sendMsg(guacutils.encode('chat', '', 'Go to your account settings to change your username.'));
client.sendMsg(guac.guacEncode('chat', '', 'Go to your account settings to change your username.'));
return;
}
if (this.Config.auth.enabled && msgArr[1] !== undefined) {
// 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(guacutils.encode('chat', '', 'You need to log in to do that.'));
if (client.username) client.sendMsg(guac.guacEncode('chat', '', 'You need to log in to do that.'));
if (client.rank !== Rank.Unregistered) return;
this.renameUser(client, undefined);
return;
@@ -281,7 +282,7 @@ export default class CollabVMServer {
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(guacutils.encode('chat', '', 'You need to login to do that.'));
client.sendMsg(guac.guacEncode('chat', '', 'You need to login to do that.'));
return;
}
var msg = Utilities.HTMLSanitize(msgArr[1]);
@@ -289,14 +290,14 @@ export default class CollabVMServer {
if (msg.length > this.Config.collabvm.maxChatLength) msg = msg.substring(0, this.Config.collabvm.maxChatLength);
if (msg.trim().length < 1) return;
this.clients.forEach((c) => c.sendMsg(guacutils.encode('chat', client.username!, msg)));
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('chat', client.username!, msg)));
this.ChatHistory.push({ user: client.username, msg: msg });
client.onMsgSent();
break;
case 'turn':
if ((!this.turnsAllowed || this.Config.collabvm.turnwhitelist) && client.rank !== Rank.Admin && client.rank !== Rank.Moderator && client.rank !== Rank.Turn) return;
if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.turn) {
client.sendMsg(guacutils.encode('chat', '', 'You need to login to do that.'));
client.sendMsg(guac.guacEncode('chat', '', 'You need to login to do that.'));
return;
}
if (!client.TurnRateLimit.request()) return;
@@ -366,33 +367,33 @@ export default class CollabVMServer {
case '1':
if (!this.voteInProgress) {
if (this.Config.auth.enabled && client.rank === Rank.Unregistered && !this.Config.auth.guestPermissions.callForReset) {
client.sendMsg(guacutils.encode('chat', '', 'You need to login to do that.'));
client.sendMsg(guac.guacEncode('chat', '', 'You need to login to do that.'));
return;
}
if (this.voteCooldown !== 0) {
client.sendMsg(guacutils.encode('vote', '3', this.voteCooldown.toString()));
client.sendMsg(guac.guacEncode('vote', '3', this.voteCooldown.toString()));
return;
}
this.startVote();
this.clients.forEach((c) => c.sendMsg(guacutils.encode('chat', '', `${client.username} has started a vote to reset the VM.`)));
this.clients.forEach((c) => c.sendMsg(guac.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(guacutils.encode('chat', '', 'You need to login to do that.'));
client.sendMsg(guac.guacEncode('chat', '', 'You need to login to do that.'));
return;
} else if (client.IP.vote !== true) {
this.clients.forEach((c) => c.sendMsg(guacutils.encode('chat', '', `${client.username} has voted yes.`)));
this.clients.forEach((c) => c.sendMsg(guac.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(guacutils.encode('chat', '', 'You need to login to do that.'));
client.sendMsg(guac.guacEncode('chat', '', 'You need to login to do that.'));
return;
}
if (client.IP.vote !== false) {
this.clients.forEach((c) => c.sendMsg(guacutils.encode('chat', '', `${client.username} has voted no.`)));
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('chat', '', `${client.username} has voted no.`)));
}
client.IP.vote = false;
break;
@@ -405,7 +406,7 @@ export default class CollabVMServer {
case '2':
// Login
if (this.Config.auth.enabled) {
client.sendMsg(guacutils.encode('chat', '', 'This server does not support staff passwords. Please log in to become staff.'));
client.sendMsg(guac.guacEncode('chat', '', 'This server does not support staff passwords. Please log in to become staff.'));
return;
}
if (!client.LoginRateLimit.request() || !client.username) return;
@@ -416,37 +417,37 @@ export default class CollabVMServer {
sha256.destroy();
if (pwdHash === this.Config.collabvm.adminpass) {
client.rank = Rank.Admin;
client.sendMsg(guacutils.encode('admin', '0', '1'));
client.sendMsg(guac.guacEncode('admin', '0', '1'));
} else if (this.Config.collabvm.moderatorEnabled && pwdHash === this.Config.collabvm.modpass) {
client.rank = Rank.Moderator;
client.sendMsg(guacutils.encode('admin', '0', '3', this.ModPerms.toString()));
client.sendMsg(guac.guacEncode('admin', '0', '3', this.ModPerms.toString()));
} else if (this.Config.collabvm.turnwhitelist && pwdHash === this.Config.collabvm.turnpass) {
client.rank = Rank.Turn;
client.sendMsg(guacutils.encode('chat', '', 'You may now take turns.'));
client.sendMsg(guac.guacEncode('chat', '', 'You may now take turns.'));
} else {
client.sendMsg(guacutils.encode('admin', '0', '0'));
client.sendMsg(guac.guacEncode('admin', '0', '0'));
return;
}
if (this.screenHidden) {
await this.SendFullScreenWithSize(client);
client.sendMsg(guacutils.encode('sync', Date.now().toString()));
client.sendMsg(guac.guacEncode('sync', Date.now().toString()));
}
this.clients.forEach((c) => c.sendMsg(guacutils.encode('adduser', '1', client.username!, client.rank.toString())));
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('adduser', '1', client.username!, client.rank.toString())));
break;
case '5':
// QEMU Monitor
if (client.rank !== Rank.Admin) return;
/* Surely there could be rudimentary processing to convert some qemu monitor syntax to [XYZ hypervisor] if possible
if (!(this.VM instanceof QEMUVM)) {
client.sendMsg(guacutils.encode("admin", "2", "This is not a QEMU VM and therefore QEMU monitor commands cannot be run."));
client.sendMsg(guac.guacEncode("admin", "2", "This is not a QEMU VM and therefore QEMU monitor commands cannot be run."));
return;
}
*/
if (msgArr.length !== 4 || msgArr[2] !== this.Config.collabvm.node) return;
var output = await this.VM.MonitorCommand(msgArr[3]);
client.sendMsg(guacutils.encode('admin', '2', String(output)));
client.sendMsg(guac.guacEncode('admin', '2', String(output)));
break;
case '8':
// Restore
@@ -523,7 +524,7 @@ export default class CollabVMServer {
// Rename user
if (client.rank !== Rank.Admin && (client.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.rename)) return;
if (this.Config.auth.enabled) {
client.sendMsg(guacutils.encode('chat', '', 'Cannot rename users on a server that uses authentication.'));
client.sendMsg(guac.guacEncode('chat', '', 'Cannot rename users on a server that uses authentication.'));
}
if (msgArr.length !== 4) return;
var user = this.clients.find((c) => c.username === msgArr[2]);
@@ -536,7 +537,7 @@ export default class CollabVMServer {
if (msgArr.length !== 3) return;
var user = this.clients.find((c) => c.username === msgArr[2]);
if (!user) return;
client.sendMsg(guacutils.encode('admin', '19', msgArr[2], user.IP.address));
client.sendMsg(guac.guacEncode('admin', '19', msgArr[2], user.IP.address));
break;
case '20':
// Steal turn
@@ -549,14 +550,14 @@ export default class CollabVMServer {
if (msgArr.length !== 3) return;
switch (client.rank) {
case Rank.Admin:
this.clients.forEach((c) => c.sendMsg(guacutils.encode('chat', client.username!, msgArr[2])));
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('chat', client.username!, msgArr[2])));
this.ChatHistory.push({ user: client.username!, msg: msgArr[2] });
break;
case Rank.Moderator:
this.clients.filter((c) => c.rank !== Rank.Admin).forEach((c) => c.sendMsg(guacutils.encode('chat', client.username!, msgArr[2])));
this.clients.filter((c) => c.rank !== Rank.Admin).forEach((c) => c.sendMsg(guac.guacEncode('chat', client.username!, msgArr[2])));
this.clients.filter((c) => c.rank === Rank.Admin).forEach((c) => c.sendMsg(guacutils.encode('chat', client.username!, Utilities.HTMLSanitize(msgArr[2]))));
this.clients.filter((c) => c.rank === Rank.Admin).forEach((c) => c.sendMsg(guac.guacEncode('chat', client.username!, Utilities.HTMLSanitize(msgArr[2]))));
break;
}
break;
@@ -591,9 +592,9 @@ export default class CollabVMServer {
this.clients
.filter((c) => c.rank == Rank.Unregistered)
.forEach((client) => {
client.sendMsg(guacutils.encode('size', '0', '1024', '768'));
client.sendMsg(guacutils.encode('png', '0', '0', '0', '0', this.screenHiddenImg));
client.sendMsg(guacutils.encode('sync', Date.now().toString()));
client.sendMsg(guac.guacEncode('size', '0', '1024', '768'));
client.sendMsg(guac.guacEncode('png', '0', '0', '0', '0', this.screenHiddenImg));
client.sendMsg(guac.guacEncode('sync', Date.now().toString()));
});
break;
case '1':
@@ -608,20 +609,25 @@ export default class CollabVMServer {
});
this.clients.forEach(async (client) => {
client.sendMsg(guacutils.encode('size', '0', displaySize.width.toString(), displaySize.height.toString()));
client.sendMsg(guacutils.encode('png', '0', '0', '0', '0', encoded));
client.sendMsg(guacutils.encode('sync', Date.now().toString()));
client.sendMsg(guac.guacEncode('size', '0', displaySize.width.toString(), displaySize.height.toString()));
client.sendMsg(guac.guacEncode('png', '0', '0', '0', '0', encoded));
client.sendMsg(guac.guacEncode('sync', Date.now().toString()));
});
break;
}
break;
case '25':
if (client.rank !== Rank.Admin || msgArr.length !== 3) return;
this.clients.forEach((c) => c.sendMsg(guacutils.encode('chat', '', msgArr[2])));
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('chat', '', msgArr[2])));
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();
}
}
getUsernameList(): string[] {
@@ -642,7 +648,7 @@ export default class CollabVMServer {
} else {
newName = newName.trim();
if (hadName && newName === oldname) {
client.sendMsg(guacutils.encode('rename', '0', '0', client.username!, client.rank.toString()));
client.sendMsg(guac.guacEncode('rename', '0', '0', client.username!, client.rank.toString()));
return;
}
if (this.getUsernameList().indexOf(newName) !== -1) {
@@ -659,13 +665,13 @@ export default class CollabVMServer {
} else client.username = newName;
}
client.sendMsg(guacutils.encode('rename', '0', status, client.username!, client.rank.toString()));
client.sendMsg(guac.guacEncode('rename', '0', status, client.username!, client.rank.toString()));
if (hadName) {
this.logger.Info(`Rename ${client.IP.address} from ${oldname} to ${client.username}`);
this.clients.forEach((c) => c.sendMsg(guacutils.encode('rename', '1', oldname, client.username!, client.rank.toString())));
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('rename', '1', oldname, client.username!, client.rank.toString())));
} else {
this.logger.Info(`Rename ${client.IP.address} to ${client.username}`);
this.clients.forEach((c) => c.sendMsg(guacutils.encode('adduser', '1', client.username!, client.rank.toString())));
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('adduser', '1', client.username!, client.rank.toString())));
}
}
@@ -673,13 +679,13 @@ export default class CollabVMServer {
var arr: string[] = ['adduser', this.clients.filter((c) => c.username).length.toString()];
this.clients.filter((c) => c.username).forEach((c) => arr.push(c.username!, c.rank.toString()));
return guacutils.encode(...arr);
return guac.guacEncode(...arr);
}
getChatHistoryMsg(): string {
var arr: string[] = ['chat'];
this.ChatHistory.forEach((c) => arr.push(c.user, c.msg));
return guacutils.encode(...arr);
return guac.guacEncode(...arr);
}
private sendTurnUpdate(client?: User) {
@@ -692,7 +698,7 @@ export default class CollabVMServer {
this.TurnQueue.forEach((c) => arr.push(c.username));
var currentTurningUser = this.TurnQueue.peek();
if (client) {
client.sendMsg(guacutils.encode(...arr));
client.sendMsg(guac.guacEncode(...arr));
return;
}
this.clients
@@ -702,12 +708,12 @@ export default class CollabVMServer {
var time;
if (this.indefiniteTurn === null) time = this.TurnTime * 1000 + (turnQueueArr.indexOf(c) - 1) * this.Config.collabvm.turnTime * 1000;
else time = 9999999999;
c.sendMsg(guacutils.encode(...arr, time.toString()));
c.sendMsg(guac.guacEncode(...arr, time.toString()));
} else {
c.sendMsg(guacutils.encode(...arr));
c.sendMsg(guac.guacEncode(...arr));
}
});
if (currentTurningUser) currentTurningUser.sendMsg(guacutils.encode(...arr));
if (currentTurningUser) currentTurningUser.sendMsg(guac.guacEncode(...arr));
}
private nextTurn() {
clearInterval(this.TurnInterval);
@@ -754,8 +760,8 @@ export default class CollabVMServer {
.filter((c) => c.connectedToNode || c.viewMode == 1)
.forEach((c) => {
if (this.screenHidden && c.rank == Rank.Unregistered) return;
c.sendMsg(guacutils.encode('png', '0', '0', rect.x.toString(), rect.y.toString(), encodedb64));
c.sendMsg(guacutils.encode('sync', Date.now().toString()));
c.sendMsg(guac.guacEncode('png', '0', '0', rect.x.toString(), rect.y.toString(), encodedb64));
c.sendMsg(guac.guacEncode('sync', Date.now().toString()));
});
}
@@ -764,7 +770,7 @@ export default class CollabVMServer {
.filter((c) => c.connectedToNode || c.viewMode == 1)
.forEach((c) => {
if (this.screenHidden && c.rank == Rank.Unregistered) return;
c.sendMsg(guacutils.encode('size', '0', size.width.toString(), size.height.toString()));
c.sendMsg(guac.guacEncode('size', '0', size.width.toString(), size.height.toString()));
});
}
@@ -779,8 +785,8 @@ export default class CollabVMServer {
height: displaySize.height
});
client.sendMsg(guacutils.encode('size', '0', displaySize.width.toString(), displaySize.height.toString()));
client.sendMsg(guacutils.encode('png', '0', '0', '0', '0', encoded));
client.sendMsg(guac.guacEncode('size', '0', displaySize.width.toString(), displaySize.height.toString()));
client.sendMsg(guac.guacEncode('png', '0', '0', '0', '0', encoded));
}
private async MakeRectData(rect: Rect) {
@@ -796,8 +802,7 @@ export default class CollabVMServer {
let display = this.VM.GetDisplay();
// oh well
if (!display.Connected())
return "";
if (!display.Connected()) return '';
let buf = await JPEGEncoder.EncodeThumbnail(display.Buffer(), display.Size());
return buf.toString('base64');
@@ -806,7 +811,7 @@ export default class CollabVMServer {
startVote() {
if (this.voteInProgress) return;
this.voteInProgress = true;
this.clients.forEach((c) => c.sendMsg(guacutils.encode('vote', '0')));
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('vote', '0')));
this.voteTime = this.Config.collabvm.voteTime;
this.voteInterval = setInterval(() => {
this.voteTime--;
@@ -821,12 +826,12 @@ export default class CollabVMServer {
this.voteInProgress = false;
clearInterval(this.voteInterval);
var count = this.getVoteCounts();
this.clients.forEach((c) => c.sendMsg(guacutils.encode('vote', '2')));
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('vote', '2')));
if (result === true || (result === undefined && count.yes >= count.no)) {
this.clients.forEach((c) => c.sendMsg(guacutils.encode('chat', '', 'The vote to reset the VM has won.')));
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('chat', '', 'The vote to reset the VM has won.')));
this.VM.Reset();
} else {
this.clients.forEach((c) => c.sendMsg(guacutils.encode('chat', '', 'The vote to reset the VM has lost.')));
this.clients.forEach((c) => c.sendMsg(guac.guacEncode('chat', '', 'The vote to reset the VM has lost.')));
}
this.clients.forEach((c) => {
c.IP.vote = null;
@@ -841,7 +846,7 @@ export default class CollabVMServer {
sendVoteUpdate(client?: User) {
if (!this.voteInProgress) return;
var count = this.getVoteCounts();
var msg = guacutils.encode('vote', '1', (this.voteTime * 1000).toString(), count.yes.toString(), count.no.toString());
var msg = guac.guacEncode('vote', '1', (this.voteTime * 1000).toString(), count.yes.toString(), count.no.toString());
if (client) client.sendMsg(msg);
else this.clients.forEach((c) => c.sendMsg(msg));
}

View File

@@ -1,5 +1,5 @@
import * as Utilities from './Utilities.js';
import * as guacutils from './guacutils.js';
import * as guac from '@cvmts/guac-rs';
import { IPData } from './IPData.js';
import IConfig from './IConfig.js';
import RateLimiter from './RateLimiter.js';
@@ -89,7 +89,7 @@ export class User {
}
closeConnection() {
this.socket.send(guacutils.encode('disconnect'));
this.socket.send(guac.guacEncode('disconnect'));
this.socket.close();
}
@@ -109,7 +109,7 @@ export class User {
mute(permanent: boolean) {
this.IP.muted = true;
this.sendMsg(guacutils.encode('chat', '', `You have been muted${permanent ? '' : ` for ${this.Config.collabvm.tempMuteTime} seconds`}.`));
this.sendMsg(guac.guacEncode('chat', '', `You have been muted${permanent ? '' : ` for ${this.Config.collabvm.tempMuteTime} seconds`}.`));
if (!permanent) {
clearTimeout(this.IP.tempMuteExpireTimeout);
this.IP.tempMuteExpireTimeout = setTimeout(() => this.unmute(), this.Config.collabvm.tempMuteTime * 1000);
@@ -118,7 +118,7 @@ export class User {
unmute() {
clearTimeout(this.IP.tempMuteExpireTimeout);
this.IP.muted = false;
this.sendMsg(guacutils.encode('chat', '', 'You are no longer muted.'));
this.sendMsg(guac.guacEncode('chat', '', 'You are no longer muted.'));
}
private banCmdArgs(arg: string): string {

View File

@@ -1,37 +0,0 @@
export function decode(string: string): string[] {
let pos = -1;
let sections = [];
for (;;) {
let len = string.indexOf('.', pos + 1);
if (len === -1) break;
pos = parseInt(string.slice(pos + 1, len)) + len + 1;
// don't allow funky protocol length
if (pos > string.length) return [];
sections.push(string.slice(len + 1, pos));
const sep = string.slice(pos, pos + 1);
if (sep === ',') continue;
else if (sep === ';') break;
// Invalid data.
else return [];
}
return sections;
}
export function encode(...string: string[]): string {
let command = '';
for (var i = 0; i < string.length; i++) {
let current = string[i];
command += current.toString().length + '.' + current;
command += i < string.length - 1 ? ',' : ';';
}
return command;
}

209
guac-rs/Cargo.lock generated Normal file
View File

@@ -0,0 +1,209 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "guac-rs"
version = "0.1.0"
dependencies = [
"neon",
]
[[package]]
name = "libc"
version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "libloading"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
dependencies = [
"cfg-if",
"windows-targets",
]
[[package]]
name = "neon"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d75440242411c87dc39847b0e33e961ec1f10326a9d8ecf9c1ea64a3b3c13dc"
dependencies = [
"getrandom",
"libloading",
"neon-macros",
"once_cell",
"semver",
"send_wrapper",
"smallvec",
]
[[package]]
name = "neon-macros"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6813fde79b646e47e7ad75f480aa80ef76a5d9599e2717407961531169ee38b"
dependencies = [
"quote",
"syn",
"syn-mid",
]
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "proc-macro2"
version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "semver"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "send_wrapper"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
[[package]]
name = "smallvec"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "syn"
version = "2.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn-mid"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5dc35bb08dd1ca3dfb09dce91fd2d13294d6711c88897d9a9d60acf39bce049"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "windows-targets"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
[[package]]
name = "windows_i686_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
[[package]]
name = "windows_i686_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"

13
guac-rs/Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "guac-rs"
description = "Rust guacamole decoding :)"
version = "0.1.0"
license = "MIT"
edition = "2021"
exclude = ["index.node"]
[lib]
crate-type = ["cdylib"]
[dependencies]
neon = "1"

3
guac-rs/index.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
export function guacDecode(input: string): string[];
export function guacEncode(...items: string[]): string;

6
guac-rs/index.js Normal file
View File

@@ -0,0 +1,6 @@
// *sigh*
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
export let {guacDecode, guacEncode} = require('./index.node');

15
guac-rs/package.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "@cvmts/guac-rs",
"packageManager": "yarn@4.1.1",
"type": "module",
"main": "index.js",
"types": "index.d.ts",
"scripts": {
"build": "cargo-cp-artifact -nc index.node -- cargo build --release --message-format=json-render-diagnostics",
"install": "yarn build",
"test": "cargo test"
},
"devDependencies": {
"cargo-cp-artifact": "^0.1"
}
}

193
guac-rs/src/guac.rs Normal file
View File

@@ -0,0 +1,193 @@
use std::fmt;
// type of a guac message
pub type Elements = Vec<String>;
// FIXME: thiserror, please.
/// Errors during decoding
#[derive(Debug, Clone)]
pub enum DecodeError {
/// Invalid guacamole instruction format
InvalidFormat,
/// Instruction is too long for the current decode policy.
InstructionTooLong,
/// Element is too long for the current decode policy.
ElementTooLong,
/// Invalid element size.
ElementSizeInvalid,
}
pub type DecodeResult<T> = std::result::Result<T, DecodeError>;
impl fmt::Display for DecodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidFormat => write!(f, "Invalid Guacamole instruction while decoding"),
Self::InstructionTooLong => write!(f, "Instruction too long for current decode policy"),
Self::ElementTooLong => write!(f, "Element too long for current decode policy"),
Self::ElementSizeInvalid => write!(f, "Element size is invalid")
}
}
}
// this decode policy abstraction would in theory be useful,
// but idk how to do this kind of thing in rust very well
pub struct StaticDecodePolicy<const INST_SIZE: usize, const ELEM_SIZE: usize>();
impl<const INST_SIZE: usize, const ELEM_SIZE: usize> StaticDecodePolicy<INST_SIZE, ELEM_SIZE> {
fn max_instruction_size(&self) -> usize {
INST_SIZE
}
fn max_element_size(&self) -> usize {
ELEM_SIZE
}
}
/// The default decode policy.
pub type DefaultDecodePolicy = StaticDecodePolicy<12288, 4096>;
/// Encodes elements into a Guacamole instruction
pub fn encode_instruction(elements: &Elements) -> String {
let mut str = String::new();
for elem in elements.iter() {
str.push_str(&format!("{}.{},", elem.len(), elem));
}
// hacky, but whatever
str.pop();
str.push(';');
str
}
/// Decodes a Guacamole instruction to individual elements
pub fn decode_instruction(element_string: &String) -> DecodeResult<Elements> {
let policy = DefaultDecodePolicy {};
let mut vec: Elements = Vec::new();
let mut current_position: usize = 0;
// Instruction is too long. Don't even bother
if policy.max_instruction_size() < element_string.len() {
return Err(DecodeError::InstructionTooLong);
}
let chars = element_string.chars().collect::<Vec<_>>();
loop {
let mut element_size: usize = 0;
// Scan the integer value in by hand. This is mostly because
// I'm stupid, and the Rust integer parsing routines (seemingly)
// require a substring (or a slice, but, if you can generate a slice,
// you can also just scan the value in by hand.)
//
// We bound this anyways and do quite the checks, so even though it's not great,
// it should be generally fine (TM).
loop {
let c = chars[current_position];
if c >= '0' && c <= '9' {
element_size = element_size * 10 + (c as usize) - ('0' as usize);
} else {
if c == '.' {
break;
}
return Err(DecodeError::InvalidFormat);
}
current_position += 1;
}
// Eat the '.' seperating the size and the element data;
// our integer scanning ensures we only get here in the case that this is actually the '.'
// character.
current_position += 1;
// Make sure the element size doesn't overflow the decode policy
// or the size of the whole instruction.
if element_size >= policy.max_element_size() {
return Err(DecodeError::ElementTooLong);
}
if element_size >= element_string.len() {
return Err(DecodeError::ElementSizeInvalid);
}
// cutoff elements or something
if current_position + element_size > chars.len()-1 {
//println!("? {current_position} a {}", chars.len());
return Err(DecodeError::InvalidFormat);
}
let element = chars
.iter()
.skip(current_position)
.take(element_size)
.collect::<String>();
current_position += element_size;
vec.push(element);
// make sure seperator is proper
match chars[current_position] {
',' => {}
';' => break,
_ => return Err(DecodeError::InvalidFormat),
}
// eat the ','
current_position += 1;
}
Ok(vec)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_basic() {
let test = String::from("7.connect,3.vm1;");
let res = decode_instruction(&test);
assert!(res.is_ok());
assert_eq!(res.unwrap(), vec!["connect", "vm1"]);
}
#[test]
fn decode_errors() {
let test = String::from("700.connect,3.vm1;");
let res = decode_instruction(&test);
eprintln!("Error for: {}", res.clone().unwrap_err());
assert!(res.is_err())
}
// generally just test that the codec even works
// (we can decode a instruction we created)
#[test]
fn general_codec_works() {
let vec = vec![String::from("connect"), String::from("vm1")];
let test = encode_instruction(&vec);
assert_eq!(test, "7.connect,3.vm1;");
let res = decode_instruction(&test);
assert!(res.is_ok());
assert_eq!(res.unwrap(), vec);
}
}

80
guac-rs/src/lib.rs Normal file
View File

@@ -0,0 +1,80 @@
mod guac;
use neon::prelude::*;
fn guac_decode_impl<'a>(cx: &mut FunctionContext<'a>) -> JsResult<'a, JsArray> {
let input = cx.argument::<JsString>(0)?.value(cx);
match guac::decode_instruction(&input) {
Ok(data) => {
let array = JsArray::new(cx, data.len());
let conv = data.iter()
.map(|v| {
cx.string(v)
})
.collect::<Vec<Handle<JsString>>>();
for (i, str) in conv.iter().enumerate() {
array.set(cx, i as u32, *str)?;
}
return Ok(array);
}
Err(e) => {
let err = cx.string(format!("Error decoding guacamole: {}", e));
return cx.throw(err);
}
}
}
fn guac_encode_impl<'a>(cx: &mut FunctionContext<'a>) -> JsResult<'a, JsString> {
let mut elements: Vec<String> = Vec::with_capacity(cx.len());
// Capture varadic arguments
for i in 0..cx.len() {
let input = cx.argument::<JsString>(i)?.value(cx);
elements.push(input);
}
// old array stuff
/*
let input = cx.argument::<JsArray>(0)?;
let raw_elements = input.to_vec(cx)?;
// bleh
let vecres: Result<Vec<_>, _> = raw_elements
.iter()
.map(|item| match item.to_string(cx) {
Ok(s) => {
return Ok(s.value(cx));
}
Err(e) => {
return Err(e);
}
})
.collect();
let vec = vecres?;
*/
Ok(cx.string(guac::encode_instruction(&elements)))
}
fn guac_decode(mut cx: FunctionContext) -> JsResult<JsArray> {
guac_decode_impl(&mut cx)
}
fn guac_encode(mut cx: FunctionContext) -> JsResult<JsString> {
guac_encode_impl(&mut cx)
}
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
cx.export_function("guacDecode", guac_decode)?;
cx.export_function("guacEncode", guac_encode)?;
Ok(())
}

View File

@@ -2,6 +2,7 @@
"name": "cvmts-repo",
"workspaces": [
"shared",
"guac-rs",
"jpeg-turbo",
"nodejs-rfb",
"qemu",

View File

@@ -75,6 +75,14 @@ __metadata:
languageName: unknown
linkType: soft
"@cvmts/guac-rs@workspace:guac-rs":
version: 0.0.0-use.local
resolution: "@cvmts/guac-rs@workspace:guac-rs"
dependencies:
cargo-cp-artifact: "npm:^0.1"
languageName: unknown
linkType: soft
"@cvmts/qemu@npm:*, @cvmts/qemu@workspace:qemu":
version: 0.0.0-use.local
resolution: "@cvmts/qemu@workspace:qemu"
@@ -1772,6 +1780,15 @@ __metadata:
languageName: node
linkType: hard
"cargo-cp-artifact@npm:^0.1":
version: 0.1.9
resolution: "cargo-cp-artifact@npm:0.1.9"
bin:
cargo-cp-artifact: bin/cargo-cp-artifact.js
checksum: 10c0/60eb1845917cfb021920fcf600a72379890b385396f9c69107face3b16b347960b66cd3d82cc169c6ac8b1212cf0706584125bc36fbc08353b033310c17ca0a6
languageName: node
linkType: hard
"chalk@npm:^2.4.2":
version: 2.4.2
resolution: "chalk@npm:2.4.2"