diff --git a/.prettierrc.json b/.prettierrc.json
index d41a549..7b2c8c5 100644
--- a/.prettierrc.json
+++ b/.prettierrc.json
@@ -16,5 +16,6 @@
"tabWidth": 4,
"trailingComma": "none",
"useTabs": true,
- "vueIndentScriptAndStyle": false
+ "vueIndentScriptAndStyle": false,
+ "plugins": ["prettier-plugin-toml"]
}
diff --git a/config.example.toml b/config.example.toml
index a93eebf..3a95758 100644
--- a/config.example.toml
+++ b/config.example.toml
@@ -21,15 +21,21 @@ accountID = ""
licenseKey = ""
[tcp]
+# Enabled the raw TCP socket server
+# You usually want to leave this disabled
enabled = false
host = "0.0.0.0"
port = 6014
[auth]
+# Enables the CollabVM account authentication system
+# Requires an authentication server (https://git.computernewb.com/collabvm/CollabVMAuthServer)
enabled = false
apiEndpoint = ""
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]
chat = true
turn = false
@@ -37,13 +43,21 @@ callForReset = false
vote = true
[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"
+# QEMU options
+# Only used if vm.type is set to "qemu"
[qemu]
qemuArgs = "qemu-system-x86_64"
vncPort = 5900
snapshots = true
+# VNC options
+# Only used if vm.type is set to "vncvm"
[vncvm]
vncHost = "127.0.0.1"
vncPort = 5900
@@ -69,12 +83,16 @@ database = "cvmts"
cvmban = false
[collabvm]
+# Node ID for this server
+# Make sure this is unique among all the other nodes in a webapp
node = "acoolvm"
+# HTML display name shown on the VM list
displayname = "A Really Cool CollabVM Instance"
+# HTML message shown in the chat when a user joins
motd = "welcome!"
# Maximum amount of active connections allowed from the same IP.
maxConnections = 3
-# Moderator rank enabled
+# Moderator rank enabled (permissions are defined below)
moderatorEnabled = true
# List of disallowed usernames
usernameblacklist = []
@@ -83,9 +101,9 @@ maxChatLength = 100
# Maximum messages in the chat history buffer before old messages are overwritten
maxChatHistoryLength = 10
# 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
-automute = {enabled = true, seconds = 5, messages = 5}
+automute = { enabled = true, seconds = 5, messages = 5 }
# How long a temporary mute lasts, in seconds
tempMuteTime = 30
# How long a turn lasts, in seconds
@@ -103,8 +121,9 @@ modpass = "fb8c2e2b85ca81eb4350199faddd983cb26af3064614e737ea9f479621cfa57a"
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
turnpass = ""
-[collabvm.moderatorPermissions]
+
# What a moderator can and can't do
+[collabvm.moderatorPermissions]
restore = true
reboot = true
ban = true
diff --git a/cvm-rs/index.d.ts b/cvm-rs/index.d.ts
index 9e664f2..af6e202 100644
--- a/cvm-rs/index.d.ts
+++ b/cvm-rs/index.d.ts
@@ -5,21 +5,20 @@ export function guacDecode(input: string): string[];
export function guacEncode(...items: string[]): string;
interface JpegInputArgs {
- width: number,
- height: number,
- stride: number, // The width of your input framebuffer OR your image width (if encoding a full image)
- buffer: Buffer
+ width: number;
+ height: number;
+ stride: number; // The width of your input framebuffer OR your image width (if encoding a full image)
+ buffer: Buffer;
// TODO: Allow different formats, or export a boxed ffi object which can store a format
// (i.e: new JpegEncoder(FORMAT_xxx)).
}
/// Performs JPEG encoding.
-export function jpegEncode(input: JpegInputArgs) : Promise;
+export function jpegEncode(input: JpegInputArgs): Promise;
// TODO: Version that can downscale?
-
/* remoting API?
js side api:
diff --git a/cvm-rs/package.json b/cvm-rs/package.json
index 6154c9e..1f82bc6 100644
--- a/cvm-rs/package.json
+++ b/cvm-rs/package.json
@@ -8,7 +8,8 @@
"scripts": {
"build": "cargo-cp-artifact -nc index.node -- cargo build --release --message-format=json-render-diagnostics",
"install": "yarn build",
- "test": "cargo test"
+ "test": "cargo test",
+ "format": "cargo fmt"
},
"devDependencies": {
"cargo-cp-artifact": "^0.1"
diff --git a/cvm-rs/src/guac.rs b/cvm-rs/src/guac.rs
index b92f289..c87bf5d 100644
--- a/cvm-rs/src/guac.rs
+++ b/cvm-rs/src/guac.rs
@@ -8,30 +8,30 @@ pub type Elements = Vec;
/// Errors during decoding
#[derive(Debug, Clone)]
pub enum DecodeError {
- /// Invalid guacamole instruction format
- InvalidFormat,
+ /// Invalid guacamole instruction format
+ InvalidFormat,
- /// Instruction is too long for the current decode policy.
- InstructionTooLong,
+ /// Instruction is too long for the current decode policy.
+ InstructionTooLong,
- /// Element is too long for the current decode policy.
- ElementTooLong,
+ /// Element is too long for the current decode policy.
+ ElementTooLong,
- /// Invalid element size.
- ElementSizeInvalid,
+ /// Invalid element size.
+ ElementSizeInvalid,
}
pub type DecodeResult = std::result::Result;
impl fmt::Display for DecodeError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::InvalidFormat => write!(f, "Invalid Guacamole instruction while decoding"),
- Self::InstructionTooLong => write!(f, "Instruction too long for current decode policy"),
- Self::ElementTooLong => write!(f, "Element too long for current decode policy"),
- Self::ElementSizeInvalid => write!(f, "Element size is invalid")
- }
- }
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::InvalidFormat => write!(f, "Invalid Guacamole instruction while decoding"),
+ Self::InstructionTooLong => write!(f, "Instruction too long for current decode policy"),
+ Self::ElementTooLong => write!(f, "Element too long for current decode policy"),
+ Self::ElementSizeInvalid => write!(f, "Element size is invalid"),
+ }
+ }
}
// this decode policy abstraction would in theory be useful,
@@ -40,154 +40,153 @@ impl fmt::Display for DecodeError {
pub struct StaticDecodePolicy();
impl StaticDecodePolicy {
- fn max_instruction_size(&self) -> usize {
- INST_SIZE
- }
+ fn max_instruction_size(&self) -> usize {
+ INST_SIZE
+ }
- fn max_element_size(&self) -> usize {
- ELEM_SIZE
- }
+ fn max_element_size(&self) -> usize {
+ ELEM_SIZE
+ }
}
/// The default decode policy.
pub type DefaultDecodePolicy = StaticDecodePolicy<12288, 4096>;
-
/// Encodes elements into a Guacamole instruction
pub fn encode_instruction(elements: &Elements) -> String {
- let mut str = String::new();
+ let mut str = String::new();
- for elem in elements.iter() {
- str.push_str(&format!("{}.{},", elem.len(), elem));
- }
+ for elem in elements.iter() {
+ str.push_str(&format!("{}.{},", elem.len(), elem));
+ }
- // hacky, but whatever
- str.pop();
- str.push(';');
+ // hacky, but whatever
+ str.pop();
+ str.push(';');
- str
+ str
}
/// Decodes a Guacamole instruction to individual elements
pub fn decode_instruction(element_string: &String) -> DecodeResult {
- let policy = DefaultDecodePolicy {};
+ let policy = DefaultDecodePolicy {};
- let mut vec: Elements = Vec::new();
- let mut current_position: usize = 0;
+ let mut vec: Elements = Vec::new();
+ let mut current_position: usize = 0;
- // Instruction is too long. Don't even bother
- if policy.max_instruction_size() < element_string.len() {
- return Err(DecodeError::InstructionTooLong);
- }
+ // Instruction is too long. Don't even bother
+ if policy.max_instruction_size() < element_string.len() {
+ return Err(DecodeError::InstructionTooLong);
+ }
- let chars = element_string.chars().collect::>();
+ let chars = element_string.chars().collect::>();
- loop {
- let mut element_size: usize = 0;
+ loop {
+ let mut element_size: usize = 0;
- // Scan the integer value in by hand. This is mostly because
- // I'm stupid, and the Rust integer parsing routines (seemingly)
- // require a substring (or a slice, but, if you can generate a slice,
- // you can also just scan the value in by hand.)
- //
- // We bound this anyways and do quite the checks, so even though it's not great,
- // it should be generally fine (TM).
- loop {
- let c = chars[current_position];
+ // Scan the integer value in by hand. This is mostly because
+ // I'm stupid, and the Rust integer parsing routines (seemingly)
+ // require a substring (or a slice, but, if you can generate a slice,
+ // you can also just scan the value in by hand.)
+ //
+ // We bound this anyways and do quite the checks, so even though it's not great,
+ // it should be generally fine (TM).
+ loop {
+ let c = chars[current_position];
- if c >= '0' && c <= '9' {
- element_size = element_size * 10 + (c as usize) - ('0' as usize);
- } else {
- if c == '.' {
- break;
- }
+ if c >= '0' && c <= '9' {
+ element_size = element_size * 10 + (c as usize) - ('0' as usize);
+ } else {
+ if c == '.' {
+ break;
+ }
- return Err(DecodeError::InvalidFormat);
- }
- current_position += 1;
- }
+ return Err(DecodeError::InvalidFormat);
+ }
+ current_position += 1;
+ }
- // Eat the '.' seperating the size and the element data;
- // our integer scanning ensures we only get here in the case that this is actually the '.'
- // character.
- current_position += 1;
+ // Eat the '.' seperating the size and the element data;
+ // our integer scanning ensures we only get here in the case that this is actually the '.'
+ // character.
+ current_position += 1;
- // Make sure the element size doesn't overflow the decode policy
- // or the size of the whole instruction.
+ // Make sure the element size doesn't overflow the decode policy
+ // or the size of the whole instruction.
- if element_size >= policy.max_element_size() {
- return Err(DecodeError::ElementTooLong);
- }
+ if element_size >= policy.max_element_size() {
+ return Err(DecodeError::ElementTooLong);
+ }
- if element_size >= element_string.len() {
- return Err(DecodeError::ElementSizeInvalid);
- }
+ if element_size >= element_string.len() {
+ return Err(DecodeError::ElementSizeInvalid);
+ }
- // cutoff elements or something
- if current_position + element_size > chars.len()-1 {
- //println!("? {current_position} a {}", chars.len());
- return Err(DecodeError::InvalidFormat);
- }
-
- let element = chars
- .iter()
- .skip(current_position)
- .take(element_size)
- .collect::();
+ // cutoff elements or something
+ if current_position + element_size > chars.len() - 1 {
+ //println!("? {current_position} a {}", chars.len());
+ return Err(DecodeError::InvalidFormat);
+ }
- current_position += element_size;
+ let element = chars
+ .iter()
+ .skip(current_position)
+ .take(element_size)
+ .collect::();
- vec.push(element);
-
- // make sure seperator is proper
- match chars[current_position] {
- ',' => {}
- ';' => break,
- _ => return Err(DecodeError::InvalidFormat),
- }
+ current_position += element_size;
- // eat the ','
- current_position += 1;
- }
+ vec.push(element);
- 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)]
mod tests {
- use super::*;
+ use super::*;
- #[test]
- fn decode_basic() {
- let test = String::from("7.connect,3.vm1;");
- let res = decode_instruction(&test);
+ #[test]
+ fn decode_basic() {
+ let test = String::from("7.connect,3.vm1;");
+ let res = decode_instruction(&test);
- assert!(res.is_ok());
- assert_eq!(res.unwrap(), vec!["connect", "vm1"]);
- }
+ assert!(res.is_ok());
+ assert_eq!(res.unwrap(), vec!["connect", "vm1"]);
+ }
- #[test]
- fn decode_errors() {
- let test = String::from("700.connect,3.vm1;");
- let res = decode_instruction(&test);
+ #[test]
+ fn decode_errors() {
+ let test = String::from("700.connect,3.vm1;");
+ 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
- // (we can decode a instruction we created)
- #[test]
- fn general_codec_works() {
- let vec = vec![String::from("connect"), String::from("vm1")];
- let test = encode_instruction(&vec);
+ // generally just test that the codec even works
+ // (we can decode a instruction we created)
+ #[test]
+ fn general_codec_works() {
+ let vec = vec![String::from("connect"), String::from("vm1")];
+ let test = encode_instruction(&vec);
- assert_eq!(test, "7.connect,3.vm1;");
+ assert_eq!(test, "7.connect,3.vm1;");
- let res = decode_instruction(&test);
+ let res = decode_instruction(&test);
- assert!(res.is_ok());
- assert_eq!(res.unwrap(), vec);
- }
+ assert!(res.is_ok());
+ assert_eq!(res.unwrap(), vec);
+ }
}
diff --git a/cvm-rs/src/guac_js.rs b/cvm-rs/src/guac_js.rs
index 1a8dd01..4a9aaee 100644
--- a/cvm-rs/src/guac_js.rs
+++ b/cvm-rs/src/guac_js.rs
@@ -1,5 +1,5 @@
-use neon::prelude::*;
use crate::guac;
+use neon::prelude::*;
fn guac_decode_impl<'a>(cx: &mut FunctionContext<'a>) -> JsResult<'a, JsArray> {
let input = cx.argument::(0)?.value(cx);
diff --git a/cvm-rs/src/jpeg_compressor.rs b/cvm-rs/src/jpeg_compressor.rs
index 40dd77f..f716192 100644
--- a/cvm-rs/src/jpeg_compressor.rs
+++ b/cvm-rs/src/jpeg_compressor.rs
@@ -61,13 +61,13 @@ impl JpegCompressor {
(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 {
return Vec::new();
}
- // Truncate down to the size we're given back
- vec.truncate(size as usize);
+ // Truncate down to the size we're given back
+ vec.truncate(size as usize);
return vec;
}
}
diff --git a/cvm-rs/src/jpeg_js.rs b/cvm-rs/src/jpeg_js.rs
index de87f9b..4547036 100644
--- a/cvm-rs/src/jpeg_js.rs
+++ b/cvm-rs/src/jpeg_js.rs
@@ -41,7 +41,7 @@ fn jpeg_encode_impl<'a>(cx: &mut FunctionContext<'a>) -> JsResult<'a, JsPromise>
let copy: Arc>> = 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 cap = locked.capacity();
@@ -49,8 +49,8 @@ fn jpeg_encode_impl<'a>(cx: &mut FunctionContext<'a>) -> JsResult<'a, JsPromise>
locked.copy_from_slice(buf);
}
- // Spawn off a tokio blocking pool thread that will do the work for us
- runtime.spawn_blocking(move || {
+ // Spawn off a tokio blocking pool thread that will do the work for us
+ runtime.spawn_blocking(move || {
let clone = Arc::clone(©);
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)
});
- // Fulfill the Javascript promise with our encoded buffer
+ // Fulfill the Javascript promise with our encoded buffer
deferred.settle_with(&channel, move |mut cx| {
let mut buf = cx.buffer(vec.len())?;
let slice = buf.as_mut_slice(&mut cx);
diff --git a/cvm-rs/src/lib.rs b/cvm-rs/src/lib.rs
index e6c6291..39f4bbd 100644
--- a/cvm-rs/src/lib.rs
+++ b/cvm-rs/src/lib.rs
@@ -4,13 +4,11 @@ mod guac_js;
mod jpeg_compressor;
mod jpeg_js;
-
use neon::prelude::*;
-
#[neon::main]
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("guacDecode", guac_js::guac_decode)?;
diff --git a/cvmts/package.json b/cvmts/package.json
index cf9497c..dddf500 100644
--- a/cvmts/package.json
+++ b/cvmts/package.json
@@ -1,35 +1,35 @@
{
- "name": "@cvmts/cvmts",
- "version": "1.0.0",
- "description": "replacement for collabvm 1.2.11 because the old one :boom:",
- "type": "module",
- "main": "dist/index.js",
- "scripts": {
- "build": "tsc -outDir dist -rootDir src/",
- "serve": "node dist/index.js"
- },
- "author": "Elijah R, modeco80",
- "license": "GPL-3.0",
- "dependencies": {
- "@computernewb/nodejs-rfb": "^0.3.0",
- "@computernewb/superqemu": "^0.1.0",
- "@cvmts/cvm-rs": "*",
- "@maxmind/geoip2-node": "^5.0.0",
- "execa": "^8.0.1",
- "ip-address": "^9.0.5",
- "mariadb": "^3.3.1",
- "mnemonist": "^0.39.5",
- "msgpackr": "^1.10.2",
- "pino": "^9.3.1",
- "sharp": "^0.33.3",
- "toml": "^3.0.0",
- "ws": "^8.17.1"
- },
- "devDependencies": {
- "@types/node": "^20.12.5",
- "@types/ws": "^8.5.5",
- "pino-pretty": "^11.2.1",
- "prettier": "^3.2.5",
- "typescript": "^5.4.4"
- }
+ "name": "@cvmts/cvmts",
+ "version": "1.0.0",
+ "description": "replacement for collabvm 1.2.11 because the old one :boom:",
+ "type": "module",
+ "main": "dist/index.js",
+ "scripts": {
+ "build": "tsc -outDir dist -rootDir src/",
+ "serve": "node dist/index.js",
+ "format": "prettier --write ."
+ },
+ "author": "Elijah R, modeco80",
+ "license": "GPL-3.0",
+ "dependencies": {
+ "@computernewb/nodejs-rfb": "^0.3.0",
+ "@computernewb/superqemu": "^0.1.0",
+ "@cvmts/cvm-rs": "*",
+ "@maxmind/geoip2-node": "^5.0.0",
+ "execa": "^8.0.1",
+ "ip-address": "^9.0.5",
+ "mariadb": "^3.3.1",
+ "mnemonist": "^0.39.5",
+ "msgpackr": "^1.10.2",
+ "pino": "^9.3.1",
+ "sharp": "^0.33.3",
+ "toml": "^3.0.0",
+ "ws": "^8.17.1"
+ },
+ "devDependencies": {
+ "@types/node": "^20.12.5",
+ "@types/ws": "^8.5.5",
+ "pino-pretty": "^11.2.1",
+ "typescript": "^5.4.4"
+ }
}
diff --git a/cvmts/src/BanManager.ts b/cvmts/src/BanManager.ts
index b214d6b..23094f3 100644
--- a/cvmts/src/BanManager.ts
+++ b/cvmts/src/BanManager.ts
@@ -1,81 +1,80 @@
-import { ExecaSyncError, execa, execaCommand } from "execa";
-import { BanConfig } from "./IConfig";
-import pino from "pino";
-import { Database } from "./Database";
-import { Address6 } from "ip-address";
-import { isIP } from "net";
+import { ExecaSyncError, execa, execaCommand } from 'execa';
+import { BanConfig } from './IConfig';
+import pino from 'pino';
+import { Database } from './Database';
+import { Address6 } from 'ip-address';
+import { isIP } from 'net';
export class BanManager {
- private cfg: BanConfig;
- private logger: pino.Logger;
- private db: Database | undefined;
+ private cfg: BanConfig;
+ private logger: pino.Logger;
+ private db: Database | undefined;
- constructor(config: BanConfig, db: Database | undefined) {
- this.cfg = config;
- this.logger = pino({
- name: "CVMTS.BanManager"
- });
- this.db = db;
- }
+ constructor(config: BanConfig, db: Database | undefined) {
+ this.cfg = config;
+ this.logger = pino({
+ name: 'CVMTS.BanManager'
+ });
+ this.db = db;
+ }
- private formatIP(ip: string) {
- switch (isIP(ip)) {
- case 4:
- // If IPv4, just return as-is
- return ip;
- case 6: {
- // If IPv6, return the /64 equivalent
- let addr = new Address6(ip);
- addr.subnetMask = 64;
- return addr.startAddress().canonicalForm() + '/64';
- }
- case 0:
- default:
- // Invalid IP
- throw new Error("Invalid IP address (what the hell did you even do???)");
- }
- }
+ private formatIP(ip: string) {
+ switch (isIP(ip)) {
+ case 4:
+ // If IPv4, just return as-is
+ return ip;
+ case 6: {
+ // If IPv6, return the /64 equivalent
+ let addr = new Address6(ip);
+ addr.subnetMask = 64;
+ return addr.startAddress().canonicalForm() + '/64';
+ }
+ case 0:
+ default:
+ // Invalid IP
+ throw new Error('Invalid IP address (what the hell did you even do???)');
+ }
+ }
- async BanUser(ip: string, username: string) {
- ip = this.formatIP(ip);
- // If cvmban enabled, write to DB
- if (this.cfg.cvmban) {
- if (!this.db) throw new Error("CVMBAN enabled but Database is undefined");
- await this.db.banIP(ip, username);
- }
- // If ban command enabled, run it
- try {
+ async BanUser(ip: string, username: string) {
+ ip = this.formatIP(ip);
+ // If cvmban enabled, write to DB
+ if (this.cfg.cvmban) {
+ if (!this.db) throw new Error('CVMBAN enabled but Database is undefined');
+ await this.db.banIP(ip, username);
+ }
+ // If ban command enabled, run it
+ try {
if (Array.isArray(this.cfg.bancmd)) {
let args: string[] = this.cfg.bancmd.map((a: string) => this.banCmdArgs(a, ip, username));
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 });
}
} else if (typeof this.cfg.bancmd == 'string') {
let cmd: string = this.banCmdArgs(this.cfg.bancmd, ip, username);
if (cmd.length) {
- // Run through JSON.stringify for char escaping
- this.logger.info(`Running ${JSON.stringify(cmd)}`);
+ // Run through JSON.stringify for char escaping
+ this.logger.info(`Running ${JSON.stringify(cmd)}`);
await execaCommand(cmd, { stdout: process.stdout, stderr: process.stderr });
}
}
} catch (e) {
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);
}
-}
\ No newline at end of file
+ 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);
+ }
+}
diff --git a/cvmts/src/CollabVMServer.ts b/cvmts/src/CollabVMServer.ts
index 1c25c6b..3d1eb2b 100644
--- a/cvmts/src/CollabVMServer.ts
+++ b/cvmts/src/CollabVMServer.ts
@@ -39,7 +39,6 @@ type VoteTally = {
no: number;
};
-
export default class CollabVMServer {
private Config: IConfig;
diff --git a/cvmts/src/Database.ts b/cvmts/src/Database.ts
index 9049b54..775344c 100644
--- a/cvmts/src/Database.ts
+++ b/cvmts/src/Database.ts
@@ -1,44 +1,46 @@
-import pino, { Logger } from "pino";
-import { MySQLConfig } from "./IConfig";
+import pino, { Logger } from 'pino';
+import { MySQLConfig } from './IConfig';
import mariadb from 'mariadb';
export class Database {
- cfg: MySQLConfig;
- logger: Logger;
- db: mariadb.Pool;
- constructor(config: MySQLConfig) {
- this.cfg = config;
- this.logger = pino({
- name: "CVMTS.Database"
- });
- this.db = mariadb.createPool({
- host: this.cfg.host,
- user: this.cfg.username,
- password: this.cfg.password,
- database: this.cfg.database,
- connectionLimit: 5,
- multipleStatements: false,
- });
- }
+ cfg: MySQLConfig;
+ logger: Logger;
+ db: mariadb.Pool;
+ constructor(config: MySQLConfig) {
+ this.cfg = config;
+ this.logger = pino({
+ name: 'CVMTS.Database'
+ });
+ this.db = mariadb.createPool({
+ host: this.cfg.host,
+ user: this.cfg.username,
+ password: this.cfg.password,
+ database: this.cfg.database,
+ connectionLimit: 5,
+ multipleStatements: false
+ });
+ }
- async init() {
- // Make sure tables exist
- 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);");
- conn.release();
- this.logger.info("MySQL successfully initialized");
- }
+ async init() {
+ // Make sure tables exist
+ 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);'
+ );
+ conn.release();
+ this.logger.info('MySQL successfully initialized');
+ }
- async banIP(ip: string, username: string, reason: string | null = null) {
- let conn = await this.db.getConnection();
- await conn.execute("INSERT INTO bans (ip, username, reason) VALUES (?, ?, ?);", [ip, username, reason]);
- conn.release();
- }
+ async banIP(ip: string, username: string, reason: string | null = null) {
+ let conn = await this.db.getConnection();
+ await conn.execute('INSERT INTO bans (ip, username, reason) VALUES (?, ?, ?);', [ip, username, reason]);
+ conn.release();
+ }
- async isIPBanned(ip: string): Promise {
- let conn = await this.db.getConnection();
- let res = (await conn.query('SELECT COUNT(ip) AS cnt FROM bans WHERE ip = ?', [ip]));
- conn.release();
- return res[0]['cnt'] !== 0n;
- }
-}
\ No newline at end of file
+ async isIPBanned(ip: string): Promise {
+ let conn = await this.db.getConnection();
+ let res = await conn.query('SELECT COUNT(ip) AS cnt FROM bans WHERE ip = ?', [ip]);
+ conn.release();
+ return res[0]['cnt'] !== 0n;
+ }
+}
diff --git a/cvmts/src/IConfig.ts b/cvmts/src/IConfig.ts
index 99fd10b..0c309c1 100644
--- a/cvmts/src/IConfig.ts
+++ b/cvmts/src/IConfig.ts
@@ -14,7 +14,7 @@ export default interface IConfig {
directory: string;
accountID: string;
licenseKey: string;
- }
+ };
tcp: {
enabled: boolean;
host: string;
diff --git a/cvmts/src/TCP/TCPClient.ts b/cvmts/src/TCP/TCPClient.ts
index f20498e..7d3ff9c 100644
--- a/cvmts/src/TCP/TCPClient.ts
+++ b/cvmts/src/TCP/TCPClient.ts
@@ -37,7 +37,7 @@ export default class TCPClient extends EventEmitter implements NetworkClient {
send(msg: string): Promise {
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) => {
if (err) {
rej(err);
diff --git a/cvmts/src/TCP/TCPServer.ts b/cvmts/src/TCP/TCPServer.ts
index 9af8cb5..8a8158b 100644
--- a/cvmts/src/TCP/TCPServer.ts
+++ b/cvmts/src/TCP/TCPServer.ts
@@ -11,7 +11,7 @@ import { BanManager } from '../BanManager.js';
export default class TCPServer extends EventEmitter implements NetworkServer {
listener: Server;
Config: IConfig;
- logger = pino({name: 'CVMTS.TCPServer'});
+ logger = pino({ name: 'CVMTS.TCPServer' });
clients: TCPClient[];
private banmgr: BanManager;
@@ -27,7 +27,7 @@ export default class TCPServer extends EventEmitter implements NetworkServer {
private async onConnection(socket: Socket) {
this.logger.info(`New TCP connection from ${socket.remoteAddress}`);
if (await this.banmgr.isIPBanned(socket.remoteAddress!)) {
- socket.write("6.banned;");
+ socket.write('6.banned;');
socket.destroy();
return;
}
diff --git a/cvmts/src/WebSocket/WSServer.ts b/cvmts/src/WebSocket/WSServer.ts
index 8af7cec..82e7a24 100644
--- a/cvmts/src/WebSocket/WSServer.ts
+++ b/cvmts/src/WebSocket/WSServer.ts
@@ -124,7 +124,7 @@ export default class WSServer extends EventEmitter implements NetworkServer {
}
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();
return;
}
diff --git a/cvmts/src/index.ts b/cvmts/src/index.ts
index a985e96..ac4bb91 100644
--- a/cvmts/src/index.ts
+++ b/cvmts/src/index.ts
@@ -56,11 +56,11 @@ async function start() {
let auth = Config.auth.enabled ? new AuthManager(Config.auth.apiEndpoint, Config.auth.secretKey) : null;
// Database and ban manager
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);
}
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;
if (Config.mysql.enabled) {
diff --git a/cvmts/tsconfig.json b/cvmts/tsconfig.json
index baa2339..517be24 100644
--- a/cvmts/tsconfig.json
+++ b/cvmts/tsconfig.json
@@ -1,7 +1,7 @@
{
- "extends": "../tsconfig.json",
- "include": [ "src/**/*" ],
- "compilerOptions": {
- "resolveJsonModule": true,
- }
+ "extends": "../tsconfig.json",
+ "include": ["src/**/*"],
+ "compilerOptions": {
+ "resolveJsonModule": true
+ }
}
diff --git a/package.json b/package.json
index fcb9aa2..9678684 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,6 @@
{
"name": "cvmts-repo",
"workspaces": [
- "shared",
"cvm-rs",
"cvmts",
"collab-vm-1.2-binary-protocol"
@@ -13,7 +12,8 @@
"@types/jsbn": "^1.2.33",
"@types/node": "^20.14.10",
"parcel": "^2.12.0",
- "prettier": "^3.2.5",
+ "prettier": "^3.3.3",
+ "prettier-plugin-toml": "^2.0.1",
"rimraf": "^6.0.1",
"typescript": "^5.4.4"
},
@@ -21,6 +21,7 @@
"scripts": {
"build": "yarn workspaces foreach -Apt run build",
"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"
}
}
diff --git a/yarn.lock b/yarn.lock
index 6d8793f..f7510d2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -88,7 +88,6 @@ __metadata:
msgpackr: "npm:^1.10.2"
pino: "npm:^9.3.1"
pino-pretty: "npm:^11.2.1"
- prettier: "npm:^3.2.5"
sharp: "npm:^0.33.3"
toml: "npm:^3.0.0"
typescript: "npm:^5.4.4"
@@ -1466,6 +1465,22 @@ __metadata:
languageName: node
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":
version: 0.2.0
resolution: "@trysound/sax@npm:0.2.0"
@@ -1966,7 +1981,8 @@ __metadata:
"@types/jsbn": "npm:^1.2.33"
"@types/node": "npm:^20.14.10"
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"
typescript: "npm:^5.4.4"
languageName: unknown
@@ -3504,7 +3520,18 @@ __metadata:
languageName: node
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
resolution: "prettier@npm:3.3.3"
bin: