diff --git a/src/main.rs b/src/main.rs index 40ed4d6..6b80040 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,13 +3,12 @@ use std::net::ToSocketAddrs; use clap::Parser; use axum::{extract::Query, routing::get, Json, Router}; -use mumble::{ServerInfo, ping_mumble_server}; - -use crate::tcp::ControlChannel; +use session::Session; mod tcp; mod udp; -mod mumble; +mod session; +mod model; #[derive(Parser)] struct CliArgs { @@ -23,7 +22,7 @@ async fn main() { // build our application with a route let app = Router::new() .route("/ping", get(ping_server)) - .route("/join", get(test_explore_server)); + .route("/users", get(list_server_users)); tracing::info!("serving mumble-stats-api"); let listener = tokio::net::TcpListener::bind("127.0.0.1:57039").await @@ -33,132 +32,20 @@ async fn main() { .expect("could not serve axum app"); } -#[derive(serde::Deserialize)] -struct ExploreOptions { - host: String, - port: Option, - username: Option, - password: Option, - tokens: Option>, -} - -#[derive(Debug, serde::Serialize)] -pub struct User { - /// Unique user session ID of the user whose state this is, may change on - /// reconnect. - pub session: u32, - - /// The session of the user who is updating this user. - pub actor: u32, - - /// User name, UTF-8 encoded. - pub name: String, - - /// Registered user ID if the user is registered. - pub user_id: Option, - - /// User comment if it is less than 128 bytes. - pub comment: Option, - - pub properties: UserProperties, -} - -#[derive(Debug, serde::Serialize)] -pub struct UserProperties { - /// True if the user is muted by admin. - pub mute: bool, - /// True if the user is deafened by admin. - pub deaf: bool, - /// True if the user has been suppressed from talking by a reason other than - /// being muted. - pub suppress: bool, - /// True if the user has muted self. - pub self_mute: bool, - /// True if the user has deafened self. - pub self_deaf: bool, - /// True if the user is a priority speaker. - pub priority_speaker: bool, - /// True if the user is currently recording. - pub recording: bool, -} - -impl From for User { - fn from(value: tcp::proto::UserState) -> Self { - User { - session: value.session(), - actor: value.actor(), - name: value.name().to_string(), - user_id: value.user_id, - comment: value.comment.clone(), - properties: UserProperties { - mute: value.mute(), - deaf: value.deaf(), - suppress: value.suppress(), - self_mute: value.self_mute(), - self_deaf: value.self_deaf(), - priority_speaker: value.priority_speaker(), - recording: value.recording(), - }, - } +async fn list_server_users(Query(options): Query) -> Result>, String> { + match Session::users(&options.host, options.port, options.username, options.password).await { + Ok(users) => Ok(Json(users)), + Err(e) => Err(format!("could not list users: {e}")), } } -async fn test_explore_server(Query(options): Query) -> Result>, String> { - let Ok(mut channel) = ControlChannel::new(&options.host, options.port).await else { - return Err("could not connect to server".into()); - }; - let version = tcp::proto::Version { - version_v1: None, - version_v2: Some(281496485429248), - release: Some("1.5.517".into()), - os: None, - os_version: None, - }; - let authenticate = tcp::proto::Authenticate { - username: Some(options.username.unwrap_or_else(|| ".mumble-stats-api".to_string())), - password: options.password, - tokens: options.tokens.clone().unwrap_or_else(Vec::new), - celt_versions: vec![], - opus: Some(true), - client_type: Some(1), - }; - - for pkt in [ - tcp::proto::Packet::Version(version), - tcp::proto::Packet::Authenticate(authenticate), - ] { - if let Err(e) = channel.send(pkt).await { - return Err(format!("could not authenticate: {e}")); - } - } - - let mut users = Vec::new(); - - loop { - match channel.recv().await { - Err(e) => break Err(format!("error receiving from server: {e}")), - // Ok(tcp::proto::Packet::TextMessage(msg)) => tracing::info!("{}", msg.message), - // Ok(tcp::proto::Packet::ChannelState(channel)) => tracing::info!("discovered channel: {:?}", channel.name), - Ok(tcp::proto::Packet::UserState(user)) => users.push(user.into()), - Ok(tcp::proto::Packet::ServerSync(_sync)) => break Ok(Json(users)), - Ok(pkt) => tracing::debug!("ignoring packet {:#?}", pkt), - } - } -} - -#[derive(serde::Deserialize)] -struct PingOptions { - host: String, - port: Option, -} - -async fn ping_server(Query(options): Query) -> Result, String> { +async fn ping_server(Query(options): Query) -> Result, String> { let tuple = (options.host, options.port.unwrap_or(64738)); match tuple.to_socket_addrs() { // TODO do it manually so we have control Err(e) => Err(format!("invalid address: {e}")), Ok(addrs) => { for addr in addrs { - if let Ok(info) = ping_mumble_server(addr).await { + if let Ok(info) = Session::ping(addr).await { return Ok(Json(info)); } } diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..65622fc --- /dev/null +++ b/src/model.rs @@ -0,0 +1,86 @@ + +#[derive(serde::Deserialize)] +pub struct PingOptions { + pub host: String, + pub port: Option, +} + +#[derive(Debug, serde::Serialize)] +pub struct ServerInfo { + pub version: String, + pub users: i32, + pub max_users: i32, + pub bandwidth: i32, + pub latency: i64, +} + +#[derive(serde::Deserialize)] +pub struct ListUsersOptions { + pub host: String, + pub port: Option, + pub username: Option, + pub password: Option, + pub tokens: Option>, +} + +#[derive(Debug, serde::Serialize)] +pub struct User { + /// Unique user session ID of the user whose state this is, may change on + /// reconnect. + pub session: u32, + + /// The session of the user who is updating this user. + pub actor: u32, + + /// User name, UTF-8 encoded. + pub name: String, + + /// Registered user ID if the user is registered. + pub user_id: Option, + + /// User comment if it is less than 128 bytes. + pub comment: Option, + + pub properties: UserProperties, +} + +#[derive(Debug, serde::Serialize)] +pub struct UserProperties { + /// True if the user is muted by admin. + pub mute: bool, + /// True if the user is deafened by admin. + pub deaf: bool, + /// True if the user has been suppressed from talking by a reason other than + /// being muted. + pub suppress: bool, + /// True if the user has muted self. + pub self_mute: bool, + /// True if the user has deafened self. + pub self_deaf: bool, + /// True if the user is a priority speaker. + pub priority_speaker: bool, + /// True if the user is currently recording. + pub recording: bool, +} + + +impl From for User { + fn from(value: crate::tcp::proto::UserState) -> Self { + User { + session: value.session(), + actor: value.actor(), + name: value.name().to_string(), + user_id: value.user_id, + comment: value.comment.clone(), + properties: UserProperties { + mute: value.mute(), + deaf: value.deaf(), + suppress: value.suppress(), + self_mute: value.self_mute(), + self_deaf: value.self_deaf(), + priority_speaker: value.priority_speaker(), + recording: value.recording(), + }, + } + } +} diff --git a/src/mumble.rs b/src/mumble.rs deleted file mode 100644 index eb9df99..0000000 --- a/src/mumble.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::net::SocketAddr; - -use tokio::net::UdpSocket; - -use crate::udp::proto::{PingPacket, PongPacket}; - -#[derive(serde::Serialize)] -pub struct ServerInfo { - pub version: String, - pub users: i32, - pub max_users: i32, - pub bandwidth: i32, - pub latency: i64, -} - -pub async fn ping_mumble_server(addr: SocketAddr) -> std::io::Result { - // by default bind on any interface and request OS to give us a port - let from_addr : SocketAddr = "0.0.0.0:0".parse().expect("could not create socketaddr from '0.0.0.0:0'"); - let socket = UdpSocket::bind(from_addr).await?; - - let packet = PingPacket { action: 0, iden: chrono::Utc::now().timestamp_micros() as u64 }; - let pkt = packet.serialize().await?; - let mut buf = [0u8; 64]; - - let before = chrono::Utc::now(); - socket.send_to(&pkt, addr).await?; - socket.recv(&mut buf).await?; - let latency = chrono::Utc::now() - before; - - let pong = PongPacket::deserialize(&buf).await?; - - - Ok(ServerInfo { - version: parse_version(pong.version), - users: pong.users, - max_users: pong.max_users, - bandwidth: pong.bandwidth, - latency: latency.num_milliseconds(), - }) -} - -fn parse_version(version: u32) -> String { - let mut segments = Vec::new(); - for b in version.to_be_bytes() { - if b != 255 { - segments.push(format!("{b}")); - } - } - segments.join(".") -} diff --git a/src/session.rs b/src/session.rs new file mode 100644 index 0000000..5690c75 --- /dev/null +++ b/src/session.rs @@ -0,0 +1,78 @@ +use std::net::SocketAddr; + +use tokio::net::UdpSocket; + +use crate::{tcp::{proto, control::ControlChannel}, udp::proto::{PingPacket, PongPacket}}; + +pub struct Session { + +} + +impl Session { + pub async fn ping(addr: SocketAddr) -> std::io::Result { + // by default bind on any interface and request OS to give us a port + let from_addr : SocketAddr = "0.0.0.0:0".parse().expect("could not create socketaddr from '0.0.0.0:0'"); + let socket = UdpSocket::bind(from_addr).await?; + + let packet = PingPacket { action: 0, iden: chrono::Utc::now().timestamp_micros() as u64 }; + let pkt = packet.serialize().await?; + let mut buf = [0u8; 64]; + + let before = chrono::Utc::now(); + socket.send_to(&pkt, addr).await?; + socket.recv(&mut buf).await?; + let latency = chrono::Utc::now() - before; + + let pong = PongPacket::deserialize(&buf).await?; + + Ok(crate::model::ServerInfo { + users: pong.users, + max_users: pong.max_users, + bandwidth: pong.bandwidth, + latency: latency.num_milliseconds(), + version: format!("{}.{}.{}", + (pong.version >> 16) & 65535, + (pong.version >> 8 ) & 255, + (pong.version ) & 255 + ), + }) + } + + pub async fn users(host: &str, port: Option, username: Option, password: Option) -> std::io::Result> { + let mut channel = ControlChannel::new(host, port).await?; + let version = proto::Version { + version_v1: None, + version_v2: Some(281496485429248), + release: Some("1.5.517".into()), + os: None, + os_version: None, + }; + let authenticate = proto::Authenticate { + username: Some(username.unwrap_or_else(|| ".mumble-stats-api".to_string())), + password, + tokens: Vec::new(), + celt_versions: Vec::new(), + opus: Some(true), + client_type: Some(1), + }; + + for pkt in [ + proto::Packet::Version(version), + proto::Packet::Authenticate(authenticate), + ] { + channel.send(pkt).await?; + } + + let mut users = Vec::new(); + + loop { + match channel.recv().await? { + // Ok(tcp::proto::Packet::TextMessage(msg)) => tracing::info!("{}", msg.message), + // Ok(tcp::proto::Packet::ChannelState(channel)) => tracing::info!("discovered channel: {:?}", channel.name), + proto::Packet::UserState(user) => users.push(user.into()), + proto::Packet::ServerSync(_sync) => break Ok(users), + pkt => tracing::debug!("ignoring packet {:#?}", pkt), + } + } + } +} diff --git a/src/tcp/control.rs b/src/tcp/control.rs new file mode 100644 index 0000000..ca3defd --- /dev/null +++ b/src/tcp/control.rs @@ -0,0 +1,42 @@ +use std::net::ToSocketAddrs; + +use tokio::{io::{AsyncReadExt, AsyncWriteExt}, net::TcpStream}; +use tokio_native_tls::TlsStream; + +pub struct ControlChannel { + stream: TlsStream, +} + +impl ControlChannel { + pub async fn new(host: &str, port: Option) -> std::io::Result { + let addr = (host, port.unwrap_or(64738)).to_socket_addrs()? + .filter(|a| a.is_ipv4()) + .next() + .ok_or(std::io::Error::from(std::io::ErrorKind::AddrNotAvailable))?; + let socket = TcpStream::connect(addr).await?; + // use native_tls builder and then .into() so we can pass options to the builder + let connector : tokio_native_tls::TlsConnector = native_tls::TlsConnector::builder() + .danger_accept_invalid_certs(true) + .build() + .expect("could not create TLS connector").into(); + let stream = connector.connect(host, socket).await.unwrap(); + Ok(ControlChannel { stream }) + } + + pub async fn send(&mut self, pkt: super::proto::Packet) -> std::io::Result<()> { + let (id, buffer) = pkt.encode(); + self.stream.write_u16(id).await?; + self.stream.write_u32(buffer.len() as u32).await?; + self.stream.write_all(&buffer).await?; + self.stream.flush().await?; + Ok(()) + } + + pub async fn recv(&mut self) -> std::io::Result { + let id = self.stream.read_u16().await?; + let size = self.stream.read_u32().await?; + let mut buffer = vec![0u8; size as usize]; + self.stream.read_exact(&mut buffer).await?; + Ok(super::proto::Packet::decode(id, &buffer)?) + } +} diff --git a/src/tcp/mod.rs b/src/tcp/mod.rs index 48df93a..e6e1e6a 100644 --- a/src/tcp/mod.rs +++ b/src/tcp/mod.rs @@ -1,7 +1,4 @@ -use std::net::ToSocketAddrs; - -use tokio::{io::{AsyncReadExt, AsyncWriteExt}, net::TcpStream}; -use tokio_native_tls::TlsStream; +pub mod control; pub mod proto { use prost::Message; @@ -104,40 +101,3 @@ pub mod proto { } } -pub struct ControlChannel { - stream: TlsStream, -} - -impl ControlChannel { - pub async fn new(host: &str, port: Option) -> std::io::Result { - let addr = (host, port.unwrap_or(64738)).to_socket_addrs()? - .filter(|a| a.is_ipv4()) - .next() - .ok_or(std::io::Error::from(std::io::ErrorKind::AddrNotAvailable))?; - let socket = TcpStream::connect(addr).await?; - // use native_tls builder and then .into() so we can pass options to the builder - let connector : tokio_native_tls::TlsConnector = native_tls::TlsConnector::builder() - .danger_accept_invalid_certs(true) - .build() - .expect("could not create TLS connector").into(); - let stream = connector.connect(host, socket).await.unwrap(); - Ok(ControlChannel { stream }) - } - - pub async fn send(&mut self, pkt: proto::Packet) -> std::io::Result<()> { - let (id, buffer) = pkt.encode(); - self.stream.write_u16(id).await?; - self.stream.write_u32(buffer.len() as u32).await?; - self.stream.write_all(&buffer).await?; - self.stream.flush().await?; - Ok(()) - } - - pub async fn recv(&mut self) -> std::io::Result { - let id = self.stream.read_u16().await?; - let size = self.stream.read_u32().await?; - let mut buffer = vec![0u8; size as usize]; - self.stream.read_exact(&mut buffer).await?; - Ok(proto::Packet::decode(id, &buffer)?) - } -} diff --git a/src/udp/proto.rs b/src/udp/proto.rs index 7bef3de..bf2b4d6 100644 --- a/src/udp/proto.rs +++ b/src/udp/proto.rs @@ -1,5 +1,6 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; +#[derive(Debug)] pub struct PingPacket { pub action: i32, // must be 0 for ping, what are other numbers? TODO! pub iden: u64, @@ -14,7 +15,7 @@ impl PingPacket { } } -#[derive(Debug, serde::Serialize)] +#[derive(Debug)] pub struct PongPacket { pub version: u32, pub iden: u64,