use std::collections::VecDeque;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::{Duration, Instant};
use bytes::{Bytes, BytesMut};
use prost::Message;
use rustls::pki_types::ServerName;
use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf};
use tokio::net::TcpStream;
use tokio::sync::{Notify, mpsc};
use tokio_rustls::TlsConnector;
use tokio_rustls::client::TlsStream;
use crate::error::{CommandError, ConnectError, PermissionDenied, ProtocolError, RejectReason};
use crate::framing::{self, Frame, MessageType};
use crate::proto;
use crate::ratelimit::{self, LeakyBucket, Limit};
use crate::state::{Channel, CryptSetup, ServerInfo, ServerState, User, UserChange};
use crate::tls::{self, CertVerifier, ClientIdentity};
use crate::version::MumbleVersion;
use crate::voice::CryptStats;
const PING_INTERVAL: Duration = Duration::from_secs(5);
const READ_TIMEOUT: Duration = Duration::from_secs(30);
pub struct ConnectConfig {
pub host: String,
pub port: u16,
pub username: String,
pub password: Option<String>,
pub tokens: Vec<String>,
pub identity: Option<ClientIdentity>,
pub verify_cert: CertVerifier,
pub login_timeout: Duration,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TextTarget {
pub sessions: Vec<u32>,
pub channels: Vec<u32>,
pub trees: Vec<u32>,
}
impl TextTarget {
pub fn channel(id: u32) -> Self {
Self {
channels: vec![id],
..Default::default()
}
}
pub fn user(session: u32) -> Self {
Self {
sessions: vec![session],
..Default::default()
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TextMessage {
pub actor: Option<u32>,
pub target: TextTarget,
pub message: String,
}
pub const PLUGIN_DATA_MAX: usize = 1000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginData {
pub sender: Option<u32>,
pub data_id: String,
pub data: Bytes,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DisconnectReason {
Requested,
Kicked {
actor: Option<u32>,
reason: Option<String>,
ban: bool,
},
Closed,
Timeout,
Error(String),
}
#[derive(Debug, Clone, PartialEq)]
pub enum Event {
ChannelUpdated(Channel),
ChannelRemoved(Channel),
UserUpdated(UserChange),
UserRemoved {
user: User,
actor: Option<u32>,
reason: Option<String>,
ban: bool,
},
TextMessage(TextMessage),
PermissionDenied(PermissionDenied),
PermissionsUpdated {
channel_id: Option<u32>,
},
ServerConfigUpdated(ServerInfo),
CryptSetup(CryptSetup),
UdpTunnel(Bytes),
PluginData(PluginData),
UserStats(Box<proto::UserStats>),
Disconnected(DisconnectReason),
}
pub struct Connection {
pub handle: Handle,
pub events: mpsc::UnboundedReceiver<Event>,
}
enum Outgoing {
Frame(Frame),
Disconnect,
}
struct Shared {
state: Mutex<ServerState>,
shutdown: Notify,
reason: Mutex<Option<DisconnectReason>>,
connected: AtomicBool,
ping: Mutex<PingReport>,
}
impl Shared {
fn state(&self) -> MutexGuard<'_, ServerState> {
self.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn ping(&self) -> MutexGuard<'_, PingReport> {
self.ping.lock().unwrap_or_else(|p| p.into_inner())
}
fn shut_down(&self, reason: DisconnectReason) {
let mut slot = self.reason.lock().unwrap_or_else(|p| p.into_inner());
if slot.is_none() {
*slot = Some(reason);
}
self.shutdown.notify_one();
}
}
struct PingReport {
started: Instant,
tcp_packets: u32,
tcp_avg_ms: f64,
tcp_m2: f64,
udp: Option<UdpPing>,
crypt: CryptStats,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct UdpPing {
pub avg_ms: f32,
pub packets: u32,
}
impl Default for PingReport {
fn default() -> Self {
Self {
started: Instant::now(),
tcp_packets: 0,
tcp_avg_ms: 0.0,
tcp_m2: 0.0,
udp: None,
crypt: CryptStats::default(),
}
}
}
impl PingReport {
fn now_us(&self) -> u64 {
self.started.elapsed().as_micros() as u64
}
fn on_reply(&mut self, sent_us: u64) {
let Some(rtt_us) = self.now_us().checked_sub(sent_us) else {
return;
};
let rtt_ms = rtt_us as f64 / 1000.0;
self.tcp_packets = self.tcp_packets.saturating_add(1);
let delta = rtt_ms - self.tcp_avg_ms;
self.tcp_avg_ms += delta / f64::from(self.tcp_packets);
self.tcp_m2 += delta * (rtt_ms - self.tcp_avg_ms);
}
fn message(&self) -> proto::Ping {
let tcp = self.tcp_packets > 0;
proto::Ping {
timestamp: Some(self.now_us()),
tcp_packets: tcp.then_some(self.tcp_packets),
tcp_ping_avg: tcp.then_some(self.tcp_avg_ms as f32),
tcp_ping_var: tcp.then(|| (self.tcp_m2 / f64::from(self.tcp_packets)) as f32),
udp_packets: self.udp.map(|u| u.packets),
udp_ping_avg: self.udp.map(|u| u.avg_ms),
good: Some(self.crypt.good),
late: Some(self.crypt.late),
lost: Some(self.crypt.lost),
resync: Some(self.crypt.resync),
..Default::default()
}
}
}
#[derive(Clone)]
pub struct Handle {
tx: mpsc::UnboundedSender<Outgoing>,
shared: Arc<Shared>,
}
pub async fn connect(config: ConnectConfig) -> Result<Connection, ConnectError> {
connect_with_progress(config, None).await
}
pub type SyncProgress = Arc<dyn Fn(usize) + Send + Sync>;
pub async fn connect_with_progress(
config: ConnectConfig,
progress: Option<SyncProgress>,
) -> Result<Connection, ConnectError> {
let limit = config.login_timeout;
tokio::time::timeout(limit, connect_inner(config, progress))
.await
.map_err(|_| ConnectError::Timeout)?
}
async fn connect_inner(
config: ConnectConfig,
progress: Option<SyncProgress>,
) -> Result<Connection, ConnectError> {
let server_name = ServerName::try_from(config.host.clone())
.map_err(|_| ConnectError::InvalidAddress(config.host.clone()))?;
let (tls_config, seen_cert) = tls::client_config(config.identity, config.verify_cert)?;
let addrs: Vec<SocketAddr> = tokio::net::lookup_host((config.host.as_str(), config.port))
.await
.map_err(ConnectError::Resolve)?
.collect();
if addrs.is_empty() {
return Err(ConnectError::Resolve(std::io::Error::new(
std::io::ErrorKind::NotFound,
"no addresses found",
)));
}
let tcp = TcpStream::connect(addrs.as_slice()).await?;
tcp.set_nodelay(true)?;
let mut stream = match TlsConnector::from(tls_config)
.connect(server_name, tcp)
.await
{
Ok(stream) => stream,
Err(err) => {
let tls_err = err
.get_ref()
.and_then(|e| e.downcast_ref::<rustls::Error>())
.cloned();
return Err(match (tls_err, seen_cert.take()) {
(
Some(rustls::Error::InvalidCertificate(
rustls::CertificateError::ApplicationVerificationFailure,
)),
Some(cert),
) => ConnectError::CertificateRejected(cert),
(Some(e), _) => ConnectError::Tls(e),
(None, _) => ConnectError::Io(err),
});
}
};
let mut handshake = BytesMut::new();
for frame in handshake_frames(config.username, config.password, config.tokens) {
frame.encode(&mut handshake);
}
async {
stream.write_all(&handshake).await?;
stream.flush().await
}
.await?;
let (mut reader, writer) = tokio::io::split(stream);
let (tx, rx) = mpsc::unbounded_channel();
let (events_tx, events) = mpsc::unbounded_channel();
let shared = Arc::new(Shared {
state: Mutex::new(ServerState::default()),
shutdown: Notify::new(),
reason: Mutex::new(None),
connected: AtomicBool::new(true),
ping: Mutex::default(),
});
tokio::spawn(write_loop(writer, rx, shared.clone()));
let handle = Handle {
tx,
shared: shared.clone(),
};
let mut dispatcher = Dispatcher {
shared: shared.clone(),
events: events_tx,
synced: false,
};
let mut buf = BytesMut::with_capacity(16 * 1024);
let mut reported = 0;
loop {
if reader
.read_buf(&mut buf)
.await
.inspect_err(|_| handle.close())?
== 0
{
handle.close();
return Err(ConnectError::Closed);
}
while let Some(frame) = framing::decode(&mut buf).inspect_err(|_| handle.close())? {
match dispatcher.handle(frame) {
Control::Continue => {
if let Some(progress) = &progress {
let count = handle.with_state(|s| s.channels.len());
if count != reported {
reported = count;
progress(count);
}
}
}
Control::Synced => {
tokio::spawn(read_loop(reader, buf, dispatcher, handle.tx.downgrade()));
return Ok(Connection { handle, events });
}
Control::Rejected(reason, message) => {
handle.close();
return Err(ConnectError::Rejected { reason, message });
}
Control::Kicked(_) => {
handle.close();
return Err(ConnectError::Closed);
}
}
}
}
}
fn handshake_frames(username: String, password: Option<String>, tokens: Vec<String>) -> [Frame; 2] {
let version = proto::Version {
version_v1: Some(MumbleVersion::CLIENT.to_v1()),
version_v2: Some(MumbleVersion::CLIENT.to_v2()),
release: Some(format!("Mumcord {}", env!("CARGO_PKG_VERSION"))),
os: Some(std::env::consts::OS.to_owned()),
os_version: None,
};
let auth = proto::Authenticate {
username: Some(username),
password,
tokens,
opus: Some(true),
..Default::default()
};
[
Frame::from_message(MessageType::Version, &version),
Frame::from_message(MessageType::Authenticate, &auth),
]
}
fn access_tokens_message(tokens: Vec<String>) -> proto::Authenticate {
proto::Authenticate {
tokens,
..Default::default()
}
}
async fn write_loop(
mut writer: WriteHalf<TlsStream<TcpStream>>,
mut rx: mpsc::UnboundedReceiver<Outgoing>,
shared: Arc<Shared>,
) {
let mut ping =
tokio::time::interval_at(tokio::time::Instant::now() + PING_INTERVAL, PING_INTERVAL);
let mut buf = BytesMut::new();
let start = tokio::time::Instant::now();
let mut messages = Paced::new(Limit::Messages, start);
let mut plugin_data = Paced::new(Limit::PluginData, start);
loop {
let now = tokio::time::Instant::now();
let messages_ready_at = messages.ready_at(now);
let plugin_data_ready_at = plugin_data.ready_at(now);
let frame = tokio::select! {
msg = rx.recv() => match msg {
Some(Outgoing::Frame(frame)) => match ratelimit::limit_of(frame.kind) {
Some(Limit::Messages) => {
messages.queue.push_back(frame);
continue;
}
Some(Limit::PluginData) => {
plugin_data.queue.push_back(frame);
continue;
}
None => frame,
},
Some(Outgoing::Disconnect) | None => {
let _ = writer.shutdown().await;
shared.shut_down(DisconnectReason::Requested);
return;
}
},
() = sleep_until_some(messages_ready_at) => match messages.pop() {
Some(frame) => frame,
None => continue,
},
() = sleep_until_some(plugin_data_ready_at) => match plugin_data.pop() {
Some(frame) => frame,
None => continue,
},
_ = ping.tick() => {
Frame::from_message(MessageType::Ping, &shared.ping().message())
}
};
buf.clear();
frame.encode(&mut buf);
if let Err(err) = async {
writer.write_all(&buf).await?;
writer.flush().await
}
.await
{
tracing::warn!(error = %err, "control channel write failed");
shared.shut_down(DisconnectReason::Error(err.to_string()));
return;
}
}
}
struct Paced {
bucket: LeakyBucket,
queue: VecDeque<Frame>,
}
impl Paced {
fn new(limit: Limit, now: tokio::time::Instant) -> Self {
Self {
bucket: LeakyBucket::new(limit, now),
queue: VecDeque::new(),
}
}
fn ready_at(&self, now: tokio::time::Instant) -> Option<tokio::time::Instant> {
(!self.queue.is_empty()).then(|| now + self.bucket.wait_time(now))
}
fn pop(&mut self) -> Option<Frame> {
let frame = self.queue.pop_front()?;
self.bucket.take(tokio::time::Instant::now());
Some(frame)
}
}
async fn sleep_until_some(at: Option<tokio::time::Instant>) {
match at {
Some(at) => tokio::time::sleep_until(at).await,
None => std::future::pending().await,
}
}
async fn read_loop(
mut reader: ReadHalf<TlsStream<TcpStream>>,
mut buf: BytesMut,
mut dispatcher: Dispatcher,
writer: mpsc::WeakUnboundedSender<Outgoing>,
) {
let shared = dispatcher.shared.clone();
let reason = 'outer: loop {
loop {
match framing::decode(&mut buf) {
Ok(Some(frame)) => {
if let Control::Kicked(reason) = dispatcher.handle(frame) {
break 'outer reason;
}
}
Ok(None) => break,
Err(err) => break 'outer DisconnectReason::Error(err.to_string()),
}
}
tokio::select! {
read = tokio::time::timeout(READ_TIMEOUT, reader.read_buf(&mut buf)) => match read {
Err(_) => break DisconnectReason::Timeout,
Ok(Ok(0)) => break DisconnectReason::Closed,
Ok(Ok(_)) => {}
Ok(Err(err)) => break DisconnectReason::Error(err.to_string()),
},
() = shared.shutdown.notified() => {
let stored = shared.reason.lock().unwrap_or_else(|p| p.into_inner()).take();
break stored.unwrap_or(DisconnectReason::Requested);
}
}
};
shared.connected.store(false, Ordering::Release);
if let Some(tx) = writer.upgrade() {
let _ = tx.send(Outgoing::Disconnect);
}
tracing::info!(?reason, "control channel closed");
let _ = dispatcher.events.send(Event::Disconnected(reason));
}
enum Control {
Continue,
Synced,
Rejected(RejectReason, String),
Kicked(DisconnectReason),
}
struct Dispatcher {
shared: Arc<Shared>,
events: mpsc::UnboundedSender<Event>,
synced: bool,
}
fn decode<M: Message + Default>(kind: &'static str, payload: Bytes) -> Result<M, ProtocolError> {
M::decode(payload).map_err(|source| ProtocolError::Decode { kind, source })
}
impl Dispatcher {
fn emit(&self, event: Event) {
let _ = self.events.send(event);
}
fn emit_synced(&self, event: Event) {
if self.synced {
self.emit(event);
}
}
fn handle(&mut self, frame: Frame) -> Control {
let Some(kind) = frame.message_type() else {
tracing::debug!(kind = frame.kind, "ignoring unknown control message");
return Control::Continue;
};
match self.handle_known(kind, frame.payload) {
Ok(control) => control,
Err(err) => {
tracing::warn!(error = %err, "skipping malformed control message");
Control::Continue
}
}
}
fn handle_known(
&mut self,
kind: MessageType,
payload: Bytes,
) -> Result<Control, ProtocolError> {
let shared = self.shared.clone();
match kind {
MessageType::UdpTunnel => self.emit(Event::UdpTunnel(payload)),
MessageType::Version => {
let m: proto::Version = decode("Version", payload)?;
let mut state = shared.state();
state.server.version = MumbleVersion::from_wire(m.version_v1, m.version_v2);
state.server.release = m.release;
state.server.os = m.os;
}
MessageType::Reject => {
let m: proto::Reject = decode("Reject", payload)?;
let reason = RejectReason::from_wire(m.r#type);
return Ok(Control::Rejected(reason, m.reason.unwrap_or_default()));
}
MessageType::ServerSync => {
let m: proto::ServerSync = decode("ServerSync", payload)?;
shared.state().apply_server_sync(&m);
self.synced = true;
return Ok(Control::Synced);
}
MessageType::ChannelState => {
let m: proto::ChannelState = decode("ChannelState", payload)?;
let channel = shared.state().apply_channel_state(m).cloned();
if let Some(channel) = channel {
self.emit_synced(Event::ChannelUpdated(channel));
}
}
MessageType::ChannelRemove => {
let m: proto::ChannelRemove = decode("ChannelRemove", payload)?;
let removed = shared.state().apply_channel_remove(m.channel_id);
if let Some(channel) = removed {
self.emit_synced(Event::ChannelRemoved(channel));
}
}
MessageType::UserState => {
let m: proto::UserState = decode("UserState", payload)?;
let change = shared.state().apply_user_state(m);
if let Some(change) = change {
self.emit_synced(Event::UserUpdated(change));
}
}
MessageType::UserRemove => {
let m: proto::UserRemove = decode("UserRemove", payload)?;
let ban = m.ban.unwrap_or(false);
let (removed, own) = {
let mut state = shared.state();
(
state.apply_user_remove(m.session),
state.session == Some(m.session),
)
};
if own {
return Ok(Control::Kicked(DisconnectReason::Kicked {
actor: m.actor,
reason: m.reason,
ban,
}));
}
if let Some(user) = removed {
self.emit_synced(Event::UserRemoved {
user,
actor: m.actor,
reason: m.reason,
ban,
});
}
}
MessageType::TextMessage => {
let m: proto::TextMessage = decode("TextMessage", payload)?;
self.emit(Event::TextMessage(TextMessage {
actor: m.actor,
target: TextTarget {
sessions: m.session,
channels: m.channel_id,
trees: m.tree_id,
},
message: m.message,
}));
}
MessageType::PluginDataTransmission => {
let m: proto::PluginDataTransmission = decode("PluginDataTransmission", payload)?;
self.emit(Event::PluginData(PluginData {
sender: m.sender_session,
data_id: m.data_id.unwrap_or_default(),
data: m.data.map(Bytes::from).unwrap_or_default(),
}));
}
MessageType::UserStats => {
let m: proto::UserStats = decode("UserStats", payload)?;
self.emit(Event::UserStats(Box::new(m)));
}
MessageType::PermissionDenied => {
let m: proto::PermissionDenied = decode("PermissionDenied", payload)?;
self.emit(Event::PermissionDenied(m.into()));
}
MessageType::CryptSetup => {
let m: proto::CryptSetup = decode("CryptSetup", payload)?;
let crypt = CryptSetup::from(m);
{
let mut state = shared.state();
match (&mut state.crypt, crypt.key.is_some()) {
(Some(current), false) => {
if crypt.server_nonce.is_some() {
current.server_nonce.clone_from(&crypt.server_nonce);
}
}
(slot, _) => *slot = Some(crypt.clone()),
}
}
self.emit(Event::CryptSetup(crypt));
}
MessageType::PermissionQuery => {
let m: proto::PermissionQuery = decode("PermissionQuery", payload)?;
shared.state().apply_permission_query(&m);
self.emit_synced(Event::PermissionsUpdated {
channel_id: m.channel_id,
});
}
MessageType::ServerConfig => {
let m: proto::ServerConfig = decode("ServerConfig", payload)?;
let info = {
let mut state = shared.state();
state.apply_server_config(m);
state.server.clone()
};
self.emit_synced(Event::ServerConfigUpdated(info));
}
MessageType::Ping => {
let m: proto::Ping = decode("Ping", payload)?;
if let Some(sent) = m.timestamp {
self.shared.ping().on_reply(sent);
}
}
_ => tracing::trace!(?kind, "ignoring control message"),
}
Ok(Control::Continue)
}
}
impl Handle {
fn send(&self, frame: Frame) -> Result<(), CommandError> {
if !self.is_connected() {
return Err(CommandError::NotConnected);
}
self.tx
.send(Outgoing::Frame(frame))
.map_err(|_| CommandError::NotConnected)
}
fn close(&self) {
let _ = self.tx.send(Outgoing::Disconnect);
}
fn send_user_state(&self, mut m: proto::UserState) -> Result<(), CommandError> {
m.session = self.shared.state().session;
self.send(Frame::from_message(MessageType::UserState, &m))
}
fn require(&self, feature: &'static str, required: MumbleVersion) -> Result<(), CommandError> {
let server = self.server_version().unwrap_or(MumbleVersion::new(0, 0, 0));
if server >= required {
Ok(())
} else {
Err(CommandError::Unsupported {
feature,
required,
server,
})
}
}
pub fn is_connected(&self) -> bool {
self.shared.connected.load(Ordering::Acquire)
}
pub fn state(&self) -> ServerState {
self.shared.state().clone()
}
pub fn with_state<R>(&self, f: impl FnOnce(&ServerState) -> R) -> R {
f(&self.shared.state())
}
pub fn server_version(&self) -> Option<MumbleVersion> {
self.shared.state().server.version
}
pub fn join_channel(&self, channel_id: u32) -> Result<(), CommandError> {
self.send_user_state(proto::UserState {
channel_id: Some(channel_id),
..Default::default()
})
}
pub fn set_self_mute(&self, mute: bool) -> Result<(), CommandError> {
self.send_user_state(proto::UserState {
self_mute: Some(mute),
..Default::default()
})
}
pub fn set_self_deaf(&self, deaf: bool) -> Result<(), CommandError> {
let mut m = proto::UserState {
self_deaf: Some(deaf),
..Default::default()
};
if deaf {
m.self_mute = Some(true);
}
self.send_user_state(m)
}
pub fn listen(&self, channels: &[u32]) -> Result<(), CommandError> {
self.require("listening", MumbleVersion::LISTENING)?;
self.send_user_state(proto::UserState {
listening_channel_add: channels.to_vec(),
..Default::default()
})
}
pub fn unlisten(&self, channels: &[u32]) -> Result<(), CommandError> {
self.require("listening", MumbleVersion::LISTENING)?;
self.send_user_state(proto::UserState {
listening_channel_remove: channels.to_vec(),
..Default::default()
})
}
pub fn set_listener_volume(&self, channel_id: u32, volume: f32) -> Result<(), CommandError> {
self.require("listener volume", MumbleVersion::LISTENER_VOLUME)?;
self.send_user_state(proto::UserState {
listening_volume_adjustment: vec![proto::user_state::VolumeAdjustment {
listening_channel: Some(channel_id),
volume_adjustment: Some(volume),
}],
..Default::default()
})
}
pub fn set_texture(&self, image: Vec<u8>) -> Result<(), CommandError> {
self.send_user_state(proto::UserState {
texture: Some(image),
..Default::default()
})
}
pub fn set_plugin_context(
&self,
context: Vec<u8>,
identity: String,
) -> Result<(), CommandError> {
self.send_user_state(proto::UserState {
plugin_context: Some(context),
plugin_identity: Some(identity),
..Default::default()
})
}
pub fn set_comment(&self, comment: String) -> Result<(), CommandError> {
self.send_user_state(proto::UserState {
comment: Some(comment),
..Default::default()
})
}
pub fn register_self(&self) -> Result<(), CommandError> {
self.send_user_state(proto::UserState {
user_id: Some(0),
..Default::default()
})
}
pub fn send_text(&self, target: &TextTarget, message: &str) -> Result<(), CommandError> {
let m = proto::TextMessage {
actor: None,
session: target.sessions.clone(),
channel_id: target.channels.clone(),
tree_id: target.trees.clone(),
message: message.to_owned(),
};
self.send(Frame::from_message(MessageType::TextMessage, &m))
}
pub fn send_plugin_data(
&self,
receivers: &[u32],
data_id: &str,
data: Vec<u8>,
) -> Result<(), CommandError> {
self.require("plugin data", MumbleVersion::PLUGIN_DATA)?;
let m = proto::PluginDataTransmission {
sender_session: self.shared.state().session,
receiver_sessions: receivers.to_vec(),
data: Some(data),
data_id: Some(data_id.to_owned()),
};
self.send(Frame::from_message(MessageType::PluginDataTransmission, &m))
}
pub fn request_blobs(
&self,
textures: &[u32],
comments: &[u32],
channel_descriptions: &[u32],
) -> Result<(), CommandError> {
let m = proto::RequestBlob {
session_texture: textures.to_vec(),
session_comment: comments.to_vec(),
channel_description: channel_descriptions.to_vec(),
};
self.send(Frame::from_message(MessageType::RequestBlob, &m))
}
pub fn request_user_stats(&self, session: u32, stats_only: bool) -> Result<(), CommandError> {
let m = proto::UserStats {
session: Some(session),
stats_only: Some(stats_only),
..Default::default()
};
self.send(Frame::from_message(MessageType::UserStats, &m))
}
pub fn report_voice_link(&self, ping: Option<UdpPing>, crypt: CryptStats) {
let mut report = self.shared.ping();
report.udp = ping;
report.crypt = crypt;
}
pub fn set_access_tokens(&self, tokens: Vec<String>) -> Result<(), CommandError> {
self.send(Frame::from_message(
MessageType::Authenticate,
&access_tokens_message(tokens),
))
}
pub fn query_permissions(&self, channel_id: u32) -> Result<(), CommandError> {
let m = proto::PermissionQuery {
channel_id: Some(channel_id),
..Default::default()
};
self.send(Frame::from_message(MessageType::PermissionQuery, &m))
}
pub fn send_acl(&self, acl: proto::Acl) -> Result<(), CommandError> {
self.send(Frame::from_message(MessageType::Acl, &acl))
}
pub fn send_admin_user_state(&self, m: proto::UserState) -> Result<(), CommandError> {
self.send(Frame::from_message(MessageType::UserState, &m))
}
pub fn send_channel_state(&self, state: proto::ChannelState) -> Result<(), CommandError> {
self.send(Frame::from_message(MessageType::ChannelState, &state))
}
pub fn send_tunnel(&self, packet: Bytes) -> Result<(), CommandError> {
self.send(Frame {
kind: MessageType::UdpTunnel as u16,
payload: packet,
})
}
pub fn send_crypt_setup(&self, crypt: CryptSetup) -> Result<(), CommandError> {
let m = proto::CryptSetup::from(crypt);
self.send(Frame::from_message(MessageType::CryptSetup, &m))
}
pub fn disconnect(&self) {
self.close();
}
}
#[cfg(test)]
mod tests {
use prost::Message;
use super::*;
#[test]
fn pings_report_the_own_tcp_and_udp_figures() {
let mut report = PingReport {
started: Instant::now() - Duration::from_secs(1),
..Default::default()
};
assert_eq!(report.message().tcp_ping_avg, None);
let now = report.now_us();
report.on_reply(now - 30_000);
report.on_reply(now - 10_000);
report.udp = Some(UdpPing {
avg_ms: 12.5,
packets: 40,
});
let m = report.message();
assert_eq!(m.tcp_packets, Some(2));
let avg = m.tcp_ping_avg.expect("tcp ping");
assert!((20.0..21.0).contains(&avg), "{avg}");
assert!(m.tcp_ping_var.expect("var") > 90.0);
assert_eq!((m.udp_packets, m.udp_ping_avg), (Some(40), Some(12.5)));
assert_eq!(m.good, Some(0));
}
#[test]
fn send_acl_queues_an_acl_frame() {
let (tx, mut rx) = mpsc::unbounded_channel();
let handle = Handle {
tx,
shared: Arc::new(Shared {
state: Mutex::new(ServerState::default()),
shutdown: Notify::new(),
reason: Mutex::new(None),
connected: AtomicBool::new(true),
ping: Mutex::default(),
}),
};
let acl = proto::Acl {
channel_id: 3,
inherit_acls: Some(true),
..Default::default()
};
handle.send_acl(acl.clone()).expect("queued");
let Ok(Outgoing::Frame(frame)) = rx.try_recv() else {
panic!("expected a frame");
};
assert_eq!(frame.kind, MessageType::Acl as u16);
assert_eq!(proto::Acl::decode(frame.payload).expect("decode"), acl);
}
#[test]
fn plugin_data_needs_a_1_4_server_and_names_the_receivers() {
let (tx, mut rx) = mpsc::unbounded_channel();
let state = ServerState {
session: Some(7),
..Default::default()
};
let handle = Handle {
tx,
shared: Arc::new(Shared {
state: Mutex::new(state),
shutdown: Notify::new(),
reason: Mutex::new(None),
connected: AtomicBool::new(true),
ping: Mutex::default(),
}),
};
assert!(matches!(
handle.send_plugin_data(&[3], "id", vec![1]),
Err(CommandError::Unsupported { .. })
));
handle.shared.state().server.version = Some(MumbleVersion::new(1, 4, 0));
handle
.send_plugin_data(&[3, 4], "mumcord.test", b"{}".to_vec())
.expect("queued");
let Ok(Outgoing::Frame(frame)) = rx.try_recv() else {
panic!("expected a frame");
};
assert_eq!(frame.kind, MessageType::PluginDataTransmission as u16);
let m = proto::PluginDataTransmission::decode(frame.payload).expect("decode");
assert_eq!(m.sender_session, Some(7));
assert_eq!(m.receiver_sessions, vec![3, 4]);
assert_eq!(m.data_id.as_deref(), Some("mumcord.test"));
assert_eq!(m.data.as_deref(), Some(&b"{}"[..]));
}
}
#[cfg(test)]
mod handshake_tests {
use super::*;
#[test]
fn version_precedes_authenticate_with_both_encodings() {
let [version, auth] = handshake_frames("alice".to_owned(), None, vec!["secret".into()]);
assert_eq!(version.message_type(), Some(MessageType::Version));
assert_eq!(auth.message_type(), Some(MessageType::Authenticate));
let v = proto::Version::decode(version.payload).expect("version");
assert_eq!(v.version_v2, Some(0x0001_0005_0000_0000));
assert_eq!(v.version_v1, Some(0x0001_0500));
assert!(
MumbleVersion::from_wire(v.version_v1, v.version_v2)
.is_some_and(|v| v.supports_listening())
);
assert!(v.release.is_some_and(|r| r.starts_with("Mumcord ")));
let a = proto::Authenticate::decode(auth.payload).expect("authenticate");
assert_eq!(a.username.as_deref(), Some("alice"));
assert_eq!(a.opus, Some(true));
assert_eq!(a.tokens, ["secret"]);
}
#[test]
fn access_tokens_resend_carries_only_the_tokens() {
let frame = Frame::from_message(
MessageType::Authenticate,
&access_tokens_message(vec!["one".into(), "two".into()]),
);
let a = proto::Authenticate::decode(frame.payload).expect("authenticate");
assert_eq!(a.tokens, ["one", "two"]);
assert_eq!(a.username, None);
assert_eq!(a.password, None);
}
}