Skip to main content

mumcord_protocol/
state.rs

1//! The client's view of the server: channel tree, connected users and server settings, built
2//! from the deltas in `ChannelState`/`UserState`/`*Remove` messages.
3
4use std::collections::{BTreeMap, BTreeSet};
5
6use crate::proto;
7use crate::version::MumbleVersion;
8
9/// One channel. `parent` is `None` only for the root channel (id 0).
10#[derive(Debug, Clone, Default, PartialEq)]
11pub struct Channel {
12    pub id: u32,
13    pub parent: Option<u32>,
14    pub name: String,
15    /// HTML description (untrusted). May be absent while only `description_hash` is known;
16    /// request it with `Handle::request_blobs`.
17    pub description: Option<String>,
18    pub description_hash: Option<Vec<u8>>,
19    pub links: BTreeSet<u32>,
20    pub temporary: bool,
21    pub position: i32,
22    pub max_users: Option<u32>,
23    pub is_enter_restricted: bool,
24    /// Whether this user may enter (only sent for enter-restricted channels).
25    pub can_enter: Option<bool>,
26}
27
28/// One connected user, keyed by session id.
29#[derive(Debug, Clone, Default, PartialEq)]
30pub struct User {
31    pub session: u32,
32    pub name: String,
33    /// Registration id; `None` for unregistered users.
34    pub user_id: Option<u32>,
35    pub channel_id: u32,
36    /// Server-side mute/deafen by an admin.
37    pub mute: bool,
38    pub deaf: bool,
39    pub suppress: bool,
40    pub self_mute: bool,
41    pub self_deaf: bool,
42    pub priority_speaker: bool,
43    pub recording: bool,
44    /// HTML comment (untrusted); may be absent while only `comment_hash` is known.
45    pub comment: Option<String>,
46    pub comment_hash: Option<Vec<u8>>,
47    /// Avatar image bytes (untrusted); may be absent while only `texture_hash` is known.
48    pub texture: Option<Vec<u8>>,
49    pub texture_hash: Option<Vec<u8>>,
50    /// SHA-1 hex of the user's certificate; `None` for users without one.
51    pub cert_hash: Option<String>,
52    /// Channels this user listens to (follows).
53    pub listening: BTreeSet<u32>,
54    /// Listener volume factors per channel (1.0 = unchanged). Servers only send these for the
55    /// own user.
56    pub listening_volume: BTreeMap<u32, f32>,
57}
58
59/// UDP crypto parameters, passed through untouched for the voice transport.
60#[derive(Clone, Default, PartialEq, Eq)]
61pub struct CryptSetup {
62    pub key: Option<Vec<u8>>,
63    pub client_nonce: Option<Vec<u8>>,
64    pub server_nonce: Option<Vec<u8>>,
65}
66
67impl std::fmt::Debug for CryptSetup {
68    // Key material stays out of logs.
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        f.debug_struct("CryptSetup")
71            .field("has_key", &self.key.is_some())
72            .field("has_client_nonce", &self.client_nonce.is_some())
73            .field("has_server_nonce", &self.server_nonce.is_some())
74            .finish()
75    }
76}
77
78impl From<proto::CryptSetup> for CryptSetup {
79    fn from(m: proto::CryptSetup) -> Self {
80        Self {
81            key: m.key,
82            client_nonce: m.client_nonce,
83            server_nonce: m.server_nonce,
84        }
85    }
86}
87
88impl From<CryptSetup> for proto::CryptSetup {
89    fn from(c: CryptSetup) -> Self {
90        Self {
91            key: c.key,
92            client_nonce: c.client_nonce,
93            server_nonce: c.server_nonce,
94        }
95    }
96}
97
98/// Server facts gathered from `Version`, `ServerSync` and `ServerConfig`.
99#[derive(Debug, Clone, Default, PartialEq)]
100pub struct ServerInfo {
101    pub version: Option<MumbleVersion>,
102    pub release: Option<String>,
103    pub os: Option<String>,
104    pub max_bandwidth: Option<u32>,
105    /// Welcome text HTML (untrusted).
106    pub welcome_text: Option<String>,
107    pub allow_html: Option<bool>,
108    /// Maximum text message length in characters (0 = unlimited).
109    pub message_length: Option<u32>,
110    /// Maximum length of a message containing images (0 = unlimited).
111    pub image_message_length: Option<u32>,
112    pub max_users: Option<u32>,
113    pub recording_allowed: Option<bool>,
114}
115
116impl ServerInfo {
117    /// Whether the server supports following channels. Unknown version counts as unsupported.
118    pub fn supports_listening(&self) -> bool {
119        self.version.is_some_and(MumbleVersion::supports_listening)
120    }
121
122    pub fn supports_listener_volume(&self) -> bool {
123        self.version
124            .is_some_and(MumbleVersion::supports_listener_volume)
125    }
126}
127
128/// Everything known about the connected server.
129#[derive(Debug, Clone, Default, PartialEq)]
130pub struct ServerState {
131    pub channels: BTreeMap<u32, Channel>,
132    pub users: BTreeMap<u32, User>,
133    /// Own session id, known after `ServerSync`.
134    pub session: Option<u32>,
135    pub server: ServerInfo,
136    /// Latest UDP crypto parameters from the server.
137    pub crypt: Option<CryptSetup>,
138    /// Effective permissions per channel, see [`crate::permissions`].
139    pub permissions: BTreeMap<u32, u32>,
140}
141
142/// What a `UserState` did to the user list.
143#[derive(Debug, Clone, PartialEq)]
144pub struct UserChange {
145    pub user: User,
146    /// `true` if this session was not known before.
147    pub joined: bool,
148    /// Channel before the update, if the user moved.
149    pub moved_from: Option<u32>,
150    /// Session that caused the change (e.g. an admin moving someone).
151    pub actor: Option<u32>,
152    /// The admin rights ([`crate::permissions`]) whose fields this message carried: MuteDeafen
153    /// for mute, deafen, suppress and priority speaker, Move for the channel, Register for the
154    /// user id. An open admin action is only confirmed by an echo of the field it changed.
155    pub echoed_rights: u32,
156}
157
158/// The admin rights whose fields a `UserState` carries (see [`UserChange::echoed_rights`]).
159fn echoed_rights(m: &proto::UserState) -> u32 {
160    use crate::permissions::{MOVE, MUTE_DEAFEN, REGISTER};
161    let mut rights = 0;
162    if m.mute.is_some() || m.deaf.is_some() || m.suppress.is_some() || m.priority_speaker.is_some()
163    {
164        rights |= MUTE_DEAFEN;
165    }
166    if m.channel_id.is_some() {
167        rights |= MOVE;
168    }
169    if m.user_id.is_some() {
170        rights |= REGISTER;
171    }
172    rights
173}
174
175impl ServerState {
176    /// The own user, once the session is known.
177    pub fn own_user(&self) -> Option<&User> {
178        self.session.and_then(|s| self.users.get(&s))
179    }
180
181    /// Direct children of a channel, sorted by position then name (Mumble's order).
182    pub fn children(&self, parent: u32) -> Vec<&Channel> {
183        let mut out: Vec<&Channel> = self
184            .channels
185            .values()
186            .filter(|c| c.parent == Some(parent))
187            .collect();
188        out.sort_by(|a, b| {
189            a.position
190                .cmp(&b.position)
191                .then_with(|| a.name.cmp(&b.name))
192        });
193        out
194    }
195
196    /// Users whose voice channel is `channel`.
197    pub fn users_in(&self, channel: u32) -> impl Iterator<Item = &User> {
198        self.users.values().filter(move |u| u.channel_id == channel)
199    }
200
201    /// Users listening to `channel`.
202    pub fn listeners_of(&self, channel: u32) -> impl Iterator<Item = &User> {
203        self.users
204            .values()
205            .filter(move |u| u.listening.contains(&channel))
206    }
207
208    /// Finds a direct child channel by name.
209    pub fn child_by_name(&self, parent: u32, name: &str) -> Option<&Channel> {
210        self.channels
211            .values()
212            .find(|c| c.parent == Some(parent) && c.name == name)
213    }
214
215    /// `id` and then its parents up to the root. A cyclic tree from a malicious server cannot
216    /// make this endless: no real chain is longer than the number of channels.
217    pub fn ancestry(&self, id: u32) -> impl Iterator<Item = &Channel> {
218        let mut next = Some(id);
219        std::iter::from_fn(move || {
220            let channel = self.channels.get(&next?)?;
221            next = channel.parent;
222            Some(channel)
223        })
224        .take(self.channels.len())
225    }
226
227    /// Whether `parent` is `id` itself or lies below it, so moving `id` there forms a loop.
228    fn would_loop(&self, id: u32, parent: u32) -> bool {
229        parent == id || self.ancestry(parent).any(|c| c.parent == Some(id))
230    }
231
232    pub fn apply_channel_state(&mut self, m: proto::ChannelState) -> Option<&Channel> {
233        let id = m.channel_id?;
234        let parent = match m.parent {
235            // The root has no parent; servers never send one for it.
236            Some(_) if id == 0 => Some(None),
237            Some(parent) if self.would_loop(id, parent) => {
238                tracing::warn!("ignored a channel parent that would form a loop");
239                None
240            }
241            parent => parent.map(Some),
242        };
243        let ch = self.channels.entry(id).or_insert_with(|| Channel {
244            id,
245            ..Default::default()
246        });
247        if let Some(parent) = parent {
248            ch.parent = parent;
249        }
250        if let Some(name) = m.name {
251            ch.name = name;
252        }
253        if let Some(hash) = m.description_hash {
254            // A new hash invalidates the cached text until it is requested again.
255            if ch.description_hash.as_ref() != Some(&hash) {
256                ch.description = None;
257            }
258            ch.description_hash = Some(hash);
259        }
260        if let Some(description) = m.description {
261            ch.description = Some(description);
262        }
263        if !m.links.is_empty() {
264            ch.links = m.links.into_iter().collect();
265        }
266        for l in m.links_add {
267            ch.links.insert(l);
268        }
269        for l in &m.links_remove {
270            ch.links.remove(l);
271        }
272        if let Some(t) = m.temporary {
273            ch.temporary = t;
274        }
275        if let Some(p) = m.position {
276            ch.position = p;
277        }
278        if let Some(max) = m.max_users {
279            ch.max_users = Some(max);
280        }
281        if let Some(r) = m.is_enter_restricted {
282            ch.is_enter_restricted = r;
283        }
284        if let Some(c) = m.can_enter {
285            ch.can_enter = Some(c);
286        }
287        self.channels.get(&id)
288    }
289
290    /// Removes a channel; returns it if it was known.
291    pub fn apply_channel_remove(&mut self, id: u32) -> Option<Channel> {
292        let removed = self.channels.remove(&id)?;
293        for ch in self.channels.values_mut() {
294            ch.links.remove(&id);
295        }
296        for user in self.users.values_mut() {
297            user.listening.remove(&id);
298            user.listening_volume.remove(&id);
299        }
300        self.permissions.remove(&id);
301        Some(removed)
302    }
303
304    pub fn apply_user_state(&mut self, m: proto::UserState) -> Option<UserChange> {
305        let session = m.session?;
306        let joined = !self.users.contains_key(&session);
307        let echoed_rights = echoed_rights(&m);
308        let user = self.users.entry(session).or_insert_with(|| User {
309            session,
310            ..Default::default()
311        });
312        let mut moved_from = None;
313        if let Some(name) = m.name {
314            user.name = name;
315        }
316        if let Some(id) = m.user_id {
317            // Mumble uses u32::MAX ("-1") to clear a registration.
318            user.user_id = (id != u32::MAX).then_some(id);
319        }
320        if let Some(c) = m.channel_id {
321            if !joined && user.channel_id != c {
322                moved_from = Some(user.channel_id);
323            }
324            user.channel_id = c;
325        }
326        set(&mut user.mute, m.mute);
327        set(&mut user.deaf, m.deaf);
328        set(&mut user.suppress, m.suppress);
329        set(&mut user.self_mute, m.self_mute);
330        set(&mut user.self_deaf, m.self_deaf);
331        set(&mut user.priority_speaker, m.priority_speaker);
332        set(&mut user.recording, m.recording);
333        // Hashes first: a changed hash drops the cached blob unless the blob itself is included.
334        if let Some(hash) = m.comment_hash {
335            if user.comment_hash.as_ref() != Some(&hash) {
336                user.comment = None;
337            }
338            user.comment_hash = Some(hash);
339        }
340        if let Some(comment) = m.comment {
341            // An empty comment means "removed".
342            user.comment = (!comment.is_empty()).then_some(comment);
343            if user.comment.is_none() {
344                user.comment_hash = None;
345            }
346        }
347        if let Some(hash) = m.texture_hash {
348            if user.texture_hash.as_ref() != Some(&hash) {
349                user.texture = None;
350            }
351            user.texture_hash = Some(hash);
352        }
353        if let Some(texture) = m.texture {
354            user.texture = (!texture.is_empty()).then_some(texture);
355            if user.texture.is_none() {
356                user.texture_hash = None;
357            }
358        }
359        if let Some(hash) = m.hash {
360            user.cert_hash = (!hash.is_empty()).then_some(hash);
361        }
362        for c in m.listening_channel_add {
363            user.listening.insert(c);
364        }
365        for c in &m.listening_channel_remove {
366            user.listening.remove(c);
367            user.listening_volume.remove(c);
368        }
369        for adj in m.listening_volume_adjustment {
370            if let (Some(c), Some(v)) = (adj.listening_channel, adj.volume_adjustment) {
371                user.listening_volume.insert(c, v);
372            }
373        }
374        Some(UserChange {
375            user: user.clone(),
376            joined,
377            moved_from,
378            actor: m.actor,
379            echoed_rights,
380        })
381    }
382
383    pub fn apply_user_remove(&mut self, session: u32) -> Option<User> {
384        self.users.remove(&session)
385    }
386
387    pub(crate) fn apply_server_sync(&mut self, m: &proto::ServerSync) {
388        if let Some(s) = m.session {
389            self.session = Some(s);
390        }
391        if m.max_bandwidth.is_some() {
392            self.server.max_bandwidth = m.max_bandwidth;
393        }
394        if m.welcome_text.is_some() {
395            self.server.welcome_text.clone_from(&m.welcome_text);
396        }
397        if let Some(p) = m.permissions {
398            // ServerSync carries the root channel's permissions (truncated to 32 bits on the wire).
399            self.permissions.insert(0, p as u32);
400        }
401    }
402
403    pub(crate) fn apply_server_config(&mut self, m: proto::ServerConfig) {
404        let s = &mut self.server;
405        if m.max_bandwidth.is_some() {
406            s.max_bandwidth = m.max_bandwidth;
407        }
408        if m.welcome_text.is_some() {
409            s.welcome_text = m.welcome_text;
410        }
411        if m.allow_html.is_some() {
412            s.allow_html = m.allow_html;
413        }
414        if m.message_length.is_some() {
415            s.message_length = m.message_length;
416        }
417        if m.image_message_length.is_some() {
418            s.image_message_length = m.image_message_length;
419        }
420        if m.max_users.is_some() {
421            s.max_users = m.max_users;
422        }
423        if m.recording_allowed.is_some() {
424            s.recording_allowed = m.recording_allowed;
425        }
426    }
427
428    pub(crate) fn apply_permission_query(&mut self, m: &proto::PermissionQuery) {
429        if m.flush == Some(true) {
430            self.permissions.clear();
431        }
432        if let (Some(c), Some(p)) = (m.channel_id, m.permissions) {
433            self.permissions.insert(c, p);
434        }
435    }
436}
437
438fn set(target: &mut bool, value: Option<bool>) {
439    if let Some(v) = value {
440        *target = v;
441    }
442}
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447
448    fn channel(id: u32, parent: Option<u32>, name: &str) -> proto::ChannelState {
449        proto::ChannelState {
450            channel_id: Some(id),
451            parent,
452            name: Some(name.into()),
453            ..Default::default()
454        }
455    }
456
457    fn tree() -> ServerState {
458        let mut s = ServerState::default();
459        s.apply_channel_state(channel(0, None, "Root"));
460        s.apply_channel_state(proto::ChannelState {
461            position: Some(1),
462            ..channel(1, Some(0), "Lobby")
463        });
464        s.apply_channel_state(channel(2, Some(0), "Raid"));
465        s.apply_channel_state(channel(3, Some(0), "AFK"));
466        s
467    }
468
469    #[test]
470    fn builds_tree_in_mumble_order() {
471        let s = tree();
472        let names: Vec<&str> = s.children(0).iter().map(|c| c.name.as_str()).collect();
473        assert_eq!(names, ["AFK", "Raid", "Lobby"]);
474        assert_eq!(s.channels[&0].parent, None);
475        assert_eq!(s.child_by_name(0, "Raid").map(|c| c.id), Some(2));
476    }
477
478    #[test]
479    fn a_channel_cannot_become_its_own_parent() {
480        let mut s = tree();
481        s.apply_channel_state(proto::ChannelState {
482            channel_id: Some(2),
483            parent: Some(2),
484            ..Default::default()
485        });
486        assert_eq!(s.channels[&2].parent, Some(0));
487    }
488
489    #[test]
490    fn a_parent_change_cannot_form_a_loop() {
491        let mut s = tree();
492        s.apply_channel_state(channel(4, Some(2), "Team"));
493        s.apply_channel_state(channel(5, Some(4), "Squad"));
494        // Raid under its own grandchild would cut it off from the root: A→B→A.
495        s.apply_channel_state(proto::ChannelState {
496            channel_id: Some(2),
497            parent: Some(5),
498            ..Default::default()
499        });
500        assert_eq!(s.channels[&2].parent, Some(0));
501        // Moving to an unrelated channel still works.
502        s.apply_channel_state(proto::ChannelState {
503            channel_id: Some(5),
504            parent: Some(3),
505            ..Default::default()
506        });
507        assert_eq!(s.channels[&5].parent, Some(3));
508    }
509
510    #[test]
511    fn a_loop_through_a_channel_not_yet_known_is_refused() {
512        let mut s = tree();
513        // The server announces 6 under 7 before 7 exists; 7 then claims 6 as its parent.
514        s.apply_channel_state(channel(6, Some(7), "Early"));
515        s.apply_channel_state(channel(7, Some(6), "Late"));
516        assert_eq!(s.channels[&7].parent, None);
517        assert_eq!(s.ancestry(6).count(), 2);
518    }
519
520    #[test]
521    fn ancestry_ends_on_a_cyclic_tree() {
522        let mut s = tree();
523        s.channels.get_mut(&2).expect("raid").parent = Some(2);
524        assert_eq!(s.ancestry(2).count(), s.channels.len());
525    }
526
527    #[test]
528    fn partial_channel_update_keeps_fields() {
529        let mut s = tree();
530        s.apply_channel_state(proto::ChannelState {
531            channel_id: Some(2),
532            description: Some("raid night".into()),
533            ..Default::default()
534        });
535        assert_eq!(s.channels[&2].name, "Raid");
536        assert_eq!(s.channels[&2].description.as_deref(), Some("raid night"));
537    }
538
539    #[test]
540    fn user_join_move_and_remove() {
541        let mut s = tree();
542        let c = s
543            .apply_user_state(proto::UserState {
544                session: Some(5),
545                name: Some("alice".into()),
546                channel_id: Some(1),
547                hash: Some("abc".into()),
548                ..Default::default()
549            })
550            .expect("change");
551        assert!(c.joined);
552        assert_eq!(c.moved_from, None);
553
554        let c = s
555            .apply_user_state(proto::UserState {
556                session: Some(5),
557                channel_id: Some(2),
558                self_mute: Some(true),
559                ..Default::default()
560            })
561            .expect("change");
562        assert!(!c.joined);
563        assert_eq!(c.moved_from, Some(1));
564        assert_eq!(c.user.name, "alice");
565        assert!(c.user.self_mute);
566        assert_eq!(c.user.cert_hash.as_deref(), Some("abc"));
567        assert_eq!(s.users_in(2).count(), 1);
568
569        assert!(s.apply_user_remove(5).is_some());
570        assert!(s.users.is_empty());
571    }
572
573    #[test]
574    fn listening_add_volume_remove() {
575        let mut s = tree();
576        s.apply_user_state(proto::UserState {
577            session: Some(1),
578            channel_id: Some(1),
579            listening_channel_add: vec![2, 3],
580            listening_volume_adjustment: vec![proto::user_state::VolumeAdjustment {
581                listening_channel: Some(2),
582                volume_adjustment: Some(0.0),
583            }],
584            ..Default::default()
585        });
586        assert_eq!(s.listeners_of(2).count(), 1);
587        assert_eq!(s.users[&1].listening_volume.get(&2), Some(&0.0));
588
589        s.apply_user_state(proto::UserState {
590            session: Some(1),
591            listening_channel_remove: vec![2],
592            ..Default::default()
593        });
594        assert_eq!(s.listeners_of(2).count(), 0);
595        assert!(s.users[&1].listening_volume.is_empty());
596
597        s.apply_channel_remove(3);
598        assert!(s.users[&1].listening.is_empty());
599        assert!(!s.channels.contains_key(&3));
600    }
601
602    #[test]
603    fn blobs_and_registration() {
604        let mut s = tree();
605        s.apply_user_state(proto::UserState {
606            session: Some(1),
607            comment: Some("hi".into()),
608            texture: Some(vec![1, 2]),
609            ..Default::default()
610        });
611        // A new hash invalidates the cached blob until it is requested again.
612        s.apply_user_state(proto::UserState {
613            session: Some(1),
614            comment_hash: Some(vec![9]),
615            texture: Some(vec![]),
616            user_id: Some(7),
617            ..Default::default()
618        });
619        let u = &s.users[&1];
620        assert_eq!(u.comment, None);
621        assert_eq!(u.texture, None);
622        assert_eq!(u.user_id, Some(7));
623    }
624
625    #[test]
626    fn sync_config_and_permissions() {
627        let mut s = ServerState::default();
628        s.apply_server_sync(&proto::ServerSync {
629            session: Some(3),
630            max_bandwidth: Some(72000),
631            welcome_text: Some("hello".into()),
632            permissions: Some(0x200),
633        });
634        s.apply_server_config(proto::ServerConfig {
635            message_length: Some(5000),
636            allow_html: Some(true),
637            ..Default::default()
638        });
639        s.apply_permission_query(&proto::PermissionQuery {
640            channel_id: Some(2),
641            permissions: Some(0x4),
642            flush: None,
643        });
644        assert_eq!(s.session, Some(3));
645        assert_eq!(s.server.max_bandwidth, Some(72000));
646        assert_eq!(s.server.message_length, Some(5000));
647        assert_eq!(s.permissions.get(&0), Some(&0x200));
648        assert_eq!(s.permissions.get(&2), Some(&0x4));
649        assert!(!s.server.supports_listening());
650    }
651}