chore: comment config.example.toml and format code with prettier/cargo
This commit is contained in:
@@ -16,5 +16,6 @@
|
|||||||
"tabWidth": 4,
|
"tabWidth": 4,
|
||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"useTabs": true,
|
"useTabs": true,
|
||||||
"vueIndentScriptAndStyle": false
|
"vueIndentScriptAndStyle": false,
|
||||||
|
"plugins": ["prettier-plugin-toml"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
11
cvm-rs/index.d.ts
vendored
@@ -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:
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(©);
|
let clone = Arc::clone(©);
|
||||||
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);
|
||||||
|
|||||||
@@ -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)?;
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ type VoteTally = {
|
|||||||
no: number;
|
no: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default class CollabVMServer {
|
export default class CollabVMServer {
|
||||||
private Config: IConfig;
|
private Config: IConfig;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "../tsconfig.json",
|
"extends": "../tsconfig.json",
|
||||||
"include": [ "src/**/*" ],
|
"include": ["src/**/*"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
33
yarn.lock
33
yarn.lock
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user