Compare commits

..

10 Commits

Author SHA1 Message Date
Ctrl
ef75f135c6 Merge pull request #50 from ctrlcn/feature/logs
Adds more structured logs
2025-11-22 17:13:29 +00:00
ctrlcn
3bbdf1c1b6 🌿 2025-11-20 17:35:45 -05:00
ctrlcn
698fb19014 🪵 2025-11-20 02:41:30 -05:00
modeco80
fb3c91221c rrr 2025-06-15 15:05:16 -04:00
modeco80
4211941560 cvmts/protocol: Make protocols stateless
Instead of creating an instance of a protocol per user and storing state there, we just have the protocol implementations take in all of the state they should need in processMessage(), and store them all globally (with some wrappers to make it easier to handle this). This makes things slightly cleaner (and probably also helps memory usage, since we now don't need to create protocol instances as soon as a user connects/swaps, and they don't need to be garbage collected since they are held in the manager.)
2025-06-15 15:03:13 -04:00
modeco80
bce2a0172a cvmts: Add hackfix to CGroup.Self() for hybrid cgroups
systemd-nspawn, and probably other container runtimes, can sometimes
mount a cgroupsv1 hierarchy in a cgroup namespace. This ends up
infecting the host system, so we optionally allow that. We still will
error out if we happen to be the one running in a legacy cgroupsv1
controller hierarchy, however. We still depend on unified/cgroupsv2.
2025-06-09 02:57:33 -04:00
modeco80
c4c08ae830 cvmts: Revert to using main quality for thumbnails
meh
2025-06-05 17:14:37 -04:00
Elijah R
d3db220c1f update lockfile 2025-06-05 17:08:31 -04:00
modeco80
857eb46d2a cvm-rs: allow setting JPEG quality from JS 2025-06-05 16:45:46 -04:00
modeco80
23c57dbb3b cvm-rs: Add jpegResizeEncode(), remove usage of sharp
sharp sucks.
2025-06-05 16:40:46 -04:00
18 changed files with 654 additions and 610 deletions

27
cvm-rs/Cargo.lock generated
View File

