mirror of
https://git.alemi.dev/mumble-stats-api.git
synced 2024-12-22 12:44:53 +01:00
feat: mumble control channel, simple userlist route
This commit is contained in:
parent
2f4b52e09d
commit
4daacc9712
2 changed files with 193 additions and 4 deletions
57
src/main.rs
57
src/main.rs
|
@ -5,7 +5,10 @@ use clap::Parser;
|
|||
use axum::{extract::Query, routing::get, Json, Router};
|
||||
use mumble::{ServerInfo, ping_mumble_server};
|
||||
|
||||
mod proto;
|
||||
use crate::tcp::ControlChannel;
|
||||
|
||||
mod tcp;
|
||||
mod udp;
|
||||
mod mumble;
|
||||
|
||||
#[derive(Parser)]
|
||||
|
@ -19,10 +22,56 @@ async fn main() {
|
|||
|
||||
// build our application with a route
|
||||
let app = Router::new()
|
||||
.route("/ping", get(ping_server));
|
||||
.route("/ping", get(ping_server))
|
||||
.route("/join", get(test_explore_server));
|
||||
|
||||
tracing::info!("serving mumble-stats-api");
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:57039").await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap()
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct ExploreOptions {
|
||||
host: String,
|
||||
port: Option<u16>,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
tokens: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
async fn test_explore_server(Query(options): Query<ExploreOptions>) -> Result<Json<Vec<String>>, String> {
|
||||
let mut channel = ControlChannel::new(&options.host, options.port).await.unwrap();
|
||||
let version = tcp::proto::Version {
|
||||
version_v1: None, // Some(67071),
|
||||
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),
|
||||
};
|
||||
|
||||
channel.send(tcp::proto::Packet::Version(version)).await.unwrap();
|
||||
channel.send(tcp::proto::Packet::Authenticate(authenticate)).await.unwrap();
|
||||
|
||||
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.name.as_deref().unwrap_or("???").to_string()),
|
||||
Ok(tcp::proto::Packet::ServerSync(_sync)) => break Ok(Json(users)),
|
||||
Ok(pkt) => tracing::debug!("ignoring packet {:#?}", pkt),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
|
@ -33,7 +82,7 @@ struct PingOptions {
|
|||
|
||||
async fn ping_server(Query(options): Query<PingOptions>) -> Result<Json<ServerInfo>, String> {
|
||||
let tuple = (options.host, options.port.unwrap_or(64738));
|
||||
match tuple.to_socket_addrs() {
|
||||
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 {
|
||||
|
|
140
src/tcp/mod.rs
140
src/tcp/mod.rs
|
@ -1,3 +1,143 @@
|
|||
use std::net::ToSocketAddrs;
|
||||
|
||||
use tokio::{io::{AsyncReadExt, AsyncWriteExt}, net::TcpStream};
|
||||
use tokio_native_tls::TlsStream;
|
||||
|
||||
pub mod proto {
|
||||
use prost::Message;
|
||||
|
||||
tonic::include_proto!("mumble");
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Packet {
|
||||
Version(Version),
|
||||
#[allow(unused)] UDPTunnel(UdpTunnel),
|
||||
Authenticate(Authenticate),
|
||||
Ping(Ping),
|
||||
Reject(Reject),
|
||||
ServerSync(ServerSync),
|
||||
ChannelRemove(ChannelRemove),
|
||||
ChannelState(ChannelState),
|
||||
UserRemove(UserRemove),
|
||||
UserState(UserState),
|
||||
BanList(BanList),
|
||||
TextMessage(TextMessage),
|
||||
PermissionDenied(PermissionDenied),
|
||||
ACL(Acl),
|
||||
QueryUsers(QueryUsers),
|
||||
CryptSetup(CryptSetup),
|
||||
ContextActionModify(ContextActionModify),
|
||||
ContextAction(ContextAction),
|
||||
UserList(UserList),
|
||||
VoiceTarget(VoiceTarget),
|
||||
PermissionQuery(PermissionQuery),
|
||||
CodecVersion(CodecVersion),
|
||||
UserStats(UserStats),
|
||||
RequestBlob(RequestBlob),
|
||||
ServerConfig(ServerConfig),
|
||||
SuggestConfig(SuggestConfig),
|
||||
}
|
||||
|
||||
impl Packet {
|
||||
pub fn decode(id: u16, buffer: &[u8]) -> std::io::Result<Self> {
|
||||
match id {
|
||||
0 => Ok(Packet::Version(Version::decode(buffer)?)),
|
||||
1 => todo!(),
|
||||
2 => Ok(Packet::Authenticate(Authenticate::decode(buffer)?)),
|
||||
3 => Ok(Packet::Ping(Ping::decode(buffer)?)),
|
||||
4 => Ok(Packet::Reject(Reject::decode(buffer)?)),
|
||||
5 => Ok(Packet::ServerSync(ServerSync::decode(buffer)?)),
|
||||
6 => Ok(Packet::ChannelRemove(ChannelRemove::decode(buffer)?)),
|
||||
7 => Ok(Packet::ChannelState(ChannelState::decode(buffer)?)),
|
||||
8 => Ok(Packet::UserRemove(UserRemove::decode(buffer)?)),
|
||||
9 => Ok(Packet::UserState(UserState::decode(buffer)?)),
|
||||
10 => Ok(Packet::BanList(BanList::decode(buffer)?)),
|
||||
11 => Ok(Packet::TextMessage(TextMessage::decode(buffer)?)),
|
||||
12 => Ok(Packet::PermissionDenied(PermissionDenied::decode(buffer)?)),
|
||||
13 => Ok(Packet::ACL(Acl::decode(buffer)?)),
|
||||
14 => Ok(Packet::QueryUsers(QueryUsers::decode(buffer)?)),
|
||||
15 => Ok(Packet::CryptSetup(CryptSetup::decode(buffer)?)),
|
||||
16 => Ok(Packet::ContextActionModify(ContextActionModify::decode(buffer)?)),
|
||||
17 => Ok(Packet::ContextAction(ContextAction::decode(buffer)?)),
|
||||
18 => Ok(Packet::UserList(UserList::decode(buffer)?)),
|
||||
19 => Ok(Packet::VoiceTarget(VoiceTarget::decode(buffer)?)),
|
||||
20 => Ok(Packet::PermissionQuery(PermissionQuery::decode(buffer)?)),
|
||||
21 => Ok(Packet::CodecVersion(CodecVersion::decode(buffer)?)),
|
||||
22 => Ok(Packet::UserStats(UserStats::decode(buffer)?)),
|
||||
23 => Ok(Packet::RequestBlob(RequestBlob::decode(buffer)?)),
|
||||
24 => Ok(Packet::ServerConfig(ServerConfig::decode(buffer)?)),
|
||||
25 => Ok(Packet::SuggestConfig(SuggestConfig::decode(buffer)?)),
|
||||
_ => Err(std::io::Error::from(std::io::ErrorKind::InvalidData)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode(self) -> (u16, Vec<u8>) {
|
||||
match self {
|
||||
Packet::Version(x) => (0, x.encode_to_vec()),
|
||||
Packet::UDPTunnel(_) => todo!(),
|
||||
Packet::Authenticate(x) => (2, x.encode_to_vec()),
|
||||
Packet::Ping(x) => (3, x.encode_to_vec()),
|
||||
Packet::Reject(x) => (4, x.encode_to_vec()),
|
||||
Packet::ServerSync(x) => (5, x.encode_to_vec()),
|
||||
Packet::ChannelRemove(x) => (6, x.encode_to_vec()),
|
||||
Packet::ChannelState(x) => (7, x.encode_to_vec()),
|
||||
Packet::UserRemove(x) => (8, x.encode_to_vec()),
|
||||
Packet::UserState(x) => (9, x.encode_to_vec()),
|
||||
Packet::BanList(x) => (10, x.encode_to_vec()),
|
||||
Packet::TextMessage(x) => (11, x.encode_to_vec()),
|
||||
Packet::PermissionDenied(x) => (12, x.encode_to_vec()),
|
||||
Packet::ACL(x) => (13, x.encode_to_vec()),
|
||||
Packet::QueryUsers(x) => (14, x.encode_to_vec()),
|
||||
Packet::CryptSetup(x) => (15, x.encode_to_vec()),
|
||||
Packet::ContextActionModify(x) => (16, x.encode_to_vec()),
|
||||
Packet::ContextAction(x) => (17, x.encode_to_vec()),
|
||||
Packet::UserList(x) => (18, x.encode_to_vec()),
|
||||
Packet::VoiceTarget(x) => (19, x.encode_to_vec()),
|
||||
Packet::PermissionQuery(x) => (20, x.encode_to_vec()),
|
||||
Packet::CodecVersion(x) => (21, x.encode_to_vec()),
|
||||
Packet::UserStats(x) => (22, x.encode_to_vec()),
|
||||
Packet::RequestBlob(x) => (23, x.encode_to_vec()),
|
||||
Packet::ServerConfig(x) => (24, x.encode_to_vec()),
|
||||
Packet::SuggestConfig(x) => (25, x.encode_to_vec()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ControlChannel {
|
||||
stream: TlsStream<TcpStream>,
|
||||
}
|
||||
|
||||
impl ControlChannel {
|
||||
pub async fn new(host: &str, port: Option<u16>) -> std::io::Result<Self> {
|
||||
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<proto::Packet> {
|
||||
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)?)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue