chore: comment config.example.toml and format code with prettier/cargo

This commit is contained in:
Elijah R
2024-08-04 15:50:00 -04:00
parent 5aa842bb3e
commit 64d4774d00
21 changed files with 340 additions and 295 deletions

View File

@@ -16,5 +16,6 @@
"tabWidth": 4, "tabWidth": 4,
"trailingComma": "none", "trailingComma": "none",
"useTabs": true, "useTabs": true,
"vueIndentScriptAndStyle": false "vueIndentScriptAndStyle": false,
"plugins": ["prettier-plugin-toml"]
} }

View File

@@ -21,15 +21,21 @@ accountID = ""
licenseKey = "" licenseKey = ""
[tcp] [tcp]
# Enabled the raw TCP socket server
# You usually want to leave this disabled
enabled = false enabled = false
host = "0.0.0.0" host = "0.0.0.0"
port = 6014 port = 6014
[auth] [auth]
# Enables the CollabVM account authentication system
# Requires an authentication server (https://git.computernewb.com/collabvm/CollabVMAuthServer)
enabled = false enabled = false
apiEndpoint = "" apiEndpoint = ""
secretKey = "hunter2" secretKey = "hunter2"
# When account authentication is enabled, this section defines what guests can and can't do
# Has no effect if auth is disabled
[auth.guestPermissions] [auth.guestPermissions]
chat = true chat = true
turn = false turn = false
@@ -37,13 +43,21 @@ callForReset = false
vote = true vote = true
[vm] [vm]
# Configures the type of VM this server will use
# Supported values:
# "qemu" - Runs a QEMU VM
# "vncvm" - Connects to an existing VNC server
type = "qemu" type = "qemu"
# QEMU options
# Only used if vm.type is set to "qemu"
[qemu] [qemu]
qemuArgs = "qemu-system-x86_64" qemuArgs = "qemu-system-x86_64"
vncPort = 5900 vncPort = 5900
snapshots = true snapshots = true
# VNC options
# Only used if vm.type is set to "vncvm"
[vncvm] [vncvm]
vncHost = "127.0.0.1" vncHost = "127.0.0.1"
vncPort = 5900 vncPort = 5900
@@ -69,12 +83,16 @@ database = "cvmts"
cvmban = false cvmban = false
[collabvm] [collabvm]
# Node ID for this server
# Make sure this is unique among all the other nodes in a webapp
node = "acoolvm" node = "acoolvm"
# HTML display name shown on the VM list
displayname = "A <b>Really</b> Cool CollabVM Instance" displayname = "A <b>Really</b> Cool CollabVM Instance"
# HTML message shown in the chat when a user joins
motd = "welcome!" motd = "welcome!"
# Maximum amount of active connections allowed from the same IP. # Maximum amount of active connections allowed from the same IP.
maxConnections = 3 maxConnections = 3
# Moderator rank enabled # Moderator rank enabled (permissions are defined below)
moderatorEnabled = true moderatorEnabled = true
# List of disallowed usernames # List of disallowed usernames
usernameblacklist = [] usernameblacklist = []
@@ -83,9 +101,9 @@ maxChatLength = 100
# Maximum messages in the chat history buffer before old messages are overwritten # Maximum messages in the chat history buffer before old messages are overwritten
maxChatHistoryLength = 10 maxChatHistoryLength = 10
# Limit the amount of users allowed in the turn queue at the same time from the same IP # Limit the amount of users allowed in the turn queue at the same time from the same IP
turnlimit = {enabled = true, maximum = 1} turnlimit = { enabled = true, maximum = 1 }
# Temporarily mute a user if they send more than x messages in n seconds # Temporarily mute a user if they send more than x messages in n seconds
automute = {enabled = true, seconds = 5, messages = 5} automute = { enabled = true, seconds = 5, messages = 5 }
# How long a temporary mute lasts, in seconds # How long a temporary mute lasts, in seconds
tempMuteTime = 30 tempMuteTime = 30
# How long a turn lasts, in seconds # How long a turn lasts, in seconds
@@ -103,8 +121,9 @@ modpass = "fb8c2e2b85ca81eb4350199faddd983cb26af3064614e737ea9f479621cfa57a"
turnwhitelist = false turnwhitelist = false
# SHA256 sum for the password to take a turn. Only takes effect if turnwhitelist == true. If set to an empty string or not provided, only admins and mods can take turns # SHA256 sum for the password to take a turn. Only takes effect if turnwhitelist == true. If set to an empty string or not provided, only admins and mods can take turns
turnpass = "" turnpass = ""
[collabvm.moderatorPermissions]
# What a moderator can and can't do # What a moderator can and can't do
[collabvm.moderatorPermissions]
restore = true restore = true
reboot = true reboot = true
ban = true ban = true

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