@@ -53,6 +53,12 @@ version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "bytemuck"
version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.23" version = "1.2.23"
@@ -132,6 +138,8 @@ dependencies = [
"napi-derive", "napi-derive",
"once_cell", "once_cell",
"rayon", "rayon",
"resize",
"rgb",
"turbojpeg-sys", "turbojpeg-sys",
] ]
@@ -331,6 +339,25 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "resize"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87a103d0b47e783f4579149402f7499397ab25540c7a57b2f70487a5d2d20ef0"
dependencies = [
"rayon",
"rgb",
]
[[package]]
name = "rgb"
version = "0.8.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a"
dependencies = [
"bytemuck",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.24" version = "0.1.24"

View File

@@ -20,6 +20,8 @@ rayon = "1.10.0"
napi = { version = "2.16.9", features = [ "async", "napi8", "error_anyhow" ] } napi = { version = "2.16.9", features = [ "async", "napi8", "error_anyhow" ] }
napi-derive = "2.16.11" napi-derive = "2.16.11"
anyhow = "1.0.86" anyhow = "1.0.86"
resize = "0.8.8"
rgb = "0.8.50"
[build-dependencies] [build-dependencies]
napi-build = "2.1.3" napi-build = "2.1.3"

14
cvm-rs/index.d.ts vendored
View File

@@ -9,15 +9,27 @@ interface JpegInputArgs {
height: number; height: number;
stride: number; // The width of your input framebuffer OR your image width (if encoding a full image) stride: number; // The width of your input framebuffer OR your image width (if encoding a full image)
buffer: Buffer; buffer: Buffer;
quality: number;
// TODO: Allow different formats, or export a boxed ffi object which can store a format // TODO: Allow different formats, or export a boxed ffi object which can store a format
// (i.e: new JpegEncoder(FORMAT_xxx)). // (i.e: new JpegEncoder(FORMAT_xxx)).
} }
interface JpegResizeInputArgs {
width: number; // source width
height: number; // source height
desiredWidth: number; // dest width
desiredHeight: number; // dest height
buffer: Buffer; // source raw pixel buffer
quality: number;
}
/// Performs JPEG encoding. /// Performs JPEG encoding.
export function jpegEncode(input: JpegInputArgs): Promise<Buffer>; export function jpegEncode(input: JpegInputArgs): Promise<Buffer>;
// TODO: Version that can downscale? /// Performs JPEG encoding with resizing.
export function jpegResizeEncode(input: JpegResizeInputArgs): Promise<Buffer>;
/* remoting API? /* remoting API?

View File

@@ -2,9 +2,9 @@
import { createRequire } from 'module'; import { createRequire } from 'module';
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
let {guacDecode, guacEncodeImpl, jpegEncode} = require('./index.node'); let {guacDecode, guacEncodeImpl, jpegEncode, jpegResizeEncode} = require('./index.node');
export { guacDecode, jpegEncode }; export { guacDecode, jpegEncode, jpegResizeEncode };
// shim for js->rust interop, because napi-rs kind of blows in this regard // shim for js->rust interop, because napi-rs kind of blows in this regard
export function guacEncode(...args) { export function guacEncode(...args) {

View File

@@ -9,6 +9,12 @@ use rayon::{ThreadPool, ThreadPoolBuilder};
use crate::jpeg_compressor::*; use crate::jpeg_compressor::*;
use resize::Pixel::RGBA8;
use resize::Type::Triangle;
use rgb::FromSlice;
/// Gives a Rayon thread pool we use for parallelism /// Gives a Rayon thread pool we use for parallelism
fn rayon_pool() -> &'static ThreadPool { fn rayon_pool() -> &'static ThreadPool {
static RUNTIME: OnceCell<ThreadPool> = OnceCell::new(); static RUNTIME: OnceCell<ThreadPool> = OnceCell::new();
@@ -41,6 +47,7 @@ pub struct JpegInputArgs {
pub height: u32, pub height: u32,
pub stride: u32, pub stride: u32,
pub buffer: napi::JsBuffer, pub buffer: napi::JsBuffer,
pub quality: u32,
} }
#[napi(js_name = "jpegEncode")] #[napi(js_name = "jpegEncode")]
@@ -63,15 +70,83 @@ pub fn jpeg_encode(env: Env, input: JpegInputArgs) -> napi::Result<napi::JsObjec
let vec = COMPRESSOR.with(|lazy| { let vec = COMPRESSOR.with(|lazy| {
let mut b = lazy.borrow_mut(); let mut b = lazy.borrow_mut();
b.set_quality(35); b.set_quality(input.quality);
b.set_subsamp(turbojpeg_sys::TJSAMP_TJSAMP_420); b.set_subsamp(turbojpeg_sys::TJSAMP_TJSAMP_420);
b.compress_buffer(&image) b.compress_buffer(&image)
}); });
deferred_resolver.resolve(move |env| { deferred_resolver.resolve(move |env| {
let buffer = env let buffer = env.create_buffer_with_data(vec).expect(
.create_buffer_with_data(vec) "Couldn't create node Buffer, things are probably very broken by this point",
.expect("Couldn't create node Buffer, things are probably very broken by this point"); );
// no longer need the input buffer
buf.unref(env)?;
Ok(buffer.into_raw())
});
});
Ok(promise)
}
#[napi(object)]
pub struct JpegResizeInputArgs {
pub width: u32,
pub height: u32,
pub desired_width: u32,
pub desired_height: u32,
pub buffer: napi::JsBuffer,
pub quality: u32,
}
#[napi(js_name = "jpegResizeEncode")]
#[allow(unused)]
pub fn jpeg_resize_and_encode(
env: Env,
input: JpegResizeInputArgs,
) -> napi::Result<napi::JsObject> {
let (deferred_resolver, promise) = env.create_deferred::<napi::JsBuffer, _>()?;
let mut buf = input.buffer.into_ref()?;
// Spawn a task on the rayon pool that encodes the JPEG and fufills the promise
// once it is done encoding.
rayon_pool().spawn_fifo(move || {
let mut new_data: Vec<u8> =
vec![0; (input.desired_width * input.desired_height) as usize * 4];
let mut resizer = resize::new(
input.width as usize,
input.height as usize,
input.desired_width as usize,
input.desired_height as usize,
RGBA8,
Triangle,
)
.expect("Could not create resizer");
resizer
.resize(&buf.as_rgba(), new_data.as_rgba_mut())
.expect("Resize operation failed");
// then just jpeg encode. Ideally this would be shared :(
let image = Image {
buffer: &new_data,
width: input.desired_width as u32,
height: input.desired_height as u32,
stride: (input.desired_width as u64 * 4u64) as u32,
format: turbojpeg_sys::TJPF_TJPF_RGBA,
};
let vec = COMPRESSOR.with(|lazy| {
let mut b = lazy.borrow_mut();
b.set_quality(input.quality);
b.set_subsamp(turbojpeg_sys::TJSAMP_TJSAMP_420);
b.compress_buffer(&image)
});
deferred_resolver.resolve(move |env| {
let buffer = env.create_buffer_with_data(vec).expect(
"Couldn't create node Buffer, things are probably very broken by this point",
);
// no longer need the input buffer // no longer need the input buffer
buf.unref(env)?; buf.unref(env)?;
Ok(buffer.into_raw()) Ok(buffer.into_raw())

View File

@@ -22,8 +22,8 @@
"mnemonist": "^0.39.5", "mnemonist": "^0.39.5",
"msgpackr": "^1.10.2", "msgpackr": "^1.10.2",
"pino": "^9.3.1", "pino": "^9.3.1",
"sharp": "^0.33.3",
"toml": "^3.0.0", "toml": "^3.0.0",
"uuid": "^13.0.0",
"ws": "^8.17.1" "ws": "^8.17.1"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -170,18 +170,18 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (this.Config.geoip.enabled) { if (this.Config.geoip.enabled) {
try { try {
user.countryCode = this.geoipReader!.country(user.IP.address).country!.isoCode; user.countryCode = this.geoipReader!.country(user.IP.address).country!.isoCode;
user.logger.info({event: "geoip/resolved", geoip: user.countryCode});
} catch (error) { } catch (error) {
this.logger.warn(`Failed to get country code for ${user.IP.address}: ${(error as Error).message}`); user.logger.warn({event: "geoip/unresolved", msg: `${(error as Error)}`});
} }
} }
user.socket.on('msg', (buf: Buffer, binary: boolean) => { user.socket.on('msg', (buf: Buffer, binary: boolean) => {
try { try {
user.protocol.processMessage(buf); user.processMessage(this, buf);
} catch (err) { } catch (err) {
this.logger.error({ user.logger.error({
ip: user.IP.address, event: "msg/general error",
username: user.username,
error_message: (err as Error).message error_message: (err as Error).message
}, 'Error in %s#processMessage.', Object.getPrototypeOf(user.protocol).constructor?.name); }, 'Error in %s#processMessage.', Object.getPrototypeOf(user.protocol).constructor?.name);
user.kick(); user.kick();
@@ -190,17 +190,14 @@ export default class CollabVMServer implements IProtocolMessageHandler {
user.socket.on('disconnect', () => this.connectionClosed(user)); user.socket.on('disconnect', () => this.connectionClosed(user));
// Set ourselves as the handler
user.protocol.setHandler(this as IProtocolMessageHandler);
if (this.Config.auth.enabled) { if (this.Config.auth.enabled) {
user.protocol.sendAuth(this.Config.auth.apiEndpoint); user.sendAuth(this.Config.auth.apiEndpoint);
} }
user.protocol.sendAddUser(this.getAddUser()); user.sendAddUser(this.getAddUser());
if (this.Config.geoip.enabled) { if (this.Config.geoip.enabled) {
let flags = this.getFlags(); let flags = this.getFlags();
user.protocol.sendFlag(flags); user.sendFlag(flags);
} }
} }
@@ -217,9 +214,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
this.clients.splice(clientIndex, 1); this.clients.splice(clientIndex, 1);
user.protocol.dispose(); user.logger.info({event: "user/disconnect"});
this.logger.info(`Disconnect From ${user.IP.address}${user.username ? ` with username ${user.username}` : ''}`);
if (!user.username) return; if (!user.username) return;
if (this.TurnQueue.toArray().indexOf(user) !== -1) { if (this.TurnQueue.toArray().indexOf(user) !== -1) {
var hadturn = this.TurnQueue.peek() === user; var hadturn = this.TurnQueue.peek() === user;
@@ -227,7 +222,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (hadturn) this.nextTurn(); if (hadturn) this.nextTurn();
} }
this.clients.forEach((c) => c.protocol.sendRemUser([user.username!])); this.clients.forEach((c) => c.sendRemUser([user.username!]));
} }
// Protocol message handlers // Protocol message handlers
@@ -237,7 +232,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (!this.Config.auth.enabled) return true; if (!this.Config.auth.enabled) return true;
if (user.rank === Rank.Unregistered && !guestPermission) { if (user.rank === Rank.Unregistered && !guestPermission) {
user.protocol.sendChatMessage('', 'You need to login to do that.'); user.sendChatMessage('', 'You need to login to do that.');
return false; return false;
} }
@@ -252,7 +247,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (!this.Config.auth.enabled) return; if (!this.Config.auth.enabled) return;
if (!user.connectedToNode) { if (!user.connectedToNode) {
user.protocol.sendLoginResponse(false, 'You must connect to the VM before logging in.'); user.sendLoginResponse(false, 'You must connect to the VM before logging in.');
return; return;
} }
@@ -260,8 +255,8 @@ export default class CollabVMServer implements IProtocolMessageHandler {
let res = await this.auth!.Authenticate(token, user); let res = await this.auth!.Authenticate(token, user);
if (res.clientSuccess) { if (res.clientSuccess) {
this.logger.info(`${user.IP.address} logged in as ${res.username}`); user.logger.info({ event: "user/auth/login", username: res.username });
user.protocol.sendLoginResponse(true, ''); user.sendLoginResponse(true, '');
let old = this.clients.find((c) => c.username === res.username); let old = this.clients.find((c) => c.username === res.username);
if (old) { if (old) {
@@ -274,19 +269,19 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (user.countryCode !== null && user.noFlag) { if (user.countryCode !== null && user.noFlag) {
// privacy // privacy
for (let cl of this.clients.filter((c) => c !== user)) { for (let cl of this.clients.filter((c) => c !== user)) {
cl.protocol.sendRemUser([user.username!]); cl.sendRemUser([user.username!]);
} }
this.renameUser(user, res.username, false); this.renameUser(user, res.username, false);
} else this.renameUser(user, res.username, true); } else this.renameUser(user, res.username, true);
// Set rank // Set rank
user.rank = res.rank; user.rank = res.rank;
if (user.rank === Rank.Admin) { if (user.rank === Rank.Admin) {
user.protocol.sendAdminLoginResponse(true, undefined); user.sendAdminLoginResponse(true, undefined);
} else if (user.rank === Rank.Moderator) { } else if (user.rank === Rank.Moderator) {
user.protocol.sendAdminLoginResponse(true, this.ModPerms); user.sendAdminLoginResponse(true, this.ModPerms);
} }
this.clients.forEach((c) => this.clients.forEach((c) =>
c.protocol.sendAddUser([ c.sendAddUser([
{ {
username: user.username!, username: user.username!,
rank: user.rank rank: user.rank
@@ -294,15 +289,15 @@ export default class CollabVMServer implements IProtocolMessageHandler {
]) ])
); );
} else { } else {
user.protocol.sendLoginResponse(false, res.error!); user.sendLoginResponse(false, res.error!);
if (res.error === 'You are banned') { if (res.error === 'You are banned') {
user.kick(); user.kick();
} }
} }
} catch (err) { } catch (err) {
this.logger.error(`Error authenticating client ${user.IP.address}: ${(err as Error).message}`); this.logger.error({event: "user/auth/internal error", msg: `${(err as Error).message}`});
user.protocol.sendLoginResponse(false, 'There was an internal error while authenticating. Please let a staff member know as soon as possible'); user.sendLoginResponse(false, 'There was an internal error while authenticating. Please let a staff member know as soon as possible');
} }
} }
@@ -323,26 +318,31 @@ export default class CollabVMServer implements IProtocolMessageHandler {
case ProtocolUpgradeCapability.BinRects: case ProtocolUpgradeCapability.BinRects:
enabledCaps.push(cap as ProtocolUpgradeCapability); enabledCaps.push(cap as ProtocolUpgradeCapability);
user.Capabilities.bin = true; user.Capabilities.bin = true;
user.protocol.dispose(); user.protocol = TheProtocolManager.getProtocol('binary1');
user.protocol = TheProtocolManager.createProtocol('binary1', user);
user.protocol.setHandler(this as IProtocolMessageHandler);
break; break;
default: default:
break; break;
} }
} }
user.protocol.sendCapabilities(enabledCaps); user.sendCapabilities(enabledCaps);
return true; return true;
} }
onTurnRequest(user: User, forfeit: boolean): void { onTurnRequest(user: User, forfeit: boolean): void {
user.logger.trace({event: "turn/requested"});
if ((!this.turnsAllowed || this.Config.collabvm.turnwhitelist) && user.rank !== Rank.Admin && user.rank !== Rank.Moderator && !user.turnWhitelist) return; if ((!this.turnsAllowed || this.Config.collabvm.turnwhitelist) && user.rank !== Rank.Admin && user.rank !== Rank.Moderator && !user.turnWhitelist) return;
if (!this.authCheck(user, this.Config.auth.guestPermissions.turn)) return; if (!this.authCheck(user, this.Config.auth.guestPermissions.turn)) return;
if (!user.TurnRateLimit.request()) return; if (!user.TurnRateLimit.request()) {
if (!user.connectedToNode) return; user.logger.warn({event: "turn/ratelimited"});
return;
}
if (!user.connectedToNode) {
user.logger.warn({event: "turn/requested when not in queue"})
return;
}
if (forfeit == false) { if (forfeit == false) {
var currentQueue = this.TurnQueue.toArray(); var currentQueue = this.TurnQueue.toArray();
@@ -355,8 +355,12 @@ export default class CollabVMServer implements IProtocolMessageHandler {
// Get the amount of users in the turn queue with the same IP as the user requesting a turn. // Get the amount of users in the turn queue with the same IP as the user requesting a turn.
let turns = currentQueue.filter((otheruser) => otheruser.IP.address == user.IP.address); let turns = currentQueue.filter((otheruser) => otheruser.IP.address == user.IP.address);
// If it exceeds the limit set in the config, ignore the turn request. // If it exceeds the limit set in the config, ignore the turn request.
if (turns.length + 1 > this.Config.collabvm.turnlimit.maximum) return; if (turns.length + 1 > this.Config.collabvm.turnlimit.maximum) {
user.logger.warn({event: "turn/ignoring request due to turn limit"});
return;
} }
}
user.logger.info({event: "turn/entering queue"});
this.TurnQueue.enqueue(user); this.TurnQueue.enqueue(user);
if (this.TurnQueue.size === 1) this.nextTurn(); if (this.TurnQueue.size === 1) this.nextTurn();
} else { } else {
@@ -367,29 +371,41 @@ export default class CollabVMServer implements IProtocolMessageHandler {
} }
onVote(user: User, choice: number): void { onVote(user: User, choice: number): void {
if (!this.VM.SnapshotsSupported()) return; if (!this.VM.SnapshotsSupported()) {
user.logger.warn({event: "vote/voted without snapshots enabled"});
return;
}
if ((!this.turnsAllowed || this.Config.collabvm.turnwhitelist) && user.rank !== Rank.Admin && user.rank !== Rank.Moderator && !user.turnWhitelist) return; if ((!this.turnsAllowed || this.Config.collabvm.turnwhitelist) && user.rank !== Rank.Admin && user.rank !== Rank.Moderator && !user.turnWhitelist) return;
if (!user.connectedToNode) return; if (!user.connectedToNode) {
if (!user.VoteRateLimit.request()) return; user.logger.warn({event: "vote/not connected to node"});
return;
}
if (!user.VoteRateLimit.request()) {
user.logger.warn({event: "vote/voted but was ratelimited"});
return;
}
switch (choice) { switch (choice) {
case 1: case 1:
if (!this.voteInProgress) { if (!this.voteInProgress) {
if (!this.authCheck(user, this.Config.auth.guestPermissions.callForReset)) return; if (!this.authCheck(user, this.Config.auth.guestPermissions.callForReset)) return;
if (this.voteCooldown !== 0) { if (this.voteCooldown !== 0) {
user.protocol.sendVoteCooldown(this.voteCooldown); user.sendVoteCooldown(this.voteCooldown);
return; return;
} }
user.logger.info({event: "vote/user initiated a vote"});
this.startVote(); this.startVote();
this.clients.forEach((c) => c.protocol.sendChatMessage('', `${user.username} has started a vote to reset the VM.`)); this.clients.forEach((c) => c.sendChatMessage('', `${user.username} has started a vote to reset the VM.`));
} }
if (!this.authCheck(user, this.Config.auth.guestPermissions.vote)) return; if (!this.authCheck(user, this.Config.auth.guestPermissions.vote)) return;
if (user.IP.vote !== true) { if (user.IP.vote !== true) {
this.clients.forEach((c) => c.protocol.sendChatMessage('', `${user.username} has voted yes.`)); this.clients.forEach((c) => c.sendChatMessage('', `${user.username} has voted yes.`));
} }
user.logger.info({event: "vote/yes"});
user.IP.vote = true; user.IP.vote = true;
break; break;
case 0: case 0:
@@ -398,8 +414,10 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (!this.authCheck(user, this.Config.auth.guestPermissions.vote)) return; if (!this.authCheck(user, this.Config.auth.guestPermissions.vote)) return;
if (user.IP.vote !== false) { if (user.IP.vote !== false) {
this.clients.forEach((c) => c.protocol.sendChatMessage('', `${user.username} has voted no.`)); this.clients.forEach((c) => c.sendChatMessage('', `${user.username} has voted no.`));
} }
user.logger.info({event: "vote/no"});
user.IP.vote = false; user.IP.vote = false;
break; break;
default: default:
@@ -416,13 +434,13 @@ export default class CollabVMServer implements IProtocolMessageHandler {
}; };
if (this.VM.GetState() == VMState.Started) { if (this.VM.GetState() == VMState.Started) {
user.protocol.sendListResponse([listEntry]); user.sendListResponse([listEntry]);
} }
} }
private async connectViewShared(user: User, node: string, viewMode: number | undefined) { private async connectViewShared(user: User, node: string, viewMode: number | undefined) {
if (!user.username || node !== this.Config.collabvm.node) { if (!user.username || node !== this.Config.collabvm.node) {
user.protocol.sendConnectFailResponse(); user.sendConnectFailResponse();
return; return;
} }
@@ -430,23 +448,23 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (viewMode !== undefined) { if (viewMode !== undefined) {
if (viewMode !== 0 && viewMode !== 1) { if (viewMode !== 0 && viewMode !== 1) {
user.protocol.sendConnectFailResponse(); user.sendConnectFailResponse();
return; return;
} }
user.viewMode = viewMode; user.viewMode = viewMode;
} }
user.protocol.sendConnectOKResponse(this.VM.SnapshotsSupported()); user.sendConnectOKResponse(this.VM.SnapshotsSupported());
if (this.ChatHistory.size !== 0) { if (this.ChatHistory.size !== 0) {
let history = this.ChatHistory.toArray() as ChatHistory[]; let history = this.ChatHistory.toArray() as ChatHistory[];
user.protocol.sendChatHistoryMessage(history); user.sendChatHistoryMessage(history);
} }
if (this.Config.collabvm.motd) user.protocol.sendChatMessage('', this.Config.collabvm.motd); if (this.Config.collabvm.motd) user.sendChatMessage('', this.Config.collabvm.motd);
if (this.screenHidden) { if (this.screenHidden) {
user?.protocol.sendScreenResize(1024, 768); user?.sendScreenResize(1024, 768);
user?.protocol.sendScreenUpdate({ user?.sendScreenUpdate({
x: 0, x: 0,
y: 0, y: 0,
data: this.screenHiddenImg data: this.screenHiddenImg
@@ -455,30 +473,38 @@ export default class CollabVMServer implements IProtocolMessageHandler {
await this.SendFullScreenWithSize(user); await this.SendFullScreenWithSize(user);
} }
user.protocol.sendSync(Date.now()); user.sendSync(Date.now());
if (this.voteInProgress) this.sendVoteUpdate(user); if (this.voteInProgress) this.sendVoteUpdate(user);
this.sendTurnUpdate(user); this.sendTurnUpdate(user);
} }
async onConnect(user: User, node: string) { async onConnect(user: User, node: string) {
user.logger.info({event: "user/joined node", node});
return this.connectViewShared(user, node, undefined); return this.connectViewShared(user, node, undefined);
} }
async onView(user: User, node: string, viewMode: number) { async onView(user: User, node: string, viewMode: number) {
user.logger.info({event: "user/entering view", node, viewMode});
return this.connectViewShared(user, node, viewMode); return this.connectViewShared(user, node, viewMode);
} }
onRename(user: User, newName: string | undefined): void { onRename(user: User, newName: string | undefined): void {
if (!user.RenameRateLimit.request()) return; if (!user.RenameRateLimit.request()) {
if (user.connectedToNode && user.IP.muted) return; user.logger.warn({event: "rename/ratelimit"});
return;
}
if (user.connectedToNode && user.IP.muted) {
user.logger.warn({event: "rename/attempted to rename while muted"});
return;
}
if (this.Config.auth.enabled && user.rank !== Rank.Unregistered) { if (this.Config.auth.enabled && user.rank !== Rank.Unregistered) {
user.protocol.sendChatMessage('', 'Go to your account settings to change your username.'); user.sendChatMessage('', 'Go to your account settings to change your username.');
return; return;
} }
if (this.Config.auth.enabled && newName !== undefined) { if (this.Config.auth.enabled && newName !== undefined) {
// Don't send system message to a user without a username since it was likely an automated attempt by the webapp // Don't send system message to a user without a username since it was likely an automated attempt by the webapp
if (user.username) user.protocol.sendChatMessage('', 'You need to log in to do that.'); if (user.username) user.sendChatMessage('', 'You need to log in to do that.');
if (user.rank !== Rank.Unregistered) return; if (user.rank !== Rank.Unregistered) return;
this.renameUser(user, undefined); this.renameUser(user, undefined);
return; return;
@@ -487,7 +513,10 @@ export default class CollabVMServer implements IProtocolMessageHandler {
} }
onChat(user: User, message: string): void { onChat(user: User, message: string): void {
if (!user.username) return; if (!user.username) {
user.logger.warn({event: "chat/dropped message without username", message});
return;
}
if (user.IP.muted) return; if (user.IP.muted) return;
if (!this.authCheck(user, this.Config.auth.guestPermissions.chat)) return; if (!this.authCheck(user, this.Config.auth.guestPermissions.chat)) return;
@@ -496,13 +525,15 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (msg.length > this.Config.collabvm.maxChatLength) msg = msg.substring(0, this.Config.collabvm.maxChatLength); if (msg.length > this.Config.collabvm.maxChatLength) msg = msg.substring(0, this.Config.collabvm.maxChatLength);
if (msg.trim().length < 1) return; if (msg.trim().length < 1) return;
this.clients.forEach((c) => c.protocol.sendChatMessage(user.username!, msg)); user.logger.info({event: "chat/message", msg});
this.clients.forEach((c) => c.sendChatMessage(user.username!, msg));
this.ChatHistory.push({ user: user.username, msg: msg }); this.ChatHistory.push({ user: user.username, msg: msg });
user.onChatMsgSent(); user.onChatMsgSent();
} }
onKey(user: User, keysym: number, pressed: boolean): void { onKey(user: User, keysym: number, pressed: boolean): void {
if (this.TurnQueue.peek() !== user && user.rank !== Rank.Admin) return; if (this.TurnQueue.peek() !== user && user.rank !== Rank.Admin) return;
user.logger.info({event: "key", keysym, pressed});
this.VM.GetDisplay()?.KeyboardEvent(keysym, pressed); this.VM.GetDisplay()?.KeyboardEvent(keysym, pressed);
} }
@@ -519,24 +550,28 @@ export default class CollabVMServer implements IProtocolMessageHandler {
sha256.destroy(); sha256.destroy();
if (this.Config.collabvm.turnwhitelist && pwdHash === this.Config.collabvm.turnpass) { if (this.Config.collabvm.turnwhitelist && pwdHash === this.Config.collabvm.turnpass) {
user.logger.info({event: "admin/granted turnpass"})
user.turnWhitelist = true; user.turnWhitelist = true;
user.protocol.sendChatMessage('', 'You may now take turns.'); user.sendChatMessage('', 'You may now take turns.');
return; return;
} }
if (this.Config.auth.enabled) { if (this.Config.auth.enabled) {
user.protocol.sendChatMessage('', 'This server does not support staff passwords. Please log in to become staff.'); user.sendChatMessage('', 'This server does not support staff passwords. Please log in to become staff.');
return; return;
} }
if (pwdHash === this.Config.collabvm.adminpass) { if (pwdHash === this.Config.collabvm.adminpass) {
user.logger.info({event: "admin/granted adminpass"})
user.rank = Rank.Admin; user.rank = Rank.Admin;
user.protocol.sendAdminLoginResponse(true, undefined); user.sendAdminLoginResponse(true, undefined);
} else if (this.Config.collabvm.moderatorEnabled && pwdHash === this.Config.collabvm.modpass) { } else if (this.Config.collabvm.moderatorEnabled && pwdHash === this.Config.collabvm.modpass) {
user.logger.info({event: "admin/granted modpass"})
user.rank = Rank.Moderator; user.rank = Rank.Moderator;
user.protocol.sendAdminLoginResponse(true, this.ModPerms); user.sendAdminLoginResponse(true, this.ModPerms);
} else { } else {
user.protocol.sendAdminLoginResponse(false, undefined); user.logger.warn({event: "admin/failed login attempt"})
user.sendAdminLoginResponse(false, undefined);
return; return;
} }
@@ -546,7 +581,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
// Update rank // Update rank
this.clients.forEach((c) => this.clients.forEach((c) =>
c.protocol.sendAddUser([ c.sendAddUser([
{ {
username: user.username!, username: user.username!,
rank: user.rank rank: user.rank
@@ -560,7 +595,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (node !== this.Config.collabvm.node) return; if (node !== this.Config.collabvm.node) return;
TheAuditLog.onMonitorCommand(user, command); TheAuditLog.onMonitorCommand(user, command);
let output = await this.VM.MonitorCommand(command); let output = await this.VM.MonitorCommand(command);
user.protocol.sendAdminMonitorResponse(String(output)); user.sendAdminMonitorResponse(String(output));
} }
onAdminRestore(user: User, node: string): void { onAdminRestore(user: User, node: string): void {
@@ -624,7 +659,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
onAdminRename(user: User, target: string, newName: string): void { onAdminRename(user: User, target: string, newName: string): void {
if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.rename)) return; if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.rename)) return;
if (this.Config.auth.enabled) { if (this.Config.auth.enabled) {
user.protocol.sendChatMessage('', 'Cannot rename users on a server that uses authentication.'); user.sendChatMessage('', 'Cannot rename users on a server that uses authentication.');
} }
var targetUser = this.clients.find((c) => c.username === target); var targetUser = this.clients.find((c) => c.username === target);
if (!targetUser) return; if (!targetUser) return;
@@ -635,7 +670,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.grabip)) return; if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.grabip)) return;
let target = this.clients.find((c) => c.username === username); let target = this.clients.find((c) => c.username === username);
if (!target) return; if (!target) return;
user.protocol.sendAdminIPResponse(username, target.IP.address); user.sendAdminIPResponse(username, target.IP.address);
} }
onAdminBypassTurn(user: User): void { onAdminBypassTurn(user: User): void {
@@ -647,14 +682,14 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.xss)) return; if (user.rank !== Rank.Admin && (user.rank !== Rank.Moderator || !this.Config.collabvm.moderatorPermissions.xss)) return;
switch (user.rank) { switch (user.rank) {
case Rank.Admin: case Rank.Admin:
this.clients.forEach((c) => c.protocol.sendChatMessage(user.username!, message)); this.clients.forEach((c) => c.sendChatMessage(user.username!, message));
this.ChatHistory.push({ user: user.username!, msg: message }); this.ChatHistory.push({ user: user.username!, msg: message });
break; break;
case Rank.Moderator: case Rank.Moderator:
this.clients.filter((c) => c.rank !== Rank.Admin).forEach((c) => c.protocol.sendChatMessage(user.username!, message)); this.clients.filter((c) => c.rank !== Rank.Admin).forEach((c) => c.sendChatMessage(user.username!, message));
this.clients.filter((c) => c.rank === Rank.Admin).forEach((c) => c.protocol.sendChatMessage(user.username!, Utilities.HTMLSanitize(message))); this.clients.filter((c) => c.rank === Rank.Admin).forEach((c) => c.sendChatMessage(user.username!, Utilities.HTMLSanitize(message)));
break; break;
} }
} }
@@ -700,8 +735,8 @@ export default class CollabVMServer implements IProtocolMessageHandler {
this.clients this.clients
.filter((c) => c.rank == Rank.Unregistered) .filter((c) => c.rank == Rank.Unregistered)
.forEach((client) => { .forEach((client) => {
client.protocol.sendScreenResize(1024, 768); client.sendScreenResize(1024, 768);
client.protocol.sendScreenUpdate({ client.sendScreenUpdate({
x: 0, x: 0,
y: 0, y: 0,
data: this.screenHiddenImg data: this.screenHiddenImg
@@ -712,7 +747,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
onAdminSystemMessage(user: User, message: string): void { onAdminSystemMessage(user: User, message: string): void {
if (user.rank !== Rank.Admin) return; if (user.rank !== Rank.Admin) return;
this.clients.forEach((c) => c.protocol.sendChatMessage('', message)); this.clients.forEach((c) => c.sendChatMessage('', message));
} }
// end protocol message handlers // end protocol message handlers
@@ -736,7 +771,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
} else { } else {
newName = newName.trim(); newName = newName.trim();
if (hadName && newName === oldname) { if (hadName && newName === oldname) {
client.protocol.sendSelfRename(ProtocolRenameStatus.Ok, client.username!, client.rank); client.sendSelfRename(ProtocolRenameStatus.Ok, client.username!, client.rank);
return; return;
} }
@@ -754,16 +789,16 @@ export default class CollabVMServer implements IProtocolMessageHandler {
} else client.username = newName; } else client.username = newName;
} }
client.protocol.sendSelfRename(status, client.username!, client.rank); client.sendSelfRename(status, client.username!, client.rank);
if (hadName) { if (hadName) {
this.logger.info(`Rename ${client.IP.address} from ${oldname} to ${client.username}`); client.logger.info({event: "rename", from: oldname, to: client.username});
if (announce) this.clients.forEach((c) => c.protocol.sendRename(oldname, client.username!, client.rank)); if (announce) this.clients.forEach((c) => c.sendRename(oldname, client.username!, client.rank));
} else { } else {
this.logger.info(`Rename ${client.IP.address} to ${client.username}`); client.logger.info({event: "rename", to: client.username});
if (announce) if (announce)
this.clients.forEach((c) => { this.clients.forEach((c) => {
c.protocol.sendAddUser([ c.sendAddUser([
{ {
username: client.username!, username: client.username!,
rank: client.rank rank: client.rank
@@ -771,7 +806,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
]); ]);
if (client.countryCode !== null) { if (client.countryCode !== null) {
c.protocol.sendFlag([ c.sendFlag([
{ {
username: client.username!, username: client.username!,
countryCode: client.countryCode countryCode: client.countryCode
@@ -816,7 +851,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
var currentTurningUser = this.TurnQueue.peek(); var currentTurningUser = this.TurnQueue.peek();
if (client) { if (client) {
client.protocol.sendTurnQueue(turntime, users); client.sendTurnQueue(turntime, users);
return; return;
} }
@@ -827,13 +862,17 @@ export default class CollabVMServer implements IProtocolMessageHandler {
var time; var time;
if (this.indefiniteTurn === null) time = this.TurnTime * 1000 + (turnQueueArr.indexOf(c) - 1) * this.Config.collabvm.turnTime * 1000; if (this.indefiniteTurn === null) time = this.TurnTime * 1000 + (turnQueueArr.indexOf(c) - 1) * this.Config.collabvm.turnTime * 1000;
else time = 9999999999; else time = 9999999999;
c.protocol.sendTurnQueueWaiting(turntime, users, time); c.sendTurnQueueWaiting(turntime, users, time);
} else { } else {
c.protocol.sendTurnQueue(turntime, users); c.sendTurnQueue(turntime, users);
} }
}); });
if (currentTurningUser) currentTurningUser.protocol.sendTurnQueue(turntime, users); if (currentTurningUser) {
currentTurningUser.logger.info({event: "turn/held"});
currentTurningUser.sendTurnQueue(turntime, users);
} }
}
private nextTurn() { private nextTurn() {
clearInterval(this.TurnInterval); clearInterval(this.TurnInterval);
if (this.TurnQueue.size === 0) { if (this.TurnQueue.size === 0) {
@@ -845,18 +884,21 @@ export default class CollabVMServer implements IProtocolMessageHandler {
} }
clearTurns() { clearTurns() {
this.logger.info({event: "turn/clearing turn queue"});
clearInterval(this.TurnInterval); clearInterval(this.TurnInterval);
this.TurnQueue.clear(); this.TurnQueue.clear();
this.sendTurnUpdate(); this.sendTurnUpdate();
} }
bypassTurn(client: User) { bypassTurn(client: User) {
client.logger.info({event: "turn/bypassing"});
var a = this.TurnQueue.toArray().filter((c) => c !== client); var a = this.TurnQueue.toArray().filter((c) => c !== client);
this.TurnQueue = Queue.from([client, ...a]); this.TurnQueue = Queue.from([client, ...a]);
this.nextTurn(); this.nextTurn();
} }
endTurn(client: User) { endTurn(client: User) {
client.logger.info({event: "turn/ending"});
// I must have somehow accidentally removed this while scalpaling everything out // I must have somehow accidentally removed this while scalpaling everything out
if (this.indefiniteTurn === client) this.indefiniteTurn = null; if (this.indefiniteTurn === client) this.indefiniteTurn = null;
var hasTurn = this.TurnQueue.peek() === client; var hasTurn = this.TurnQueue.peek() === client;
@@ -883,7 +925,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
.filter((c) => c.connectedToNode || c.viewMode == 1) .filter((c) => c.connectedToNode || c.viewMode == 1)
.forEach((c) => { .forEach((c) => {
if (this.screenHidden && c.rank == Rank.Unregistered) return; if (this.screenHidden && c.rank == Rank.Unregistered) return;
c.protocol.sendScreenResize(size.width, size.height); c.sendScreenResize(size.width, size.height);
}); });
} }
@@ -898,7 +940,7 @@ export default class CollabVMServer implements IProtocolMessageHandler {
.forEach((c) => { .forEach((c) => {
if (self.screenHidden && c.rank == Rank.Unregistered) return; if (self.screenHidden && c.rank == Rank.Unregistered) return;
c.protocol.sendScreenUpdate({ c.sendScreenUpdate({
x: rect.x, x: rect.x,
y: rect.y, y: rect.y,
data: encoded data: encoded
@@ -930,9 +972,9 @@ export default class CollabVMServer implements IProtocolMessageHandler {
height: displaySize.height height: displaySize.height
}); });
client.protocol.sendScreenResize(displaySize.width, displaySize.height); client.sendScreenResize(displaySize.width, displaySize.height);
client.protocol.sendScreenUpdate({ client.sendScreenUpdate({
x: 0, x: 0,
y: 0, y: 0,
data: encoded data: encoded
@@ -963,7 +1005,8 @@ export default class CollabVMServer implements IProtocolMessageHandler {
startVote() { startVote() {
if (this.voteInProgress) return; if (this.voteInProgress) return;
this.voteInProgress = true; this.voteInProgress = true;
this.clients.forEach((c) => c.protocol.sendVoteStarted()); this.logger.info({event: "vote/start"});
this.clients.forEach((c) => c.sendVoteStarted());
this.voteTime = this.Config.collabvm.voteTime; this.voteTime = this.Config.collabvm.voteTime;
this.voteInterval = setInterval(() => { this.voteInterval = setInterval(() => {
this.voteTime--; this.voteTime--;
@@ -978,12 +1021,12 @@ export default class CollabVMServer implements IProtocolMessageHandler {
this.voteInProgress = false; this.voteInProgress = false;
clearInterval(this.voteInterval); clearInterval(this.voteInterval);
var count = this.getVoteCounts(); var count = this.getVoteCounts();
this.clients.forEach((c) => c.protocol.sendVoteEnded()); this.clients.forEach((c) => c.sendVoteEnded());
if (result === true || (result === undefined && count.yes >= count.no)) { if (result === true || (result === undefined && count.yes >= count.no)) {
this.clients.forEach((c) => c.protocol.sendChatMessage('', 'The vote to reset the VM has won.')); this.clients.forEach((c) => c.sendChatMessage('', 'The vote to reset the VM has won.'));
this.VM.Reset(); this.VM.Reset();
} else { } else {
this.clients.forEach((c) => c.protocol.sendChatMessage('', 'The vote to reset the VM has lost.')); this.clients.forEach((c) => c.sendChatMessage('', 'The vote to reset the VM has lost.'));
} }
this.clients.forEach((c) => { this.clients.forEach((c) => {
c.IP.vote = null; c.IP.vote = null;
@@ -999,8 +1042,8 @@ export default class CollabVMServer implements IProtocolMessageHandler {
if (!this.voteInProgress) return; if (!this.voteInProgress) return;
var count = this.getVoteCounts(); var count = this.getVoteCounts();
if (client) client.protocol.sendVoteStats(this.voteTime * 1000, count.yes, count.no); if (client) client.sendVoteStats(this.voteTime * 1000, count.yes, count.no);
else this.clients.forEach((c) => c.protocol.sendVoteStats(this.voteTime * 1000, count.yes, count.no)); else this.clients.forEach((c) => c.sendVoteStats(this.voteTime * 1000, count.yes, count.no));
} }
getVoteCounts(): VoteTally { getVoteCounts(): VoteTally {

View File

@@ -1,5 +1,4 @@
import { Size, Rect } from './Utilities'; import { Size, Rect } from './Utilities';
import sharp from 'sharp';
import * as cvm from '@cvmts/cvm-rs'; import * as cvm from '@cvmts/cvm-rs';
// A good balance. TODO: Configurable? // A good balance. TODO: Configurable?
@@ -10,17 +9,6 @@ const kThumbnailSize: Size = {
height: 300 height: 300
}; };
// this returns appropiate Sharp options to deal with CVMTS raw framebuffers
// (which are RGBA bitmaps, essentially. We probably should abstract that out but
// that'd mean having to introduce that to rfb and oihwekjtgferklds;./tghnredsltg;erhds)
function GetRawSharpOptions(size: Size): sharp.CreateRaw {
return {
width: size.width,
height: size.height,
channels: 4
};
}
export class JPEGEncoder { export class JPEGEncoder {
static SetQuality(quality: number) { static SetQuality(quality: number) {
gJpegQuality = quality; gJpegQuality = quality;
@@ -32,21 +20,19 @@ export class JPEGEncoder {
width: rect.width, width: rect.width,
height: rect.height, height: rect.height,
stride: displaySize.width, stride: displaySize.width,
buffer: canvas.subarray(offset) buffer: canvas.subarray(offset),
quality: gJpegQuality
}); });
} }
static async EncodeThumbnail(buffer: Buffer, size: Size): Promise<Buffer> { static async EncodeThumbnail(buffer: Buffer, size: Size): Promise<Buffer> {
let { data, info } = await sharp(buffer, { raw: GetRawSharpOptions(size) }) return cvm.jpegResizeEncode({
.resize(kThumbnailSize.width, kThumbnailSize.height, { fit: 'fill' }) width: size.width,
.raw() height: size.height,
.toBuffer({ resolveWithObject: true }); desiredWidth: kThumbnailSize.width,
desiredHeight: kThumbnailSize.height,
return cvm.jpegEncode({ buffer: buffer,
width: kThumbnailSize.width, quality: gJpegQuality
height: kThumbnailSize.height,
stride: kThumbnailSize.width,
buffer: data
}); });
} }
} }

View File

@@ -5,9 +5,10 @@ import IConfig from './IConfig.js';
import RateLimiter from './RateLimiter.js'; import RateLimiter from './RateLimiter.js';
import { NetworkClient } from './net/NetworkClient.js'; import { NetworkClient } from './net/NetworkClient.js';
import { CollabVMCapabilities } from '@cvmts/collab-vm-1.2-binary-protocol'; import { CollabVMCapabilities } from '@cvmts/collab-vm-1.2-binary-protocol';
import pino from 'pino'; import { pino, type Logger } from 'pino';
import { v4 as uuid4 } from 'uuid';
import { BanManager } from './BanManager.js'; import { BanManager } from './BanManager.js';
import { IProtocol } from './protocol/Protocol.js'; import { IProtocol, IProtocolMessageHandler, ListEntry, ProtocolAddUser, ProtocolChatHistory, ProtocolFlag, ProtocolRenameStatus, ProtocolUpgradeCapability, ScreenRect } from './protocol/Protocol.js';
import { TheProtocolManager } from './protocol/Manager.js'; import { TheProtocolManager } from './protocol/Manager.js';
export class User { export class User {
@@ -15,7 +16,7 @@ export class User {
nopSendInterval: NodeJS.Timeout; nopSendInterval: NodeJS.Timeout;
msgRecieveInterval: NodeJS.Timeout; msgRecieveInterval: NodeJS.Timeout;
nopRecieveTimeout?: NodeJS.Timeout; nopRecieveTimeout?: NodeJS.Timeout;
username?: string; private _username?: string;
connectedToNode: boolean; connectedToNode: boolean;
viewMode: number; viewMode: number;
rank: Rank; rank: Rank;
@@ -34,8 +35,9 @@ export class User {
RenameRateLimit: RateLimiter; RenameRateLimit: RateLimiter;
TurnRateLimit: RateLimiter; TurnRateLimit: RateLimiter;
VoteRateLimit: RateLimiter; VoteRateLimit: RateLimiter;
uuid: string;
private logger = pino({ name: 'CVMTS.User' }); logger: Logger;
constructor(socket: NetworkClient, protocol: string, ip: IPData, config: IConfig, username?: string, node?: string) { constructor(socket: NetworkClient, protocol: string, ip: IPData, config: IConfig, username?: string, node?: string) {
this.IP = ip; this.IP = ip;
@@ -44,12 +46,19 @@ export class User {
this.Config = config; this.Config = config;
this.socket = socket; this.socket = socket;
this.msgsSent = 0; this.msgsSent = 0;
this.uuid = uuid4();
this.logger = pino().child({
name: "CVMTS.User",
"uuid/user": this.uuid,
ip: ip.address,
});
this.Capabilities = new CollabVMCapabilities(); this.Capabilities = new CollabVMCapabilities();
// All clients default to the Guacamole protocol. // All clients default to the Guacamole protocol.
this.protocol = TheProtocolManager.createProtocol(protocol, this); this.protocol = TheProtocolManager.getProtocol(protocol);
this.socket.on('disconnect', () => { this.socket.on('disconnect', () => {
this.logger.info({event: "user disconnected", username});
// Unref the ip data for this connection // Unref the ip data for this connection
this.IP.Unref(); this.IP.Unref();
@@ -80,6 +89,7 @@ export class User {
do { do {
username = 'guest' + Utilities.Randint(10000, 99999); username = 'guest' + Utilities.Randint(10000, 99999);
} while (existingUsers.indexOf(username) !== -1); } while (existingUsers.indexOf(username) !== -1);
this.logger.info({event: "assign guest username"});
this.username = username; this.username = username;
return username; return username;
} }
@@ -90,10 +100,6 @@ export class User {
this.msgRecieveInterval = setInterval(() => this.onNoMsg(), 10000); this.msgRecieveInterval = setInterval(() => this.onNoMsg(), 10000);
} }
sendNop() {
this.protocol.sendNop();
}
sendMsg(msg: string) { sendMsg(msg: string) {
if (!this.socket.isOpen()) return; if (!this.socket.isOpen()) return;
clearInterval(this.nopSendInterval); clearInterval(this.nopSendInterval);
@@ -104,11 +110,13 @@ export class User {
private onNoMsg() { private onNoMsg() {
this.sendNop(); this.sendNop();
this.nopRecieveTimeout = setTimeout(() => { this.nopRecieveTimeout = setTimeout(() => {
this.logger.info({event: "nop timeout"});
this.closeConnection(); this.closeConnection();
}, 3000); }, 3000);
} }
closeConnection() { closeConnection() {
this.logger.info({event: "closing connection"});
this.socket.send(cvm.guacEncode('disconnect')); this.socket.send(cvm.guacEncode('disconnect'));
this.socket.close(); this.socket.close();
} }
@@ -128,6 +136,7 @@ export class User {
} }
mute(permanent: boolean) { mute(permanent: boolean) {
this.logger.info({event: "mute", time_seconds: this.Config.collabvm.tempMuteTime, permanent});
this.IP.muted = true; this.IP.muted = true;
this.sendMsg(cvm.guacEncode('chat', '', `You have been muted${permanent ? '' : ` for ${this.Config.collabvm.tempMuteTime} seconds`}.`)); this.sendMsg(cvm.guacEncode('chat', '', `You have been muted${permanent ? '' : ` for ${this.Config.collabvm.tempMuteTime} seconds`}.`));
if (!permanent) { if (!permanent) {
@@ -135,13 +144,16 @@ export class User {
this.IP.tempMuteExpireTimeout = setTimeout(() => this.unmute(), this.Config.collabvm.tempMuteTime * 1000); this.IP.tempMuteExpireTimeout = setTimeout(() => this.unmute(), this.Config.collabvm.tempMuteTime * 1000);
} }
} }
unmute() { unmute() {
this.logger.info({event: "unmute"});
clearTimeout(this.IP.tempMuteExpireTimeout); clearTimeout(this.IP.tempMuteExpireTimeout);
this.IP.muted = false; this.IP.muted = false;
this.sendMsg(cvm.guacEncode('chat', '', 'You are no longer muted.')); this.sendMsg(cvm.guacEncode('chat', '', 'You are no longer muted.'));
} }
async ban(banmgr: BanManager) { async ban(banmgr: BanManager) {
this.logger.info({event: "ban"});
// Prevent the user from taking turns or chatting, in case the ban command takes a while // Prevent the user from taking turns or chatting, in case the ban command takes a while
this.IP.muted = true; this.IP.muted = true;
await banmgr.BanUser(this.IP.address, this.username || ''); await banmgr.BanUser(this.IP.address, this.username || '');
@@ -149,9 +161,134 @@ export class User {
} }
async kick() { async kick() {
this.logger.info({event: "kick"});
this.sendMsg('10.disconnect;'); this.sendMsg('10.disconnect;');
this.socket.close(); this.socket.close();
} }
// These wrap the currently set IProtocol instance to feed state to them.
// This is probably grody, but /shrug. It works, and feels less awful than
// manually wrapping state (and probably prevents mixup bugs too.)
processMessage(handler: IProtocolMessageHandler, buffer: Buffer) {
this.protocol.processMessage(this, handler, buffer);
}
sendNop(): void {
this.protocol.sendNop(this);
}
sendSync(now: number): void {
this.protocol.sendSync(this, now);
}
sendAuth(authServer: string): void {
this.protocol.sendAuth(this, authServer);
}
sendCapabilities(caps: ProtocolUpgradeCapability[]): void {
this.protocol.sendCapabilities(this, caps);
}
sendConnectFailResponse(): void {
this.protocol.sendConnectFailResponse(this);
}
sendConnectOKResponse(votes: boolean): void {
this.protocol.sendConnectOKResponse(this, votes);
}
sendLoginResponse(ok: boolean, message: string | undefined): void {
this.protocol.sendLoginResponse(this, ok, message);
}
sendAdminLoginResponse(ok: boolean, modPerms: number | undefined): void {
this.protocol.sendAdminLoginResponse(this, ok, modPerms);
}
sendAdminMonitorResponse(output: string): void {
this.protocol.sendAdminMonitorResponse(this, output);
}
sendAdminIPResponse(username: string, ip: string): void {
this.protocol.sendAdminIPResponse(this, username, ip);
}
sendChatMessage(username: '' | string, message: string): void {
this.protocol.sendChatMessage(this, username, message);
}
sendChatHistoryMessage(history: ProtocolChatHistory[]): void {
this.protocol.sendChatHistoryMessage(this, history);
}
sendAddUser(users: ProtocolAddUser[]): void {
this.protocol.sendAddUser(this, users);
}
sendRemUser(users: string[]): void {
this.protocol.sendRemUser(this, users);
}
sendFlag(flag: ProtocolFlag[]): void {
this.protocol.sendFlag(this, flag);
}
sendSelfRename(status: ProtocolRenameStatus, newUsername: string, rank: Rank): void {
this.protocol.sendSelfRename(this, status, newUsername, rank);
}
sendRename(oldUsername: string, newUsername: string, rank: Rank): void {
this.protocol.sendRename(this, oldUsername, newUsername, rank);
}
sendListResponse(list: ListEntry[]): void {
this.protocol.sendListResponse(this, list);
}
sendTurnQueue(turnTime: number, users: string[]): void {
this.protocol.sendTurnQueue(this, turnTime, users);
}
sendTurnQueueWaiting(turnTime: number, users: string[], waitTime: number): void {
this.protocol.sendTurnQueueWaiting(this, turnTime, users, waitTime);
}
sendVoteStarted(): void {
this.protocol.sendVoteStarted(this);
}
sendVoteStats(msLeft: number, nrYes: number, nrNo: number): void {
this.protocol.sendVoteStats(this, msLeft, nrYes, nrNo);
}
sendVoteEnded(): void {
this.protocol.sendVoteEnded(this);
}
sendVoteCooldown(ms: number): void {
this.protocol.sendVoteCooldown(this, ms);
}
sendScreenResize(width: number, height: number): void {
this.protocol.sendScreenResize(this, width, height);
}
sendScreenUpdate(rect: ScreenRect): void {
this.protocol.sendScreenUpdate(this, rect);
}
get username(): string {
return this._username!;
}
set username(updated: string) {
this.logger = this.logger.child({
username: updated,
});
this._username = updated;
}
} }
export enum Rank { export enum Rank {

View File

@@ -1,22 +1,30 @@
import { WebSocket } from 'ws'; import { WebSocket } from 'ws';
import { NetworkClient } from '../NetworkClient.js'; import { NetworkClient } from '../NetworkClient.js';
import EventEmitter from 'events'; import EventEmitter from 'events';
import pino from 'pino'; import { pino, type Logger } from 'pino';
export default class WSClient extends EventEmitter implements NetworkClient { export default class WSClient extends EventEmitter implements NetworkClient {
socket: WebSocket; socket: WebSocket;
ip: string; ip: string;
uuid: string;
enforceTextOnly = true enforceTextOnly = true
private logger = pino({ name: "CVMTS.WebsocketClient" }); private logger: Logger;
constructor(ws: WebSocket, ip: string) { constructor(ws: WebSocket, ip: string, uuid: string) {
super(); super();
this.socket = ws; this.socket = ws;
this.ip = ip; this.ip = ip;
this.uuid = uuid;
this.logger = pino().child({
name: "CVMTS.WebsocketClient",
"uuid/websocket/client": uuid,
src_ip: ip,
});
this.socket.on('message', (buf: Buffer, isBinary: boolean) => { this.socket.on('message', (buf: Buffer, isBinary: boolean) => {
// Close the user's connection if they send a binary message // Close the user's connection if they send a binary message
// when we are not expecting them yet. // when we are not expecting them yet.
if (isBinary && this.enforceTextOnly) { if (isBinary && this.enforceTextOnly) {
this.logger.info({event: "received unexpected binary message"});
this.close(); this.close();
return; return;
} }
@@ -25,10 +33,11 @@ export default class WSClient extends EventEmitter implements NetworkClient {
}); });
this.socket.on('error', (err: Error) => { this.socket.on('error', (err: Error) => {
this.logger.error(err, 'WebSocket recv error'); this.logger.error({event: "websocket recv error", msg: err});
}) })
this.socket.on('close', () => { this.socket.on('close', () => {
this.logger.info({event: "disconnecting client"});
this.emit('disconnect'); this.emit('disconnect');
}); });
} }
@@ -42,12 +51,13 @@ export default class WSClient extends EventEmitter implements NetworkClient {
} }
send(msg: string): Promise<void> { send(msg: string): Promise<void> {
this.logger.trace({event: "outgoing message", msg});
return new Promise((res, rej) => { return new Promise((res, rej) => {
if (!this.isOpen()) return res(); if (!this.isOpen()) return res();
this.socket.send(msg, (err) => { this.socket.send(msg, (err) => {
if (err) { if (err) {
this.logger.error(err, 'WebSocket send error'); this.logger.error({event: "websocket send error", msg: err});
this.close(); this.close();
res(); res();
return; return;
@@ -58,12 +68,13 @@ export default class WSClient extends EventEmitter implements NetworkClient {
} }
sendBinary(msg: Uint8Array): Promise<void> { sendBinary(msg: Uint8Array): Promise<void> {
this.logger.trace({event: "outgoing message", msg});
return new Promise((res, rej) => { return new Promise((res, rej) => {
if (!this.isOpen()) return res(); if (!this.isOpen()) return res();
this.socket.send(msg, (err) => { this.socket.send(msg, (err) => {
if (err) { if (err) {
this.logger.error(err, 'WebSocket send error'); this.logger.error({event: "websocket send error", msg: err});
this.close(); this.close();
res(); res();
return; return;

View File

@@ -8,8 +8,9 @@ import { isIP } from 'net';
import { IPDataManager } from '../../IPData.js'; import { IPDataManager } from '../../IPData.js';
import WSClient from './WSClient.js'; import WSClient from './WSClient.js';
import { User } from '../../User.js'; import { User } from '../../User.js';
import pino from 'pino'; import { pino, type Logger } from 'pino';
import { BanManager } from '../../BanManager.js'; import { BanManager } from '../../BanManager.js';
import { v4 as uuid4 } from 'uuid';
const kAllowedProtocols = [ const kAllowedProtocols = [
"guacamole" // Regular ol' collabvm1 protocol "guacamole" // Regular ol' collabvm1 protocol
@@ -20,17 +21,25 @@ export default class WSServer extends EventEmitter implements NetworkServer {
private wsServer: WebSocketServer; private wsServer: WebSocketServer;
private clients: WSClient[]; private clients: WSClient[];
private Config: IConfig; private Config: IConfig;
private logger = pino({ name: 'CVMTS.WSServer' }); private logger: Logger;
private banmgr: BanManager; private banmgr: BanManager;
private uuid: string;
constructor(config: IConfig, banmgr: BanManager) { constructor(config: IConfig, banmgr: BanManager) {
super(); super();
this.Config = config; this.Config = config;
this.clients = []; this.clients = [];
this.uuid = uuid4();
this.logger = pino().child({
stream: 'CVMTS.WSServer',
"uuid/websocket/server": this.uuid,
node: config.collabvm.node,
});
this.httpServer = http.createServer(); this.httpServer = http.createServer();
this.wsServer = new WebSocketServer({ noServer: true, perMessageDeflate: false, clientTracking: false }); this.wsServer = new WebSocketServer({ noServer: true, perMessageDeflate: false, clientTracking: false });
this.httpServer.on('upgrade', (req: http.IncomingMessage, socket: internal.Duplex, head: Buffer) => this.httpOnUpgrade(req, socket, head)); this.httpServer.on('upgrade', (req: http.IncomingMessage, socket: internal.Duplex, head: Buffer) => this.httpOnUpgrade(req, socket, head));
this.httpServer.on('request', (req, res) => { this.httpServer.on('request', (req, res) => {
this.logger.debug({ event: "request", path: req.url });
res.writeHead(426); res.writeHead(426);
res.write('This server only accepts WebSocket connections.'); res.write('This server only accepts WebSocket connections.');
res.end(); res.end();
@@ -39,13 +48,33 @@ export default class WSServer extends EventEmitter implements NetworkServer {
} }
start(): void { start(): void {
this.logger.info({
event: "websocket server starting",
host: this.Config.http.host,
port: this.Config.http.port,
});
this.httpServer.listen(this.Config.http.port, this.Config.http.host, () => { this.httpServer.listen(this.Config.http.port, this.Config.http.host, () => {
this.logger.info(`WebSocket server listening on ${this.Config.http.host}:${this.Config.http.port}`); this.logger.info({
event: "websocket server started",
host: this.Config.http.host,
port: this.Config.http.port,
});
}); });
} }
stop(): void { stop(): void {
this.httpServer.close(); this.logger.info({
event: "websocket server stopping",
host: this.Config.http.host,
port: this.Config.http.port,
});
this.httpServer.close(() => {
this.logger.info({
event: "websocket server stopped",
host: this.Config.http.host,
port: this.Config.http.port,
});
});
} }
private async httpOnUpgrade(req: http.IncomingMessage, socket: internal.Duplex, head: Buffer) { private async httpOnUpgrade(req: http.IncomingMessage, socket: internal.Duplex, head: Buffer) {
@@ -142,17 +171,34 @@ export default class WSServer extends EventEmitter implements NetworkServer {
} }
private onConnection(ws: WebSocket, req: http.IncomingMessage, ip: string, protocol: string) { private onConnection(ws: WebSocket, req: http.IncomingMessage, ip: string, protocol: string) {
let client = new WSClient(ws, ip); const uuid = uuid4();
const connectionId = {
"uuid/websocket/client": uuid,
src_ip: ip
};
this.logger.info({ ...connectionId, event: "websocket client connecting" });
let client = new WSClient(ws, ip, uuid);
this.clients.push(client); this.clients.push(client);
let user = new User(client, protocol, IPDataManager.GetIPData(ip), this.Config); let user = new User(client, protocol, IPDataManager.GetIPData(ip), this.Config);
this.logger.info({
...connectionId,
event: "websocket client connection bound to user",
"uuid/user": user.uuid
});
this.emit('connect', user); this.emit('connect', user);
ws.on('error', (e) => { ws.on('error', (e) => {
this.logger.error(`${e} (caused by connection ${ip})`); this.logger.error({ ...connectionId, event: "websocket connection error" });
ws.close(); ws.close();
}); });
this.logger.info(`New WebSocket connection from ${user.IP.address}`); ws.on('close', () => {
this.logger.error({ ...connectionId, event: "websocket connection closed" });
});
this.logger.info({ ...connectionId, event: "websocket client connected" });
} }
} }

View File

@@ -3,14 +3,15 @@ import { CollabVMProtocolMessage, CollabVMProtocolMessageType } from '@cvmts/col
import { GuacamoleProtocol } from './GuacamoleProtocol.js'; import { GuacamoleProtocol } from './GuacamoleProtocol.js';
import { ScreenRect } from './Protocol'; import { ScreenRect } from './Protocol';
import { User } from '../User.js';
export class BinRectsProtocol extends GuacamoleProtocol { export class BinRectsProtocol extends GuacamoleProtocol {
sendScreenUpdate(rect: ScreenRect): void { sendScreenUpdate(user: User, rect: ScreenRect): void {
let bmsg: CollabVMProtocolMessage = { let bmsg: CollabVMProtocolMessage = {
type: CollabVMProtocolMessageType.rect, type: CollabVMProtocolMessageType.rect,
rect: rect rect: rect
}; };
this.user?.socket.sendBinary(msgpack.encode(bmsg)); user.socket.sendBinary(msgpack.encode(bmsg));
} }
} }

View File

@@ -1,42 +1,37 @@
import pino from 'pino'; import { IProtocol, IProtocolMessageHandler, ListEntry, ProtocolAddUser, ProtocolChatHistory, ProtocolFlag, ProtocolRenameStatus, ProtocolUpgradeCapability, ScreenRect } from './Protocol.js';
import { IProtocol, IProtocolMessageHandler, ListEntry, ProtocolAddUser, ProtocolBase, ProtocolChatHistory, ProtocolFlag, ProtocolRenameStatus, ProtocolUpgradeCapability, ScreenRect } from './Protocol.js';
import { Rank, User } from '../User.js'; import { Rank, User } from '../User.js';
import * as cvm from '@cvmts/cvm-rs'; import * as cvm from '@cvmts/cvm-rs';
// CollabVM protocol implementation for Guacamole. // CollabVM protocol implementation for Guacamole.
export class GuacamoleProtocol extends ProtocolBase implements IProtocol { export class GuacamoleProtocol implements IProtocol {
private logger = pino({ private __processMessage_admin(user: User, handler: IProtocolMessageHandler, decodedElements: string[]): boolean {
name: 'CVMTS.GuacamoleProtocol'
});
private __processMessage_admin(decodedElements: string[]): boolean {
switch (decodedElements[1]) { switch (decodedElements[1]) {
case '2': case '2':
if (decodedElements.length !== 3) return false; if (decodedElements.length !== 3) return false;
this.handlers?.onAdminLogin(this.user!, decodedElements[2]); handler.onAdminLogin(user, decodedElements[2]);
break; break;
case '5': case '5':
if (decodedElements.length !== 4) return false; if (decodedElements.length !== 4) return false;
this.handlers?.onAdminMonitor(this.user!, decodedElements[2], decodedElements[3]); handler.onAdminMonitor(user, decodedElements[2], decodedElements[3]);
break; break;
case '8': case '8':
if (decodedElements.length !== 3) return false; if (decodedElements.length !== 3) return false;
this.handlers?.onAdminRestore(this.user!, decodedElements[2]); handler.onAdminRestore(user, decodedElements[2]);
break; break;
case '10': case '10':
if (decodedElements.length !== 3) return false; if (decodedElements.length !== 3) return false;
this.handlers?.onAdminReboot(this.user!, decodedElements[2]); handler.onAdminReboot(user, decodedElements[2]);
break; break;
case '12': case '12':
if (decodedElements.length < 3) return false; if (decodedElements.length < 3) return false;
this.handlers?.onAdminBanUser(this.user!, decodedElements[2]); handler.onAdminBanUser(user, decodedElements[2]);
case '13': case '13':
{ {
if (decodedElements.length !== 3) return false; if (decodedElements.length !== 3) return false;
let choice = parseInt(decodedElements[2]); let choice = parseInt(decodedElements[2]);
if (choice == undefined) return false; if (choice == undefined) return false;
this.handlers?.onAdminForceVote(this.user!, choice); handler.onAdminForceVote(user, choice);
} }
break; break;
case '14': case '14':
@@ -46,35 +41,35 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
if (decodedElements[3] == '0') temporary = true; if (decodedElements[3] == '0') temporary = true;
else if (decodedElements[3] == '1') temporary = false; else if (decodedElements[3] == '1') temporary = false;
else return false; else return false;
this.handlers?.onAdminMuteUser(this.user!, decodedElements[2], temporary); handler.onAdminMuteUser(user, decodedElements[2], temporary);
} }
break; break;
case '15': case '15':
if (decodedElements.length !== 3) return false; if (decodedElements.length !== 3) return false;
this.handlers?.onAdminKickUser(this.user!, decodedElements[2]); handler.onAdminKickUser(user, decodedElements[2]);
break; break;
case '16': case '16':
if (decodedElements.length !== 3) return false; if (decodedElements.length !== 3) return false;
this.handlers?.onAdminEndTurn(this.user!, decodedElements[2]); handler.onAdminEndTurn(user, decodedElements[2]);
break; break;
case '17': case '17':
if (decodedElements.length !== 3) return false; if (decodedElements.length !== 3) return false;
this.handlers?.onAdminClearQueue(this.user!, decodedElements[2]); handler.onAdminClearQueue(user, decodedElements[2]);
break; break;
case '18': case '18':
if (decodedElements.length !== 4) return false; if (decodedElements.length !== 4) return false;
this.handlers?.onAdminRename(this.user!, decodedElements[2], decodedElements[3]); handler.onAdminRename(user, decodedElements[2], decodedElements[3]);
break; break;
case '19': case '19':
if (decodedElements.length !== 3) return false; if (decodedElements.length !== 3) return false;
this.handlers?.onAdminGetIP(this.user!, decodedElements[2]); handler.onAdminGetIP(user, decodedElements[2]);
break; break;
case '20': case '20':
this.handlers?.onAdminBypassTurn(this.user!); handler.onAdminBypassTurn(user);
break; break;
case '21': case '21':
if (decodedElements.length !== 3) return false; if (decodedElements.length !== 3) return false;
this.handlers?.onAdminRawMessage(this.user!, decodedElements[2]); handler.onAdminRawMessage(user, decodedElements[2]);
break; break;
case '22': case '22':
{ {
@@ -84,11 +79,11 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
if (decodedElements[2] == '0') enabled = false; if (decodedElements[2] == '0') enabled = false;
else if (decodedElements[2] == '1') enabled = true; else if (decodedElements[2] == '1') enabled = true;
else return false; else return false;
this.handlers?.onAdminToggleTurns(this.user!, enabled); handler.onAdminToggleTurns(user, enabled);
} }
break; break;
case '23': case '23':
this.handlers?.onAdminIndefiniteTurn(this.user!); handler.onAdminIndefiniteTurn(user);
break; break;
case '24': case '24':
{ {
@@ -97,43 +92,43 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
if (decodedElements[2] == '0') show = false; if (decodedElements[2] == '0') show = false;
else if (decodedElements[2] == '1') show = true; else if (decodedElements[2] == '1') show = true;
else return false; else return false;
this.handlers?.onAdminHideScreen(this.user!, show); handler.onAdminHideScreen(user, show);
} }
break; break;
case '25': case '25':
if (decodedElements.length !== 3) return false; if (decodedElements.length !== 3) return false;
this.handlers?.onAdminSystemMessage(this.user!, decodedElements[2]); handler.onAdminSystemMessage(user, decodedElements[2]);
break; break;
} }
return true; return true;
} }
processMessage(buffer: Buffer): boolean { processMessage(user: User, handler: IProtocolMessageHandler, buffer: Buffer): boolean {
let decodedElements = cvm.guacDecode(buffer.toString('utf-8')); let decodedElements = cvm.guacDecode(buffer.toString('utf-8'));
if (decodedElements.length < 1) return false; if (decodedElements.length < 1) return false;
// The first element is the "opcode". // The first element is the "opcode".
switch (decodedElements[0]) { switch (decodedElements[0]) {
case 'nop': case 'nop':
this.handlers?.onNop(this.user!); handler.onNop(user);
break; break;
case 'cap': case 'cap':
if (decodedElements.length < 2) return false; if (decodedElements.length < 2) return false;
this.handlers?.onCapabilityUpgrade(this.user!, decodedElements.slice(1)); handler.onCapabilityUpgrade(user, decodedElements.slice(1));
break; break;
case 'login': case 'login':
if (decodedElements.length !== 2) return false; if (decodedElements.length !== 2) return false;
this.handlers?.onLogin(this.user!, decodedElements[1]); handler.onLogin(user, decodedElements[1]);
break; break;
case 'noflag': case 'noflag':
this.handlers?.onNoFlag(this.user!); handler.onNoFlag(user);
break; break;
case 'list': case 'list':
this.handlers?.onList(this.user!); handler.onList(user);
break; break;
case 'connect': case 'connect':
if (decodedElements.length !== 2) return false; if (decodedElements.length !== 2) return false;
this.handlers?.onConnect(this.user!, decodedElements[1]); handler.onConnect(user, decodedElements[1]);
break; break;
case 'view': case 'view':
{ {
@@ -141,15 +136,15 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
let viewMode = parseInt(decodedElements[2]); let viewMode = parseInt(decodedElements[2]);
if (viewMode == undefined) return false; if (viewMode == undefined) return false;
this.handlers?.onView(this.user!, decodedElements[1], viewMode); handler.onView(user, decodedElements[1], viewMode);
} }
break; break;
case 'rename': case 'rename':
this.handlers?.onRename(this.user!, decodedElements[1]); handler.onRename(user, decodedElements[1]);
break; break;
case 'chat': case 'chat':
if (decodedElements.length !== 2) return false; if (decodedElements.length !== 2) return false;
this.handlers?.onChat(this.user!, decodedElements[1]); handler.onChat(user, decodedElements[1]);
break; break;
case 'turn': case 'turn':
let forfeit = false; let forfeit = false;
@@ -161,7 +156,7 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
else if (decodedElements[1] == '1') forfeit = false; else if (decodedElements[1] == '1') forfeit = false;
} }
this.handlers?.onTurnRequest(this.user!, forfeit); handler.onTurnRequest(user, forfeit);
break; break;
case 'mouse': case 'mouse':
if (decodedElements.length !== 4) return false; if (decodedElements.length !== 4) return false;
@@ -171,25 +166,25 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
let mask = parseInt(decodedElements[3]); let mask = parseInt(decodedElements[3]);
if (x === undefined || y === undefined || mask === undefined) return false; if (x === undefined || y === undefined || mask === undefined) return false;
this.handlers?.onMouse(this.user!, x, y, mask); handler.onMouse(user, x, y, mask);
break; break;
case 'key': case 'key':
if (decodedElements.length !== 3) return false; if (decodedElements.length !== 3) return false;
var keysym = parseInt(decodedElements[1]); var keysym = parseInt(decodedElements[1]);
var down = parseInt(decodedElements[2]); var down = parseInt(decodedElements[2]);
if (keysym === undefined || (down !== 0 && down !== 1)) return false; if (keysym === undefined || (down !== 0 && down !== 1)) return false;
this.handlers?.onKey(this.user!, keysym, down === 1); handler.onKey(user, keysym, down === 1);
break; break;
case 'vote': case 'vote':
if (decodedElements.length !== 2) return false; if (decodedElements.length !== 2) return false;
let choice = parseInt(decodedElements[1]); let choice = parseInt(decodedElements[1]);
if (choice == undefined) return false; if (choice == undefined) return false;
this.handlers?.onVote(this.user!, choice); handler.onVote(user, choice);
break; break;
case 'admin': case 'admin':
if (decodedElements.length < 2) return false; if (decodedElements.length < 2) return false;
return this.__processMessage_admin(decodedElements); return this.__processMessage_admin(user, handler, decodedElements);
} }
return true; return true;
@@ -197,109 +192,109 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
// Senders // Senders
sendAuth(authServer: string): void { sendAuth(user: User, authServer: string): void {
this.user?.sendMsg(cvm.guacEncode('auth', authServer)); user.sendMsg(cvm.guacEncode('auth', authServer));
} }
sendNop(): void { sendNop(user: User): void {
this.user?.sendMsg(cvm.guacEncode('nop')); user.sendMsg(cvm.guacEncode('nop'));
} }
sendSync(now: number): void { sendSync(user: User, now: number): void {
this.user?.sendMsg(cvm.guacEncode('sync', now.toString())); user.sendMsg(cvm.guacEncode('sync', now.toString()));
} }
sendCapabilities(caps: ProtocolUpgradeCapability[]): void { sendCapabilities(user: User, caps: ProtocolUpgradeCapability[]): void {
let arr = ['cap', ...caps]; let arr = ['cap', ...caps];
this?.user?.sendMsg(cvm.guacEncode(...arr)); user.sendMsg(cvm.guacEncode(...arr));
} }
sendConnectFailResponse(): void { sendConnectFailResponse(user: User): void {
this.user?.sendMsg(cvm.guacEncode('connect', '0')); user.sendMsg(cvm.guacEncode('connect', '0'));
} }
sendConnectOKResponse(votes: boolean): void { sendConnectOKResponse(user: User, votes: boolean): void {
this.user?.sendMsg(cvm.guacEncode('connect', '1', '1', votes ? '1' : '0', '0')); user.sendMsg(cvm.guacEncode('connect', '1', '1', votes ? '1' : '0', '0'));
} }
sendLoginResponse(ok: boolean, message: string | undefined): void { sendLoginResponse(user: User, ok: boolean, message: string | undefined): void {
if (ok) { if (ok) {
this.user?.sendMsg(cvm.guacEncode('login', '1')); user.sendMsg(cvm.guacEncode('login', '1'));
return; return;
} else { } else {
this.user?.sendMsg(cvm.guacEncode('login', '0', message!)); user.sendMsg(cvm.guacEncode('login', '0', message!));
} }
} }
sendAdminLoginResponse(ok: boolean, modPerms: number | undefined): void { sendAdminLoginResponse(user: User, ok: boolean, modPerms: number | undefined): void {
if (ok) { if (ok) {
if (modPerms == undefined) { if (modPerms == undefined) {
this.user?.sendMsg(cvm.guacEncode('admin', '0', '1')); user.sendMsg(cvm.guacEncode('admin', '0', '1'));
} else { } else {
this.user?.sendMsg(cvm.guacEncode('admin', '0', '3', modPerms.toString())); user.sendMsg(cvm.guacEncode('admin', '0', '3', modPerms.toString()));
} }
} else { } else {
this.user?.sendMsg(cvm.guacEncode('admin', '0', '0')); user.sendMsg(cvm.guacEncode('admin', '0', '0'));
} }
} }
sendAdminMonitorResponse(output: string): void { sendAdminMonitorResponse(user: User, output: string): void {
this.user?.sendMsg(cvm.guacEncode('admin', '2', output)); user.sendMsg(cvm.guacEncode('admin', '2', output));
} }
sendAdminIPResponse(username: string, ip: string): void { sendAdminIPResponse(user: User, username: string, ip: string): void {
this.user?.sendMsg(cvm.guacEncode('admin', '19', username, ip)); user.sendMsg(cvm.guacEncode('admin', '19', username, ip));
} }
sendChatMessage(username: string, message: string): void { sendChatMessage(user: User, username: string, message: string): void {
this.user?.sendMsg(cvm.guacEncode('chat', username, message)); user.sendMsg(cvm.guacEncode('chat', username, message));
} }
sendChatHistoryMessage(history: ProtocolChatHistory[]): void { sendChatHistoryMessage(user: User, history: ProtocolChatHistory[]): void {
let arr = ['chat']; let arr = ['chat'];
for (let a of history) { for (let a of history) {
arr.push(a.user, a.msg); arr.push(a.user, a.msg);
} }
this.user?.sendMsg(cvm.guacEncode(...arr)); user.sendMsg(cvm.guacEncode(...arr));
} }
sendAddUser(users: ProtocolAddUser[]): void { sendAddUser(user: User, users: ProtocolAddUser[]): void {
let arr = ['adduser', users.length.toString()]; let arr = ['adduser', users.length.toString()];
for (let user of users) { for (let user of users) {
arr.push(user.username); arr.push(user.username);
arr.push(user.rank.toString()); arr.push(user.rank.toString());
} }
this.user?.sendMsg(cvm.guacEncode(...arr)); user.sendMsg(cvm.guacEncode(...arr));
} }
sendRemUser(users: string[]): void { sendRemUser(user: User, users: string[]): void {
let arr = ['remuser', users.length.toString()]; let arr = ['remuser', users.length.toString()];
for (let user of users) { for (let user of users) {
arr.push(user); arr.push(user);
} }
this.user?.sendMsg(cvm.guacEncode(...arr)); user.sendMsg(cvm.guacEncode(...arr));
} }
sendFlag(flag: ProtocolFlag[]): void { sendFlag(user: User, flag: ProtocolFlag[]): void {
// Basically this does the same as the above manual for of things // Basically this does the same as the above manual for of things
// but in one line of code // but in one line of code
let arr = ['flag', ...flag.flatMap((flag) => [flag.username, flag.countryCode])]; let arr = ['flag', ...flag.flatMap((flag) => [flag.username, flag.countryCode])];
this.user?.sendMsg(cvm.guacEncode(...arr)); user.sendMsg(cvm.guacEncode(...arr));
} }
sendSelfRename(status: ProtocolRenameStatus, newUsername: string, rank: Rank): void { sendSelfRename(user: User, status: ProtocolRenameStatus, newUsername: string, rank: Rank): void {
this.user?.sendMsg(cvm.guacEncode('rename', '0', status.toString(), newUsername)); user.sendMsg(cvm.guacEncode('rename', '0', status.toString(), newUsername));
} }
sendRename(oldUsername: string, newUsername: string, rank: Rank): void { sendRename(user: User, oldUsername: string, newUsername: string, rank: Rank): void {
this.user?.sendMsg(cvm.guacEncode('rename', '1', oldUsername, newUsername)); user.sendMsg(cvm.guacEncode('rename', '1', oldUsername, newUsername));
} }
sendListResponse(list: ListEntry[]): void { sendListResponse(user: User, list: ListEntry[]): void {
let arr = ['list']; let arr = ['list'];
for (let node of list) { for (let node of list) {
arr.push(node.id); arr.push(node.id);
@@ -307,45 +302,45 @@ export class GuacamoleProtocol extends ProtocolBase implements IProtocol {
arr.push(node.thumbnail.toString('base64')); arr.push(node.thumbnail.toString('base64'));
} }
this.user?.sendMsg(cvm.guacEncode(...arr)); user.sendMsg(cvm.guacEncode(...arr));
} }
sendVoteStarted(): void { sendVoteStarted(user: User): void {
this.user?.sendMsg(cvm.guacEncode('vote', '0')); user.sendMsg(cvm.guacEncode('vote', '0'));
} }
sendVoteStats(msLeft: number, nrYes: number, nrNo: number): void { sendVoteStats(user: User, msLeft: number, nrYes: number, nrNo: number): void {
this.user?.sendMsg(cvm.guacEncode('vote', '1', msLeft.toString(), nrYes.toString(), nrNo.toString())); user.sendMsg(cvm.guacEncode('vote', '1', msLeft.toString(), nrYes.toString(), nrNo.toString()));
} }
sendVoteEnded(): void { sendVoteEnded(user: User): void {
this.user?.sendMsg(cvm.guacEncode('vote', '2')); user.sendMsg(cvm.guacEncode('vote', '2'));
} }
sendVoteCooldown(ms: number): void { sendVoteCooldown(user: User, ms: number): void {
this.user?.sendMsg(cvm.guacEncode('vote', '3', ms.toString())); user.sendMsg(cvm.guacEncode('vote', '3', ms.toString()));
} }
private getTurnQueueBase(turnTime: number, users: string[]): string[] { private getTurnQueueBase(turnTime: number, users: string[]): string[] {
return ['turn', turnTime.toString(), users.length.toString(), ...users]; return ['turn', turnTime.toString(), users.length.toString(), ...users];
} }
sendTurnQueue(turnTime: number, users: string[]): void { sendTurnQueue(user: User, turnTime: number, users: string[]): void {
this.user?.sendMsg(cvm.guacEncode(...this.getTurnQueueBase(turnTime, users))); user.sendMsg(cvm.guacEncode(...this.getTurnQueueBase(turnTime, users)));
} }
sendTurnQueueWaiting(turnTime: number, users: string[], waitTime: number): void { sendTurnQueueWaiting(user: User, turnTime: number, users: string[], waitTime: number): void {
let queue = this.getTurnQueueBase(turnTime, users); let queue = this.getTurnQueueBase(turnTime, users);
queue.push(waitTime.toString()); queue.push(waitTime.toString());
this.user?.sendMsg(cvm.guacEncode(...queue)); user.sendMsg(cvm.guacEncode(...queue));
} }
sendScreenResize(width: number, height: number): void { sendScreenResize(user: User, width: number, height: number): void {
this.user?.sendMsg(cvm.guacEncode('size', '0', width.toString(), height.toString())); user.sendMsg(cvm.guacEncode('size', '0', width.toString(), height.toString()));
} }
sendScreenUpdate(rect: ScreenRect): void { sendScreenUpdate(user: User, rect: ScreenRect): void {
this.user?.sendMsg(cvm.guacEncode('png', '0', '0', rect.x.toString(), rect.y.toString(), rect.data.toString('base64'))); user.sendMsg(cvm.guacEncode('png', '0', '0', rect.x.toString(), rect.y.toString(), rect.data.toString('base64')));
this.sendSync(Date.now()); this.sendSync(user, Date.now());
} }
} }

View File

@@ -1,24 +1,23 @@
import { IProtocol } from "./Protocol"; import { IProtocol } from './Protocol';
import { User } from "../User"; import { User } from '../User';
// The protocol manager. Holds protocol factories, and provides the ability // The protocol manager.
// to create a protocol by name. Avoids direct dependency on a given list of protocols, // Holds protocols, and provides the ability to obtain them by name.
// and allows (relatively simple) expansion. //
// Avoids direct dependency on a given list of protocols,
// and allows (relatively simple) expansion of the supported protocols.
export class ProtocolManager { export class ProtocolManager {
private protocols = new Map<String, () => IProtocol>(); private protocols = new Map<String, IProtocol>();
// Registers a protocol with the given name. // Registers a protocol with the given name, creates it, and stores it for later use.
registerProtocol(name: string, protocolFactory: () => IProtocol) { registerProtocol(name: string, protocolFactory: () => IProtocol) {
if (!this.protocols.has(name)) this.protocols.set(name, protocolFactory); if (!this.protocols.has(name)) this.protocols.set(name, protocolFactory());
} }
// Creates an instance of a given protocol for a user. // Gets an instance of a protocol.
createProtocol(name: string, user: User): IProtocol { getProtocol(name: string): IProtocol {
if (!this.protocols.has(name)) throw new Error(`ProtocolManager does not have protocol \"${name}\"`); let proto = this.protocols.get(name);
if (proto == undefined) throw new Error(`ProtocolManager does not have protocol \"${name}\"`);
let factory = this.protocols.get(name)!;
let proto = factory();
proto.init(user);
return proto; return proto;
} }
} }

View File

@@ -90,13 +90,6 @@ export interface IProtocolMessageHandler {
// allowing it to be protocol-independent (as long as the client and server // allowing it to be protocol-independent (as long as the client and server
// are able to speak the same protocol.) // are able to speak the same protocol.)
export interface IProtocol { export interface IProtocol {
// don't implement this yourself, extend from ProtocolBase
init(u: User): void;
dispose(): void;
// Sets handler object.
setHandler(handlers: IProtocolMessageHandler): void;
// Protocol implementation stuff // Protocol implementation stuff
// Parses a single message and fires the given handler with deserialized arguments. // Parses a single message and fires the given handler with deserialized arguments.
@@ -104,67 +97,48 @@ export interface IProtocol {
// to handle errors. It should, however, catch invalid parameters without failing. // to handle errors. It should, however, catch invalid parameters without failing.
// //
// This function will perform conversion to text if it is required. // This function will perform conversion to text if it is required.
processMessage(buffer: Buffer): boolean; processMessage(user: User, handler: IProtocolMessageHandler, buffer: Buffer): boolean;
// Senders // Senders
sendNop(): void; sendNop(user: User): void;
sendSync(now: number): void; sendSync(user: User, now: number): void;
sendAuth(authServer: string): void; sendAuth(user: User, authServer: string): void;
sendCapabilities(caps: ProtocolUpgradeCapability[]): void; sendCapabilities(user: User, caps: ProtocolUpgradeCapability[]): void;
sendConnectFailResponse(): void; sendConnectFailResponse(user: User): void;
sendConnectOKResponse(votes: boolean): void; sendConnectOKResponse(user: User, votes: boolean): void;
sendLoginResponse(ok: boolean, message: string | undefined): void; sendLoginResponse(user: User, ok: boolean, message: string | undefined): void;
sendAdminLoginResponse(ok: boolean, modPerms: number | undefined): void; sendAdminLoginResponse(user: User, ok: boolean, modPerms: number | undefined): void;
sendAdminMonitorResponse(output: string): void; sendAdminMonitorResponse(user: User, output: string): void;
sendAdminIPResponse(username: string, ip: string): void; sendAdminIPResponse(user: User, username: string, ip: string): void;
sendChatMessage(username: '' | string, message: string): void; sendChatMessage(user: User, username: '' | string, message: string): void;
sendChatHistoryMessage(history: ProtocolChatHistory[]): void; sendChatHistoryMessage(user: User, history: ProtocolChatHistory[]): void;
sendAddUser(users: ProtocolAddUser[]): void; sendAddUser(user: User, users: ProtocolAddUser[]): void;
sendRemUser(users: string[]): void; sendRemUser(user: User, users: string[]): void;
sendFlag(flag: ProtocolFlag[]): void; sendFlag(user: User, flag: ProtocolFlag[]): void;
sendSelfRename(status: ProtocolRenameStatus, newUsername: string, rank: Rank): void; sendSelfRename(user: User, status: ProtocolRenameStatus, newUsername: string, rank: Rank): void;
sendRename(oldUsername: string, newUsername: string, rank: Rank): void; sendRename(user: User, oldUsername: string, newUsername: string, rank: Rank): void;
sendListResponse(list: ListEntry[]): void; sendListResponse(user: User, list: ListEntry[]): void;
sendTurnQueue(turnTime: number, users: string[]): void; sendTurnQueue(user: User, turnTime: number, users: string[]): void;
sendTurnQueueWaiting(turnTime: number, users: string[], waitTime: number): void; sendTurnQueueWaiting(user: User, turnTime: number, users: string[], waitTime: number): void;
sendVoteStarted(): void; sendVoteStarted(user: User): void;
sendVoteStats(msLeft: number, nrYes: number, nrNo: number): void; sendVoteStats(user: User, msLeft: number, nrYes: number, nrNo: number): void;
sendVoteEnded(): void; sendVoteEnded(user: User): void;
sendVoteCooldown(ms: number): void; sendVoteCooldown(user: User, ms: number): void;
sendScreenResize(width: number, height: number): void; sendScreenResize(user: User, width: number, height: number): void;
// Sends a rectangle update to the user. // Sends a rectangle update to the user.
sendScreenUpdate(rect: ScreenRect): void; sendScreenUpdate(user: User, rect: ScreenRect): void;
}
// Base mixin for all concrete protocols to use. Inherit from this!
export class ProtocolBase {
protected handlers: IProtocolMessageHandler | null = null;
protected user: User | null = null;
init(u: User): void {
this.user = u;
}
dispose(): void {
this.user = null;
this.handlers = null;
}
setHandler(handlers: IProtocolMessageHandler): void {
this.handlers = handlers;
}
} }

View File

@@ -104,13 +104,27 @@ export class CGroup {
if (!existsSync(kCgroupSelfPath)) throw new Error('This process is not in a CGroup.'); if (!existsSync(kCgroupSelfPath)) throw new Error('This process is not in a CGroup.');
let res = readFileSync(kCgroupSelfPath, { encoding: 'utf-8' }); let res = readFileSync(kCgroupSelfPath, { encoding: 'utf-8' });
// Make sure the first/only line is a cgroups2 0::/path/to/cgroup entry. for (let item of res.split('\n')) {
// Legacy cgroups1 is not supported. switch (item[0]) {
if (res[0] != '0') throw new Error('CGroup.Self() does not work with cgroups 1 systems. Please do not the cgroups 1.'); // This is techinically cgroupsv1. However, on a unified system
let cg_path = res.substring(3, res.indexOf('\n')); // `systemd-nspawn` by default will mount /sys/fs/cgroup/systemd in the container,
// and this leaks out slightly to the host with `1:name=systemd:/`.. for some reason.
// Therefore we can allow this. Other controllers not so much.
case '1':
continue;
break;
case '0': // cgroups2
let cg_path = item.substring(3);
let cg = new CGroup(path.join('/sys/fs/cgroup', cg_path)); let cg = new CGroup(path.join('/sys/fs/cgroup', cg_path));
return cg; return cg;
break;
// Legacy CGroups 1 is not supported.
default:
throw new Error('CGroup.Self() does not work with cgroups 1 systems. Please do not the cgroups 1.');
}
}
throw new Error('1 2 3 5 4 6');
} }
} }

View File

@@ -12,6 +12,7 @@
"@types/jsbn": "^1.2.33", "@types/jsbn": "^1.2.33",
"@types/node": "^20.14.10", "@types/node": "^20.14.10",
"parcel": "^2.12.0", "parcel": "^2.12.0",
"pino-pretty": "^11.2.1",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"prettier-plugin-toml": "^2.0.1", "prettier-plugin-toml": "^2.0.1",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",

307
yarn.lock
View File

@@ -90,197 +90,13 @@ __metadata:
msgpackr: "npm:^1.10.2" msgpackr: "npm:^1.10.2"
pino: "npm:^9.3.1" pino: "npm:^9.3.1"
pino-pretty: "npm:^11.2.1" pino-pretty: "npm:^11.2.1"
sharp: "npm:^0.33.3"
toml: "npm:^3.0.0" toml: "npm:^3.0.0"
typescript: "npm:^5.4.4" typescript: "npm:^5.4.4"
uuid: "npm:^13.0.0"
ws: "npm:^8.17.1" ws: "npm:^8.17.1"
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"@emnapi/runtime@npm:^1.1.1":
version: 1.2.0
resolution: "@emnapi/runtime@npm:1.2.0"
dependencies:
tslib: "npm:^2.4.0"
checksum: 10c0/7005ff8b67724c9e61b6cd79a3decbdb2ce25d24abd4d3d187472f200ee6e573329c30264335125fb136bd813aa9cf9f4f7c9391d04b07dd1e63ce0a3427be57
languageName: node
linkType: hard
"@img/sharp-darwin-arm64@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-darwin-arm64@npm:0.33.4"
dependencies:
"@img/sharp-libvips-darwin-arm64": "npm:1.0.2"
dependenciesMeta:
"@img/sharp-libvips-darwin-arm64":
optional: true
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@img/sharp-darwin-x64@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-darwin-x64@npm:0.33.4"
dependencies:
"@img/sharp-libvips-darwin-x64": "npm:1.0.2"
dependenciesMeta:
"@img/sharp-libvips-darwin-x64":
optional: true
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@img/sharp-libvips-darwin-arm64@npm:1.0.2":
version: 1.0.2
resolution: "@img/sharp-libvips-darwin-arm64@npm:1.0.2"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@img/sharp-libvips-darwin-x64@npm:1.0.2":
version: 1.0.2
resolution: "@img/sharp-libvips-darwin-x64@npm:1.0.2"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@img/sharp-libvips-linux-arm64@npm:1.0.2":
version: 1.0.2
resolution: "@img/sharp-libvips-linux-arm64@npm:1.0.2"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@img/sharp-libvips-linux-arm@npm:1.0.2":
version: 1.0.2
resolution: "@img/sharp-libvips-linux-arm@npm:1.0.2"
conditions: os=linux & cpu=arm & libc=glibc
languageName: node
linkType: hard
"@img/sharp-libvips-linux-s390x@npm:1.0.2":
version: 1.0.2
resolution: "@img/sharp-libvips-linux-s390x@npm:1.0.2"
conditions: os=linux & cpu=s390x & libc=glibc
languageName: node
linkType: hard
"@img/sharp-libvips-linux-x64@npm:1.0.2":
version: 1.0.2
resolution: "@img/sharp-libvips-linux-x64@npm:1.0.2"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@img/sharp-libvips-linuxmusl-arm64@npm:1.0.2":
version: 1.0.2
resolution: "@img/sharp-libvips-linuxmusl-arm64@npm:1.0.2"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@img/sharp-libvips-linuxmusl-x64@npm:1.0.2":
version: 1.0.2
resolution: "@img/sharp-libvips-linuxmusl-x64@npm:1.0.2"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@img/sharp-linux-arm64@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-linux-arm64@npm:0.33.4"
dependencies:
"@img/sharp-libvips-linux-arm64": "npm:1.0.2"
dependenciesMeta:
"@img/sharp-libvips-linux-arm64":
optional: true
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@img/sharp-linux-arm@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-linux-arm@npm:0.33.4"
dependencies:
"@img/sharp-libvips-linux-arm": "npm:1.0.2"
dependenciesMeta:
"@img/sharp-libvips-linux-arm":
optional: true
conditions: os=linux & cpu=arm & libc=glibc
languageName: node
linkType: hard
"@img/sharp-linux-s390x@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-linux-s390x@npm:0.33.4"
dependencies:
"@img/sharp-libvips-linux-s390x": "npm:1.0.2"
dependenciesMeta:
"@img/sharp-libvips-linux-s390x":
optional: true
conditions: os=linux & cpu=s390x & libc=glibc
languageName: node
linkType: hard
"@img/sharp-linux-x64@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-linux-x64@npm:0.33.4"
dependencies:
"@img/sharp-libvips-linux-x64": "npm:1.0.2"
dependenciesMeta:
"@img/sharp-libvips-linux-x64":
optional: true
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@img/sharp-linuxmusl-arm64@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-linuxmusl-arm64@npm:0.33.4"
dependencies:
"@img/sharp-libvips-linuxmusl-arm64": "npm:1.0.2"
dependenciesMeta:
"@img/sharp-libvips-linuxmusl-arm64":
optional: true
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@img/sharp-linuxmusl-x64@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-linuxmusl-x64@npm:0.33.4"
dependencies:
"@img/sharp-libvips-linuxmusl-x64": "npm:1.0.2"
dependenciesMeta:
"@img/sharp-libvips-linuxmusl-x64":
optional: true
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@img/sharp-wasm32@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-wasm32@npm:0.33.4"
dependencies:
"@emnapi/runtime": "npm:^1.1.1"
conditions: cpu=wasm32
languageName: node
linkType: hard
"@img/sharp-win32-ia32@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-win32-ia32@npm:0.33.4"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@img/sharp-win32-x64@npm:0.33.4":
version: 0.33.4
resolution: "@img/sharp-win32-x64@npm:0.33.4"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@isaacs/cliui@npm:^8.0.2": "@isaacs/cliui@npm:^8.0.2":
version: 8.0.2 version: 8.0.2
resolution: "@isaacs/cliui@npm:8.0.2" resolution: "@isaacs/cliui@npm:8.0.2"
@@ -1858,33 +1674,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"color-name@npm:^1.0.0, color-name@npm:~1.1.4": "color-name@npm:~1.1.4":
version: 1.1.4 version: 1.1.4
resolution: "color-name@npm:1.1.4" resolution: "color-name@npm:1.1.4"
checksum: 10c0/a1a3f914156960902f46f7f56bc62effc6c94e84b2cae157a526b1c1f74b677a47ec602bf68a61abfa2b42d15b7c5651c6dbe72a43af720bc588dff885b10f95 checksum: 10c0/a1a3f914156960902f46f7f56bc62effc6c94e84b2cae157a526b1c1f74b677a47ec602bf68a61abfa2b42d15b7c5651c6dbe72a43af720bc588dff885b10f95
languageName: node languageName: node
linkType: hard linkType: hard
"color-string@npm:^1.9.0":
version: 1.9.1
resolution: "color-string@npm:1.9.1"
dependencies:
color-name: "npm:^1.0.0"
simple-swizzle: "npm:^0.2.2"
checksum: 10c0/b0bfd74c03b1f837f543898b512f5ea353f71630ccdd0d66f83028d1f0924a7d4272deb278b9aef376cacf1289b522ac3fb175e99895283645a2dc3a33af2404
languageName: node
linkType: hard
"color@npm:^4.2.3":
version: 4.2.3
resolution: "color@npm:4.2.3"
dependencies:
color-convert: "npm:^2.0.1"
color-string: "npm:^1.9.0"
checksum: 10c0/7fbe7cfb811054c808349de19fb380252e5e34e61d7d168ec3353e9e9aacb1802674bddc657682e4e9730c2786592a4de6f8283e7e0d3870b829bb0b7b2f6118
languageName: node
linkType: hard
"colorette@npm:^2.0.7": "colorette@npm:^2.0.7":
version: 2.0.20 version: 2.0.20
resolution: "colorette@npm:2.0.20" resolution: "colorette@npm:2.0.20"
@@ -1983,6 +1779,7 @@ __metadata:
"@types/jsbn": "npm:^1.2.33" "@types/jsbn": "npm:^1.2.33"
"@types/node": "npm:^20.14.10" "@types/node": "npm:^20.14.10"
parcel: "npm:^2.12.0" parcel: "npm:^2.12.0"
pino-pretty: "npm:^11.2.1"
prettier: "npm:^3.3.3" prettier: "npm:^3.3.3"
prettier-plugin-toml: "npm:^2.0.1" prettier-plugin-toml: "npm:^2.0.1"
rimraf: "npm:^6.0.1" rimraf: "npm:^6.0.1"
@@ -2035,7 +1832,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"detect-libc@npm:^2.0.1, detect-libc@npm:^2.0.3": "detect-libc@npm:^2.0.1":
version: 2.0.3 version: 2.0.3
resolution: "detect-libc@npm:2.0.3" resolution: "detect-libc@npm:2.0.3"
checksum: 10c0/88095bda8f90220c95f162bf92cad70bd0e424913e655c20578600e35b91edc261af27531cf160a331e185c0ced93944bc7e09939143225f56312d7fd800fdb7 checksum: 10c0/88095bda8f90220c95f162bf92cad70bd0e424913e655c20578600e35b91edc261af27531cf160a331e185c0ced93944bc7e09939143225f56312d7fd800fdb7
@@ -2576,13 +2373,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-arrayish@npm:^0.3.1":
version: 0.3.2
resolution: "is-arrayish@npm:0.3.2"
checksum: 10c0/f59b43dc1d129edb6f0e282595e56477f98c40278a2acdc8b0a5c57097c9eff8fe55470493df5775478cf32a4dc8eaf6d3a749f07ceee5bc263a78b2434f6a54
languageName: node
linkType: hard
"is-binary-path@npm:~2.1.0": "is-binary-path@npm:~2.1.0":
version: 2.1.0 version: 2.1.0
resolution: "is-binary-path@npm:2.1.0" resolution: "is-binary-path@npm:2.1.0"
@@ -3738,7 +3528,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"semver@npm:^7.3.5, semver@npm:^7.5.2, semver@npm:^7.6.0": "semver@npm:^7.3.5, semver@npm:^7.5.2":
version: 7.6.3 version: 7.6.3
resolution: "semver@npm:7.6.3" resolution: "semver@npm:7.6.3"
bin: bin:
@@ -3747,75 +3537,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"sharp@npm:^0.33.3":
version: 0.33.4
resolution: "sharp@npm:0.33.4"
dependencies:
"@img/sharp-darwin-arm64": "npm:0.33.4"
"@img/sharp-darwin-x64": "npm:0.33.4"
"@img/sharp-libvips-darwin-arm64": "npm:1.0.2"
"@img/sharp-libvips-darwin-x64": "npm:1.0.2"
"@img/sharp-libvips-linux-arm": "npm:1.0.2"
"@img/sharp-libvips-linux-arm64": "npm:1.0.2"
"@img/sharp-libvips-linux-s390x": "npm:1.0.2"
"@img/sharp-libvips-linux-x64": "npm:1.0.2"
"@img/sharp-libvips-linuxmusl-arm64": "npm:1.0.2"
"@img/sharp-libvips-linuxmusl-x64": "npm:1.0.2"
"@img/sharp-linux-arm": "npm:0.33.4"
"@img/sharp-linux-arm64": "npm:0.33.4"
"@img/sharp-linux-s390x": "npm:0.33.4"
"@img/sharp-linux-x64": "npm:0.33.4"
"@img/sharp-linuxmusl-arm64": "npm:0.33.4"
"@img/sharp-linuxmusl-x64": "npm:0.33.4"
"@img/sharp-wasm32": "npm:0.33.4"
"@img/sharp-win32-ia32": "npm:0.33.4"
"@img/sharp-win32-x64": "npm:0.33.4"
color: "npm:^4.2.3"
detect-libc: "npm:^2.0.3"
semver: "npm:^7.6.0"
dependenciesMeta:
"@img/sharp-darwin-arm64":
optional: true
"@img/sharp-darwin-x64":
optional: true
"@img/sharp-libvips-darwin-arm64":
optional: true
"@img/sharp-libvips-darwin-x64":
optional: true
"@img/sharp-libvips-linux-arm":
optional: true
"@img/sharp-libvips-linux-arm64":
optional: true
"@img/sharp-libvips-linux-s390x":
optional: true
"@img/sharp-libvips-linux-x64":
optional: true
"@img/sharp-libvips-linuxmusl-arm64":
optional: true
"@img/sharp-libvips-linuxmusl-x64":
optional: true
"@img/sharp-linux-arm":
optional: true
"@img/sharp-linux-arm64":
optional: true
"@img/sharp-linux-s390x":
optional: true
"@img/sharp-linux-x64":
optional: true
"@img/sharp-linuxmusl-arm64":
optional: true
"@img/sharp-linuxmusl-x64":
optional: true
"@img/sharp-wasm32":
optional: true
"@img/sharp-win32-ia32":
optional: true
"@img/sharp-win32-x64":
optional: true
checksum: 10c0/428c5c6a84ff8968effe50c2de931002f5f30b9f263e1c026d0384e581673c13088a49322f7748114d3d9be4ae9476a74bf003a3af34743e97ef2f880d1cfe45
languageName: node
linkType: hard
"shebang-command@npm:^2.0.0": "shebang-command@npm:^2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "shebang-command@npm:2.0.0" resolution: "shebang-command@npm:2.0.0"
@@ -3839,15 +3560,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"simple-swizzle@npm:^0.2.2":
version: 0.2.2
resolution: "simple-swizzle@npm:0.2.2"
dependencies:
is-arrayish: "npm:^0.3.1"
checksum: 10c0/df5e4662a8c750bdba69af4e8263c5d96fe4cd0f9fe4bdfa3cbdeb45d2e869dff640beaaeb1ef0e99db4d8d2ec92f85508c269f50c972174851bc1ae5bd64308
languageName: node
linkType: hard
"smart-buffer@npm:^4.2.0": "smart-buffer@npm:^4.2.0":
version: 4.2.0 version: 4.2.0
resolution: "smart-buffer@npm:4.2.0" resolution: "smart-buffer@npm:4.2.0"
@@ -4181,6 +3893,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"uuid@npm:^13.0.0":
version: 13.0.0
resolution: "uuid@npm:13.0.0"
bin:
uuid: dist-node/bin/uuid
checksum: 10c0/950e4c18d57fef6c69675344f5700a08af21e26b9eff2bf2180427564297368c538ea11ac9fb2e6528b17fc3966a9fd2c5049361b0b63c7d654f3c550c9b3d67
languageName: node
linkType: hard
"verror@npm:1.10.0": "verror@npm:1.10.0":
version: 1.10.0 version: 1.10.0
resolution: "verror@npm:1.10.0" resolution: "verror@npm:1.10.0"