xash3d_protocol/
server.rs

1// SPDX-License-Identifier: LGPL-3.0-only
2// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
3
4//! Game server packets.
5
6use std::fmt;
7
8use bitflags::bitflags;
9
10use super::cursor::{Cursor, CursorMut, GetKeyValue, PutKeyValue};
11use super::filter::Version;
12use super::wrappers::Str;
13use super::{CursorError, Error};
14
15/// Sended to a master server before `ServerAdd` packet.
16#[derive(Clone, Debug, PartialEq)]
17pub struct Challenge {
18    /// A number that the server must return in response.
19    pub server_challenge: Option<u32>,
20}
21
22impl Challenge {
23    /// Packet header.
24    pub const HEADER: &'static [u8] = b"q\xff";
25
26    /// Creates a new `Challenge`.
27    pub fn new(server_challenge: Option<u32>) -> Self {
28        Self { server_challenge }
29    }
30
31    /// Decode packet from `src`.
32    pub fn decode(src: &[u8]) -> Result<Self, Error> {
33        let mut cur = Cursor::new(src);
34        cur.expect(Self::HEADER)?;
35        let server_challenge = if cur.remaining() == 4 {
36            Some(cur.get_u32_le()?)
37        } else {
38            None
39        };
40        cur.expect_empty()?;
41        Ok(Self { server_challenge })
42    }
43
44    /// Encode packet to `buf`.
45    pub fn encode<const N: usize>(&self, buf: &mut [u8; N]) -> Result<usize, Error> {
46        let mut cur = CursorMut::new(buf);
47        cur.put_bytes(Self::HEADER)?;
48        if let Some(server_challenge) = self.server_challenge {
49            cur.put_u32_le(server_challenge)?;
50        }
51        Ok(cur.pos())
52    }
53}
54
55/// The operating system on which the game server runs.
56#[derive(Copy, Clone, Debug, PartialEq, Eq)]
57#[repr(u8)]
58pub enum Os {
59    /// GNU/Linux.
60    Linux,
61    /// Microsoft Windows
62    Windows,
63    /// Apple macOS, OS X, Mac OS X
64    Mac,
65    /// Unknown
66    Unknown,
67}
68
69impl Default for Os {
70    fn default() -> Os {
71        Os::Unknown
72    }
73}
74
75impl TryFrom<&[u8]> for Os {
76    type Error = CursorError;
77
78    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
79        match value {
80            b"l" => Ok(Os::Linux),
81            b"w" => Ok(Os::Windows),
82            b"m" => Ok(Os::Mac),
83            _ => Ok(Os::Unknown),
84        }
85    }
86}
87
88impl GetKeyValue<'_> for Os {
89    fn get_key_value(cur: &mut Cursor) -> Result<Self, CursorError> {
90        cur.get_key_value_raw()?.try_into()
91    }
92}
93
94impl PutKeyValue for Os {
95    fn put_key_value<'a, 'b>(
96        &self,
97        cur: &'b mut CursorMut<'a>,
98    ) -> Result<&'b mut CursorMut<'a>, CursorError> {
99        match self {
100            Self::Linux => cur.put_str("l"),
101            Self::Windows => cur.put_str("w"),
102            Self::Mac => cur.put_str("m"),
103            Self::Unknown => cur.put_str("?"),
104        }
105    }
106}
107
108impl fmt::Display for Os {
109    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
110        let s = match self {
111            Os::Linux => "Linux",
112            Os::Windows => "Windows",
113            Os::Mac => "Mac",
114            Os::Unknown => "Unknown",
115        };
116        write!(fmt, "{}", s)
117    }
118}
119
120/// Game server type.
121#[derive(Copy, Clone, Debug, PartialEq)]
122#[repr(u8)]
123pub enum ServerType {
124    /// Dedicated server.
125    Dedicated,
126    /// Game client.
127    Local,
128    /// Spectator proxy.
129    Proxy,
130    /// Unknown.
131    Unknown,
132}
133
134impl Default for ServerType {
135    fn default() -> Self {
136        Self::Unknown
137    }
138}
139
140impl TryFrom<&[u8]> for ServerType {
141    type Error = CursorError;
142
143    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
144        match value {
145            b"d" => Ok(Self::Dedicated),
146            b"l" => Ok(Self::Local),
147            b"p" => Ok(Self::Proxy),
148            _ => Ok(Self::Unknown),
149        }
150    }
151}
152
153impl GetKeyValue<'_> for ServerType {
154    fn get_key_value(cur: &mut Cursor) -> Result<Self, CursorError> {
155        cur.get_key_value_raw()?.try_into()
156    }
157}
158
159impl PutKeyValue for ServerType {
160    fn put_key_value<'a, 'b>(
161        &self,
162        cur: &'b mut CursorMut<'a>,
163    ) -> Result<&'b mut CursorMut<'a>, CursorError> {
164        match self {
165            Self::Dedicated => cur.put_str("d"),
166            Self::Local => cur.put_str("l"),
167            Self::Proxy => cur.put_str("p"),
168            Self::Unknown => cur.put_str("?"),
169        }
170    }
171}
172
173impl fmt::Display for ServerType {
174    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
175        use ServerType as E;
176
177        let s = match self {
178            E::Dedicated => "dedicated",
179            E::Local => "local",
180            E::Proxy => "proxy",
181            E::Unknown => "unknown",
182        };
183
184        write!(fmt, "{}", s)
185    }
186}
187
188/// The region of the world in which the server is located.
189#[derive(Copy, Clone, Debug, PartialEq, Eq)]
190#[repr(u8)]
191pub enum Region {
192    /// US East coast.
193    USEastCoast = 0x00,
194    /// US West coast.
195    USWestCoast = 0x01,
196    /// South America.
197    SouthAmerica = 0x02,
198    /// Europe.
199    Europe = 0x03,
200    /// Asia.
201    Asia = 0x04,
202    /// Australia.
203    Australia = 0x05,
204    /// Middle East.
205    MiddleEast = 0x06,
206    /// Africa.
207    Africa = 0x07,
208    /// Rest of the world.
209    RestOfTheWorld = 0xff,
210}
211
212impl Default for Region {
213    fn default() -> Self {
214        Self::RestOfTheWorld
215    }
216}
217
218impl TryFrom<u8> for Region {
219    type Error = CursorError;
220
221    fn try_from(value: u8) -> Result<Self, Self::Error> {
222        match value {
223            0x00 => Ok(Region::USEastCoast),
224            0x01 => Ok(Region::USWestCoast),
225            0x02 => Ok(Region::SouthAmerica),
226            0x03 => Ok(Region::Europe),
227            0x04 => Ok(Region::Asia),
228            0x05 => Ok(Region::Australia),
229            0x06 => Ok(Region::MiddleEast),
230            0x07 => Ok(Region::Africa),
231            0xff => Ok(Region::RestOfTheWorld),
232            _ => Err(CursorError::InvalidNumber),
233        }
234    }
235}
236
237impl GetKeyValue<'_> for Region {
238    fn get_key_value(cur: &mut Cursor) -> Result<Self, CursorError> {
239        cur.get_key_value::<u8>()?.try_into()
240    }
241}
242
243bitflags! {
244    /// Additional server flags.
245    #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
246    pub struct ServerFlags: u8 {
247        /// Server has bots.
248        const BOTS      = 1 << 0;
249        /// Server is behind a password.
250        const PASSWORD  = 1 << 1;
251        /// Server using anti-cheat.
252        const SECURE    = 1 << 2;
253        /// Server is LAN.
254        const LAN       = 1 << 3;
255        /// Server behind NAT.
256        const NAT       = 1 << 4;
257    }
258}
259
260/// Add/update game server information on the master server.
261#[derive(Clone, Debug, PartialEq, Default)]
262pub struct ServerAdd<T> {
263    /// Server is running the specified modification.
264    ///
265    /// ## Examples:
266    ///
267    /// * valve - Half-Life
268    /// * cstrike - Counter-Strike 1.6
269    /// * portal - Portal
270    /// * dod - Day of Defeat
271    /// * left4dead - Left 4 Dead
272    pub gamedir: T,
273    /// Server is running `map`.
274    pub map: T,
275    /// Server version.
276    pub version: Version,
277    /// Master server challenge number.
278    pub challenge: u32,
279    /// Server type.
280    pub server_type: ServerType,
281    /// Server is running on an operating system.
282    pub os: Os,
283    /// Server is located in a `region`.
284    pub region: Region,
285    /// Server protocol version.
286    pub protocol: u8,
287    /// Current number of players on the server.
288    pub players: u8,
289    /// Maximum number of players on the server.
290    pub max: u8,
291    /// See `ServerFalgs`.
292    pub flags: ServerFlags,
293}
294
295impl ServerAdd<()> {
296    /// Packet header.
297    pub const HEADER: &'static [u8] = b"0\n";
298}
299
300impl<'a, T> ServerAdd<T>
301where
302    T: 'a + Default + GetKeyValue<'a>,
303{
304    /// Decode packet from `src`.
305    pub fn decode(src: &'a [u8]) -> Result<Self, Error> {
306        trait Helper<'a> {
307            fn get<T: GetKeyValue<'a>>(&mut self, key: &'static str) -> Result<T, Error>;
308        }
309
310        impl<'a> Helper<'a> for Cursor<'a> {
311            fn get<T: GetKeyValue<'a>>(&mut self, key: &'static str) -> Result<T, Error> {
312                T::get_key_value(self).map_err(|e| Error::InvalidServerValue(key, e))
313            }
314        }
315
316        let mut cur = Cursor::new(src);
317        cur.expect(ServerAdd::HEADER)?;
318
319        let mut ret = Self::default();
320        let mut challenge = None;
321        loop {
322            let key = match cur.get_key_raw() {
323                Ok(s) => s,
324                Err(CursorError::TableEnd) => break,
325                Err(e) => Err(e)?,
326            };
327
328            match key {
329                b"protocol" => ret.protocol = cur.get("protocol")?,
330                b"challenge" => challenge = Some(cur.get("challenge")?),
331                b"players" => ret.players = cur.get("players")?,
332                b"max" => ret.max = cur.get("max")?,
333                b"gamedir" => ret.gamedir = cur.get("gamedir")?,
334                b"product" => cur.skip_key_value::<&[u8]>()?, // legacy key, ignore
335                b"map" => ret.map = cur.get("map")?,
336                b"type" => ret.server_type = cur.get("type")?,
337                b"os" => ret.os = cur.get("os")?,
338                b"version" => {
339                    ret.version = cur
340                        .get_key_value()
341                        .map_err(|e| {
342                            debug!("invalid server version");
343                            e
344                        })
345                        .unwrap_or_default()
346                }
347                b"region" => ret.region = cur.get("region")?,
348                b"bots" => ret
349                    .flags
350                    .set(ServerFlags::BOTS, cur.get::<u8>("bots")? != 0),
351                b"password" => ret.flags.set(ServerFlags::PASSWORD, cur.get("password")?),
352                b"secure" => ret.flags.set(ServerFlags::SECURE, cur.get("secure")?),
353                b"lan" => ret.flags.set(ServerFlags::LAN, cur.get("lan")?),
354                b"nat" => ret.flags.set(ServerFlags::NAT, cur.get("nat")?),
355                _ => {
356                    // skip unknown fields
357                    let value = cur.get_key_value::<Str<&[u8]>>()?;
358                    debug!("Invalid ServerInfo field \"{}\" = \"{}\"", Str(key), value);
359                }
360            }
361        }
362
363        match challenge {
364            Some(c) => {
365                ret.challenge = c;
366                Ok(ret)
367            }
368            None => Err(Error::InvalidServerValue("challenge", CursorError::Expect)),
369        }
370    }
371}
372
373impl<T> ServerAdd<T>
374where
375    T: PutKeyValue,
376{
377    /// Encode packet to `buf`.
378    pub fn encode(&self, buf: &mut [u8]) -> Result<usize, Error> {
379        Ok(CursorMut::new(buf)
380            .put_bytes(ServerAdd::HEADER)?
381            .put_key("protocol", self.protocol)?
382            .put_key("challenge", self.challenge)?
383            .put_key("players", self.players)?
384            .put_key("max", self.max)?
385            .put_key("gamedir", &self.gamedir)?
386            .put_key("map", &self.map)?
387            .put_key("type", self.server_type)?
388            .put_key("os", self.os)?
389            .put_key("version", self.version)?
390            .put_key("region", self.region as u8)?
391            .put_key("bots", self.flags.contains(ServerFlags::BOTS))?
392            .put_key("password", self.flags.contains(ServerFlags::PASSWORD))?
393            .put_key("secure", self.flags.contains(ServerFlags::SECURE))?
394            .put_key("lan", self.flags.contains(ServerFlags::LAN))?
395            .put_key("nat", self.flags.contains(ServerFlags::NAT))?
396            .pos())
397    }
398}
399
400/// Remove the game server from a list.
401#[derive(Clone, Debug, PartialEq)]
402pub struct ServerRemove;
403
404impl ServerRemove {
405    /// Packet header.
406    pub const HEADER: &'static [u8] = b"b\n";
407
408    /// Decode packet from `src`.
409    pub fn decode(src: &[u8]) -> Result<Self, Error> {
410        let mut cur = Cursor::new(src);
411        cur.expect(Self::HEADER)?;
412        cur.expect_empty()?;
413        Ok(Self)
414    }
415
416    /// Encode packet to `buf`.
417    pub fn encode<const N: usize>(&self, buf: &mut [u8; N]) -> Result<usize, Error> {
418        Ok(CursorMut::new(buf).put_bytes(Self::HEADER)?.pos())
419    }
420}
421
422/// Game server information to game clients.
423#[derive(Clone, Debug, PartialEq, Default)]
424pub struct GetServerInfoResponse<T> {
425    /// Server is running the specified modification.
426    ///
427    /// ## Examples:
428    ///
429    /// * valve - Half-Life
430    /// * cstrike - Counter-Strike 1.6
431    /// * portal - Portal
432    /// * dod - Day of Defeat
433    /// * left4dead - Left 4 Dead
434    pub gamedir: T,
435    /// Server is running `map`.
436    pub map: T,
437    /// Server title.
438    pub host: T,
439    /// Server protocol version.
440    pub protocol: u8,
441    /// Current number of players on the server.
442    pub numcl: u8,
443    /// Maximum number of players on the server.
444    pub maxcl: u8,
445    /// Server is running a deathmatch game mode.
446    pub dm: bool,
447    /// Players are grouped into teams.
448    pub team: bool,
449    /// Server is running in a co-op game mode.
450    pub coop: bool,
451    /// Server is behind a password.
452    pub password: bool,
453    /// Server is dedicated.
454    pub dedicated: bool,
455}
456
457impl GetServerInfoResponse<()> {
458    /// Packet header.
459    pub const HEADER: &'static [u8] = b"\xff\xff\xff\xffinfo\n";
460}
461
462impl<'a, T> GetServerInfoResponse<T>
463where
464    T: 'a + Default + GetKeyValue<'a>,
465{
466    /// Decode packet from `src`.
467    pub fn decode(src: &'a [u8]) -> Result<Self, Error> {
468        let mut cur = Cursor::new(src);
469        cur.expect(GetServerInfoResponse::HEADER)?;
470
471        if !cur.as_slice().starts_with(b"\\") {
472            let s = cur.get_bytes(cur.remaining())?;
473            let p = s
474                .iter()
475                .rev()
476                .position(|c| *c == b':')
477                .ok_or(Error::InvalidPacket)?;
478            let msg = &s[s.len() - p..];
479            return match msg {
480                b" wrong version\n" => Err(Error::InvalidProtocolVersion),
481                _ => Err(Error::InvalidPacket),
482            };
483        }
484
485        let mut ret = Self::default();
486        loop {
487            let key = match cur.get_key_raw() {
488                Ok(s) => s,
489                Err(CursorError::TableEnd) => break,
490                Err(e) => Err(e)?,
491            };
492
493            match key {
494                b"p" => ret.protocol = cur.get_key_value()?,
495                b"map" => ret.map = cur.get_key_value()?,
496                b"dm" => ret.dm = cur.get_key_value()?,
497                b"team" => ret.team = cur.get_key_value()?,
498                b"coop" => ret.coop = cur.get_key_value()?,
499                b"numcl" => ret.numcl = cur.get_key_value()?,
500                b"maxcl" => ret.maxcl = cur.get_key_value()?,
501                b"gamedir" => ret.gamedir = cur.get_key_value()?,
502                b"password" => ret.password = cur.get_key_value()?,
503                b"host" => ret.host = cur.get_key_value()?,
504                b"dedicated" => ret.dedicated = cur.get_key_value()?,
505                _ => {
506                    // skip unknown fields
507                    let value = cur.get_key_value::<Str<&[u8]>>()?;
508                    debug!(
509                        "Invalid GetServerInfo field \"{}\" = \"{}\"",
510                        Str(key),
511                        value
512                    );
513                }
514            }
515        }
516
517        Ok(ret)
518    }
519}
520
521impl<T> GetServerInfoResponse<T>
522where
523    T: PutKeyValue,
524{
525    /// Encode packet to `buf`.
526    pub fn encode(&self, buf: &mut [u8]) -> Result<usize, Error> {
527        Ok(CursorMut::new(buf)
528            .put_bytes(GetServerInfoResponse::HEADER)?
529            .put_key("p", self.protocol)?
530            .put_key("map", &self.map)?
531            .put_key("dm", self.dm)?
532            .put_key("team", self.team)?
533            .put_key("coop", self.coop)?
534            .put_key("numcl", self.numcl)?
535            .put_key("maxcl", self.maxcl)?
536            .put_key("gamedir", &self.gamedir)?
537            .put_key("password", self.password)?
538            .put_key("dedicated", self.dedicated)?
539            .put_key("host", &self.host)?
540            .pos())
541    }
542}
543
544/// Game server packet.
545#[derive(Clone, Debug, PartialEq)]
546pub enum Packet<'a> {
547    /// Sended to a master server before `ServerAdd` packet.
548    Challenge(Challenge),
549    /// Add/update game server information on the master server.
550    ServerAdd(ServerAdd<Str<&'a [u8]>>),
551    /// Remove the game server from a list.
552    ServerRemove,
553    /// Game server information to game clients.
554    GetServerInfoResponse(GetServerInfoResponse<Str<&'a [u8]>>),
555}
556
557impl<'a> Packet<'a> {
558    /// Decode packet from `src`.
559    pub fn decode(src: &'a [u8]) -> Result<Option<Self>, Error> {
560        if src.starts_with(Challenge::HEADER) {
561            Challenge::decode(src).map(Self::Challenge)
562        } else if src.starts_with(ServerAdd::HEADER) {
563            ServerAdd::decode(src).map(Self::ServerAdd)
564        } else if src.starts_with(ServerRemove::HEADER) {
565            ServerRemove::decode(src).map(|_| Self::ServerRemove)
566        } else if src.starts_with(GetServerInfoResponse::HEADER) {
567            GetServerInfoResponse::decode(src).map(Self::GetServerInfoResponse)
568        } else {
569            return Ok(None);
570        }
571        .map(Some)
572    }
573}
574
575#[cfg(test)]
576mod tests {
577    use super::*;
578
579    #[test]
580    fn challenge() {
581        let p = Challenge::new(Some(0x12345678));
582        let mut buf = [0; 128];
583        let n = p.encode(&mut buf).unwrap();
584        assert_eq!(Packet::decode(&buf[..n]), Ok(Some(Packet::Challenge(p))));
585    }
586
587    #[test]
588    fn challenge_old() {
589        let s = b"q\xff";
590        assert_eq!(
591            Packet::decode(s),
592            Ok(Some(Packet::Challenge(Challenge::new(None))))
593        );
594
595        let p = Challenge::new(None);
596        let mut buf = [0; 128];
597        let n = p.encode(&mut buf).unwrap();
598        assert_eq!(&buf[..n], b"q\xff");
599    }
600
601    #[test]
602    fn server_add() {
603        let p = ServerAdd {
604            gamedir: Str(&b"valve"[..]),
605            map: Str(&b"crossfire"[..]),
606            version: Version::new(0, 20),
607            challenge: 0x12345678,
608            server_type: ServerType::Dedicated,
609            os: Os::Linux,
610            region: Region::RestOfTheWorld,
611            protocol: 49,
612            players: 4,
613            max: 32,
614            flags: ServerFlags::all(),
615        };
616        let mut buf = [0; 512];
617        let n = p.encode(&mut buf).unwrap();
618        assert_eq!(Packet::decode(&buf[..n]), Ok(Some(Packet::ServerAdd(p))));
619    }
620
621    #[test]
622    fn server_remove() {
623        let p = ServerRemove;
624        let mut buf = [0; 64];
625        let n = p.encode(&mut buf).unwrap();
626        assert_eq!(Packet::decode(&buf[..n]), Ok(Some(Packet::ServerRemove)));
627    }
628
629    #[test]
630    fn get_server_info_response() {
631        let p = GetServerInfoResponse {
632            protocol: 49,
633            map: Str("crossfire".as_bytes()),
634            dm: true,
635            team: true,
636            coop: true,
637            numcl: 4,
638            maxcl: 32,
639            gamedir: Str("valve".as_bytes()),
640            password: true,
641            dedicated: true,
642            host: Str("Test".as_bytes()),
643        };
644        let mut buf = [0; 512];
645        let n = p.encode(&mut buf).unwrap();
646        assert_eq!(
647            Packet::decode(&buf[..n]),
648            Ok(Some(Packet::GetServerInfoResponse(p)))
649        );
650    }
651
652    #[test]
653    fn get_server_info_response_wrong_version() {
654        let s = b"\xff\xff\xff\xffinfo\nfoobar: wrong version\n";
655        assert_eq!(Packet::decode(s), Err(Error::InvalidProtocolVersion));
656
657        let s = b"\xff\xff\xff\xffinfo\nfoobar\xff: wrong version\n";
658        assert_eq!(Packet::decode(s), Err(Error::InvalidProtocolVersion));
659    }
660
661    #[test]
662    fn server_add_bots_is_a_number() {
663        let s = b"0\n\\protocol\\48\\challenge\\4161802725\\players\\0\\max\\32\\bots\\3\\gamedir\\valve\\map\\rats_bathroom\\type\\d\\password\\0\\os\\l\\secure\\0\\lan\\0\\version\\0.19.4\\region\\255\\product\\valve\\nat\\0";
664        ServerAdd::<&[u8]>::decode(s).unwrap();
665    }
666
667    #[test]
668    fn server_add_legacy() {
669        let s = b"0\n\\protocol\\48\\challenge\\1680337211\\players\\1\\max\\8\\bots\\0\\gamedir\\cstrike\\map\\cs_assault\\type\\d\\password\\0\\os\\l\\secure\\0\\lan\\0\\version\\0.17.1\\region\\255\\product\\cstrike\n";
670        ServerAdd::<&[u8]>::decode(s).unwrap();
671    }
672}