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
**/dist/ **/dist/
# Guac-rs
guac-rs/target
guac-rs/index.node

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
import * as Utilities from './Utilities.js'; 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 { IPData } from './IPData.js';
import IConfig from './IConfig.js'; import IConfig from './IConfig.js';
import RateLimiter from './RateLimiter.js'; import RateLimiter from './RateLimiter.js';
@@ -89,7 +89,7 @@ export class User {
} }
closeConnection() { closeConnection() {
this.socket.send(guacutils.encode('disconnect')); this.socket.send(guac.guacEncode('disconnect'));
this.socket.close(); this.socket.close();
} }
@@ -109,7 +109,7 @@ export class User {
mute(permanent: boolean) { mute(permanent: boolean) {
this.IP.muted = true; 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) { if (!permanent) {
clearTimeout(this.IP.tempMuteExpireTimeout); clearTimeout(this.IP.tempMuteExpireTimeout);
this.IP.tempMuteExpireTimeout = setTimeout(() => this.unmute(), this.Config.collabvm.tempMuteTime * 1000); this.IP.tempMuteExpireTimeout = setTimeout(() => this.unmute(), this.Config.collabvm.tempMuteTime * 1000);
@@ -118,7 +118,7 @@ export class User {
unmute() { unmute() {
clearTimeout(this.IP.tempMuteExpireTimeout); clearTimeout(this.IP.tempMuteExpireTimeout);
this.IP.muted = false; 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 { 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", "name": "cvmts-repo",
"workspaces": [ "workspaces": [
"shared", "shared",
"guac-rs",
"jpeg-turbo", "jpeg-turbo",
"nodejs-rfb", "nodejs-rfb",
"qemu", "qemu",

View File

@@ -75,6 +75,14 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft 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": "@cvmts/qemu@npm:*, @cvmts/qemu@workspace:qemu":
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@cvmts/qemu@workspace:qemu" resolution: "@cvmts/qemu@workspace:qemu"
@@ -1772,6 +1780,15 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "chalk@npm:^2.4.2":
version: 2.4.2 version: 2.4.2
resolution: "chalk@npm:2.4.2" resolution: "chalk@npm:2.4.2"