Skip to main content

ssq/
lib.rs

1//! Source Server Query (SSQ) client library.
2//!
3//! Implements the [Source A2S query protocol](https://developer.valvesoftware.com/wiki/Server_queries)
4//! for querying game servers running on the Source engine.
5//!
6//! # Quick start
7//!
8//! ```no_run
9//! use std::time::Duration;
10//! use ssq::Client;
11//!
12//! let client = Client::new(Duration::from_secs(5)).unwrap();
13//! let info = client.info("127.0.0.1:27015").unwrap();
14//! println!("{}: {}/{}", info.name, info.players, info.max_players);
15//! ```
16//!
17//! # Async
18//!
19//! Enable the `async` feature for a tokio-based async client at [`nonblocking::Client`].
20//!
21//! ```no_run
22//! # #[cfg(feature = "async")]
23//! # async fn example() {
24//! let client = ssq::nonblocking::Client::new().await.unwrap();
25//! let info = client.info("127.0.0.1:27015").await.unwrap();
26//! # }
27//! ```
28//!
29//! # Parsing raw response data
30//!
31//! If you already have raw response bytes (e.g. from a packet capture), you can
32//! parse them directly without a client:
33//!
34//! ```
35//! use ssq::info::Info;
36//! use ssq::players::Player;
37//! use ssq::rules::Rule;
38//! use ssq::DeOptions;
39//!
40//! # let info_bytes: &[u8] = &[0x49, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x64, 0x6c, 0x00, 0x01, 0x00];
41//! // let info = Info::from_reader(info_bytes).unwrap();
42//! // let players = Player::from_reader(player_bytes, &DeOptions::default()).unwrap();
43//! // let rules = Rule::from_reader(rules_bytes).unwrap();
44//! ```
45//!
46//! # Features
47//!
48//! - `async` -- tokio-based async client ([`nonblocking`] module)
49//! - `serialization` -- serde `Serialize`/`Deserialize` on all response types
50//! - `arma3` -- Arma 3 / DayZ server browser protocol parser ([`rules::arma3`])
51//! - `arbitrary` -- `arbitrary::Arbitrary` implementations for fuzzing
52
53/// Error types returned by query and parsing operations.
54pub mod errors;
55/// A2S_INFO query and response types.
56pub mod info;
57/// Async (tokio) client. Requires the `async` feature.
58#[cfg(feature = "async")]
59pub mod nonblocking;
60/// A2S_PLAYER query and response types.
61pub mod players;
62/// A2S_RULES query and response types.
63pub mod rules;
64
65use std::io::Cursor;
66use std::io::Read;
67use std::io::Write;
68use std::net::ToSocketAddrs;
69use std::net::UdpSocket;
70use std::ops::Deref;
71use std::time::Duration;
72
73use bstr::BString;
74use byteorder::LittleEndian;
75use byteorder::ReadBytesExt;
76use byteorder::WriteBytesExt;
77use bzip2::read::BzDecoder;
78use crc::crc32;
79
80use crate::errors::Error;
81use crate::errors::Result;
82
83pub(crate) const SINGLE_PACKET: i32 = -1;
84pub(crate) const MULTI_PACKET: i32 = -2;
85pub(crate) const MAX_CHALLENGE_RETRIES: usize = 3;
86
87/// Response header byte indicating a challenge response.
88pub const HEADER_CHALLENGE: u8 = b'A';
89/// Response header byte for A2S_INFO replies.
90pub const HEADER_INFO: u8 = b'I';
91/// Response header byte for A2S_PLAYER replies.
92pub const HEADER_PLAYER: u8 = b'D';
93/// Response header byte for A2S_RULES replies.
94pub const HEADER_RULES: u8 = b'E';
95
96// Offsets
97pub(crate) const OFS_HEADER: usize = 0;
98pub(crate) const OFS_SP_PAYLOAD: usize = 4;
99pub(crate) const OFS_MP_ID: usize = 4;
100pub(crate) const OFS_MP_SS_TOTAL: usize = 8;
101pub(crate) const OFS_MP_SS_NUMBER: usize = 9;
102pub(crate) const OFS_MP_SS_SIZE: usize = 10;
103pub(crate) const OFS_MP_SS_BZ2_SIZE: usize = 12;
104pub(crate) const OFS_MP_SS_BZ2_CRC: usize = 16;
105pub(crate) const OFS_MP_SS_PAYLOAD: usize = OFS_MP_SS_BZ2_SIZE;
106pub(crate) const OFS_MP_SS_PAYLOAD_BZ2: usize = OFS_MP_SS_BZ2_CRC + 4;
107
108macro_rules! read_buffer_offset {
109    ($buf:expr, $offset:expr, i8) => {
110        $buf[$offset].into()
111    };
112    ($buf:expr, $offset:expr, u8) => {
113        $buf[$offset].into()
114    };
115    ($buf:expr, $offset:expr, i16) => {
116        i16::from_le_bytes([$buf[$offset], $buf[$offset + 1]])
117    };
118    ($buf:expr, $offset:expr, u16) => {
119        u16::from_le_bytes([$buf[$offset], $buf[$offset + 1]])
120    };
121    ($buf:expr, $offset:expr, i32) => {
122        i32::from_le_bytes([
123            $buf[$offset],
124            $buf[$offset + 1],
125            $buf[$offset + 2],
126            $buf[$offset + 3],
127        ])
128    };
129    ($buf:expr, $offset:expr, u32) => {
130        u32::from_le_bytes([
131            $buf[$offset],
132            $buf[$offset + 1],
133            $buf[$offset + 2],
134            $buf[$offset + 3],
135        ])
136    };
137    ($buf:expr, $offset:expr, i64) => {
138        i64::from_le_bytes([
139            $buf[$offset],
140            $buf[$offset + 1],
141            $buf[$offset + 2],
142            $buf[$offset + 3],
143            $buf[$offset + 4],
144            $buf[$offset + 5],
145            $buf[$offset + 6],
146            $buf[$offset + 7],
147        ])
148    };
149    ($buf:expr, $offset:expr, u64) => {
150        u64::from_le_bytes([
151            $buf[$offset],
152            $buf[$offset + 1],
153            $buf[$offset + 2],
154            $buf[$offset + 3],
155            $buf[$offset + 4],
156            $buf[$offset + 5],
157            $buf[$offset + 6],
158            $buf[$offset + 7],
159        ])
160    };
161}
162
163#[cfg(feature = "async")]
164pub(crate) use read_buffer_offset;
165
166#[cfg(feature = "serde")]
167use serde::Deserialize;
168#[cfg(feature = "serde")]
169use serde::Serialize;
170
171/// A Steam Application ID.
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
173#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
174#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
175pub struct AppId(pub u16);
176
177impl AppId {
178    /// The Ship (2400)
179    pub const THE_SHIP: Self = Self(2400);
180}
181
182/// A 64-bit Steam ID identifying a user or server.
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
184#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
185#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
186pub struct SteamId(pub u64);
187
188/// A 64-bit Game ID. The low 24 bits contain a more accurate App ID
189/// than the 16-bit [`AppId`] field.
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
191#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
192#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
193pub struct GameId(pub u64);
194
195#[cfg(feature = "arbitrary")]
196pub(crate) fn arbitrary_bstring(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<BString> {
197    let bytes: Vec<u8> = arbitrary::Arbitrary::arbitrary(u)?;
198    Ok(BString::new(bytes))
199}
200
201#[cfg(feature = "arbitrary")]
202pub(crate) fn arbitrary_option_bstring(
203    u: &mut arbitrary::Unstructured<'_>,
204) -> arbitrary::Result<Option<BString>> {
205    if arbitrary::Arbitrary::arbitrary(u)? {
206        Ok(Some(arbitrary_bstring(u)?))
207    } else {
208        Ok(None)
209    }
210}
211
212/// Options that control deserialization of server responses.
213///
214/// Some games include extra fields in their responses (e.g. The Ship adds
215/// death/money fields to player data). Set the appropriate flags so the
216/// parser knows to expect them.
217///
218/// ```
219/// use ssq::DeOptions;
220/// use ssq::AppId;
221///
222/// // Automatically set options based on the game's app ID:
223/// let opts = DeOptions::from_app_id(AppId::THE_SHIP);
224/// assert!(opts.the_ship);
225///
226/// // Or construct directly:
227/// let opts = DeOptions { the_ship: true };
228/// ```
229#[derive(Debug, Clone, Default)]
230#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
231pub struct DeOptions {
232    /// When true, the player response parser reads extra The Ship fields
233    /// (deaths, money) for each player.
234    pub the_ship: bool,
235}
236
237impl DeOptions {
238    /// Build options from a game's [`AppId`]. Currently only sets `the_ship`
239    /// for The Ship (app ID 2400).
240    pub fn from_app_id(app_id: AppId) -> Self {
241        Self {
242            the_ship: app_id == AppId::THE_SHIP,
243        }
244    }
245}
246
247#[derive(Debug)]
248pub(crate) struct PacketFragment {
249    pub number: u8,
250    pub payload: Vec<u8>,
251}
252
253/// Blocking UDP client for sending A2S queries to Source game servers.
254///
255/// Handles single-packet and multi-packet responses, bzip2 decompression,
256/// and challenge-response negotiation.
257///
258/// ```no_run
259/// use std::time::Duration;
260/// use ssq::Client;
261///
262/// let client = Client::new(Duration::from_secs(5)).unwrap();
263///
264/// let info = client.info("127.0.0.1:27015").unwrap();
265/// println!("{}: {}/{}", info.name, info.players, info.max_players);
266///
267/// let players = client.players("127.0.0.1:27015").unwrap();
268/// for p in &players {
269///     println!("  {} (score: {}, connected: {:.0}s)", p.name, p.score, p.duration);
270/// }
271///
272/// let rules = client.rules("127.0.0.1:27015").unwrap();
273/// for r in &rules {
274///     println!("  {} = {}", r.name, r.value);
275/// }
276/// ```
277pub struct Client {
278    socket: UdpSocket,
279    max_size: usize,
280    pub(crate) de_options: DeOptions,
281}
282
283impl Client {
284    /// Create a new client with the given read/write timeout.
285    pub fn new(timeout: Duration) -> Result<Client> {
286        let socket = UdpSocket::bind("0.0.0.0:0")?;
287
288        socket.set_read_timeout(Some(timeout))?;
289        socket.set_write_timeout(Some(timeout))?;
290
291        Ok(Client {
292            socket,
293            max_size: 1400,
294            de_options: DeOptions::default(),
295        })
296    }
297
298    /// Set the maximum UDP packet size (default: 1400).
299    pub fn max_size(&mut self, size: usize) -> &mut Self {
300        self.max_size = size;
301        self
302    }
303
304    #[deprecated(since = "0.6.2", note = "use de_options")]
305    pub fn app_id(&mut self, app_id: AppId) -> &mut Self {
306        self.de_options = DeOptions::from_app_id(app_id);
307        self
308    }
309
310    /// Set deserialization options for parsing responses from specific games.
311    /// See [`DeOptions`] for details.
312    pub fn de_options(&mut self, de_options: DeOptions) -> &mut Self {
313        self.de_options = de_options;
314        self
315    }
316
317    /// Change the read/write timeout after construction.
318    pub fn set_timeout(&mut self, timeout: Duration) -> Result<&mut Self> {
319        self.socket.set_read_timeout(Some(timeout))?;
320        self.socket.set_write_timeout(Some(timeout))?;
321        Ok(self)
322    }
323
324    #[doc(hidden)]
325    pub fn send<A: ToSocketAddrs>(&self, payload: &[u8], addr: A) -> Result<Vec<u8>> {
326        self.socket.send_to(payload, addr)?;
327
328        let mut data = vec![0; self.max_size];
329
330        let read = self.socket.recv(&mut data)?;
331        data.truncate(read);
332
333        let header = read_buffer_offset!(&data, OFS_HEADER, i32);
334
335        if header == SINGLE_PACKET {
336            Ok(data[OFS_SP_PAYLOAD..].to_vec())
337        } else if header == MULTI_PACKET {
338            let id = read_buffer_offset!(&data, OFS_MP_ID, i32);
339            let total_packets: usize = data[OFS_MP_SS_TOTAL].into();
340            let switching_size: usize = read_buffer_offset!(&data, OFS_MP_SS_SIZE, u16).into();
341
342            if (switching_size > self.max_size) || (total_packets > 32) {
343                return Err(Error::MultiPacketTooLarge);
344            }
345
346            let mut packets: Vec<PacketFragment> = Vec::with_capacity(0);
347            packets.try_reserve(total_packets)?;
348            packets.push(PacketFragment {
349                number: data[OFS_MP_SS_NUMBER],
350                payload: Vec::from(&data[OFS_MP_SS_PAYLOAD + 4..]),
351            });
352
353            loop {
354                let mut data: Vec<u8> = Vec::with_capacity(0);
355                data.try_reserve(switching_size)?;
356                data.resize(switching_size, 0);
357
358                let read = self.socket.recv(&mut data)?;
359                data.truncate(read);
360
361                if data.len() <= 9 {
362                    return Err(Error::PacketTooShort {
363                        expected: 10,
364                        actual: data.len(),
365                    });
366                }
367
368                let packet_id = read_buffer_offset!(&data, OFS_MP_ID, i32);
369
370                if packet_id != id {
371                    return Err(Error::MismatchPacketId);
372                }
373
374                if id as u32 & 0x80000000 == 0 {
375                    packets.push(PacketFragment {
376                        number: data[OFS_MP_SS_NUMBER],
377                        payload: Vec::from(&data[OFS_MP_SS_PAYLOAD..]),
378                    });
379                } else {
380                    packets.push(PacketFragment {
381                        number: data[OFS_MP_SS_NUMBER],
382                        payload: Vec::from(&data[OFS_MP_SS_PAYLOAD_BZ2..]),
383                    });
384                }
385
386                if packets.len() == total_packets {
387                    break;
388                }
389            }
390
391            packets.sort_by_key(|p| p.number);
392
393            let mut aggregation = Vec::with_capacity(0);
394            aggregation.try_reserve(total_packets * self.max_size)?;
395
396            for p in packets {
397                aggregation.extend(p.payload);
398            }
399
400            if id as u32 & 0x80000000 != 0 {
401                let decompressed_size = read_buffer_offset!(&data, OFS_MP_SS_BZ2_SIZE, u32);
402                let checksum = read_buffer_offset!(&data, OFS_MP_SS_BZ2_CRC, u32);
403
404                if decompressed_size > (1024 * 1024) {
405                    return Err(Error::InvalidBz2Size);
406                }
407
408                let mut decompressed = Vec::with_capacity(0);
409                decompressed.try_reserve(decompressed_size as usize)?;
410                decompressed.resize(decompressed_size as usize, 0);
411
412                BzDecoder::new(aggregation.deref()).read_exact(&mut decompressed)?;
413
414                if crc32::checksum_ieee(&decompressed) != checksum {
415                    return Err(Error::ChecksumMismatch);
416                }
417
418                Ok(decompressed)
419            } else {
420                Ok(aggregation)
421            }
422        } else {
423            Err(Error::UnexpectedHeader {
424                expected: SINGLE_PACKET as u8,
425                actual: data[0],
426            })
427        }
428    }
429
430    #[doc(hidden)]
431    pub fn do_challenge_request<A: ToSocketAddrs>(
432        &self,
433        addr: A,
434        header: &[u8],
435    ) -> Result<Vec<u8>> {
436        let packet = Vec::with_capacity(9);
437        let mut packet = Cursor::new(packet);
438
439        packet.write_all(header)?;
440        packet.write_i32::<LittleEndian>(-1)?;
441
442        let mut data = self.send(packet.get_ref(), &addr)?;
443
444        for _ in 0..MAX_CHALLENGE_RETRIES {
445            if data.first() != Some(&HEADER_CHALLENGE) {
446                return Ok(data);
447            }
448
449            let mut cursor = Cursor::new(&data);
450            cursor.read_u8()?; // skip challenge header
451            let challenge = cursor.read_i32::<LittleEndian>()?;
452
453            packet.set_position(5);
454            packet.write_i32::<LittleEndian>(challenge)?;
455            data = self.send(packet.get_ref(), &addr)?;
456        }
457
458        Ok(data)
459    }
460}
461
462pub(crate) trait ReadCString: Read {
463    fn read_cstring(&mut self) -> Result<BString> {
464        let mut buf = Vec::with_capacity(256);
465        while let Ok(byte) = self.read_u8() {
466            if byte == 0 {
467                break;
468            }
469
470            buf.push(byte);
471        }
472
473        Ok(BString::new(buf))
474    }
475}
476
477/// Implement ReadCString for all types that implement Read
478impl<R: Read + ?Sized> ReadCString for R {}