From cfb8144561ffd25963faa28c374008c254c1befe Mon Sep 17 00:00:00 2001 From: restitux Date: Sat, 25 Oct 2025 23:42:05 -0600 Subject: [PATCH] add /status endpoint to proxy --- Cargo.lock | 63 +++++++++++++++++++++ common/src/lib.rs | 10 ++++ docker/Caddyfile | 4 ++ proxy/Cargo.toml | 3 +- proxy/src/main.rs | 91 +++++++++++++++++++++++++++++- proxy/src/ping.rs | 141 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 309 insertions(+), 3 deletions(-) create mode 100644 proxy/src/ping.rs diff --git a/Cargo.lock b/Cargo.lock index fd60604..f3f18a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2217,6 +2217,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "ghash" version = "0.5.1" @@ -3595,6 +3607,7 @@ dependencies = [ "hmac-sha256", "mumble-web2-common", "once_cell", + "rand 0.9.2", "rcgen", "rustls", "salvo", @@ -4596,6 +4609,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.7.3" @@ -4621,6 +4640,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -4641,6 +4670,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -4659,6 +4698,15 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -6575,6 +6623,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -7343,6 +7400,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + [[package]] name = "write16" version = "1.0.0" diff --git a/common/src/lib.rs b/common/src/lib.rs index 110b354..a669223 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -5,3 +5,13 @@ pub struct ClientConfig { pub proxy_url: Option, pub cert_hash: Option>, } + +#[derive(Debug, Clone, Deserialize, Serialize, Default)] +pub struct ServerStatus { + #[serde(default)] + pub success: bool, + pub version: Option<(u32, u32, u32)>, + pub users: Option, + pub max_users: Option, + pub bandwidth: Option, +} diff --git a/docker/Caddyfile b/docker/Caddyfile index c165096..247bd85 100644 --- a/docker/Caddyfile +++ b/docker/Caddyfile @@ -4,6 +4,10 @@ localhost:64444 { # Proxy /config path to mumble-web2-proxy reverse_proxy /config http://127.0.0.1:4400 + # Proxy /status path to mumble-web2-proxy + reverse_proxy /status http://127.0.0.1:4400 + + # Proxy root path to dx-serve reverse_proxy http://127.0.0.1:8080 diff --git a/proxy/Cargo.toml b/proxy/Cargo.toml index 275da94..3ccc58a 100644 --- a/proxy/Cargo.toml +++ b/proxy/Cargo.toml @@ -27,4 +27,5 @@ rustls = { version = "^0.23", features = ["aws_lc_rs"] } rcgen = "^0.13.2" hmac-sha256 = "^1.1.8" time = "0.3" -url = "2" +url = { version = "2", features = ["serde"] } +rand = "0.9.2" diff --git a/proxy/src/main.rs b/proxy/src/main.rs index 60d9bb2..74b2089 100644 --- a/proxy/src/main.rs +++ b/proxy/src/main.rs @@ -1,5 +1,10 @@ use color_eyre::eyre::{anyhow, bail, Context, Result}; -use mumble_web2_common::ClientConfig; +use color_eyre::owo_colors::OwoColorize; +use mumble_web2_common::{ClientConfig, ServerStatus}; +use once_cell::sync::OnceCell; +use rand::Rng; +use rcgen::date_time_ymd; +use rustls::server; use salvo::conn::rustls::{Keycert, RustlsConfig}; use salvo::cors::{AllowOrigin, Cors}; use salvo::logging::Logger; @@ -25,6 +30,8 @@ use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::EnvFilter; use url::Url; +mod ping; + fn default_cert_alt_names() -> Vec { vec!["localhost".into()] } @@ -122,10 +129,15 @@ async fn main() -> Result<()> { client_config, }; + let status_craft = StatusCraft { + mumble_server_address: server_config.mumble_server_address.unwrap().clone(), + }; + // Server routing let mut router = Router::new() .push(Router::with_path("/proxy").goal(config_craft.connect_proxy())) .push(Router::with_path("/config").get(config_craft.get_config())) + .push(Router::with_path("/status").get(status_craft.get_status())) .hoop(Logger::new()); if let Some(gui_path) = server_config.gui_path.clone() { router = @@ -158,6 +170,82 @@ async fn main() -> Result<()> { Ok(()) } +#[derive(Clone)] +pub struct StatusCraft { + mumble_server_address: SocketAddr, +} + +#[craft] +impl StatusCraft { + #[craft(handler)] + async fn get_status(&self) -> Json { + let mut server_status = ServerStatus::default(); + + let ping_packet = ping::PingPacket { + id: rand::rng().random(), + }; + + let sock = match tokio::net::UdpSocket::bind("0.0.0.0:0").await { + Ok(s) => s, + Err(e) => { + error!("Could not bind udp socket: {}", e); + return Json(server_status); + } + }; + + match sock.connect(self.mumble_server_address).await { + Ok(_) => {} + Err(e) => { + error!("Could not send ping packet: {}", e); + return Json(server_status); + } + } + + match sock.send(&<[u8; 12]>::from(ping_packet)).await { + Ok(_) => {} + Err(e) => { + error!("Could not send ping packet"); + return Json(server_status); + } + } + + let mut pong_buf: [u8; 24] = [0; 24]; + + match tokio::time::timeout( + tokio::time::Duration::from_secs(1), + sock.recv(&mut pong_buf), + ) + .await + { + Ok(_) => {} + Err(e) => { + error!("Could not send ping packet"); + return Json(server_status); + } + } + + let pong_packet = match ping::PongPacket::try_from(pong_buf.as_slice()) { + Ok(p) => p, + Err(e) => { + error!("Could not parse pong packet: {:?}", e); + return Json(server_status); + } + }; + + server_status.success = true; + server_status.version = Some(( + pong_packet.version & 0xFF, + (pong_packet.version >> 8) & 0xFF, + (pong_packet.version >> 16) & 0xFF, + )); + server_status.users = Some(pong_packet.users); + server_status.max_users = Some(pong_packet.max_users); + server_status.bandwidth = Some(pong_packet.bandwidth); + + Json(server_status) + } +} + #[derive(Clone)] pub struct ConfigCraft { server_config: Arc, @@ -175,7 +263,6 @@ impl ConfigCraft { async fn connect_proxy(&self, req: &mut Request, res: &mut Response) { info!("received proxy request"); let mumble_server_address = self.server_config.mumble_server_address.unwrap(); - let wt = match req.web_transport_mut().await { Ok(wt) => wt, Err(err) => { diff --git a/proxy/src/ping.rs b/proxy/src/ping.rs new file mode 100644 index 0000000..fe4a757 --- /dev/null +++ b/proxy/src/ping.rs @@ -0,0 +1,141 @@ +// This code was taken from mumble-protocol-2x (https://github.com/dblsaiko/rust-mumble-protocol) +// and originally from mumble-protocol (https://github.com/Johni0702/rust-mumble-protocol) +// These projects are licensed under MIT and Apache 2.0. + +//! Ping messages and codec +//! +//! A Mumble client can send periodic UDP [PingPacket]s to servers +//! in order to query their current state and measure latency. +//! A server will usually respond with a corresponding [PongPacket] containing +//! the requested details. +//! +//! Both packets are of fixed size and can be converted to/from `u8` arrays/slices via +//! the respective `From`/`TryFrom` impls. + +/// A ping packet sent to the server. +#[derive(Clone, Debug, PartialEq)] +pub struct PingPacket { + /// Opaque, client-generated id. + /// + /// Will be returned by the server unmodified and can be used to correlate + /// pong replies to ping requests to e.g. calculate latency. + pub id: u64, +} + +/// A pong packet sent to the client in reply to a previously received [PingPacket]. +#[derive(Clone, Debug, PartialEq)] +pub struct PongPacket { + /// Opaque, client-generated id. + /// + /// Should match the value in the corresponding [PingPacket]. + pub id: u64, + + /// Server version. E.g. `0x010300` for `1.3.0`. + pub version: u32, + + /// Current amount of users connected to the server. + pub users: u32, + + /// Configured limit on the amount of users which can be connected to the server. + pub max_users: u32, + + /// Maximum bandwidth for server-bound speech per client in bits per second + pub bandwidth: u32, +} + +/// Error during parsing of a [PingPacket]. +#[derive(Clone, Debug, PartialEq)] +pub enum ParsePingError { + /// Ping packets must always be 12 bytes in size. + InvalidSize, + /// Ping packets must have an all zero header of 4 bytes. + InvalidHeader, +} + +impl TryFrom<&[u8]> for PingPacket { + type Error = ParsePingError; + fn try_from(buf: &[u8]) -> Result { + match <[u8; 12]>::try_from(buf) { + Ok(array) => { + if array[0..4] != [0, 0, 0, 0] { + Err(ParsePingError::InvalidHeader) + } else { + Ok(Self { + id: u64::from_be_bytes(array[4..12].try_into().unwrap()), + }) + } + } + Err(_) => Err(ParsePingError::InvalidSize), + } + } +} + +impl From for [u8; 12] { + fn from(packet: PingPacket) -> Self { + let id = packet.id.to_be_bytes(); + // Is there no nicer way to do this? + [ + 0, 0, 0, 0, id[0], id[1], id[2], id[3], id[4], id[5], id[6], id[7], + ] + } +} + +/// Error during parsing of a [PongPacket]. +#[derive(Clone, Debug, PartialEq)] +pub enum ParsePongError { + /// Pong packets must always be 24 bytes in size. + InvalidSize, +} + +impl TryFrom<&[u8]> for PongPacket { + type Error = ParsePongError; + fn try_from(buf: &[u8]) -> Result { + match <[u8; 24]>::try_from(buf) { + Ok(array) => Ok(Self { + version: u32::from_be_bytes(array[0..4].try_into().unwrap()), + id: u64::from_be_bytes(array[4..12].try_into().unwrap()), + users: u32::from_be_bytes(array[12..16].try_into().unwrap()), + max_users: u32::from_be_bytes(array[16..20].try_into().unwrap()), + bandwidth: u32::from_be_bytes(array[20..24].try_into().unwrap()), + }), + Err(_) => Err(ParsePongError::InvalidSize), + } + } +} + +impl From for [u8; 24] { + fn from(packet: PongPacket) -> Self { + let version = packet.version.to_be_bytes(); + let id = packet.id.to_be_bytes(); + let users = packet.users.to_be_bytes(); + let max_users = packet.max_users.to_be_bytes(); + let bandwidth = packet.bandwidth.to_be_bytes(); + // Is there no nicer way to do this? + [ + version[0], + version[1], + version[2], + version[3], + id[0], + id[1], + id[2], + id[3], + id[4], + id[5], + id[6], + id[7], + users[0], + users[1], + users[2], + users[3], + max_users[0], + max_users[1], + max_users[2], + max_users[3], + bandwidth[0], + bandwidth[1], + bandwidth[2], + bandwidth[3], + ] + } +}