Move mumble UDP ping into common crate #33
Generated
+2
-1
@@ -4275,7 +4275,9 @@ dependencies = [
|
||||
name = "mumble-web2-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"color-eyre",
|
||||
"serde",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4298,7 +4300,6 @@ dependencies = [
|
||||
"hmac-sha256",
|
||||
"mumble-web2-common",
|
||||
"once_cell",
|
||||
"rand 0.9.2",
|
||||
"rcgen",
|
||||
"rustls",
|
||||
"salvo",
|
||||
|
||||
@@ -133,6 +133,7 @@ desktop = [
|
||||
"cpal",
|
||||
"dasp_ring_buffer",
|
||||
"etcetera",
|
||||
"mumble-web2-common/networking",
|
||||
]
|
||||
mobile = [
|
||||
"tokio",
|
||||
@@ -141,4 +142,5 @@ mobile = [
|
||||
"opus",
|
||||
"cpal",
|
||||
"dasp_ring_buffer",
|
||||
"mumble-web2-common/networking",
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::app::{Command, SharedState};
|
||||
use color_eyre::eyre::{bail, Error};
|
||||
use color_eyre::eyre::Error;
|
||||
use futures_channel::mpsc::UnboundedReceiver;
|
||||
use mumble_protocol::control::ClientControlCodec;
|
||||
use std::net::ToSocketAddrs;
|
||||
@@ -14,7 +14,7 @@ use tokio_rustls::TlsConnector;
|
||||
use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _};
|
||||
use tracing::{info, instrument};
|
||||
|
||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||
use mumble_web2_common::ProxyOverrides;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct NoCertificateVerification;
|
||||
@@ -108,10 +108,6 @@ pub async fn network_connect(
|
||||
crate::network_loop(username, state, event_rx, outgoing_send, reader).await
|
||||
}
|
||||
|
||||
pub async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||
bail!("status not supported on desktop yet")
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub use tokio::spawn;
|
||||
#[allow(unused)]
|
||||
|
||||
@@ -33,8 +33,11 @@ impl super::PlatformInterface for DesktopPlatform {
|
||||
super::connect::network_connect(address, username, event_rx, overrides, state).await
|
||||
}
|
||||
|
||||
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||
super::connect::get_status(client).await
|
||||
async fn get_status(
|
||||
_client: &reqwest::Client,
|
||||
address: &str,
|
||||
) -> color_eyre::Result<ServerStatus> {
|
||||
mumble_web2_common::ping_server(address, 64738).await
|
||||
}
|
||||
|
||||
fn init_logging() {
|
||||
|
||||
@@ -29,8 +29,11 @@ impl super::PlatformInterface for MobilePlatform {
|
||||
super::connect::network_connect(address, username, event_rx, overrides, state).await
|
||||
}
|
||||
|
||||
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||
super::connect::get_status(client).await
|
||||
async fn get_status(
|
||||
_client: &reqwest::Client,
|
||||
address: &str,
|
||||
) -> color_eyre::Result<ServerStatus> {
|
||||
mumble_web2_common::ping_server(address, 64738).await
|
||||
}
|
||||
|
||||
fn init_logging() {
|
||||
|
||||
@@ -86,9 +86,14 @@ pub trait PlatformInterface {
|
||||
state: SharedState,
|
||||
) -> impl Future<Output = Result<(), Error>>;
|
||||
|
||||
/// Get server status (user count, version, etc.).
|
||||
/// Get server status (user count, version, etc.) for the given address.
|
||||
///
|
||||
/// On web, this goes through the proxy's /status endpoint and ignores `address`
|
||||
/// (the proxy is bound to a specific server). On desktop/mobile, this pings the
|
||||
/// given address directly via UDP.
|
||||
fn get_status(
|
||||
client: &reqwest::Client,
|
||||
address: &str,
|
||||
) -> impl Future<Output = color_eyre::Result<ServerStatus>>;
|
||||
|
||||
/// Load the proxy overrides (proxy URL, cert hash, etc.).
|
||||
|
||||
@@ -32,6 +32,7 @@ impl super::PlatformInterface for StubPlatform {
|
||||
|
||||
fn get_status(
|
||||
_client: &reqwest::Client,
|
||||
_address: &str,
|
||||
) -> impl Future<Output = color_eyre::Result<ServerStatus>> {
|
||||
async { panic!("stubbed platform") }
|
||||
}
|
||||
|
||||
@@ -117,7 +117,10 @@ impl super::PlatformInterface for WebPlatform {
|
||||
network_connect(address, username, event_rx, overrides, state).await
|
||||
}
|
||||
|
||||
async fn get_status(client: &reqwest::Client) -> color_eyre::Result<ServerStatus> {
|
||||
async fn get_status(
|
||||
client: &reqwest::Client,
|
||||
_address: &str,
|
||||
) -> color_eyre::Result<ServerStatus> {
|
||||
Ok(client
|
||||
.get(absolute_url("status")?)
|
||||
.send()
|
||||
|
||||
@@ -3,5 +3,10 @@ name = "mumble-web2-common"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
networking = ["dep:tokio", "dep:color-eyre"]
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
tokio = { version = "1", features = ["net", "time"], optional = true }
|
||||
color-eyre = { version = "0.6", optional = true }
|
||||
|
||||
@@ -16,3 +16,62 @@ pub struct ServerStatus {
|
||||
pub max_users: Option<u32>,
|
||||
pub bandwidth: Option<u32>,
|
||||
}
|
||||
|
||||
/// Mumble UDP ping protocol.
|
||||
///
|
||||
/// Send a 12-byte packet: 4 zero bytes + 8-byte identifier.
|
||||
/// Receive a 24-byte response: 4 bytes version + 8 bytes identifier echo
|
||||
/// + 4 bytes current_users + 4 bytes max_users + 4 bytes bandwidth.
|
||||
#[cfg(feature = "networking")]
|
||||
pub async fn ping_server(address: &str, port: u16) -> color_eyre::Result<ServerStatus> {
|
||||
use color_eyre::eyre::{bail, eyre};
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::time::Duration;
|
||||
use tokio::net::UdpSocket;
|
||||
|
||||
let dest = format!("{}:{}", address, port)
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.ok_or_else(|| eyre!("could not resolve address"))?;
|
||||
|
||||
let bind_addr = if dest.is_ipv6() { "[::]:0" } else { "0.0.0.0:0" };
|
||||
let socket = UdpSocket::bind(bind_addr).await?;
|
||||
socket.connect(dest).await?;
|
||||
|
||||
let request_id: u64 = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos() as u64;
|
||||
|
||||
let mut buf = [0u8; 12];
|
||||
buf[4..12].copy_from_slice(&request_id.to_be_bytes());
|
||||
socket.send(&buf).await?;
|
||||
|
||||
let mut response = [0u8; 24];
|
||||
let timeout = tokio::time::timeout(Duration::from_secs(2), socket.recv(&mut response)).await;
|
||||
|
||||
match timeout {
|
||||
Ok(Ok(len)) if len >= 24 => {
|
||||
let version_major = response[0] as u32;
|
||||
let version_minor = response[1] as u32;
|
||||
let version_patch = response[2] as u32;
|
||||
let users =
|
||||
u32::from_be_bytes([response[12], response[13], response[14], response[15]]);
|
||||
let max_users =
|
||||
u32::from_be_bytes([response[16], response[17], response[18], response[19]]);
|
||||
let bandwidth =
|
||||
u32::from_be_bytes([response[20], response[21], response[22], response[23]]);
|
||||
|
||||
Ok(ServerStatus {
|
||||
success: true,
|
||||
version: Some((version_major, version_minor, version_patch)),
|
||||
users: Some(users),
|
||||
max_users: Some(max_users),
|
||||
bandwidth: Some(bandwidth),
|
||||
})
|
||||
}
|
||||
Ok(Ok(_)) => bail!("ping response too short"),
|
||||
Ok(Err(e)) => Err(e.into()),
|
||||
Err(_) => bail!("ping timed out"),
|
||||
}
|
||||
}
|
||||
|
||||
+14
-10
@@ -502,15 +502,6 @@ pub fn LoginView(overrides: Resource<ProxyOverrides>) -> Element {
|
||||
let user_config = use_context::<ConfigSystem>();
|
||||
let net: Coroutine<Command> = use_coroutine_handle();
|
||||
|
||||
let last_status = use_signal(|| None::<color_eyre::Result<ServerStatus>>);
|
||||
use_resource(move || async move {
|
||||
let client = reqwest::Client::new();
|
||||
loop {
|
||||
*last_status.write_unchecked() = Some(Platform::get_status(&client).await);
|
||||
Platform::sleep(std::time::Duration::from_secs_f32(1.0)).await;
|
||||
}
|
||||
});
|
||||
|
||||
let mut address_input = use_signal(|| user_config.config_get::<String>("server_url"));
|
||||
let address = use_memo(move || {
|
||||
if let Some(addr) = address_input() {
|
||||
@@ -522,6 +513,19 @@ pub fn LoginView(overrides: Resource<ProxyOverrides>) -> Element {
|
||||
}
|
||||
});
|
||||
|
||||
let last_status = use_signal(|| None::<color_eyre::Result<ServerStatus>>);
|
||||
use_resource(move || {
|
||||
let addr = address();
|
||||
async move {
|
||||
let client = reqwest::Client::new();
|
||||
loop {
|
||||
*last_status.write_unchecked() =
|
||||
Some(Platform::get_status(&client, &addr).await);
|
||||
Platform::sleep(std::time::Duration::from_secs_f32(1.0)).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut username = use_signal(|| {
|
||||
user_config
|
||||
.config_get::<String>("username")
|
||||
@@ -640,7 +644,7 @@ pub fn LoginView(overrides: Resource<ProxyOverrides>) -> Element {
|
||||
Some(Err(_)) => rsx!(div {
|
||||
class: "login_status is_error",
|
||||
span {
|
||||
"Could not reach proxy server"
|
||||
"Could not reach server"
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
+1
-2
@@ -12,7 +12,7 @@ tokio-rustls = "0.26"
|
||||
toml = "0.8"
|
||||
tracing = { version = "^0.1.40", features = ["async-await"] }
|
||||
tracing-subscriber = { version = "^0.3.18", features = ["env-filter"] }
|
||||
mumble-web2-common = { workspace = true }
|
||||
mumble-web2-common = { workspace = true, features = ["networking"] }
|
||||
salvo = { version = "^0.84.2", features = [
|
||||
"quinn",
|
||||
"eyre",
|
||||
@@ -28,4 +28,3 @@ rcgen = "^0.13.2"
|
||||
hmac-sha256 = "^1.1.8"
|
||||
time = "0.3"
|
||||
url = { version = "2", features = ["serde"] }
|
||||
rand = "0.9.2"
|
||||
|
||||
+6
-65
@@ -1,6 +1,5 @@
|
||||
use color_eyre::eyre::{anyhow, bail, Context, Result};
|
||||
use mumble_web2_common::{ProxyOverrides, ServerStatus};
|
||||
use rand::Rng;
|
||||
use mumble_web2_common::{ping_server, ProxyOverrides, ServerStatus};
|
||||
use salvo::conn::rustls::{Keycert, RustlsConfig};
|
||||
use salvo::cors::{AllowOrigin, Cors};
|
||||
use salvo::logging::Logger;
|
||||
@@ -26,8 +25,6 @@ use tracing_subscriber::filter::LevelFilter;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use url::Url;
|
||||
|
||||
mod ping;
|
||||
|
||||
fn default_cert_alt_names() -> Vec<String> {
|
||||
vec!["localhost".into()]
|
||||
}
|
||||
@@ -179,70 +176,14 @@ pub struct StatusCraft {
|
||||
impl StatusCraft {
|
||||
#[craft(handler)]
|
||||
async fn get_status(&self) -> Json<ServerStatus> {
|
||||
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,
|
||||
let addr = self.mumble_server_address;
|
||||
match ping_server(&addr.ip().to_string(), addr.port()).await {
|
||||
Ok(status) => Json(status),
|
||||
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);
|
||||
error!("ping failed: {e:#}");
|
||||
Json(ServerStatus::default())
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
// 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<Self, Self::Error> {
|
||||
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<PingPacket> 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<Self, Self::Error> {
|
||||
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<PongPacket> 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],
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user