You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
506 lines
14 KiB
506 lines
14 KiB
// SPDX-License-Identifier: GPL-3.0-only |
|
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com> |
|
|
|
use std::fmt; |
|
|
|
use bitflags::bitflags; |
|
use log::debug; |
|
|
|
use super::cursor::{Cursor, CursorMut, GetKeyValue, PutKeyValue}; |
|
use super::filter::Version; |
|
use super::types::Str; |
|
use super::Error; |
|
|
|
#[derive(Clone, Debug, PartialEq)] |
|
pub struct Challenge { |
|
pub server_challenge: u32, |
|
} |
|
|
|
impl Challenge { |
|
pub const HEADER: &'static [u8] = b"q\xff"; |
|
|
|
pub fn new(server_challenge: u32) -> Self { |
|
Self { server_challenge } |
|
} |
|
|
|
pub fn decode(src: &[u8]) -> Result<Self, Error> { |
|
let mut cur = Cursor::new(src); |
|
cur.expect(Self::HEADER)?; |
|
let server_challenge = cur.get_u32_le()?; |
|
cur.expect_empty()?; |
|
Ok(Self { server_challenge }) |
|
} |
|
|
|
pub fn encode<const N: usize>(&self, buf: &mut [u8; N]) -> Result<usize, Error> { |
|
Ok(CursorMut::new(buf) |
|
.put_bytes(Self::HEADER)? |
|
.put_u32_le(self.server_challenge)? |
|
.pos()) |
|
} |
|
} |
|
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)] |
|
#[repr(u8)] |
|
pub enum Os { |
|
Linux, |
|
Windows, |
|
Mac, |
|
Unknown, |
|
} |
|
|
|
impl Default for Os { |
|
fn default() -> Os { |
|
Os::Unknown |
|
} |
|
} |
|
|
|
impl TryFrom<&[u8]> for Os { |
|
type Error = Error; |
|
|
|
fn try_from(value: &[u8]) -> Result<Self, Self::Error> { |
|
match value { |
|
b"l" => Ok(Os::Linux), |
|
b"w" => Ok(Os::Windows), |
|
b"m" => Ok(Os::Mac), |
|
_ => Ok(Os::Unknown), |
|
} |
|
} |
|
} |
|
|
|
impl GetKeyValue<'_> for Os { |
|
fn get_key_value(cur: &mut Cursor) -> Result<Self, Error> { |
|
cur.get_key_value_raw()?.try_into() |
|
} |
|
} |
|
|
|
impl PutKeyValue for Os { |
|
fn put_key_value<'a, 'b>( |
|
&self, |
|
cur: &'b mut CursorMut<'a>, |
|
) -> Result<&'b mut CursorMut<'a>, Error> { |
|
match self { |
|
Self::Linux => cur.put_str("l"), |
|
Self::Windows => cur.put_str("w"), |
|
Self::Mac => cur.put_str("m"), |
|
Self::Unknown => cur.put_str("?"), |
|
} |
|
} |
|
} |
|
|
|
impl fmt::Display for Os { |
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { |
|
let s = match self { |
|
Os::Linux => "Linux", |
|
Os::Windows => "Windows", |
|
Os::Mac => "Mac", |
|
Os::Unknown => "Unknown", |
|
}; |
|
write!(fmt, "{}", s) |
|
} |
|
} |
|
|
|
#[derive(Copy, Clone, Debug, PartialEq)] |
|
#[repr(u8)] |
|
pub enum ServerType { |
|
Dedicated, |
|
Local, |
|
Proxy, |
|
Unknown, |
|
} |
|
|
|
impl Default for ServerType { |
|
fn default() -> Self { |
|
Self::Unknown |
|
} |
|
} |
|
|
|
impl TryFrom<&[u8]> for ServerType { |
|
type Error = Error; |
|
|
|
fn try_from(value: &[u8]) -> Result<Self, Self::Error> { |
|
match value { |
|
b"d" => Ok(Self::Dedicated), |
|
b"l" => Ok(Self::Local), |
|
b"p" => Ok(Self::Proxy), |
|
_ => Ok(Self::Unknown), |
|
} |
|
} |
|
} |
|
|
|
impl GetKeyValue<'_> for ServerType { |
|
fn get_key_value(cur: &mut Cursor) -> Result<Self, Error> { |
|
cur.get_key_value_raw()?.try_into() |
|
} |
|
} |
|
|
|
impl PutKeyValue for ServerType { |
|
fn put_key_value<'a, 'b>( |
|
&self, |
|
cur: &'b mut CursorMut<'a>, |
|
) -> Result<&'b mut CursorMut<'a>, Error> { |
|
match self { |
|
Self::Dedicated => cur.put_str("d"), |
|
Self::Local => cur.put_str("l"), |
|
Self::Proxy => cur.put_str("p"), |
|
Self::Unknown => cur.put_str("?"), |
|
} |
|
} |
|
} |
|
|
|
impl fmt::Display for ServerType { |
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { |
|
use ServerType as E; |
|
|
|
let s = match self { |
|
E::Dedicated => "dedicated", |
|
E::Local => "local", |
|
E::Proxy => "proxy", |
|
E::Unknown => "unknown", |
|
}; |
|
|
|
write!(fmt, "{}", s) |
|
} |
|
} |
|
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)] |
|
#[repr(u8)] |
|
pub enum Region { |
|
USEastCoast = 0x00, |
|
USWestCoast = 0x01, |
|
SouthAmerica = 0x02, |
|
Europe = 0x03, |
|
Asia = 0x04, |
|
Australia = 0x05, |
|
MiddleEast = 0x06, |
|
Africa = 0x07, |
|
RestOfTheWorld = 0xff, |
|
} |
|
|
|
impl Default for Region { |
|
fn default() -> Self { |
|
Self::RestOfTheWorld |
|
} |
|
} |
|
|
|
impl TryFrom<u8> for Region { |
|
type Error = Error; |
|
|
|
fn try_from(value: u8) -> Result<Self, Self::Error> { |
|
match value { |
|
0x00 => Ok(Region::USEastCoast), |
|
0x01 => Ok(Region::USWestCoast), |
|
0x02 => Ok(Region::SouthAmerica), |
|
0x03 => Ok(Region::Europe), |
|
0x04 => Ok(Region::Asia), |
|
0x05 => Ok(Region::Australia), |
|
0x06 => Ok(Region::MiddleEast), |
|
0x07 => Ok(Region::Africa), |
|
0xff => Ok(Region::RestOfTheWorld), |
|
_ => Err(Error::InvalidPacket), |
|
} |
|
} |
|
} |
|
|
|
impl GetKeyValue<'_> for Region { |
|
fn get_key_value(cur: &mut Cursor) -> Result<Self, Error> { |
|
cur.get_key_value::<u8>()?.try_into() |
|
} |
|
} |
|
|
|
bitflags! { |
|
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] |
|
pub struct ServerFlags: u8 { |
|
const BOTS = 1 << 0; |
|
const PASSWORD = 1 << 1; |
|
const SECURE = 1 << 2; |
|
const LAN = 1 << 3; |
|
const NAT = 1 << 4; |
|
} |
|
} |
|
|
|
#[derive(Clone, Debug, PartialEq, Default)] |
|
pub struct ServerAdd<T> { |
|
pub gamedir: T, |
|
pub map: T, |
|
pub version: Version, |
|
pub product: T, |
|
pub challenge: u32, |
|
pub server_type: ServerType, |
|
pub os: Os, |
|
pub region: Region, |
|
pub protocol: u8, |
|
pub players: u8, |
|
pub max: u8, |
|
pub flags: ServerFlags, |
|
} |
|
|
|
impl ServerAdd<()> { |
|
pub const HEADER: &'static [u8] = b"0\n"; |
|
} |
|
|
|
impl<'a, T> ServerAdd<T> |
|
where |
|
T: 'a + Default + GetKeyValue<'a>, |
|
{ |
|
pub fn decode(src: &'a [u8]) -> Result<Self, Error> { |
|
let mut cur = Cursor::new(src); |
|
cur.expect(ServerAdd::HEADER)?; |
|
|
|
let mut ret = Self::default(); |
|
let mut challenge = None; |
|
loop { |
|
let key = match cur.get_key_raw() { |
|
Ok(s) => s, |
|
Err(Error::UnexpectedEnd) => break, |
|
Err(e) => return Err(e), |
|
}; |
|
|
|
match key { |
|
b"protocol" => ret.protocol = cur.get_key_value()?, |
|
b"challenge" => challenge = Some(cur.get_key_value()?), |
|
b"players" => ret.players = cur.get_key_value()?, |
|
b"max" => ret.max = cur.get_key_value()?, |
|
b"gamedir" => ret.gamedir = cur.get_key_value()?, |
|
b"map" => ret.map = cur.get_key_value()?, |
|
b"type" => ret.server_type = cur.get_key_value()?, |
|
b"os" => ret.os = cur.get_key_value()?, |
|
b"version" => ret.version = cur.get_key_value()?, |
|
b"region" => ret.region = cur.get_key_value()?, |
|
b"product" => ret.product = cur.get_key_value()?, |
|
b"bots" => ret.flags.set(ServerFlags::BOTS, cur.get_key_value()?), |
|
b"password" => ret.flags.set(ServerFlags::PASSWORD, cur.get_key_value()?), |
|
b"secure" => ret.flags.set(ServerFlags::SECURE, cur.get_key_value()?), |
|
b"lan" => ret.flags.set(ServerFlags::LAN, cur.get_key_value()?), |
|
b"nat" => ret.flags.set(ServerFlags::NAT, cur.get_key_value()?), |
|
_ => { |
|
// skip unknown fields |
|
let value = cur.get_key_value::<Str<&[u8]>>()?; |
|
debug!("Invalid ServerInfo field \"{}\" = \"{}\"", Str(key), value); |
|
} |
|
} |
|
} |
|
|
|
match challenge { |
|
Some(c) => { |
|
ret.challenge = c; |
|
Ok(ret) |
|
} |
|
None => Err(Error::InvalidPacket), |
|
} |
|
} |
|
} |
|
|
|
impl<T> ServerAdd<T> |
|
where |
|
T: PutKeyValue + Clone, |
|
{ |
|
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, Error> { |
|
Ok(CursorMut::new(buf) |
|
.put_bytes(ServerAdd::HEADER)? |
|
.put_key("protocol", self.protocol)? |
|
.put_key("challenge", self.challenge)? |
|
.put_key("players", self.players)? |
|
.put_key("max", self.max)? |
|
.put_key("gamedir", self.gamedir.clone())? |
|
.put_key("map", self.map.clone())? |
|
.put_key("type", self.server_type)? |
|
.put_key("os", self.os)? |
|
.put_key("version", self.version)? |
|
.put_key("region", self.region as u8)? |
|
.put_key("product", self.product.clone())? |
|
.put_key("bots", self.flags.contains(ServerFlags::BOTS))? |
|
.put_key("password", self.flags.contains(ServerFlags::PASSWORD))? |
|
.put_key("secure", self.flags.contains(ServerFlags::SECURE))? |
|
.put_key("lan", self.flags.contains(ServerFlags::LAN))? |
|
.put_key("nat", self.flags.contains(ServerFlags::NAT))? |
|
.pos()) |
|
} |
|
} |
|
|
|
#[derive(Clone, Debug, PartialEq)] |
|
pub struct ServerRemove; |
|
|
|
impl ServerRemove { |
|
pub const HEADER: &'static [u8] = b"b\n"; |
|
|
|
pub fn decode(src: &[u8]) -> Result<Self, Error> { |
|
let mut cur = Cursor::new(src); |
|
cur.expect(Self::HEADER)?; |
|
cur.expect_empty()?; |
|
Ok(Self) |
|
} |
|
|
|
pub fn encode<const N: usize>(&self, buf: &mut [u8; N]) -> Result<usize, Error> { |
|
Ok(CursorMut::new(buf).put_bytes(Self::HEADER)?.pos()) |
|
} |
|
} |
|
|
|
#[derive(Clone, Debug, PartialEq, Default)] |
|
pub struct GetServerInfoResponse<T> { |
|
pub gamedir: T, |
|
pub map: T, |
|
pub host: T, |
|
pub protocol: u8, |
|
pub numcl: u8, |
|
pub maxcl: u8, |
|
pub dm: bool, |
|
pub team: bool, |
|
pub coop: bool, |
|
pub password: bool, |
|
} |
|
|
|
impl GetServerInfoResponse<()> { |
|
pub const HEADER: &'static [u8] = b"\xff\xff\xff\xffinfo\n"; |
|
} |
|
|
|
impl<'a, T> GetServerInfoResponse<T> |
|
where |
|
T: 'a + Default + GetKeyValue<'a>, |
|
{ |
|
pub fn decode(src: &'a [u8]) -> Result<Self, Error> { |
|
let mut cur = Cursor::new(src); |
|
cur.expect(GetServerInfoResponse::HEADER)?; |
|
|
|
let mut ret = Self::default(); |
|
loop { |
|
let key = match cur.get_key_raw() { |
|
Ok(s) => s, |
|
Err(Error::UnexpectedEnd) => break, |
|
Err(e) => return Err(e), |
|
}; |
|
|
|
match key { |
|
b"p" => ret.protocol = cur.get_key_value()?, |
|
b"map" => ret.map = cur.get_key_value()?, |
|
b"dm" => ret.dm = cur.get_key_value()?, |
|
b"team" => ret.team = cur.get_key_value()?, |
|
b"coop" => ret.coop = cur.get_key_value()?, |
|
b"numcl" => ret.numcl = cur.get_key_value()?, |
|
b"maxcl" => ret.maxcl = cur.get_key_value()?, |
|
b"gamedir" => ret.gamedir = cur.get_key_value()?, |
|
b"password" => ret.password = cur.get_key_value()?, |
|
b"host" => ret.host = cur.get_key_value()?, |
|
_ => { |
|
// skip unknown fields |
|
let value = cur.get_key_value::<Str<&[u8]>>()?; |
|
debug!( |
|
"Invalid GetServerInfo field \"{}\" = \"{}\"", |
|
Str(key), |
|
value |
|
); |
|
} |
|
} |
|
} |
|
|
|
Ok(ret) |
|
} |
|
} |
|
|
|
impl<'a> GetServerInfoResponse<&'a str> { |
|
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, Error> { |
|
Ok(CursorMut::new(buf) |
|
.put_bytes(GetServerInfoResponse::HEADER)? |
|
.put_key("p", self.protocol)? |
|
.put_key("map", self.map)? |
|
.put_key("dm", self.dm)? |
|
.put_key("team", self.team)? |
|
.put_key("coop", self.coop)? |
|
.put_key("numcl", self.numcl)? |
|
.put_key("maxcl", self.maxcl)? |
|
.put_key("gamedir", self.gamedir)? |
|
.put_key("password", self.password)? |
|
.put_key("host", self.host)? |
|
.pos()) |
|
} |
|
} |
|
|
|
#[derive(Clone, Debug, PartialEq)] |
|
pub enum Packet<'a> { |
|
Challenge(Challenge), |
|
ServerAdd(ServerAdd<Str<&'a [u8]>>), |
|
ServerRemove, |
|
GetServerInfoResponse(GetServerInfoResponse<Str<&'a [u8]>>), |
|
} |
|
|
|
impl<'a> Packet<'a> { |
|
pub fn decode(src: &'a [u8]) -> Result<Self, Error> { |
|
if let Ok(p) = Challenge::decode(src) { |
|
return Ok(Self::Challenge(p)); |
|
} |
|
|
|
if let Ok(p) = ServerAdd::decode(src) { |
|
return Ok(Self::ServerAdd(p)); |
|
} |
|
|
|
if ServerRemove::decode(src).is_ok() { |
|
return Ok(Self::ServerRemove); |
|
} |
|
|
|
if let Ok(p) = GetServerInfoResponse::decode(src) { |
|
return Ok(Self::GetServerInfoResponse(p)); |
|
} |
|
|
|
Err(Error::InvalidPacket) |
|
} |
|
} |
|
|
|
#[cfg(test)] |
|
mod tests { |
|
use super::*; |
|
|
|
#[test] |
|
fn challenge() { |
|
let p = Challenge::new(0x12345678); |
|
let mut buf = [0; 128]; |
|
let n = p.encode(&mut buf).unwrap(); |
|
assert_eq!(Challenge::decode(&buf[..n]), Ok(p)); |
|
} |
|
|
|
#[test] |
|
fn server_add() { |
|
let p = ServerAdd { |
|
gamedir: "valve", |
|
map: "crossfire", |
|
version: Version::new(0, 20), |
|
product: "foobar", |
|
challenge: 0x12345678, |
|
server_type: ServerType::Dedicated, |
|
os: Os::Linux, |
|
region: Region::RestOfTheWorld, |
|
protocol: 49, |
|
players: 4, |
|
max: 32, |
|
flags: ServerFlags::all(), |
|
}; |
|
let mut buf = [0; 512]; |
|
let n = p.encode(&mut buf).unwrap(); |
|
assert_eq!(ServerAdd::decode(&buf[..n]), Ok(p)); |
|
} |
|
|
|
#[test] |
|
fn server_remove() { |
|
let p = ServerRemove; |
|
let mut buf = [0; 64]; |
|
let n = p.encode(&mut buf).unwrap(); |
|
assert_eq!(ServerRemove::decode(&buf[..n]), Ok(p)); |
|
} |
|
|
|
#[test] |
|
fn get_server_info_response() { |
|
let p = GetServerInfoResponse { |
|
protocol: 49, |
|
map: "crossfire", |
|
dm: true, |
|
team: true, |
|
coop: true, |
|
numcl: 4, |
|
maxcl: 32, |
|
gamedir: "valve", |
|
password: true, |
|
host: "Test", |
|
}; |
|
let mut buf = [0; 512]; |
|
let n = p.encode(&mut buf).unwrap(); |
|
assert_eq!(GetServerInfoResponse::decode(&buf[..n]), Ok(p)); |
|
} |
|
}
|
|
|