1use anyhow::{bail, ensure, Context, Result};
4use serde_json::{json, Map, Value};
5use std::time::Duration;
6
7pub const MAX_PACKET: usize = 1400;
9pub const MAX_REQUEST: usize = 1400;
11pub const SPLIT_CHUNK: usize = 1248;
13pub const MAX_RESPONSE: usize = 64 * 1024;
15pub const MAX_SPLIT_PACKETS: usize = 64;
17pub const REPLY_TIMEOUT: Duration = Duration::from_secs(5);
19
20pub const SINGLE: [u8; 4] = [0xFF; 4];
21pub const SPLIT: [u8; 4] = [0xFE, 0xFF, 0xFF, 0xFF];
22pub const INFO_QUERY: &[u8] = b"Source Engine Query\0";
23pub const REQUEST_INFO: u8 = b'T';
24pub const REQUEST_PLAYERS: u8 = b'U';
25pub const REQUEST_RULES: u8 = b'V';
26pub const RESPONSE_CHALLENGE: u8 = b'A';
27pub const RESPONSE_INFO: u8 = b'I';
28pub const RESPONSE_PLAYERS: u8 = b'D';
29pub const RESPONSE_RULES: u8 = b'E';
30pub const NO_CHALLENGE: u32 = 0xFFFF_FFFF;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum Kind {
35 Info,
36 Players,
37 Rules,
38}
39
40impl Kind {
41 pub fn name(self) -> &'static str {
42 match self {
43 Kind::Info => "info",
44 Kind::Players => "players",
45 Kind::Rules => "rules",
46 }
47 }
48 pub fn parse(name: &str) -> Result<Self> {
49 Ok(match name {
50 "info" => Kind::Info,
51 "players" => Kind::Players,
52 "rules" => Kind::Rules,
53 other => bail!("query must be info, players or rules, not {other}"),
54 })
55 }
56 pub fn request_byte(self) -> u8 {
57 match self {
58 Kind::Info => REQUEST_INFO,
59 Kind::Players => REQUEST_PLAYERS,
60 Kind::Rules => REQUEST_RULES,
61 }
62 }
63 pub fn response_byte(self) -> u8 {
64 match self {
65 Kind::Info => RESPONSE_INFO,
66 Kind::Players => RESPONSE_PLAYERS,
67 Kind::Rules => RESPONSE_RULES,
68 }
69 }
70}
71
72pub fn parse_request(datagram: &[u8]) -> Result<(Kind, Option<u32>)> {
74 ensure!(
75 datagram.len() <= MAX_REQUEST,
76 "A2S request exceeds {MAX_REQUEST} bytes"
77 );
78 ensure!(
79 datagram.len() >= 5 && datagram[..4] == SINGLE,
80 "not a single-packet A2S request"
81 );
82 let challenge = |bytes: &[u8]| -> Option<u32> {
83 bytes
84 .get(..4)
85 .map(|c| u32::from_le_bytes(c.try_into().expect("four bytes")))
86 };
87 match datagram[4] {
88 REQUEST_INFO => {
89 let rest = &datagram[5..];
90 ensure!(
91 rest.starts_with(INFO_QUERY),
92 "A2S_INFO without its query string"
93 );
94 Ok((Kind::Info, challenge(&rest[INFO_QUERY.len()..])))
95 }
96 REQUEST_PLAYERS => Ok((
97 Kind::Players,
98 Some(challenge(&datagram[5..]).context("A2S_PLAYER without a challenge")?),
99 )),
100 REQUEST_RULES => Ok((
101 Kind::Rules,
102 Some(challenge(&datagram[5..]).context("A2S_RULES without a challenge")?),
103 )),
104 other => bail!("unsupported A2S request {other:#04x}"),
105 }
106}
107
108pub fn encode_request(kind: Kind, challenge: Option<u32>) -> Vec<u8> {
109 let mut out = SINGLE.to_vec();
110 out.push(kind.request_byte());
111 if kind == Kind::Info {
112 out.extend_from_slice(INFO_QUERY);
113 }
114 if let Some(challenge) = challenge {
115 out.extend_from_slice(&challenge.to_le_bytes());
116 }
117 out
118}
119
120pub fn encode_challenge(challenge: u32) -> Vec<u8> {
121 let mut out = SINGLE.to_vec();
122 out.push(RESPONSE_CHALLENGE);
123 out.extend_from_slice(&challenge.to_le_bytes());
124 out
125}
126
127fn cstring(out: &mut Vec<u8>, text: &str) {
128 out.extend(text.bytes().filter(|b| *b != 0));
129 out.push(0);
130}
131
132fn field<'a>(v: &'a Value, key: &str) -> Option<&'a Value> {
133 v.get(key).filter(|x| !x.is_null())
134}
135
136fn text(v: &Value, key: &str, default: &str, limit: usize) -> Result<String> {
137 let value = match field(v, key) {
138 None => default.to_string(),
139 Some(Value::String(s)) => s.clone(),
140 Some(_) => bail!("{key} must be a string"),
141 };
142 ensure!(value.len() <= limit, "{key} exceeds {limit} bytes");
143 Ok(value)
144}
145
146fn number(v: &Value, key: &str, default: u64, max: u64) -> Result<u64> {
147 match field(v, key) {
148 None => Ok(default),
149 Some(n) => {
150 let n = n
151 .as_u64()
152 .with_context(|| format!("{key} must be a non-negative integer"))?;
153 ensure!(n <= max, "{key} must be at most {max}");
154 Ok(n)
155 }
156 }
157}
158
159fn flag(v: &Value, key: &str) -> Result<bool> {
160 match field(v, key) {
161 None => Ok(false),
162 Some(Value::Bool(b)) => Ok(*b),
163 Some(_) => bail!("{key} must be a boolean"),
164 }
165}
166
167pub fn encode_info(v: &Value) -> Result<Vec<u8>> {
169 let mut out = SINGLE.to_vec();
170 out.push(RESPONSE_INFO);
171 out.push(17);
172 cstring(&mut out, &text(v, "name", "", 256)?);
173 cstring(&mut out, &text(v, "map", "", 128)?);
174 cstring(&mut out, &text(v, "folder", "", 128)?);
175 cstring(&mut out, &text(v, "game", "", 128)?);
176 out.extend_from_slice(&(number(v, "app_id", 0, u16::MAX as u64)? as u16).to_le_bytes());
177 out.push(number(v, "players", 0, 255)? as u8);
178 out.push(number(v, "max_players", 0, 255)? as u8);
179 out.push(number(v, "bots", 0, 255)? as u8);
180 out.push(match text(v, "server_type", "dedicated", 16)?.as_str() {
181 "dedicated" => b'd',
182 "listen" => b'l',
183 "proxy" => b'p',
184 other => bail!("server_type must be dedicated, listen or proxy, not {other}"),
185 });
186 out.push(match text(v, "environment", "linux", 16)?.as_str() {
187 "linux" => b'l',
188 "windows" => b'w',
189 "mac" => b'm',
190 other => bail!("environment must be linux, windows or mac, not {other}"),
191 });
192 out.push(flag(v, "private")? as u8);
193 out.push(flag(v, "vac")? as u8);
194 cstring(&mut out, &text(v, "version", "1.0.0.0", 64)?);
195 let port = field(v, "port")
196 .map(|_| number(v, "port", 0, u16::MAX as u64))
197 .transpose()?;
198 let steam_id = field(v, "steam_id")
199 .map(|_| number(v, "steam_id", 0, u64::MAX))
200 .transpose()?;
201 let keywords = field(v, "keywords")
202 .map(|_| text(v, "keywords", "", 512))
203 .transpose()?;
204 let game_id = field(v, "game_id")
205 .map(|_| number(v, "game_id", 0, u64::MAX))
206 .transpose()?;
207 let edf = (if port.is_some() { 0x80 } else { 0 })
208 | (if steam_id.is_some() { 0x10 } else { 0 })
209 | (if keywords.is_some() { 0x20 } else { 0 })
210 | (if game_id.is_some() { 0x01 } else { 0 });
211 if edf != 0 {
212 out.push(edf);
213 if let Some(port) = port {
214 out.extend_from_slice(&(port as u16).to_le_bytes());
215 }
216 if let Some(steam_id) = steam_id {
217 out.extend_from_slice(&steam_id.to_le_bytes());
218 }
219 if let Some(keywords) = keywords {
220 cstring(&mut out, &keywords);
221 }
222 if let Some(game_id) = game_id {
223 out.extend_from_slice(&game_id.to_le_bytes());
224 }
225 }
226 Ok(out)
227}
228
229pub fn encode_players(v: &Value) -> Result<Vec<u8>> {
231 let players = field(v, "players")
232 .map(|p| p.as_array().context("players must be an array"))
233 .transpose()?
234 .cloned()
235 .unwrap_or_default();
236 ensure!(players.len() <= 255, "at most 255 players");
237 let mut out = SINGLE.to_vec();
238 out.push(RESPONSE_PLAYERS);
239 out.push(players.len() as u8);
240 for (index, player) in players.iter().enumerate() {
241 out.push(index as u8);
242 cstring(&mut out, &text(player, "name", "", 128)?);
243 let score = match field(player, "score") {
244 None => 0,
245 Some(s) => s
246 .as_i64()
247 .filter(|s| i32::try_from(*s).is_ok())
248 .context("score must be a 32-bit integer")? as i32,
249 };
250 out.extend_from_slice(&score.to_le_bytes());
251 let duration = match field(player, "duration_secs") {
252 None => 0.0,
253 Some(d) => {
254 d.as_f64()
255 .filter(|d| d.is_finite() && *d >= 0.0)
256 .context("duration_secs must be a non-negative number")? as f32
257 }
258 };
259 out.extend_from_slice(&duration.to_le_bytes());
260 }
261 Ok(out)
262}
263
264pub fn encode_rules(v: &Value) -> Result<Vec<u8>> {
266 let rules = field(v, "rules")
267 .map(|r| {
268 r.as_object()
269 .context("rules must be an object of name to value")
270 })
271 .transpose()?
272 .cloned()
273 .unwrap_or_default();
274 ensure!(rules.len() <= 4096, "at most 4096 rules");
275 let mut out = SINGLE.to_vec();
276 out.push(RESPONSE_RULES);
277 out.extend_from_slice(&(rules.len() as u16).to_le_bytes());
278 for (name, value) in &rules {
279 cstring(&mut out, name);
280 cstring(
281 &mut out,
282 value
283 .as_str()
284 .with_context(|| format!("rule {name} must be a string"))?,
285 );
286 }
287 Ok(out)
288}
289
290pub fn packetize(payload: &[u8], id: u32) -> Result<Vec<Vec<u8>>> {
292 ensure!(
293 payload.len() <= MAX_RESPONSE,
294 "A2S response exceeds {MAX_RESPONSE} bytes"
295 );
296 if payload.len() <= MAX_PACKET {
297 return Ok(vec![payload.to_vec()]);
298 }
299 let chunks: Vec<&[u8]> = payload.chunks(SPLIT_CHUNK).collect();
300 ensure!(
301 chunks.len() <= MAX_SPLIT_PACKETS,
302 "A2S response needs more than {MAX_SPLIT_PACKETS} packets"
303 );
304 let id = id & 0x7FFF_FFFF;
305 Ok(chunks
306 .iter()
307 .enumerate()
308 .map(|(number, chunk)| {
309 let mut out = SPLIT.to_vec();
310 out.extend_from_slice(&id.to_le_bytes());
311 out.push(chunks.len() as u8);
312 out.push(number as u8);
313 out.extend_from_slice(&(SPLIT_CHUNK as u16).to_le_bytes());
314 out.extend_from_slice(chunk);
315 out
316 })
317 .collect())
318}
319
320#[derive(Default)]
322pub struct Reassembly {
323 id: Option<u32>,
324 total: usize,
325 parts: Vec<Option<Vec<u8>>>,
326}
327
328impl Reassembly {
329 pub fn feed(&mut self, datagram: &[u8]) -> Result<Option<Vec<u8>>> {
331 ensure!(datagram.len() >= 5, "A2S datagram too short");
332 if datagram[..4] == SINGLE {
333 return Ok(Some(datagram.to_vec()));
334 }
335 ensure!(datagram[..4] == SPLIT, "A2S datagram has an unknown header");
336 ensure!(datagram.len() >= 12, "A2S split packet too short");
337 let id = u32::from_le_bytes(datagram[4..8].try_into()?);
338 ensure!(
339 id & 0x8000_0000 == 0,
340 "compressed A2S responses are not supported"
341 );
342 let total = datagram[8] as usize;
343 let number = datagram[9] as usize;
344 ensure!(
345 (1..=MAX_SPLIT_PACKETS).contains(&total) && number < total,
346 "A2S split packet numbering out of range"
347 );
348 match self.id {
349 None => {
350 self.id = Some(id);
351 self.total = total;
352 self.parts = vec![None; total];
353 }
354 Some(current) => ensure!(
355 current == id && self.total == total,
356 "A2S split packets from two responses"
357 ),
358 }
359 self.parts[number] = Some(datagram[12..].to_vec());
360 if self.parts.iter().all(Option::is_some) {
361 let payload: Vec<u8> = self.parts.iter().flatten().flatten().copied().collect();
362 ensure!(
363 payload.len() <= MAX_RESPONSE,
364 "A2S response exceeds {MAX_RESPONSE} bytes"
365 );
366 ensure!(
367 payload.len() >= 5 && payload[..4] == SINGLE,
368 "reassembled A2S response lacks its header"
369 );
370 return Ok(Some(payload));
371 }
372 Ok(None)
373 }
374}
375
376struct Reader<'a> {
377 data: &'a [u8],
378 at: usize,
379}
380
381impl<'a> Reader<'a> {
382 fn u8(&mut self) -> Result<u8> {
383 let b = *self.data.get(self.at).context("A2S response truncated")?;
384 self.at += 1;
385 Ok(b)
386 }
387 fn bytes<const N: usize>(&mut self) -> Result<[u8; N]> {
388 let slice = self
389 .data
390 .get(self.at..self.at + N)
391 .context("A2S response truncated")?;
392 self.at += N;
393 Ok(slice.try_into()?)
394 }
395 fn cstring(&mut self) -> Result<String> {
396 let rest = self.data.get(self.at..).context("A2S response truncated")?;
397 let end = rest
398 .iter()
399 .position(|b| *b == 0)
400 .context("A2S string not terminated")?;
401 self.at += end + 1;
402 Ok(String::from_utf8_lossy(&rest[..end]).into_owned())
403 }
404 fn more(&self) -> bool {
405 self.at < self.data.len()
406 }
407}
408
409pub fn decode_response(kind: Kind, payload: &[u8]) -> Result<Value> {
411 ensure!(
412 payload.len() >= 5 && payload[..4] == SINGLE,
413 "A2S response lacks its header"
414 );
415 ensure!(
416 payload[4] == kind.response_byte(),
417 "A2S answered {:#04x} to a {} query",
418 payload[4],
419 kind.name()
420 );
421 let mut r = Reader {
422 data: &payload[5..],
423 at: 0,
424 };
425 Ok(match kind {
426 Kind::Info => {
427 let protocol = r.u8()?;
428 let name = r.cstring()?;
429 let map = r.cstring()?;
430 let folder = r.cstring()?;
431 let game = r.cstring()?;
432 let app_id = u16::from_le_bytes(r.bytes()?);
433 let players = r.u8()?;
434 let max_players = r.u8()?;
435 let bots = r.u8()?;
436 let server_type = match r.u8()? {
437 b'd' => "dedicated",
438 b'l' => "listen",
439 b'p' => "proxy",
440 _ => "unknown",
441 };
442 let environment = match r.u8()? {
443 b'l' => "linux",
444 b'w' => "windows",
445 b'm' | b'o' => "mac",
446 _ => "unknown",
447 };
448 let private = r.u8()? != 0;
449 let vac = r.u8()? != 0;
450 let version = r.cstring()?;
451 let mut info = json!({"protocol": protocol, "name": name, "map": map, "folder": folder, "game": game, "app_id": app_id, "players": players, "max_players": max_players, "bots": bots, "server_type": server_type, "environment": environment, "private": private, "vac": vac, "version": version});
452 if r.more() {
453 let edf = r.u8()?;
454 if edf & 0x80 != 0 {
455 info["port"] = json!(u16::from_le_bytes(r.bytes()?));
456 }
457 if edf & 0x10 != 0 {
458 info["steam_id"] = json!(u64::from_le_bytes(r.bytes()?));
459 }
460 if edf & 0x40 != 0 {
461 info["sourcetv_port"] = json!(u16::from_le_bytes(r.bytes()?));
462 info["sourcetv_name"] = json!(r.cstring()?);
463 }
464 if edf & 0x20 != 0 {
465 info["keywords"] = json!(r.cstring()?);
466 }
467 if edf & 0x01 != 0 {
468 info["game_id"] = json!(u64::from_le_bytes(r.bytes()?));
469 }
470 }
471 info
472 }
473 Kind::Players => {
474 let count = r.u8()?;
475 let mut players = Vec::new();
476 for _ in 0..count {
477 let index = r.u8()?;
478 let name = r.cstring()?;
479 let score = i32::from_le_bytes(r.bytes()?);
480 let duration = f32::from_le_bytes(r.bytes()?);
481 players.push(json!({"index": index, "name": name, "score": score, "duration_secs": duration}));
482 }
483 json!(players)
484 }
485 Kind::Rules => {
486 let count = u16::from_le_bytes(r.bytes()?);
487 let mut rules = Map::new();
488 for _ in 0..count {
489 let name = r.cstring()?;
490 let value = r.cstring()?;
491 rules.insert(name, Value::String(value));
492 }
493 Value::Object(rules)
494 }
495 })
496}