@@ -5,21 +5,20 @@ export function guacDecode(input: string): string[];
export function guacEncode(...items: string[]): string; export function guacEncode(...items: string[]): string;
interface JpegInputArgs { interface JpegInputArgs {
width: number, width: number;
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;
// 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)).
} }
/// 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? // TODO: Version that can downscale?
/* remoting API? /* remoting API?
js side api: js side api:

View File

@@ -8,7 +8,8 @@
"scripts": { "scripts": {
"build": "cargo-cp-artifact -nc index.node -- cargo build --release --message-format=json-render-diagnostics", "build": "cargo-cp-artifact -nc index.node -- cargo build --release --message-format=json-render-diagnostics",
"install": "yarn build", "install": "yarn build",
"test": "cargo test" "test": "cargo test",
"format": "cargo fmt"
}, },
"devDependencies": { "devDependencies": {
"cargo-cp-artifact": "^0.1" "cargo-cp-artifact": "^0.1"

View File

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

View File

@@ -1,5 +1,5 @@
use neon::prelude::*;
use crate::guac; use crate::guac;
use neon::prelude::*;
fn guac_decode_impl<'a>(cx: &mut FunctionContext<'a>) -> JsResult<'a, JsArray> { fn guac_decode_impl<'a>(cx: &mut FunctionContext<'a>) -> JsResult<'a, JsArray> {
let input = cx.argument::<JsString>(0)?.value(cx); let input = cx.argument::<JsString>(0)?.value(cx);

View File

@@ -61,13 +61,13 @@ impl JpegCompressor {
(TJFLAG_NOREALLOC) as i32, (TJFLAG_NOREALLOC) as i32,
); );
// TODO: Result sex so we can actually notify failure // TODO: Result sex so we can actually notify failure
if res == -1 { if res == -1 {
return Vec::new(); return Vec::new();
} }
// Truncate down to the size we're given back // Truncate down to the size we're given back
vec.truncate(size as usize); vec.truncate(size as usize);
return vec; return vec;
} }
} }

View File

@@ -41,7 +41,7 @@ fn jpeg_encode_impl<'a>(cx: &mut FunctionContext<'a>) -> JsResult<'a, JsPromise>
let copy: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::with_capacity(buf.len()))); let copy: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::with_capacity(buf.len())));
// Copy from the node buffer to our temporary buffer // Copy from the node buffer to our temporary buffer
{ {
let mut locked = copy.lock().unwrap(); let mut locked = copy.lock().unwrap();
let cap = locked.capacity(); let cap = locked.capacity();
@@ -49,8 +49,8 @@ fn jpeg_encode_impl<'a>(cx: &mut FunctionContext<'a>) -> JsResult<'a, JsPromise>
locked.copy_from_slice(buf); locked.copy_from_slice(buf);
} }
// Spawn off a tokio blocking pool thread that will do the work for us // Spawn off a tokio blocking pool thread that will do the work for us
runtime.spawn_blocking(move || { runtime.spawn_blocking(move || {
let clone = Arc::clone(&copy); let clone = Arc::clone(&copy);
let locked = clone.lock().unwrap(); let locked = clone.lock().unwrap();
@@ -70,7 +70,7 @@ fn jpeg_encode_impl<'a>(cx: &mut FunctionContext<'a>) -> JsResult<'a, JsPromise>
b.compress_buffer(&image) b.compress_buffer(&image)
}); });
// Fulfill the Javascript promise with our encoded buffer // Fulfill the Javascript promise with our encoded buffer
deferred.settle_with(&channel, move |mut cx| { deferred.settle_with(&channel, move |mut cx| {
let mut buf = cx.buffer(vec.len())?; let mut buf = cx.buffer(vec.len())?;
let slice = buf.as_mut_slice(&mut cx); let slice = buf.as_mut_slice(&mut cx);

View File

@@ -4,13 +4,11 @@ mod guac_js;
mod jpeg_compressor; mod jpeg_compressor;
mod jpeg_js; mod jpeg_js;
use neon::prelude::*; use neon::prelude::*;
#[neon::main] #[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> { fn main(mut cx: ModuleContext) -> NeonResult<()> {
// Mostly transitionary, later on API should change // Mostly transitionary, later on API should change
cx.export_function("jpegEncode", jpeg_js::jpeg_encode)?; cx.export_function("jpegEncode", jpeg_js::jpeg_encode)?;
cx.export_function("guacDecode", guac_js::guac_decode)?; cx.export_function("guacDecode", guac_js::guac_decode)?;

View File

@@ -1,35 +1,35 @@
{ {
"name": "@cvmts/cvmts", "name": "@cvmts/cvmts",
"version": "1.0.0", "version": "1.0.0",
"description": "replacement for collabvm 1.2.11 because the old one :boom:", "description": "replacement for collabvm 1.2.11 because the old one :boom:",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
"build": "tsc -outDir dist -rootDir src/", "build": "tsc -outDir dist -rootDir src/",
"serve": "node dist/index.js" "serve": "node dist/index.js",
}, "format": "prettier --write ."
"author": "Elijah R, modeco80", },
"license": "GPL-3.0", "author": "Elijah R, modeco80",
"dependencies": { "license": "GPL-3.0",
"@computernewb/nodejs-rfb": "^0.3.0", "dependencies": {
"@computernewb/superqemu": "^0.1.0", "@computernewb/nodejs-rfb": "^0.3.0",
"@cvmts/cvm-rs": "*", "@computernewb/superqemu": "^0.1.0",
"@maxmind/geoip2-node": "^5.0.0", "@cvmts/cvm-rs": "*",
"execa": "^8.0.1", "@maxmind/geoip2-node": "^5.0.0",
"ip-address": "^9.0.5", "execa": "^8.0.1",
"mariadb": "^3.3.1", "ip-address": "^9.0.5",
"mnemonist": "^0.39.5", "mariadb": "^3.3.1",
"msgpackr": "^1.10.2", "mnemonist": "^0.39.5",
"pino": "^9.3.1", "msgpackr": "^1.10.2",
"sharp": "^0.33.3", "pino": "^9.3.1",
"toml": "^3.0.0", "sharp": "^0.33.3",
"ws": "^8.17.1" "toml": "^3.0.0",
}, "ws": "^8.17.1"
"devDependencies": { },
"@types/node": "^20.12.5", "devDependencies": {
"@types/ws": "^8.5.5", "@types/node": "^20.12.5",
"pino-pretty": "^11.2.1", "@types/ws": "^8.5.5",
"prettier": "^3.2.5", "pino-pretty": "^11.2.1",
"typescript": "^5.4.4" "typescript": "^5.4.4"
} }
} }

View File

@@ -1,81 +1,80 @@
import { ExecaSyncError, execa, execaCommand } from "execa"; import { ExecaSyncError, execa, execaCommand } from 'execa';
import { BanConfig } from "./IConfig"; import { BanConfig } from './IConfig';
import pino from "pino"; import pino from 'pino';
import { Database } from "./Database"; import { Database } from './Database';
import { Address6 } from "ip-address"; import { Address6 } from 'ip-address';
import { isIP } from "net"; import { isIP } from 'net';
export class BanManager { export class BanManager {
private cfg: BanConfig; private cfg: BanConfig;
private logger: pino.Logger; private logger: pino.Logger;
private db: Database | undefined; private db: Database | undefined;
constructor(config: BanConfig, db: Database | undefined) { constructor(config: BanConfig, db: Database | undefined) {
this.cfg = config; this.cfg = config;
this.logger = pino({ this.logger = pino({
name: "CVMTS.BanManager" name: 'CVMTS.BanManager'
}); });
this.db = db; this.db = db;
} }
private formatIP(ip: string) { private formatIP(ip: string) {
switch (isIP(ip)) { switch (isIP(ip)) {
case 4: case 4:
// If IPv4, just return as-is // If IPv4, just return as-is
return ip; return ip;
case 6: { case 6: {
// If IPv6, return the /64 equivalent // If IPv6, return the /64 equivalent
let addr = new Address6(ip); let addr = new Address6(ip);
addr.subnetMask = 64; addr.subnetMask = 64;
return addr.startAddress().canonicalForm() + '/64'; return addr.startAddress().canonicalForm() + '/64';
} }
case 0: case 0:
default: default:
// Invalid IP // Invalid IP
throw new Error("Invalid IP address (what the hell did you even do???)"); throw new Error('Invalid IP address (what the hell did you even do???)');
} }
} }
async BanUser(ip: string, username: string) { async BanUser(ip: string, username: string) {
ip = this.formatIP(ip); ip = this.formatIP(ip);
// If cvmban enabled, write to DB // If cvmban enabled, write to DB
if (this.cfg.cvmban) { if (this.cfg.cvmban) {
if (!this.db) throw new Error("CVMBAN enabled but Database is undefined"); if (!this.db) throw new Error('CVMBAN enabled but Database is undefined');
await this.db.banIP(ip, username); await this.db.banIP(ip, username);
} }
// If ban command enabled, run it // If ban command enabled, run it
try { try {
if (Array.isArray(this.cfg.bancmd)) { if (Array.isArray(this.cfg.bancmd)) {
let args: string[] = this.cfg.bancmd.map((a: string) => this.banCmdArgs(a, ip, username)); let args: string[] = this.cfg.bancmd.map((a: string) => this.banCmdArgs(a, ip, username));
if (args.length || args[0].length) { if (args.length || args[0].length) {
this.logger.info(`Running "${JSON.stringify(args)}"`); this.logger.info(`Running "${JSON.stringify(args)}"`);
await execa(args.shift()!, args, { stdout: process.stdout, stderr: process.stderr }); await execa(args.shift()!, args, { stdout: process.stdout, stderr: process.stderr });
} }
} else if (typeof this.cfg.bancmd == 'string') { } else if (typeof this.cfg.bancmd == 'string') {
let cmd: string = this.banCmdArgs(this.cfg.bancmd, ip, username); let cmd: string = this.banCmdArgs(this.cfg.bancmd, ip, username);
if (cmd.length) { if (cmd.length) {
// Run through JSON.stringify for char escaping // Run through JSON.stringify for char escaping
this.logger.info(`Running ${JSON.stringify(cmd)}`); this.logger.info(`Running ${JSON.stringify(cmd)}`);
await execaCommand(cmd, { stdout: process.stdout, stderr: process.stderr }); await execaCommand(cmd, { stdout: process.stdout, stderr: process.stderr });
} }
} }
} catch (e) { } catch (e) {
this.logger.error(`Failed to ban ${ip} (${username}): ${(e as ExecaSyncError).shortMessage}`); this.logger.error(`Failed to ban ${ip} (${username}): ${(e as ExecaSyncError).shortMessage}`);
} }
}
async isIPBanned(ip: string) {
ip = this.formatIP(ip);
if (!this.db) return false;
if (await this.db.isIPBanned(ip)) {
this.logger.info(`Banned IP ${ip} tried connecting.`);
return true;
}
return false;
}
private banCmdArgs(arg: string, ip: string, username: string): string {
return arg.replace(/\$IP/g, ip).replace(/\$NAME/g, username);
} }
} async isIPBanned(ip: string) {
ip = this.formatIP(ip);
if (!this.db) return false;
if (await this.db.isIPBanned(ip)) {
this.logger.info(`Banned IP ${ip} tried connecting.`);
return true;
}
return false;
}
private banCmdArgs(arg: string, ip: string, username: string): string {
return arg.replace(/\$IP/g, ip).replace(/\$NAME/g, username);
}
}

View File

@@ -39,7 +39,6 @@ type VoteTally = {
no: number; no: number;
}; };
export default class CollabVMServer { export default class CollabVMServer {
private Config: IConfig; private Config: IConfig;

View File

@@ -1,44 +1,46 @@
import pino, { Logger } from "pino"; import pino, { Logger } from 'pino';
import { MySQLConfig } from "./IConfig"; import { MySQLConfig } from './IConfig';
import mariadb from 'mariadb'; import mariadb from 'mariadb';
export class Database { export class Database {
cfg: MySQLConfig; cfg: MySQLConfig;
logger: Logger; logger: Logger;
db: mariadb.Pool; db: mariadb.Pool;
constructor(config: MySQLConfig) { constructor(config: MySQLConfig) {
this.cfg = config; this.cfg = config;
this.logger = pino({ this.logger = pino({
name: "CVMTS.Database" name: 'CVMTS.Database'
}); });
this.db = mariadb.createPool({ this.db = mariadb.createPool({
host: this.cfg.host, host: this.cfg.host,
user: this.cfg.username, user: this.cfg.username,
password: this.cfg.password, password: this.cfg.password,
database: this.cfg.database, database: this.cfg.database,
connectionLimit: 5, connectionLimit: 5,
multipleStatements: false, multipleStatements: false
}); });
} }
async init() { async init() {
// Make sure tables exist // Make sure tables exist
let conn = await this.db.getConnection(); let conn = await this.db.getConnection();
await conn.execute("CREATE TABLE IF NOT EXISTS bans (ip VARCHAR(43) PRIMARY KEY NOT NULL, username VARCHAR(20) NOT NULL, reason TEXT DEFAULT NULL, timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP);"); await conn.execute(
conn.release(); 'CREATE TABLE IF NOT EXISTS bans (ip VARCHAR(43) PRIMARY KEY NOT NULL, username VARCHAR(20) NOT NULL, reason TEXT DEFAULT NULL, timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP);'
this.logger.info("MySQL successfully initialized"); );
} conn.release();
this.logger.info('MySQL successfully initialized');
}
async banIP(ip: string, username: string, reason: string | null = null) { async banIP(ip: string, username: string, reason: string | null = null) {
let conn = await this.db.getConnection(); let conn = await this.db.getConnection();
await conn.execute("INSERT INTO bans (ip, username, reason) VALUES (?, ?, ?);", [ip, username, reason]); await conn.execute('INSERT INTO bans (ip, username, reason) VALUES (?, ?, ?);', [ip, username, reason]);
conn.release(); conn.release();
} }
async isIPBanned(ip: string): Promise<boolean> { async isIPBanned(ip: string): Promise<boolean> {
let conn = await this.db.getConnection(); let conn = await this.db.getConnection();
let res = (await conn.query('SELECT COUNT(ip) AS cnt FROM bans WHERE ip = ?', [ip])); let res = await conn.query('SELECT COUNT(ip) AS cnt FROM bans WHERE ip = ?', [ip]);
conn.release(); conn.release();
return res[0]['cnt'] !== 0n; return res[0]['cnt'] !== 0n;
} }
} }

View File

@@ -14,7 +14,7 @@ export default interface IConfig {
directory: string; directory: string;
accountID: string; accountID: string;
licenseKey: string; licenseKey: string;
} };
tcp: { tcp: {
enabled: boolean; enabled: boolean;
host: string; host: string;

View File

@@ -37,7 +37,7 @@ export default class TCPClient extends EventEmitter implements NetworkClient {
send(msg: string): Promise<void> { send(msg: string): Promise<void> {
return new Promise((res, rej) => { return new Promise((res, rej) => {
let _msg = new Uint32Array([TextHeader, ...Buffer.from(msg, "utf-8")]); let _msg = new Uint32Array([TextHeader, ...Buffer.from(msg, 'utf-8')]);
this.socket.write(Buffer.from(_msg), (err) => { this.socket.write(Buffer.from(_msg), (err) => {
if (err) { if (err) {
rej(err); rej(err);

View File

@@ -11,7 +11,7 @@ import { BanManager } from '../BanManager.js';
export default class TCPServer extends EventEmitter implements NetworkServer { export default class TCPServer extends EventEmitter implements NetworkServer {
listener: Server; listener: Server;
Config: IConfig; Config: IConfig;
logger = pino({name: 'CVMTS.TCPServer'}); logger = pino({ name: 'CVMTS.TCPServer' });
clients: TCPClient[]; clients: TCPClient[];
private banmgr: BanManager; private banmgr: BanManager;
@@ -27,7 +27,7 @@ export default class TCPServer extends EventEmitter implements NetworkServer {
private async onConnection(socket: Socket) { private async onConnection(socket: Socket) {
this.logger.info(`New TCP connection from ${socket.remoteAddress}`); this.logger.info(`New TCP connection from ${socket.remoteAddress}`);
if (await this.banmgr.isIPBanned(socket.remoteAddress!)) { if (await this.banmgr.isIPBanned(socket.remoteAddress!)) {
socket.write("6.banned;"); socket.write('6.banned;');
socket.destroy(); socket.destroy();
return; return;
} }

View File

@@ -124,7 +124,7 @@ export default class WSServer extends EventEmitter implements NetworkServer {
} }
if (await this.banmgr.isIPBanned(ip)) { if (await this.banmgr.isIPBanned(ip)) {
socket.write("HTTP/1.1 403 Forbidden\n\nYou have been banned."); socket.write('HTTP/1.1 403 Forbidden\n\nYou have been banned.');
socket.destroy(); socket.destroy();
return; return;
} }

View File

@@ -56,11 +56,11 @@ async function start() {
let auth = Config.auth.enabled ? new AuthManager(Config.auth.apiEndpoint, Config.auth.secretKey) : null; let auth = Config.auth.enabled ? new AuthManager(Config.auth.apiEndpoint, Config.auth.secretKey) : null;
// Database and ban manager // Database and ban manager
if (Config.bans.cvmban && !Config.mysql.enabled) { if (Config.bans.cvmban && !Config.mysql.enabled) {
logger.error("MySQL must be configured to use cvmban."); logger.error('MySQL must be configured to use cvmban.');
process.exit(1); process.exit(1);
} }
if (!Config.bans.cvmban && !Config.bans.bancmd) { if (!Config.bans.cvmban && !Config.bans.bancmd) {
logger.warn("Neither cvmban nor ban command are configured. Bans will not function."); logger.warn('Neither cvmban nor ban command are configured. Bans will not function.');
} }
let db = undefined; let db = undefined;
if (Config.mysql.enabled) { if (Config.mysql.enabled) {

View File

@@ -1,7 +1,7 @@
{ {
"extends": "../tsconfig.json", "extends": "../tsconfig.json",
"include": [ "src/**/*" ], "include": ["src/**/*"],
"compilerOptions": { "compilerOptions": {
"resolveJsonModule": true, "resolveJsonModule": true
} }
} }

