cvmts: replace guacamole decoder with a node native module written in rust
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -8,3 +8,7 @@ cvmts/attic
|
||||
|
||||
/dist
|
||||
**/dist/
|
||||
|
||||
# Guac-rs
|
||||
guac-rs/target
|
||||
guac-rs/index.node
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,42 +185,42 @@ 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();
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
} 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));
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
209
guac-rs/Cargo.lock
generated
Normal 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
13
guac-rs/Cargo.toml
Normal 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
3
guac-rs/index.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
export function guacDecode(input: string): string[];
|
||||
export function guacEncode(...items: string[]): string;
|
||||
6
guac-rs/index.js
Normal file
6
guac-rs/index.js
Normal 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
15
guac-rs/package.json
Normal 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
193
guac-rs/src/guac.rs
Normal 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
80
guac-rs/src/lib.rs
Normal 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(())
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "cvmts-repo",
|
||||
"workspaces": [
|
||||
"shared",
|
||||
"guac-rs",
|
||||
"jpeg-turbo",
|
||||
"nodejs-rfb",
|
||||
"qemu",
|
||||
|
||||
17
yarn.lock
17
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user