Skip to main content

mumcord_protocol/
query.rs

1//! The public server query behind the `mumble://` link card (issue #175).
2//!
3//! A client can ask a Mumble server for its version, the users online and its user limit without
4//! connecting: the UDP ping that requests "extended information". Servers since 1.2.3 answer it
5//! when pings are allowed (murmur's default `allowPing=true`), and a server answers in the format
6//! the request was written in. Two formats are therefore used:
7//!
8//! - Protobuf (servers from 1.5): reports the version exactly.
9//! - Legacy (servers up to 1.4): the only format older servers know, and its version field cannot
10//!   hold a patch number above 255.
11//!
12//! The engine asks in the modern format and falls back to the legacy one, so old servers answer
13//! too. Everything that comes back is untrusted input.
14
15use prost::Message;
16
17use crate::udp_proto;
18use crate::version::MumbleVersion;
19
20/// Header byte of a protobuf UDP ping.
21const PROTO_PING: u8 = 1;
22/// Length of the legacy query: four zero bytes plus the value the server echoes.
23pub const LEGACY_QUERY_LEN: usize = 12;
24/// Length of the legacy answer (no header, five big-endian words plus the echoed value).
25pub const LEGACY_ANSWER_LEN: usize = 24;
26
27/// The query in the modern format. `echo` is a value the server sends back unchanged, which is
28/// how the answer is matched to the request.
29pub fn encode_query(echo: u64) -> Vec<u8> {
30    let message = udp_proto::Ping {
31        timestamp: echo,
32        request_extended_information: true,
33        ..Default::default()
34    };
35    let mut out = vec![PROTO_PING];
36    out.extend_from_slice(&message.encode_to_vec());
37    out
38}
39
40/// The query in the legacy format: the four zero bytes are the slot the server writes its version
41/// into, the eight following bytes carry `echo`.
42pub fn encode_legacy_query(echo: u64) -> Vec<u8> {
43    let mut out = Vec::with_capacity(LEGACY_QUERY_LEN);
44    out.extend_from_slice(&[0, 0, 0, 0]);
45    out.extend_from_slice(&echo.to_be_bytes());
46    out
47}
48
49/// What a server answered. `echo` is the value the query carried.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct QueryAnswer {
52    pub version: MumbleVersion,
53    pub users: u32,
54    pub max_users: u32,
55    pub echo: u64,
56}
57
58/// Reads an answer in either format; `None` when the packet is not a query answer (wrong length,
59/// undecodable message, or a plain connectivity ping without the figures).
60pub fn decode_answer(data: &[u8]) -> Option<QueryAnswer> {
61    if data.len() == LEGACY_ANSWER_LEN {
62        let word = |i: usize| u32::from_be_bytes([data[i], data[i + 1], data[i + 2], data[i + 3]]);
63        // An all-zero version means the server did not fill the field in.
64        let version = MumbleVersion::from_v1(word(0));
65        if version == MumbleVersion::new(0, 0, 0) {
66            return None;
67        }
68        return Some(QueryAnswer {
69            version,
70            users: word(12),
71            max_users: word(16),
72            echo: u64::from_be_bytes(data[4..12].try_into().ok()?),
73        });
74    }
75
76    let (&header, body) = data.split_first()?;
77    if header != PROTO_PING {
78        return None;
79    }
80    let message = udp_proto::Ping::decode(body).ok()?;
81    // A plain connectivity ping carries no figures; 0 means the field was left out.
82    if message.server_version_v2 == 0 {
83        return None;
84    }
85    Some(QueryAnswer {
86        version: MumbleVersion::from_v2(message.server_version_v2),
87        users: message.user_count,
88        max_users: message.max_user_count,
89        echo: message.timestamp,
90    })
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    fn legacy_answer(version: MumbleVersion, users: u32, max: u32, echo: u64) -> Vec<u8> {
98        let mut data = Vec::new();
99        data.extend_from_slice(&version.to_v1().to_be_bytes());
100        data.extend_from_slice(&echo.to_be_bytes());
101        for value in [users, max, 72_000] {
102            data.extend_from_slice(&value.to_be_bytes());
103        }
104        data
105    }
106
107    #[test]
108    fn legacy_query_is_the_extended_ping() {
109        let query = encode_legacy_query(0x1122_3344_5566_7788);
110        assert_eq!(query.len(), LEGACY_QUERY_LEN);
111        assert_eq!(&query[..4], &[0, 0, 0, 0]);
112        assert_eq!(
113            &query[4..],
114            &[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]
115        );
116    }
117
118    #[test]
119    fn protobuf_query_round_trips() {
120        let query = encode_query(0xDEAD_BEEF);
121        assert_eq!(query[0], PROTO_PING);
122        assert_eq!(
123            decode_answer(&query),
124            None,
125            "the query itself is not an answer"
126        );
127        // A server that answers the query puts the figures into the same message.
128        let mut answer = udp_proto::Ping {
129            timestamp: 0xDEAD_BEEF,
130            request_extended_information: true,
131            server_version_v2: MumbleVersion::new(1, 5, 735).to_v2(),
132            user_count: 7,
133            max_user_count: 50,
134            max_bandwidth_per_user: 72_000,
135        };
136        answer.request_extended_information = false;
137        let mut data = vec![PROTO_PING];
138        data.extend_from_slice(&answer.encode_to_vec());
139        assert_eq!(
140            decode_answer(&data),
141            Some(QueryAnswer {
142                version: MumbleVersion::new(1, 5, 735),
143                users: 7,
144                max_users: 50,
145                echo: 0xDEAD_BEEF
146            })
147        );
148    }
149
150    #[test]
151    fn reads_the_legacy_answer() {
152        let v = MumbleVersion::new(1, 4, 230);
153        assert_eq!(
154            decode_answer(&legacy_answer(v, 3, 100, 0xDEAD_BEEF)),
155            Some(QueryAnswer {
156                version: v,
157                users: 3,
158                max_users: 100,
159                echo: 0xDEAD_BEEF
160            })
161        );
162    }
163
164    #[test]
165    fn ignores_answers_that_are_not_figures() {
166        assert_eq!(decode_answer(&[0x20, 0x05]), None);
167        assert_eq!(decode_answer(&[]), None);
168        assert_eq!(decode_answer(&[0; LEGACY_ANSWER_LEN]), None);
169    }
170}