View File

@@ -1,7 +1,6 @@
{ {
"name": "cvmts-repo", "name": "cvmts-repo",
"workspaces": [ "workspaces": [
"shared",
"cvm-rs", "cvm-rs",
"cvmts", "cvmts",
"collab-vm-1.2-binary-protocol" "collab-vm-1.2-binary-protocol"
@@ -13,7 +12,8 @@
"@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",
"prettier": "^3.2.5", "prettier": "^3.3.3",
"prettier-plugin-toml": "^2.0.1",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"typescript": "^5.4.4" "typescript": "^5.4.4"
}, },
@@ -21,6 +21,7 @@
"scripts": { "scripts": {
"build": "yarn workspaces foreach -Apt run build", "build": "yarn workspaces foreach -Apt run build",
"serve": "node cvmts/dist/index.js", "serve": "node cvmts/dist/index.js",
"clean": "npx rimraf .parcel-cache .yarn **/node_modules **/dist cvm-rs/target cvm-rs/index.node" "clean": "npx rimraf .parcel-cache .yarn **/node_modules **/dist cvm-rs/target cvm-rs/index.node",
"format": "prettier -w config.example.toml && yarn workspaces foreach -Apt run format"
} }
} }

View File

@@ -88,7 +88,6 @@ __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"
prettier: "npm:^3.2.5"
sharp: "npm:^0.33.3" 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"
@@ -1466,6 +1465,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@taplo/core@npm:^0.1.0":
version: 0.1.1
resolution: "@taplo/core@npm:0.1.1"
checksum: 10c0/c36f761431b2e959742d8e186e74306fb8991d84589e2f03b6481244cc407275fa448a217ef87b8aa1e226615fd7ba85c60e6f0221f01d891b90dd30b45cb13b
languageName: node
linkType: hard
"@taplo/lib@npm:^0.4.0-alpha.2":
version: 0.4.0-alpha.2
resolution: "@taplo/lib@npm:0.4.0-alpha.2"
dependencies:
"@taplo/core": "npm:^0.1.0"
checksum: 10c0/650ed35ba949054eb8dcfbaf77d6154d9639f5d8fa96d89546c399a95e699d7e01f65e308f89478d41e0120baa3572f25595fe088d19e62cc56ad3eb2b5acbb5
languageName: node
linkType: hard
"@trysound/sax@npm:0.2.0": "@trysound/sax@npm:0.2.0":
version: 0.2.0 version: 0.2.0
resolution: "@trysound/sax@npm:0.2.0" resolution: "@trysound/sax@npm:0.2.0"
@@ -1966,7 +1981,8 @@ __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"
prettier: "npm:^3.2.5" prettier: "npm:^3.3.3"
prettier-plugin-toml: "npm:^2.0.1"
rimraf: "npm:^6.0.1" rimraf: "npm:^6.0.1"
typescript: "npm:^5.4.4" typescript: "npm:^5.4.4"
languageName: unknown languageName: unknown
@@ -3504,7 +3520,18 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"prettier@npm:^3.2.5": "prettier-plugin-toml@npm:^2.0.1":
version: 2.0.1
resolution: "prettier-plugin-toml@npm:2.0.1"
dependencies:
"@taplo/lib": "npm:^0.4.0-alpha.2"
peerDependencies:
prettier: ^3.0.3
checksum: 10c0/8a67133b1a71d82f6fe20fe92e18251ae2930d0fe96d18af8c2333cc8fffce408642b6d6f21441932a97a248815cc82b3f372971eae84aab80cad2f0d78bdc68
languageName: node
linkType: hard
"prettier@npm:^3.3.3":
version: 3.3.3 version: 3.3.3
resolution: "prettier@npm:3.3.3" resolution: "prettier@npm:3.3.3"
bin: bin: