Denis Drakhnia
1 year ago
24 changed files with 1968 additions and 904 deletions
@ -1,28 +1,6 @@
@@ -1,28 +1,6 @@
|
||||
[package] |
||||
name = "hlmaster" |
||||
version = "0.1.0" |
||||
license = "GPL-3.0-only" |
||||
authors = ["Denis Drakhnia <numas13@gmail.com>"] |
||||
edition = "2021" |
||||
rust-version = "1.56" |
||||
|
||||
[features] |
||||
default = ["logtime"] |
||||
logtime = ["chrono"] |
||||
|
||||
[dependencies] |
||||
thiserror = "1.0.49" |
||||
getopts = "0.2.21" |
||||
log = "<0.4.19" |
||||
bitflags = "2.4" |
||||
fastrand = "2.0.1" |
||||
serde = { version = "1.0.188", features = ["derive"] } |
||||
toml = "0.5.11" |
||||
|
||||
[dependencies.chrono] |
||||
version = "<0.4.27" |
||||
optional = true |
||||
default-features = false |
||||
features = ["clock"] |
||||
[target.wasm32-unknown-emscripten.dependencies] |
||||
once_cell = { version = "<1.18", optional = true } |
||||
[workspace] |
||||
resolver = "2" |
||||
members = [ |
||||
"protocol", |
||||
"master", |
||||
] |
||||
|
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
[package] |
||||
name = "xash3d-master" |
||||
version = "0.1.0" |
||||
license = "GPL-3.0-only" |
||||
authors = ["Denis Drakhnia <numas13@gmail.com>"] |
||||
edition = "2021" |
||||
rust-version = "1.56" |
||||
|
||||
[features] |
||||
default = ["logtime"] |
||||
logtime = ["chrono"] |
||||
|
||||
[dependencies] |
||||
thiserror = "1.0.49" |
||||
getopts = "0.2.21" |
||||
log = "<0.4.19" |
||||
bitflags = "2.4" |
||||
fastrand = "2.0.1" |
||||
serde = { version = "1.0.188", features = ["derive"] } |
||||
toml = "0.5.11" |
||||
xash3d-protocol = { path = "../protocol", version = "0.1.0" } |
||||
|
||||
[dependencies.chrono] |
||||
version = "<0.4.27" |
||||
optional = true |
||||
default-features = false |
||||
features = ["clock"] |
||||
[target.wasm32-unknown-emscripten.dependencies] |
||||
once_cell = { version = "<1.18", optional = true } |
@ -0,0 +1,287 @@
@@ -0,0 +1,287 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||
|
||||
use std::collections::HashMap; |
||||
use std::io; |
||||
use std::net::{SocketAddr, SocketAddrV4, ToSocketAddrs, UdpSocket}; |
||||
use std::ops::Deref; |
||||
use std::time::Instant; |
||||
|
||||
use fastrand::Rng; |
||||
use log::{error, info, trace, warn}; |
||||
use thiserror::Error; |
||||
use xash3d_protocol::filter::{Filter, Version}; |
||||
use xash3d_protocol::server::Region; |
||||
use xash3d_protocol::ServerInfo; |
||||
use xash3d_protocol::{game, master, server, Error as ProtocolError}; |
||||
|
||||
use crate::config::{self, Config}; |
||||
|
||||
/// The maximum size of UDP packets.
|
||||
const MAX_PACKET_SIZE: usize = 512; |
||||
|
||||
/// How many cleanup calls should be skipped before removing outdated servers.
|
||||
const SERVER_CLEANUP_MAX: usize = 100; |
||||
|
||||
/// How many cleanup calls should be skipped before removing outdated challenges.
|
||||
const CHALLENGE_CLEANUP_MAX: usize = 100; |
||||
|
||||
#[derive(Error, Debug)] |
||||
pub enum Error { |
||||
#[error("Failed to bind server socket: {0}")] |
||||
BindSocket(io::Error), |
||||
#[error(transparent)] |
||||
Protocol(#[from] ProtocolError), |
||||
#[error(transparent)] |
||||
Io(#[from] io::Error), |
||||
} |
||||
|
||||
/// HashMap entry to keep tracking creation time.
|
||||
#[derive(Clone, Debug)] |
||||
struct Entry<T> { |
||||
time: u32, |
||||
value: T, |
||||
} |
||||
|
||||
impl<T> Entry<T> { |
||||
fn new(time: u32, value: T) -> Self { |
||||
Self { time, value } |
||||
} |
||||
|
||||
fn is_valid(&self, now: u32, duration: u32) -> bool { |
||||
(now - self.time) < duration |
||||
} |
||||
} |
||||
|
||||
impl Entry<ServerInfo> { |
||||
fn matches(&self, addr: SocketAddrV4, region: Region, filter: &Filter) -> bool { |
||||
self.region == region && filter.matches(addr, &self.value) |
||||
} |
||||
} |
||||
|
||||
impl<T> Deref for Entry<T> { |
||||
type Target = T; |
||||
|
||||
fn deref(&self) -> &Self::Target { |
||||
&self.value |
||||
} |
||||
} |
||||
|
||||
struct MasterServer { |
||||
sock: UdpSocket, |
||||
challenges: HashMap<SocketAddrV4, Entry<u32>>, |
||||
servers: HashMap<SocketAddrV4, Entry<ServerInfo>>, |
||||
rng: Rng, |
||||
|
||||
start_time: Instant, |
||||
cleanup_challenges: usize, |
||||
cleanup_servers: usize, |
||||
timeout: config::TimeoutConfig, |
||||
|
||||
clver: Version, |
||||
update_title: Box<str>, |
||||
update_map: Box<str>, |
||||
update_addr: SocketAddrV4, |
||||
} |
||||
|
||||
impl MasterServer { |
||||
fn new(cfg: Config) -> Result<Self, Error> { |
||||
let addr = SocketAddr::new(cfg.server.ip, cfg.server.port); |
||||
info!("Listen address: {}", addr); |
||||
let sock = UdpSocket::bind(addr).map_err(Error::BindSocket)?; |
||||
let update_addr = |
||||
cfg.client |
||||
.update_addr |
||||
.unwrap_or_else(|| match sock.local_addr().unwrap() { |
||||
SocketAddr::V4(addr) => addr, |
||||
_ => todo!(), |
||||
}); |
||||
|
||||
Ok(Self { |
||||
sock, |
||||
start_time: Instant::now(), |
||||
challenges: Default::default(), |
||||
servers: Default::default(), |
||||
rng: Rng::new(), |
||||
cleanup_challenges: 0, |
||||
cleanup_servers: 0, |
||||
timeout: cfg.server.timeout, |
||||
clver: cfg.client.version, |
||||
update_title: cfg.client.update_title, |
||||
update_map: cfg.client.update_map, |
||||
update_addr, |
||||
}) |
||||
} |
||||
|
||||
fn run(&mut self) -> Result<(), Error> { |
||||
let mut buf = [0; MAX_PACKET_SIZE]; |
||||
loop { |
||||
let (n, from) = self.sock.recv_from(&mut buf)?; |
||||
let from = match from { |
||||
SocketAddr::V4(a) => a, |
||||
_ => { |
||||
warn!("{}: Received message from IPv6, unimplemented", from); |
||||
continue; |
||||
} |
||||
}; |
||||
|
||||
if let Err(e) = self.handle_packet(from, &buf[..n]) { |
||||
error!("{}: {}", from, e); |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn handle_packet(&mut self, from: SocketAddrV4, src: &[u8]) -> Result<(), Error> { |
||||
if let Ok(p) = server::Packet::decode(src) { |
||||
match p { |
||||
server::Packet::Challenge(p) => { |
||||
trace!("{}: recv {:?}", from, p); |
||||
let master_challenge = self.add_challenge(from); |
||||
let mut buf = [0; MAX_PACKET_SIZE]; |
||||
let p = master::ChallengeResponse::new(master_challenge, p.server_challenge); |
||||
trace!("{}: send {:?}", from, p); |
||||
let n = p.encode(&mut buf)?; |
||||
self.sock.send_to(&buf[..n], from)?; |
||||
self.remove_outdated_challenges(); |
||||
} |
||||
server::Packet::ServerAdd(p) => { |
||||
trace!("{}: recv {:?}", from, p); |
||||
let entry = match self.challenges.get(&from) { |
||||
Some(e) => e, |
||||
None => { |
||||
trace!("{}: Challenge does not exists", from); |
||||
return Ok(()); |
||||
} |
||||
}; |
||||
if !entry.is_valid(self.now(), self.timeout.challenge) { |
||||
return Ok(()); |
||||
} |
||||
if p.challenge != entry.value { |
||||
warn!( |
||||
"{}: Expected challenge {} but received {}", |
||||
from, entry.value, p.challenge |
||||
); |
||||
return Ok(()); |
||||
} |
||||
if self.challenges.remove(&from).is_some() { |
||||
self.add_server(from, ServerInfo::new(&p)); |
||||
} |
||||
self.remove_outdated_servers(); |
||||
} |
||||
_ => { |
||||
trace!("{}: recv {:?}", from, p); |
||||
} |
||||
} |
||||
} |
||||
|
||||
if let Ok(p) = game::Packet::decode(src) { |
||||
match p { |
||||
game::Packet::QueryServers(p) => { |
||||
trace!("{}: recv {:?}", from, p); |
||||
if p.filter.clver < self.clver { |
||||
let iter = std::iter::once(self.update_addr); |
||||
self.send_server_list(from, iter)?; |
||||
} else { |
||||
let now = self.now(); |
||||
let iter = self |
||||
.servers |
||||
.iter() |
||||
.filter(|i| i.1.is_valid(now, self.timeout.server)) |
||||
.filter(|i| i.1.matches(*i.0, p.region, &p.filter)) |
||||
.map(|i| *i.0); |
||||
self.send_server_list(from, iter)?; |
||||
} |
||||
} |
||||
game::Packet::GetServerInfo(p) => { |
||||
trace!("{}: recv {:?}", from, p); |
||||
let p = server::GetServerInfoResponse { |
||||
map: self.update_map.as_ref(), |
||||
host: self.update_title.as_ref(), |
||||
protocol: 49, |
||||
dm: true, |
||||
maxcl: 32, |
||||
gamedir: "valve", |
||||
..Default::default() |
||||
}; |
||||
trace!("{}: send {:?}", from, p); |
||||
let mut buf = [0; MAX_PACKET_SIZE]; |
||||
let n = p.encode(&mut buf)?; |
||||
self.sock.send_to(&buf[..n], from)?; |
||||
} |
||||
} |
||||
} |
||||
|
||||
Ok(()) |
||||
} |
||||
|
||||
fn now(&self) -> u32 { |
||||
self.start_time.elapsed().as_secs() as u32 |
||||
} |
||||
|
||||
fn add_challenge(&mut self, addr: SocketAddrV4) -> u32 { |
||||
let x = self.rng.u32(..); |
||||
let entry = Entry::new(self.now(), x); |
||||
self.challenges.insert(addr, entry); |
||||
x |
||||
} |
||||
|
||||
fn remove_outdated_challenges(&mut self) { |
||||
if self.cleanup_challenges < CHALLENGE_CLEANUP_MAX { |
||||
self.cleanup_challenges += 1; |
||||
return; |
||||
} |
||||
let now = self.now(); |
||||
let old = self.challenges.len(); |
||||
self.challenges |
||||
.retain(|_, v| v.is_valid(now, self.timeout.challenge)); |
||||
let new = self.challenges.len(); |
||||
if old != new { |
||||
trace!("Removed {} outdated challenges", old - new); |
||||
} |
||||
self.cleanup_challenges = 0; |
||||
} |
||||
|
||||
fn add_server(&mut self, addr: SocketAddrV4, server: ServerInfo) { |
||||
match self.servers.insert(addr, Entry::new(self.now(), server)) { |
||||
Some(_) => trace!("{}: Updated GameServer", addr), |
||||
None => trace!("{}: New GameServer", addr), |
||||
} |
||||
} |
||||
|
||||
fn remove_outdated_servers(&mut self) { |
||||
if self.cleanup_servers < SERVER_CLEANUP_MAX { |
||||
self.cleanup_servers += 1; |
||||
return; |
||||
} |
||||
let now = self.now(); |
||||
let old = self.servers.len(); |
||||
self.servers |
||||
.retain(|_, v| v.is_valid(now, self.timeout.server)); |
||||
let new = self.servers.len(); |
||||
if old != new { |
||||
trace!("Removed {} outdated servers", old - new); |
||||
} |
||||
self.cleanup_servers = 0; |
||||
} |
||||
|
||||
fn send_server_list<A, I>(&self, to: A, iter: I) -> Result<(), Error> |
||||
where |
||||
A: ToSocketAddrs, |
||||
I: Iterator<Item = SocketAddrV4>, |
||||
{ |
||||
let mut list = master::QueryServersResponse::new(iter); |
||||
loop { |
||||
let mut buf = [0; MAX_PACKET_SIZE]; |
||||
let (n, is_end) = list.encode(&mut buf)?; |
||||
self.sock.send_to(&buf[..n], &to)?; |
||||
if is_end { |
||||
break; |
||||
} |
||||
} |
||||
Ok(()) |
||||
} |
||||
} |
||||
|
||||
pub fn run(cfg: Config) -> Result<(), Error> { |
||||
MasterServer::new(cfg)?.run() |
||||
} |
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
[package] |
||||
name = "xash3d-protocol" |
||||
version = "0.1.0" |
||||
license = "GPL-3.0-only" |
||||
authors = ["Denis Drakhnia <numas13@gmail.com>"] |
||||
edition = "2021" |
||||
rust-version = "1.56" |
||||
|
||||
[dependencies] |
||||
thiserror = "1.0.49" |
||||
log = "<0.4.19" |
||||
bitflags = "2.4" |
@ -0,0 +1,82 @@
@@ -0,0 +1,82 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||
|
||||
use crate::cursor::{Cursor, CursorMut}; |
||||
use crate::types::Str; |
||||
use crate::Error; |
||||
|
||||
pub const HASH_LEN: usize = 64; |
||||
|
||||
#[derive(Clone, Debug, PartialEq)] |
||||
pub struct AdminChallenge; |
||||
|
||||
impl AdminChallenge { |
||||
pub const HEADER: &'static [u8] = b"adminchallenge"; |
||||
|
||||
pub fn decode(src: &[u8]) -> Result<Self, Error> { |
||||
if src == Self::HEADER { |
||||
Ok(Self) |
||||
} else { |
||||
Err(Error::InvalidPacket) |
||||
} |
||||
} |
||||
|
||||
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, Error> { |
||||
Ok(CursorMut::new(buf).put_bytes(Self::HEADER)?.pos()) |
||||
} |
||||
} |
||||
|
||||
#[derive(Clone, Debug, PartialEq)] |
||||
pub struct AdminCommand<'a> { |
||||
pub hash: &'a [u8], |
||||
pub command: Str<&'a [u8]>, |
||||
} |
||||
|
||||
impl<'a> AdminCommand<'a> { |
||||
pub const HEADER: &'static [u8] = b"admin"; |
||||
|
||||
pub fn new(hash: &'a [u8], command: &'a str) -> Self { |
||||
Self { |
||||
hash, |
||||
command: Str(command.as_bytes()), |
||||
} |
||||
} |
||||
|
||||
pub fn decode(src: &'a [u8]) -> Result<Self, Error> { |
||||
let mut cur = Cursor::new(src); |
||||
cur.expect(Self::HEADER)?; |
||||
let hash = cur.get_bytes(HASH_LEN)?; |
||||
let command = Str(cur.get_bytes(cur.remaining())?); |
||||
cur.expect_empty()?; |
||||
Ok(Self { hash, command }) |
||||
} |
||||
|
||||
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, Error> { |
||||
Ok(CursorMut::new(buf) |
||||
.put_bytes(Self::HEADER)? |
||||
.put_bytes(self.hash)? |
||||
.put_bytes(&self.command)? |
||||
.pos()) |
||||
} |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
use super::*; |
||||
|
||||
#[test] |
||||
fn admin_challenge() { |
||||
let p = AdminChallenge; |
||||
let mut buf = [0; 512]; |
||||
let n = p.encode(&mut buf).unwrap(); |
||||
assert_eq!(AdminChallenge::decode(&buf[..n]), Ok(p)); |
||||
} |
||||
|
||||
#[test] |
||||
fn admin_command() { |
||||
let p = AdminCommand::new(&[1; HASH_LEN], "foo bar baz"); |
||||
let mut buf = [0; 512]; |
||||
let n = p.encode(&mut buf).unwrap(); |
||||
assert_eq!(AdminCommand::decode(&buf[..n]), Ok(p)); |
||||
} |
||||
} |
@ -0,0 +1,516 @@
@@ -0,0 +1,516 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||
|
||||
use std::fmt; |
||||
use std::io::{self, Write as _}; |
||||
use std::mem; |
||||
use std::slice; |
||||
use std::str; |
||||
|
||||
use super::types::Str; |
||||
use super::Error; |
||||
|
||||
pub trait GetKeyValue<'a>: Sized { |
||||
fn get_key_value(cur: &mut Cursor<'a>) -> Result<Self, Error>; |
||||
} |
||||
|
||||
impl<'a> GetKeyValue<'a> for &'a [u8] { |
||||
fn get_key_value(cur: &mut Cursor<'a>) -> Result<Self, Error> { |
||||
cur.get_key_value_raw() |
||||
} |
||||
} |
||||
|
||||
impl<'a> GetKeyValue<'a> for Str<&'a [u8]> { |
||||
fn get_key_value(cur: &mut Cursor<'a>) -> Result<Self, Error> { |
||||
cur.get_key_value_raw().map(Str) |
||||
} |
||||
} |
||||
|
||||
impl<'a> GetKeyValue<'a> for &'a str { |
||||
fn get_key_value(cur: &mut Cursor<'a>) -> Result<Self, Error> { |
||||
let raw = cur.get_key_value_raw()?; |
||||
str::from_utf8(raw).map_err(|_| Error::InvalidString) |
||||
} |
||||
} |
||||
|
||||
impl<'a> GetKeyValue<'a> for bool { |
||||
fn get_key_value(cur: &mut Cursor<'a>) -> Result<Self, Error> { |
||||
match cur.get_key_value_raw()? { |
||||
b"0" => Ok(false), |
||||
b"1" => Ok(true), |
||||
_ => Err(Error::InvalidPacket), |
||||
} |
||||
} |
||||
} |
||||
|
||||
macro_rules! impl_get_value { |
||||
($($t:ty),+ $(,)?) => { |
||||
$(impl<'a> GetKeyValue<'a> for $t { |
||||
fn get_key_value(cur: &mut Cursor<'a>) -> Result<Self, Error> { |
||||
cur.get_key_value::<&str>()?.parse().map_err(|_| Error::InvalidPacket) |
||||
} |
||||
})+ |
||||
}; |
||||
} |
||||
|
||||
impl_get_value! { |
||||
u8, |
||||
u16, |
||||
u32, |
||||
u64, |
||||
|
||||
i8, |
||||
i16, |
||||
i32, |
||||
i64, |
||||
} |
||||
|
||||
// TODO: impl GetKeyValue for f32 and f64
|
||||
|
||||
#[derive(Copy, Clone)] |
||||
pub struct Cursor<'a> { |
||||
buffer: &'a [u8], |
||||
} |
||||
|
||||
macro_rules! impl_get { |
||||
($($n:ident: $t:ty = $f:ident),+ $(,)?) => ( |
||||
$(#[inline] |
||||
pub fn $n(&mut self) -> Result<$t, Error> { |
||||
const N: usize = mem::size_of::<$t>(); |
||||
self.get_array::<N>().map(<$t>::$f) |
||||
})+ |
||||
); |
||||
} |
||||
|
||||
impl<'a> Cursor<'a> { |
||||
pub fn new(buffer: &'a [u8]) -> Self { |
||||
Self { buffer } |
||||
} |
||||
|
||||
pub fn end(self) -> &'a [u8] { |
||||
self.buffer |
||||
} |
||||
|
||||
#[inline(always)] |
||||
pub fn remaining(&self) -> usize { |
||||
self.buffer.len() |
||||
} |
||||
|
||||
#[inline(always)] |
||||
pub fn has_remaining(&self) -> bool { |
||||
self.remaining() != 0 |
||||
} |
||||
|
||||
pub fn get_bytes(&mut self, count: usize) -> Result<&'a [u8], Error> { |
||||
if count <= self.remaining() { |
||||
let (head, tail) = self.buffer.split_at(count); |
||||
self.buffer = tail; |
||||
Ok(head) |
||||
} else { |
||||
Err(Error::UnexpectedEnd) |
||||
} |
||||
} |
||||
|
||||
pub fn advance(&mut self, count: usize) -> Result<(), Error> { |
||||
self.get_bytes(count).map(|_| ()) |
||||
} |
||||
|
||||
pub fn get_array<const N: usize>(&mut self) -> Result<[u8; N], Error> { |
||||
self.get_bytes(N).map(|s| { |
||||
let mut array = [0; N]; |
||||
array.copy_from_slice(s); |
||||
array |
||||
}) |
||||
} |
||||
|
||||
pub fn get_str(&mut self, n: usize) -> Result<&'a str, Error> { |
||||
let mut cur = *self; |
||||
let s = cur |
||||
.get_bytes(n) |
||||
.and_then(|s| str::from_utf8(s).map_err(|_| Error::InvalidString))?; |
||||
*self = cur; |
||||
Ok(s) |
||||
} |
||||
|
||||
pub fn get_cstr(&mut self) -> Result<Str<&'a [u8]>, Error> { |
||||
let pos = self |
||||
.buffer |
||||
.iter() |
||||
.position(|&c| c == b'\0') |
||||
.ok_or(Error::UnexpectedEnd)?; |
||||
let (head, tail) = self.buffer.split_at(pos); |
||||
self.buffer = &tail[1..]; |
||||
Ok(Str(&head[..pos])) |
||||
} |
||||
|
||||
pub fn get_cstr_as_str(&mut self) -> Result<&'a str, Error> { |
||||
str::from_utf8(&self.get_cstr()?).map_err(|_| Error::InvalidString) |
||||
} |
||||
|
||||
#[inline(always)] |
||||
pub fn get_u8(&mut self) -> Result<u8, Error> { |
||||
self.get_array::<1>().map(|s| s[0]) |
||||
} |
||||
|
||||
#[inline(always)] |
||||
pub fn get_i8(&mut self) -> Result<i8, Error> { |
||||
self.get_array::<1>().map(|s| s[0] as i8) |
||||
} |
||||
|
||||
impl_get! { |
||||
get_u16_le: u16 = from_le_bytes, |
||||
get_u32_le: u32 = from_le_bytes, |
||||
get_u64_le: u64 = from_le_bytes, |
||||
get_i16_le: i16 = from_le_bytes, |
||||
get_i32_le: i32 = from_le_bytes, |
||||
get_i64_le: i64 = from_le_bytes, |
||||
get_f32_le: f32 = from_le_bytes, |
||||
get_f64_le: f64 = from_le_bytes, |
||||
|
||||
get_u16_be: u16 = from_be_bytes, |
||||
get_u32_be: u32 = from_be_bytes, |
||||
get_u64_be: u64 = from_be_bytes, |
||||
get_i16_be: i16 = from_be_bytes, |
||||
get_i32_be: i32 = from_be_bytes, |
||||
get_i64_be: i64 = from_be_bytes, |
||||
get_f32_be: f32 = from_be_bytes, |
||||
get_f64_be: f64 = from_be_bytes, |
||||
|
||||
get_u16_ne: u16 = from_ne_bytes, |
||||
get_u32_ne: u32 = from_ne_bytes, |
||||
get_u64_ne: u64 = from_ne_bytes, |
||||
get_i16_ne: i16 = from_ne_bytes, |
||||
get_i32_ne: i32 = from_ne_bytes, |
||||
get_i64_ne: i64 = from_ne_bytes, |
||||
get_f32_ne: f32 = from_ne_bytes, |
||||
get_f64_ne: f64 = from_ne_bytes, |
||||
} |
||||
|
||||
pub fn expect(&mut self, s: &[u8]) -> Result<(), Error> { |
||||
if self.buffer.starts_with(s) { |
||||
self.advance(s.len())?; |
||||
Ok(()) |
||||
} else { |
||||
Err(Error::InvalidPacket) |
||||
} |
||||
} |
||||
|
||||
pub fn expect_empty(&self) -> Result<(), Error> { |
||||
if self.has_remaining() { |
||||
Err(Error::InvalidPacket) |
||||
} else { |
||||
Ok(()) |
||||
} |
||||
} |
||||
|
||||
pub fn take_while<F>(&mut self, mut cond: F) -> Result<&'a [u8], Error> |
||||
where |
||||
F: FnMut(u8) -> bool, |
||||
{ |
||||
self.buffer |
||||
.iter() |
||||
.position(|&i| !cond(i)) |
||||
.ok_or(Error::UnexpectedEnd) |
||||
.and_then(|n| self.get_bytes(n)) |
||||
} |
||||
|
||||
pub fn take_while_or_all<F>(&mut self, cond: F) -> &'a [u8] |
||||
where |
||||
F: FnMut(u8) -> bool, |
||||
{ |
||||
self.take_while(cond).unwrap_or_else(|_| { |
||||
let (head, tail) = self.buffer.split_at(self.buffer.len()); |
||||
self.buffer = tail; |
||||
head |
||||
}) |
||||
} |
||||
|
||||
pub fn get_key_value_raw(&mut self) -> Result<&'a [u8], Error> { |
||||
let mut cur = *self; |
||||
if cur.get_u8()? == b'\\' { |
||||
let value = cur.take_while_or_all(|c| c != b'\\' && c != b'\n'); |
||||
*self = cur; |
||||
Ok(value) |
||||
} else { |
||||
Err(Error::InvalidPacket) |
||||
} |
||||
} |
||||
|
||||
pub fn get_key_value<T: GetKeyValue<'a>>(&mut self) -> Result<T, Error> { |
||||
T::get_key_value(self) |
||||
} |
||||
|
||||
pub fn get_key_raw(&mut self) -> Result<&'a [u8], Error> { |
||||
let mut cur = *self; |
||||
if cur.get_u8()? == b'\\' { |
||||
let value = cur.take_while(|c| c != b'\\' && c != b'\n')?; |
||||
*self = cur; |
||||
Ok(value) |
||||
} else { |
||||
Err(Error::InvalidPacket) |
||||
} |
||||
} |
||||
|
||||
pub fn get_key<T: GetKeyValue<'a>>(&mut self) -> Result<(&'a [u8], T), Error> { |
||||
Ok((self.get_key_raw()?, self.get_key_value()?)) |
||||
} |
||||
} |
||||
|
||||
pub trait PutKeyValue { |
||||
fn put_key_value<'a, 'b>( |
||||
&self, |
||||
cur: &'b mut CursorMut<'a>, |
||||
) -> Result<&'b mut CursorMut<'a>, Error>; |
||||
} |
||||
|
||||
impl PutKeyValue for &str { |
||||
fn put_key_value<'a, 'b>( |
||||
&self, |
||||
cur: &'b mut CursorMut<'a>, |
||||
) -> Result<&'b mut CursorMut<'a>, Error> { |
||||
cur.put_str(self) |
||||
} |
||||
} |
||||
|
||||
impl PutKeyValue for bool { |
||||
fn put_key_value<'a, 'b>( |
||||
&self, |
||||
cur: &'b mut CursorMut<'a>, |
||||
) -> Result<&'b mut CursorMut<'a>, Error> { |
||||
cur.put_u8(if *self { b'1' } else { b'0' }) |
||||
} |
||||
} |
||||
|
||||
macro_rules! impl_put_key_value { |
||||
($($t:ty),+ $(,)?) => { |
||||
$(impl PutKeyValue for $t { |
||||
fn put_key_value<'a, 'b>(&self, cur: &'b mut CursorMut<'a>) -> Result<&'b mut CursorMut<'a>, Error> { |
||||
cur.put_as_str(self) |
||||
} |
||||
})+ |
||||
}; |
||||
} |
||||
|
||||
impl_put_key_value! { |
||||
u8, |
||||
u16, |
||||
u32, |
||||
u64, |
||||
|
||||
i8, |
||||
i16, |
||||
i32, |
||||
i64, |
||||
|
||||
f32, |
||||
f64, |
||||
} |
||||
|
||||
pub struct CursorMut<'a> { |
||||
buffer: &'a [u8], |
||||
buffer_mut: &'a mut [u8], |
||||
} |
||||
|
||||
macro_rules! impl_put { |
||||
($($n:ident: $t:ty = $f:ident),+ $(,)?) => ( |
||||
$(#[inline] |
||||
pub fn $n(&mut self, n: $t) -> Result<&mut Self, Error> { |
||||
self.put_array(&n.$f()) |
||||
})+ |
||||
); |
||||
} |
||||
|
||||
impl<'a> CursorMut<'a> { |
||||
pub fn new(buffer: &'a mut [u8]) -> Self { |
||||
Self { |
||||
buffer: unsafe { slice::from_raw_parts(buffer.as_ptr(), 0) }, |
||||
buffer_mut: buffer, |
||||
} |
||||
} |
||||
|
||||
pub fn buffer(&self) -> &'a [u8] { |
||||
self.buffer |
||||
} |
||||
|
||||
pub fn buffer_mut<'b: 'a>(&'b mut self) -> &'a mut [u8] { |
||||
self.buffer_mut |
||||
} |
||||
|
||||
pub fn end(self) -> (&'a [u8], &'a mut [u8]) { |
||||
(self.buffer, self.buffer_mut) |
||||
} |
||||
|
||||
pub fn pos(&mut self) -> usize { |
||||
self.buffer.len() |
||||
} |
||||
|
||||
#[inline(always)] |
||||
pub fn remaining(&self) -> usize { |
||||
self.buffer_mut.len() |
||||
} |
||||
|
||||
pub fn advance<F>(&mut self, count: usize, mut f: F) -> Result<&mut Self, Error> |
||||
where |
||||
F: FnMut(&'a mut [u8]), |
||||
{ |
||||
if count <= self.remaining() { |
||||
let buffer_mut = mem::take(&mut self.buffer_mut); |
||||
let (head, tail) = buffer_mut.split_at_mut(count); |
||||
f(head); |
||||
self.buffer = |
||||
unsafe { slice::from_raw_parts(self.buffer.as_ptr(), self.buffer.len() + count) }; |
||||
self.buffer_mut = tail; |
||||
Ok(self) |
||||
} else { |
||||
Err(Error::UnexpectedEnd) |
||||
} |
||||
} |
||||
|
||||
pub fn put_bytes(&mut self, s: &[u8]) -> Result<&mut Self, Error> { |
||||
self.advance(s.len(), |i| { |
||||
i.copy_from_slice(s); |
||||
}) |
||||
} |
||||
|
||||
pub fn put_array<const N: usize>(&mut self, s: &[u8; N]) -> Result<&mut Self, Error> { |
||||
self.advance(N, |i| { |
||||
i.copy_from_slice(s); |
||||
}) |
||||
} |
||||
|
||||
pub fn put_str(&mut self, s: &str) -> Result<&mut Self, Error> { |
||||
self.put_bytes(s.as_bytes()) |
||||
} |
||||
|
||||
pub fn put_cstr(&mut self, s: &str) -> Result<&mut Self, Error> { |
||||
self.put_str(s)?.put_u8(0) |
||||
} |
||||
|
||||
#[inline(always)] |
||||
pub fn put_u8(&mut self, n: u8) -> Result<&mut Self, Error> { |
||||
self.put_array(&[n]) |
||||
} |
||||
|
||||
#[inline(always)] |
||||
pub fn put_i8(&mut self, n: i8) -> Result<&mut Self, Error> { |
||||
self.put_u8(n as u8) |
||||
} |
||||
|
||||
impl_put! { |
||||
put_u16_le: u16 = to_le_bytes, |
||||
put_u32_le: u32 = to_le_bytes, |
||||
put_u64_le: u64 = to_le_bytes, |
||||
put_i16_le: i16 = to_le_bytes, |
||||
put_i32_le: i32 = to_le_bytes, |
||||
put_i64_le: i64 = to_le_bytes, |
||||
put_f32_le: f32 = to_le_bytes, |
||||
put_f64_le: f64 = to_le_bytes, |
||||
|
||||
put_u16_be: u16 = to_be_bytes, |
||||
put_u32_be: u32 = to_be_bytes, |
||||
put_u64_be: u64 = to_be_bytes, |
||||
put_i16_be: i16 = to_be_bytes, |
||||
put_i32_be: i32 = to_be_bytes, |
||||
put_i64_be: i64 = to_be_bytes, |
||||
put_f32_be: f32 = to_be_bytes, |
||||
put_f64_be: f64 = to_be_bytes, |
||||
|
||||
put_u16_ne: u16 = to_ne_bytes, |
||||
put_u32_ne: u32 = to_ne_bytes, |
||||
put_u64_ne: u64 = to_ne_bytes, |
||||
put_i16_ne: i16 = to_ne_bytes, |
||||
put_i32_ne: i32 = to_ne_bytes, |
||||
put_i64_ne: i64 = to_ne_bytes, |
||||
put_f32_ne: f32 = to_ne_bytes, |
||||
put_f64_ne: f64 = to_ne_bytes, |
||||
} |
||||
|
||||
pub fn put_as_str<T: fmt::Display>(&mut self, value: T) -> Result<&mut Self, Error> { |
||||
let mut cur = io::Cursor::new(mem::take(&mut self.buffer_mut)); |
||||
write!(&mut cur, "{}", value).map_err(|_| Error::UnexpectedEnd)?; |
||||
let n = cur.position() as usize; |
||||
self.buffer_mut = cur.into_inner(); |
||||
self.advance(n, |_| {}) |
||||
} |
||||
|
||||
pub fn put_key_value<T: PutKeyValue>(&mut self, value: T) -> Result<&mut Self, Error> { |
||||
value.put_key_value(self) |
||||
} |
||||
|
||||
pub fn put_key_raw(&mut self, key: &str, value: &[u8]) -> Result<&mut Self, Error> { |
||||
self.put_u8(b'\\')? |
||||
.put_str(key)? |
||||
.put_u8(b'\\')? |
||||
.put_bytes(value) |
||||
} |
||||
|
||||
pub fn put_key<T: PutKeyValue>(&mut self, key: &str, value: T) -> Result<&mut Self, Error> { |
||||
self.put_u8(b'\\')? |
||||
.put_str(key)? |
||||
.put_u8(b'\\')? |
||||
.put_key_value(value) |
||||
} |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
use super::*; |
||||
|
||||
#[test] |
||||
fn cursor() -> Result<(), Error> { |
||||
let mut buf = [0; 64]; |
||||
let s = CursorMut::new(&mut buf) |
||||
.put_bytes(b"12345678")? |
||||
.put_array(b"4321")? |
||||
.put_str("abc")? |
||||
.put_cstr("def")? |
||||
.put_u8(0x7f)? |
||||
.put_i8(-128)? |
||||
.put_u32_le(0x44332211)? |
||||
.buffer(); |
||||
|
||||
let mut cur = Cursor::new(s); |
||||
assert_eq!(cur.get_bytes(8), Ok(&b"12345678"[..])); |
||||
assert_eq!(cur.get_array::<4>(), Ok(*b"4321")); |
||||
assert_eq!(cur.get_str(3), Ok("abc")); |
||||
assert_eq!(cur.get_cstr(), Ok(Str(&b"def"[..]))); |
||||
assert_eq!(cur.get_u8(), Ok(0x7f)); |
||||
assert_eq!(cur.get_i8(), Ok(-128)); |
||||
assert_eq!(cur.get_u32_le(), Ok(0x44332211)); |
||||
assert_eq!(cur.get_u8(), Err(Error::UnexpectedEnd)); |
||||
|
||||
Ok(()) |
||||
} |
||||
|
||||
#[test] |
||||
fn key() -> Result<(), Error> { |
||||
let mut buf = [0; 512]; |
||||
let s = CursorMut::new(&mut buf) |
||||
.put_key("p", 49)? |
||||
.put_key("map", "crossfire")? |
||||
.put_key("dm", true)? |
||||
.put_key("team", false)? |
||||
.put_key("coop", false)? |
||||
.put_key("numcl", 4)? |
||||
.put_key("maxcl", 32)? |
||||
.put_key("gamedir", "valve")? |
||||
.put_key("password", false)? |
||||
.put_key("host", "test")? |
||||
.buffer(); |
||||
|
||||
let mut cur = Cursor::new(s); |
||||
assert_eq!(cur.get_key(), Ok((&b"p"[..], 49_u8))); |
||||
assert_eq!(cur.get_key(), Ok((&b"map"[..], "crossfire"))); |
||||
assert_eq!(cur.get_key(), Ok((&b"dm"[..], true))); |
||||
assert_eq!(cur.get_key(), Ok((&b"team"[..], false))); |
||||
assert_eq!(cur.get_key(), Ok((&b"coop"[..], false))); |
||||
assert_eq!(cur.get_key(), Ok((&b"numcl"[..], 4_u8))); |
||||
assert_eq!(cur.get_key(), Ok((&b"maxcl"[..], 32_u8))); |
||||
assert_eq!(cur.get_key(), Ok((&b"gamedir"[..], "valve"))); |
||||
assert_eq!(cur.get_key(), Ok((&b"password"[..], false))); |
||||
assert_eq!(cur.get_key(), Ok((&b"host"[..], "test"))); |
||||
assert_eq!(cur.get_key::<&[u8]>(), Err(Error::UnexpectedEnd)); |
||||
|
||||
Ok(()) |
||||
} |
||||
} |
@ -0,0 +1,128 @@
@@ -0,0 +1,128 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||
|
||||
use std::net::SocketAddrV4; |
||||
|
||||
use crate::cursor::{Cursor, CursorMut}; |
||||
use crate::filter::Filter; |
||||
use crate::server::Region; |
||||
use crate::Error; |
||||
|
||||
#[derive(Clone, Debug, PartialEq)] |
||||
pub struct QueryServers<'a> { |
||||
pub region: Region, |
||||
pub last: SocketAddrV4, |
||||
pub filter: Filter<'a>, |
||||
} |
||||
|
||||
impl<'a> QueryServers<'a> { |
||||
pub const HEADER: &'static [u8] = b"1"; |
||||
|
||||
pub fn decode(src: &'a [u8]) -> Result<Self, Error> { |
||||
let mut cur = Cursor::new(src); |
||||
cur.expect(Self::HEADER)?; |
||||
let region = cur.get_u8()?.try_into().map_err(|_| Error::InvalidPacket)?; |
||||
let last = cur.get_cstr_as_str()?; |
||||
let filter = cur.get_cstr()?; |
||||
cur.expect_empty()?; |
||||
Ok(Self { |
||||
region, |
||||
last: last.parse().map_err(|_| Error::InvalidPacket)?, |
||||
filter: Filter::from_bytes(&filter)?, |
||||
}) |
||||
} |
||||
|
||||
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, Error> { |
||||
Ok(CursorMut::new(buf) |
||||
.put_bytes(Self::HEADER)? |
||||
.put_u8(self.region as u8)? |
||||
.put_as_str(self.last)? |
||||
.put_u8(0)? |
||||
.put_as_str(&self.filter)? |
||||
.put_u8(0)? |
||||
.pos()) |
||||
} |
||||
} |
||||
|
||||
#[derive(Clone, Debug, PartialEq)] |
||||
pub struct GetServerInfo { |
||||
pub protocol: u8, |
||||
} |
||||
|
||||
impl GetServerInfo { |
||||
pub const HEADER: &'static [u8] = b"\xff\xff\xff\xffinfo "; |
||||
|
||||
pub fn new(protocol: u8) -> Self { |
||||
Self { protocol } |
||||
} |
||||
|
||||
pub fn decode(src: &[u8]) -> Result<Self, Error> { |
||||
let mut cur = Cursor::new(src); |
||||
cur.expect(Self::HEADER)?; |
||||
let protocol = cur |
||||
.get_str(cur.remaining())? |
||||
.parse() |
||||
.map_err(|_| Error::InvalidPacket)?; |
||||
Ok(Self { protocol }) |
||||
} |
||||
|
||||
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, Error> { |
||||
Ok(CursorMut::new(buf) |
||||
.put_bytes(Self::HEADER)? |
||||
.put_as_str(self.protocol)? |
||||
.pos()) |
||||
} |
||||
} |
||||
|
||||
#[derive(Clone, Debug, PartialEq)] |
||||
pub enum Packet<'a> { |
||||
QueryServers(QueryServers<'a>), |
||||
GetServerInfo(GetServerInfo), |
||||
} |
||||
|
||||
impl<'a> Packet<'a> { |
||||
pub fn decode(src: &'a [u8]) -> Result<Self, Error> { |
||||
if let Ok(p) = QueryServers::decode(src) { |
||||
return Ok(Self::QueryServers(p)); |
||||
} |
||||
|
||||
if let Ok(p) = GetServerInfo::decode(src) { |
||||
return Ok(Self::GetServerInfo(p)); |
||||
} |
||||
|
||||
Err(Error::InvalidPacket) |
||||
} |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
use super::*; |
||||
use crate::filter::{FilterFlags, Version}; |
||||
use std::net::Ipv4Addr; |
||||
|
||||
#[test] |
||||
fn query_servers() { |
||||
let p = QueryServers { |
||||
region: Region::RestOfTheWorld, |
||||
last: SocketAddrV4::new(Ipv4Addr::new(0, 0, 0, 0), 0), |
||||
filter: Filter { |
||||
gamedir: &b"valve"[..], |
||||
map: &b"crossfire"[..], |
||||
clver: Version::new(0, 20), |
||||
flags: FilterFlags::all(), |
||||
flags_mask: FilterFlags::all(), |
||||
}, |
||||
}; |
||||
let mut buf = [0; 512]; |
||||
let n = p.encode(&mut buf).unwrap(); |
||||
assert_eq!(QueryServers::decode(&buf[..n]), Ok(p)); |
||||
} |
||||
|
||||
#[test] |
||||
fn get_server_info() { |
||||
let p = GetServerInfo::new(49); |
||||
let mut buf = [0; 512]; |
||||
let n = p.encode(&mut buf).unwrap(); |
||||
assert_eq!(GetServerInfo::decode(&buf[..n]), Ok(p)); |
||||
} |
||||
} |
@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||
|
||||
mod cursor; |
||||
mod server_info; |
||||
|
||||
pub mod admin; |
||||
pub mod filter; |
||||
pub mod game; |
||||
pub mod master; |
||||
pub mod server; |
||||
pub mod types; |
||||
|
||||
pub use server_info::ServerInfo; |
||||
|
||||
use thiserror::Error; |
||||
|
||||
pub const VERSION: u32 = 49; |
||||
|
||||
#[derive(Error, Debug, PartialEq, Eq)] |
||||
pub enum Error { |
||||
#[error("Invalid packet")] |
||||
InvalidPacket, |
||||
#[error("Invalid UTF-8 string")] |
||||
InvalidString, |
||||
#[error("Unexpected end of buffer")] |
||||
UnexpectedEnd, |
||||
} |
@ -0,0 +1,170 @@
@@ -0,0 +1,170 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||
|
||||
use std::net::{Ipv4Addr, SocketAddrV4}; |
||||
|
||||
use super::cursor::{Cursor, CursorMut}; |
||||
use super::Error; |
||||
|
||||
#[derive(Clone, Debug, PartialEq)] |
||||
pub struct ChallengeResponse { |
||||
pub master_challenge: u32, |
||||
pub server_challenge: u32, |
||||
} |
||||
|
||||
impl ChallengeResponse { |
||||
pub const HEADER: &'static [u8] = b"\xff\xff\xff\xffs\n"; |
||||
|
||||
pub fn new(master_challenge: u32, server_challenge: u32) -> Self { |
||||
Self { |
||||
master_challenge, |
||||
server_challenge, |
||||
} |
||||
} |
||||
|
||||
pub fn decode(src: &[u8]) -> Result<Self, Error> { |
||||
let mut cur = Cursor::new(src); |
||||
cur.expect(Self::HEADER)?; |
||||
let master_challenge = cur.get_u32_le()?; |
||||
let server_challenge = cur.get_u32_le()?; |
||||
cur.expect_empty()?; |
||||
Ok(Self { |
||||
master_challenge, |
||||
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.master_challenge)? |
||||
.put_u32_le(self.server_challenge)? |
||||
.pos()) |
||||
} |
||||
} |
||||
|
||||
#[derive(Clone, Debug, PartialEq)] |
||||
pub struct QueryServersResponse<I> { |
||||
inner: I, |
||||
} |
||||
|
||||
impl QueryServersResponse<()> { |
||||
pub const HEADER: &'static [u8] = b"\xff\xff\xff\xfff\n"; |
||||
} |
||||
|
||||
impl<'a> QueryServersResponse<&'a [u8]> { |
||||
pub fn decode(src: &'a [u8]) -> Result<Self, Error> { |
||||
let mut cur = Cursor::new(src); |
||||
cur.expect(QueryServersResponse::HEADER)?; |
||||
if cur.remaining() % 6 != 0 { |
||||
return Err(Error::InvalidPacket); |
||||
} |
||||
let s = cur.get_bytes(cur.remaining())?; |
||||
let inner = if s.ends_with(&[0; 6]) { |
||||
&s[..s.len() - 6] |
||||
} else { |
||||
s |
||||
}; |
||||
Ok(Self { inner }) |
||||
} |
||||
|
||||
pub fn iter(&self) -> impl 'a + Iterator<Item = SocketAddrV4> { |
||||
let mut cur = Cursor::new(self.inner); |
||||
(0..self.inner.len() / 6).map(move |_| { |
||||
let ip = Ipv4Addr::from(cur.get_array().unwrap()); |
||||
let port = cur.get_u16_be().unwrap(); |
||||
SocketAddrV4::new(ip, port) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
impl<I> QueryServersResponse<I> |
||||
where |
||||
I: Iterator<Item = SocketAddrV4>, |
||||
{ |
||||
pub fn new(iter: I) -> Self { |
||||
Self { inner: iter } |
||||
} |
||||
|
||||
pub fn encode(&mut self, buf: &mut [u8]) -> Result<(usize, bool), Error> { |
||||
let mut cur = CursorMut::new(buf); |
||||
cur.put_bytes(QueryServersResponse::HEADER)?; |
||||
let mut is_end = false; |
||||
while cur.remaining() >= 12 { |
||||
match self.inner.next() { |
||||
Some(i) => { |
||||
cur.put_array(&i.ip().octets())?.put_u16_be(i.port())?; |
||||
} |
||||
None => { |
||||
is_end = true; |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
Ok((cur.put_array(&[0; 6])?.pos(), is_end)) |
||||
} |
||||
} |
||||
|
||||
#[derive(Clone, Debug, PartialEq)] |
||||
pub struct AdminChallengeResponse { |
||||
pub challenge: u32, |
||||
} |
||||
|
||||
impl AdminChallengeResponse { |
||||
pub const HEADER: &'static [u8] = b"\xff\xff\xff\xffadminchallenge"; |
||||
|
||||
pub fn new(challenge: u32) -> Self { |
||||
Self { challenge } |
||||
} |
||||
|
||||
pub fn decode(src: &[u8]) -> Result<Self, Error> { |
||||
let mut cur = Cursor::new(src); |
||||
cur.expect(Self::HEADER)?; |
||||
let challenge = cur.get_u32_le()?; |
||||
cur.expect_empty()?; |
||||
Ok(Self { challenge }) |
||||
} |
||||
|
||||
pub fn encode(&self, buf: &mut [u8]) -> Result<usize, Error> { |
||||
Ok(CursorMut::new(buf) |
||||
.put_bytes(Self::HEADER)? |
||||
.put_u32_le(self.challenge)? |
||||
.pos()) |
||||
} |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
use super::*; |
||||
|
||||
#[test] |
||||
fn challenge_response() { |
||||
let p = ChallengeResponse::new(0x12345678, 0x87654321); |
||||
let mut buf = [0; 512]; |
||||
let n = p.encode(&mut buf).unwrap(); |
||||
assert_eq!(ChallengeResponse::decode(&buf[..n]), Ok(p)); |
||||
} |
||||
|
||||
#[test] |
||||
fn query_servers_response() { |
||||
let servers: &[SocketAddrV4] = &[ |
||||
"1.2.3.4:27001".parse().unwrap(), |
||||
"1.2.3.4:27002".parse().unwrap(), |
||||
"1.2.3.4:27003".parse().unwrap(), |
||||
"1.2.3.4:27004".parse().unwrap(), |
||||
]; |
||||
let mut p = QueryServersResponse::new(servers.iter().cloned()); |
||||
let mut buf = [0; 512]; |
||||
let (n, _) = p.encode(&mut buf).unwrap(); |
||||
let e = QueryServersResponse::decode(&buf[..n]).unwrap(); |
||||
assert_eq!(e.iter().collect::<Vec<_>>(), servers); |
||||
} |
||||
|
||||
#[test] |
||||
fn admin_challenge_response() { |
||||
let p = AdminChallengeResponse::new(0x12345678); |
||||
let mut buf = [0; 64]; |
||||
let n = p.encode(&mut buf).unwrap(); |
||||
assert_eq!(AdminChallengeResponse::decode(&buf[..n]), Ok(p)); |
||||
} |
||||
} |
@ -0,0 +1,506 @@
@@ -0,0 +1,506 @@
|
||||
// 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)); |
||||
} |
||||
} |
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||
|
||||
use super::filter::{FilterFlags, Version}; |
||||
use super::server::{Region, ServerAdd}; |
||||
use super::types::Str; |
||||
|
||||
#[derive(Clone, Debug)] |
||||
pub struct ServerInfo { |
||||
pub version: Version, |
||||
pub gamedir: Box<[u8]>, |
||||
pub map: Box<[u8]>, |
||||
pub flags: FilterFlags, |
||||
pub region: Region, |
||||
} |
||||
|
||||
impl ServerInfo { |
||||
pub fn new(info: &ServerAdd<Str<&[u8]>>) -> Self { |
||||
Self { |
||||
version: info.version, |
||||
gamedir: info.gamedir.to_vec().into_boxed_slice(), |
||||
map: info.map.to_vec().into_boxed_slice(), |
||||
flags: FilterFlags::from(info), |
||||
region: info.region, |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,51 @@
@@ -0,0 +1,51 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||
|
||||
use std::fmt; |
||||
use std::ops::Deref; |
||||
|
||||
/// Wrapper for slice of bytes with printing the bytes as a string
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Default)] |
||||
pub struct Str<T>(pub T); |
||||
|
||||
impl<T> From<T> for Str<T> { |
||||
fn from(value: T) -> Self { |
||||
Self(value) |
||||
} |
||||
} |
||||
|
||||
impl<T> fmt::Debug for Str<T> |
||||
where |
||||
T: AsRef<[u8]>, |
||||
{ |
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { |
||||
for &c in self.0.as_ref() { |
||||
match c { |
||||
b'\n' => write!(fmt, "\\n")?, |
||||
b'\t' => write!(fmt, "\\t")?, |
||||
_ if c.is_ascii_graphic() || c == b' ' => { |
||||
write!(fmt, "{}", c as char)?; |
||||
} |
||||
_ => write!(fmt, "\\x{:02x}", c)?, |
||||
} |
||||
} |
||||
Ok(()) |
||||
} |
||||
} |
||||
|
||||
impl<T> fmt::Display for Str<T> |
||||
where |
||||
T: AsRef<[u8]>, |
||||
{ |
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { |
||||
<Self as fmt::Debug>::fmt(self, fmt) |
||||
} |
||||
} |
||||
|
||||
impl<T> Deref for Str<T> { |
||||
type Target = T; |
||||
|
||||
fn deref(&self) -> &Self::Target { |
||||
&self.0 |
||||
} |
||||
} |
@ -1,91 +0,0 @@
@@ -1,91 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||
|
||||
use std::fmt; |
||||
use std::io; |
||||
use std::ops::Deref; |
||||
use std::str; |
||||
|
||||
use log::debug; |
||||
use thiserror::Error; |
||||
|
||||
use crate::server_info::{Region, ServerInfo}; |
||||
|
||||
#[derive(Error, Debug)] |
||||
pub enum Error { |
||||
#[error("Invalid packet")] |
||||
InvalidPacket, |
||||
#[error(transparent)] |
||||
IoError(#[from] io::Error), |
||||
} |
||||
|
||||
pub struct Filter<'a>(&'a [u8]); |
||||
|
||||
impl fmt::Debug for Filter<'_> { |
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { |
||||
String::from_utf8_lossy(self.0).fmt(fmt) |
||||
} |
||||
} |
||||
|
||||
impl<'a> Deref for Filter<'a> { |
||||
type Target = [u8]; |
||||
|
||||
fn deref(&self) -> &Self::Target { |
||||
self.0 |
||||
} |
||||
} |
||||
|
||||
#[derive(Debug)] |
||||
pub enum Packet<'a> { |
||||
Challenge(Option<u32>), |
||||
ServerAdd(Option<u32>, ServerInfo<&'a str>), |
||||
ServerRemove, |
||||
QueryServers(Region, Filter<'a>), |
||||
ServerInfo, |
||||
} |
||||
|
||||
impl<'a> Packet<'a> { |
||||
pub fn decode(s: &'a [u8]) -> Result<Self, Error> { |
||||
match s { |
||||
[b'1', region, tail @ ..] => { |
||||
let region = Region::try_from(*region).map_err(|_| Error::InvalidPacket)?; |
||||
let (tail, _last_ip) = decode_cstr(tail)?; |
||||
let (tail, filter) = decode_cstr(tail)?; |
||||
if !tail.is_empty() { |
||||
return Err(Error::InvalidPacket); |
||||
} |
||||
|
||||
Ok(Self::QueryServers(region, Filter(filter))) |
||||
} |
||||
[b'q', 0xff, b0, b1, b2, b3] => { |
||||
let challenge = u32::from_le_bytes([*b0, *b1, *b2, *b3]); |
||||
Ok(Self::Challenge(Some(challenge))) |
||||
} |
||||
[b'0', b'\n', tail @ ..] => { |
||||
let (challenge, info, tail) = |
||||
ServerInfo::from_bytes(tail).map_err(|_| Error::InvalidPacket)?; |
||||
if tail != b"" && tail != b"\n" { |
||||
debug!("unexpected end {:?}", tail); |
||||
} |
||||
Ok(Self::ServerAdd(challenge, info)) |
||||
} |
||||
[b'b', b'\n'] => Ok(Self::ServerRemove), |
||||
[b'q'] => Ok(Self::Challenge(None)), |
||||
[0xff, 0xff, 0xff, 0xff, b'i', b'n', b'f', b'o', b' ', _, _] => Ok(Self::ServerInfo), |
||||
_ => Err(Error::InvalidPacket), |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn decode_cstr(data: &[u8]) -> Result<(&[u8], &[u8]), Error> { |
||||
data.iter() |
||||
.position(|&c| c == 0) |
||||
.ok_or(Error::InvalidPacket) |
||||
.map(|offset| (&data[offset + 1..], &data[..offset])) |
||||
} |
||||
|
||||
// fn decode_str(data: &[u8]) -> Result<(&[u8], &str), Error> {
|
||||
// let (tail, s) = decode_cstr(data)?;
|
||||
// let s = str::from_utf8(s).map_err(|_| Error::InvalidPacket)?;
|
||||
// Ok((tail, s))
|
||||
// }
|
@ -1,342 +0,0 @@
@@ -1,342 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||
|
||||
use std::collections::HashMap; |
||||
use std::io::prelude::*; |
||||
use std::io::{self, Cursor}; |
||||
use std::net::{SocketAddr, SocketAddrV4, ToSocketAddrs, UdpSocket}; |
||||
use std::ops::Deref; |
||||
use std::time::Instant; |
||||
|
||||
use fastrand::Rng; |
||||
use log::{error, info, trace, warn}; |
||||
use thiserror::Error; |
||||
|
||||
use crate::client::Packet; |
||||
use crate::config::{self, Config}; |
||||
use crate::filter::{Filter, Version}; |
||||
use crate::server::Server; |
||||
use crate::server_info::Region; |
||||
|
||||
/// The maximum size of UDP packets.
|
||||
const MAX_PACKET_SIZE: usize = 512; |
||||
|
||||
const CHALLENGE_RESPONSE_HEADER: &[u8] = b"\xff\xff\xff\xffs\n"; |
||||
const SERVER_LIST_HEADER: &[u8] = b"\xff\xff\xff\xfff\n"; |
||||
|
||||
/// How many cleanup calls should be skipped before removing outdated servers.
|
||||
const SERVER_CLEANUP_MAX: usize = 100; |
||||
|
||||
/// How many cleanup calls should be skipped before removing outdated challenges.
|
||||
const CHALLENGE_CLEANUP_MAX: usize = 100; |
||||
|
||||
#[derive(Error, Debug)] |
||||
pub enum Error { |
||||
#[error("Failed to bind server socket: {0}")] |
||||
BindSocket(io::Error), |
||||
#[error("Failed to decode packet: {0}")] |
||||
ClientPacket(#[from] crate::client::Error), |
||||
#[error("Missing challenge in ServerInfo")] |
||||
MissingChallenge, |
||||
#[error(transparent)] |
||||
Io(#[from] io::Error), |
||||
} |
||||
|
||||
/// HashMap entry to keep tracking creation time.
|
||||
struct Entry<T> { |
||||
time: u32, |
||||
value: T, |
||||
} |
||||
|
||||
impl<T> Entry<T> { |
||||
fn new(time: u32, value: T) -> Self { |
||||
Self { time, value } |
||||
} |
||||
|
||||
fn is_valid(&self, now: u32, duration: u32) -> bool { |
||||
(now - self.time) < duration |
||||
} |
||||
} |
||||
|
||||
impl Entry<Server> { |
||||
fn matches(&self, addr: SocketAddrV4, region: Region, filter: &Filter) -> bool { |
||||
self.region == region && filter.matches(addr, self) |
||||
} |
||||
} |
||||
|
||||
impl<T> Deref for Entry<T> { |
||||
type Target = T; |
||||
|
||||
fn deref(&self) -> &Self::Target { |
||||
&self.value |
||||
} |
||||
} |
||||
|
||||
struct MasterServer { |
||||
sock: UdpSocket, |
||||
challenges: HashMap<SocketAddrV4, Entry<u32>>, |
||||
servers: HashMap<SocketAddrV4, Entry<Server>>, |
||||
rng: Rng, |
||||
|
||||
start_time: Instant, |
||||
cleanup_challenges: usize, |
||||
cleanup_servers: usize, |
||||
timeout: config::TimeoutConfig, |
||||
|
||||
clver: Version, |
||||
update_title: Box<str>, |
||||
update_map: Box<str>, |
||||
update_addr: SocketAddrV4, |
||||
} |
||||
|
||||
impl MasterServer { |
||||
fn new(cfg: Config) -> Result<Self, Error> { |
||||
let addr = SocketAddr::new(cfg.server.ip, cfg.server.port); |
||||
info!("Listen address: {}", addr); |
||||
let sock = UdpSocket::bind(addr).map_err(Error::BindSocket)?; |
||||
let update_addr = |
||||
cfg.client |
||||
.update_addr |
||||
.unwrap_or_else(|| match sock.local_addr().unwrap() { |
||||
SocketAddr::V4(addr) => addr, |
||||
_ => todo!(), |
||||
}); |
||||
|
||||
Ok(Self { |
||||
sock, |
||||
start_time: Instant::now(), |
||||
challenges: Default::default(), |
||||
servers: Default::default(), |
||||
rng: Rng::new(), |
||||
cleanup_challenges: 0, |
||||
cleanup_servers: 0, |
||||
timeout: cfg.server.timeout, |
||||
clver: cfg.client.version, |
||||
update_title: cfg.client.update_title, |
||||
update_map: cfg.client.update_map, |
||||
update_addr, |
||||
}) |
||||
} |
||||
|
||||
fn run(&mut self) -> Result<(), Error> { |
||||
let mut buf = [0; MAX_PACKET_SIZE]; |
||||
loop { |
||||
let (n, from) = self.sock.recv_from(&mut buf)?; |
||||
let from = match from { |
||||
SocketAddr::V4(a) => a, |
||||
_ => { |
||||
warn!("{}: Received message from IPv6, unimplemented", from); |
||||
continue; |
||||
} |
||||
}; |
||||
|
||||
if let Err(e) = self.handle_packet(from, &buf[..n]) { |
||||
error!("{}: {}", from, e); |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn handle_packet(&mut self, from: SocketAddrV4, s: &[u8]) -> Result<(), Error> { |
||||
let packet = match Packet::decode(s) { |
||||
Ok(p) => p, |
||||
Err(_) => { |
||||
trace!("{}: Failed to decode {:?}", from, s); |
||||
return Ok(()); |
||||
} |
||||
}; |
||||
|
||||
trace!("{}: recv {:?}", from, packet); |
||||
|
||||
match packet { |
||||
Packet::Challenge(server_challenge) => { |
||||
let challenge = self.add_challenge(from); |
||||
trace!("{}: New challenge {}", from, challenge); |
||||
self.send_challenge_response(from, challenge, server_challenge)?; |
||||
self.remove_outdated_challenges(); |
||||
} |
||||
Packet::ServerAdd(challenge, info) => { |
||||
let challenge = match challenge { |
||||
Some(c) => c, |
||||
None => return Err(Error::MissingChallenge), |
||||
}; |
||||
let entry = match self.challenges.get(&from) { |
||||
Some(e) => e, |
||||
None => { |
||||
trace!("{}: Challenge does not exists", from); |
||||
return Ok(()); |
||||
} |
||||
}; |
||||
if !entry.is_valid(self.now(), self.timeout.challenge) { |
||||
return Ok(()); |
||||
} |
||||
if challenge != entry.value { |
||||
warn!( |
||||
"{}: Expected challenge {} but received {}", |
||||
from, entry.value, challenge |
||||
); |
||||
return Ok(()); |
||||
} |
||||
if self.challenges.remove(&from).is_some() { |
||||
self.add_server(from, Server::new(&info)); |
||||
} |
||||
self.remove_outdated_servers(); |
||||
} |
||||
Packet::ServerRemove => { /* ignore */ } |
||||
Packet::QueryServers(region, filter) => { |
||||
let filter = match Filter::from_bytes(&filter) { |
||||
Ok(f) => f, |
||||
_ => { |
||||
warn!("{}: Invalid filter: {:?}", from, filter); |
||||
return Ok(()); |
||||
} |
||||
}; |
||||
|
||||
if filter.clver.map_or(true, |v| v < self.clver) { |
||||
let iter = std::iter::once(&self.update_addr); |
||||
self.send_server_list(from, iter)?; |
||||
} else { |
||||
let now = self.now(); |
||||
let iter = self |
||||
.servers |
||||
.iter() |
||||
.filter(|i| i.1.is_valid(now, self.timeout.server)) |
||||
.filter(|i| i.1.matches(*i.0, region, &filter)) |
||||
.map(|i| i.0); |
||||
self.send_server_list(from, iter)?; |
||||
} |
||||
} |
||||
Packet::ServerInfo => { |
||||
let mut buf = [0; MAX_PACKET_SIZE]; |
||||
let mut cur = Cursor::new(&mut buf[..]); |
||||
cur.write_all(b"\xff\xff\xff\xffinfo\n")?; |
||||
cur.write_all(b"\\p\\49")?; |
||||
cur.write_all(b"\\map\\")?; |
||||
cur.write_all(self.update_map.as_bytes())?; |
||||
cur.write_all(b"\\dm\\1")?; |
||||
cur.write_all(b"\\team\\0")?; |
||||
cur.write_all(b"\\coop\\0")?; |
||||
cur.write_all(b"\\numcl\\0")?; |
||||
cur.write_all(b"\\maxcl\\0")?; |
||||
cur.write_all(b"\\gamedir\\valve")?; |
||||
cur.write_all(b"\\password\\0")?; |
||||
cur.write_all(b"\\host\\")?; |
||||
cur.write_all(self.update_title.as_bytes())?; |
||||
let n = cur.position() as usize; |
||||
self.sock.send_to(&buf[..n], from)?; |
||||
} |
||||
} |
||||
|
||||
Ok(()) |
||||
} |
||||
|
||||
fn now(&self) -> u32 { |
||||
self.start_time.elapsed().as_secs() as u32 |
||||
} |
||||
|
||||
fn add_challenge(&mut self, addr: SocketAddrV4) -> u32 { |
||||
let x = self.rng.u32(..); |
||||
let entry = Entry::new(self.now(), x); |
||||
self.challenges.insert(addr, entry); |
||||
x |
||||
} |
||||
|
||||
fn remove_outdated_challenges(&mut self) { |
||||
if self.cleanup_challenges < CHALLENGE_CLEANUP_MAX { |
||||
self.cleanup_challenges += 1; |
||||
return; |
||||
} |
||||
let now = self.now(); |
||||
let old = self.challenges.len(); |
||||
self.challenges |
||||
.retain(|_, v| v.is_valid(now, self.timeout.challenge)); |
||||
let new = self.challenges.len(); |
||||
if old != new { |
||||
trace!("Removed {} outdated challenges", old - new); |
||||
} |
||||
self.cleanup_challenges = 0; |
||||
} |
||||
|
||||
fn add_server(&mut self, addr: SocketAddrV4, server: Server) { |
||||
match self.servers.insert(addr, Entry::new(self.now(), server)) { |
||||
Some(_) => trace!("{}: Updated GameServer", addr), |
||||
None => trace!("{}: New GameServer", addr), |
||||
} |
||||
} |
||||
|
||||
fn remove_outdated_servers(&mut self) { |
||||
if self.cleanup_servers < SERVER_CLEANUP_MAX { |
||||
self.cleanup_servers += 1; |
||||
return; |
||||
} |
||||
let now = self.now(); |
||||
let old = self.servers.len(); |
||||
self.servers |
||||
.retain(|_, v| v.is_valid(now, self.timeout.server)); |
||||
let new = self.servers.len(); |
||||
if old != new { |
||||
trace!("Removed {} outdated servers", old - new); |
||||
} |
||||
self.cleanup_servers = 0; |
||||
} |
||||
|
||||
fn send_challenge_response<A: ToSocketAddrs>( |
||||
&self, |
||||
to: A, |
||||
challenge: u32, |
||||
server_challenge: Option<u32>, |
||||
) -> Result<(), io::Error> { |
||||
let mut buf = [0; MAX_PACKET_SIZE]; |
||||
let mut cur = Cursor::new(&mut buf[..]); |
||||
|
||||
cur.write_all(CHALLENGE_RESPONSE_HEADER)?; |
||||
cur.write_all(&challenge.to_le_bytes())?; |
||||
if let Some(x) = server_challenge { |
||||
cur.write_all(&x.to_le_bytes())?; |
||||
} |
||||
|
||||
let n = cur.position() as usize; |
||||
self.sock.send_to(&buf[..n], to)?; |
||||
Ok(()) |
||||
} |
||||
|
||||
fn send_server_list<'a, A, I>(&self, to: A, mut iter: I) -> Result<(), io::Error> |
||||
where |
||||
A: ToSocketAddrs, |
||||
I: Iterator<Item = &'a SocketAddrV4>, |
||||
{ |
||||
let mut buf = [0; MAX_PACKET_SIZE]; |
||||
let mut done = false; |
||||
while !done { |
||||
let mut cur = Cursor::new(&mut buf[..]); |
||||
cur.write_all(SERVER_LIST_HEADER)?; |
||||
|
||||
loop { |
||||
match iter.next() { |
||||
Some(i) => { |
||||
cur.write_all(&i.ip().octets()[..])?; |
||||
cur.write_all(&i.port().to_be_bytes())?; |
||||
} |
||||
None => { |
||||
done = true; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
if (cur.position() as usize) > (MAX_PACKET_SIZE - 12) { |
||||
break; |
||||
} |
||||
} |
||||
|
||||
// terminate list
|
||||
cur.write_all(&[0; 6][..])?; |
||||
|
||||
let n = cur.position() as usize; |
||||
self.sock.send_to(&buf[..n], &to)?; |
||||
} |
||||
Ok(()) |
||||
} |
||||
} |
||||
|
||||
pub fn run(cfg: Config) -> Result<(), Error> { |
||||
MasterServer::new(cfg)?.run() |
||||
} |
@ -1,26 +0,0 @@
@@ -1,26 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||
|
||||
use crate::filter::FilterFlags; |
||||
use crate::server_info::{Region, ServerInfo}; |
||||
|
||||
#[derive(Clone, Debug)] |
||||
pub struct Server { |
||||
pub version: Box<str>, |
||||
pub gamedir: Box<str>, |
||||
pub map: Box<str>, |
||||
pub flags: FilterFlags, |
||||
pub region: Region, |
||||
} |
||||
|
||||
impl Server { |
||||
pub fn new(info: &ServerInfo<&str>) -> Self { |
||||
Self { |
||||
version: info.version.to_string().into_boxed_str(), |
||||
gamedir: info.gamedir.to_string().into_boxed_str(), |
||||
map: info.map.to_string().into_boxed_str(), |
||||
flags: FilterFlags::from(info), |
||||
region: info.region, |
||||
} |
||||
} |
||||
} |
@ -1,329 +0,0 @@
@@ -1,329 +0,0 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||
|
||||
use std::fmt; |
||||
|
||||
use bitflags::bitflags; |
||||
use log::{debug, log_enabled, Level}; |
||||
use thiserror::Error; |
||||
|
||||
use crate::parser::{Error as ParserError, ParseValue, Parser}; |
||||
|
||||
#[derive(Copy, Clone, Error, Debug, PartialEq, Eq)] |
||||
pub enum Error { |
||||
#[error("Invalid region")] |
||||
InvalidRegion, |
||||
#[error(transparent)] |
||||
Parser(#[from] ParserError), |
||||
} |
||||
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>; |
||||
|
||||
#[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 ParseValue<'_> for Os { |
||||
type Err = Error; |
||||
|
||||
fn parse(p: &mut Parser) -> Result<Self, Self::Err> { |
||||
match p.parse_bytes()? { |
||||
b"l" => Ok(Os::Linux), |
||||
b"w" => Ok(Os::Windows), |
||||
b"m" => Ok(Os::Mac), |
||||
_ => Ok(Os::Unknown), |
||||
} |
||||
} |
||||
} |
||||
|
||||
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 ParseValue<'_> for ServerType { |
||||
type Err = Error; |
||||
|
||||
fn parse(p: &mut Parser) -> Result<Self, Self::Err> { |
||||
match p.parse_bytes()? { |
||||
b"d" => Ok(Self::Dedicated), |
||||
b"l" => Ok(Self::Local), |
||||
b"p" => Ok(Self::Proxy), |
||||
_ => Ok(Self::Unknown), |
||||
} |
||||
} |
||||
} |
||||
|
||||
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 = (); |
||||
|
||||
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(()), |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl ParseValue<'_> for Region { |
||||
type Err = Error; |
||||
|
||||
fn parse(p: &mut Parser<'_>) -> Result<Self, Self::Err> { |
||||
let value = p.parse::<u8>()?; |
||||
Self::try_from(value).map_err(|_| Error::InvalidRegion) |
||||
} |
||||
} |
||||
|
||||
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, Default, PartialEq)] |
||||
pub struct ServerInfo<T = Box<str>> { |
||||
pub gamedir: T, |
||||
pub map: T, |
||||
pub version: T, |
||||
pub product: T, |
||||
pub server_type: ServerType, |
||||
pub os: Os, |
||||
pub region: Region, |
||||
pub protocol: u8, |
||||
pub players: u8, |
||||
pub max: u8, |
||||
pub flags: ServerFlags, |
||||
} |
||||
|
||||
impl<'a, T> ServerInfo<T> |
||||
where |
||||
T: 'a + Default + ParseValue<'a, Err = ParserError>, |
||||
{ |
||||
pub fn from_bytes(src: &'a [u8]) -> Result<(Option<u32>, Self, &'a [u8]), Error> { |
||||
let mut parser = Parser::new(src); |
||||
let (challenge, info) = parser.parse()?; |
||||
let tail = match parser.end() { |
||||
[b'\n', tail @ ..] => tail, |
||||
tail => tail, |
||||
}; |
||||
Ok((challenge, info, tail)) |
||||
} |
||||
} |
||||
|
||||
impl<'a, T> ParseValue<'a> for (Option<u32>, ServerInfo<T>) |
||||
where |
||||
T: 'a + Default + ParseValue<'a, Err = ParserError>, |
||||
{ |
||||
type Err = Error; |
||||
|
||||
fn parse(p: &mut Parser<'a>) -> Result<Self, Self::Err> { |
||||
let mut info = ServerInfo::default(); |
||||
let mut challenge = None; |
||||
|
||||
loop { |
||||
let name = match p.parse_bytes() { |
||||
Ok(s) => s, |
||||
Err(ParserError::End) => break, |
||||
Err(e) => return Err(e.into()), |
||||
}; |
||||
|
||||
match name { |
||||
b"protocol" => info.protocol = p.parse()?, |
||||
b"challenge" => challenge = Some(p.parse()?), |
||||
b"players" => info.players = p.parse()?, |
||||
b"max" => info.max = p.parse()?, |
||||
b"gamedir" => info.gamedir = p.parse()?, |
||||
b"map" => info.map = p.parse()?, |
||||
b"type" => info.server_type = p.parse()?, |
||||
b"os" => info.os = p.parse()?, |
||||
b"version" => info.version = p.parse()?, |
||||
b"region" => info.region = p.parse()?, |
||||
b"product" => info.product = p.parse()?, |
||||
b"bots" => info.flags.set(ServerFlags::BOTS, p.parse()?), |
||||
b"password" => info.flags.set(ServerFlags::PASSWORD, p.parse()?), |
||||
b"secure" => info.flags.set(ServerFlags::SECURE, p.parse()?), |
||||
b"lan" => info.flags.set(ServerFlags::LAN, p.parse()?), |
||||
b"nat" => info.flags.set(ServerFlags::NAT, p.parse()?), |
||||
_ => { |
||||
// skip unknown fields
|
||||
let value = p.parse_bytes()?; |
||||
if log_enabled!(Level::Debug) { |
||||
let name = String::from_utf8_lossy(name); |
||||
let value = String::from_utf8_lossy(value); |
||||
debug!("Invalid ServerInfo field \"{}\" = \"{}\"", name, value); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
Ok((challenge, info)) |
||||
} |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
use super::*; |
||||
use crate::parser::parse; |
||||
|
||||
#[test] |
||||
fn parse_os() { |
||||
assert_eq!(parse(b"\\l\\"), Ok(Os::Linux)); |
||||
assert_eq!(parse(b"\\w\\"), Ok(Os::Windows)); |
||||
assert_eq!(parse(b"\\m\\"), Ok(Os::Mac)); |
||||
assert_eq!(parse::<Os>(b"\\u\\"), Ok(Os::Unknown)); |
||||
} |
||||
|
||||
#[test] |
||||
fn parse_server_type() { |
||||
use ServerType as E; |
||||
|
||||
assert_eq!(parse(b"\\d\\"), Ok(E::Dedicated)); |
||||
assert_eq!(parse(b"\\l\\"), Ok(E::Local)); |
||||
assert_eq!(parse(b"\\p\\"), Ok(E::Proxy)); |
||||
assert_eq!(parse::<E>(b"\\u\\"), Ok(E::Unknown)); |
||||
} |
||||
|
||||
#[test] |
||||
fn parse_region() { |
||||
assert_eq!(parse(b"\\0\\"), Ok(Region::USEastCoast)); |
||||
assert_eq!(parse(b"\\1\\"), Ok(Region::USWestCoast)); |
||||
assert_eq!(parse(b"\\2\\"), Ok(Region::SouthAmerica)); |
||||
assert_eq!(parse(b"\\3\\"), Ok(Region::Europe)); |
||||
assert_eq!(parse(b"\\4\\"), Ok(Region::Asia)); |
||||
assert_eq!(parse(b"\\5\\"), Ok(Region::Australia)); |
||||
assert_eq!(parse(b"\\6\\"), Ok(Region::MiddleEast)); |
||||
assert_eq!(parse(b"\\7\\"), Ok(Region::Africa)); |
||||
assert_eq!(parse(b"\\-1\\"), Ok(Region::RestOfTheWorld)); |
||||
assert_eq!(parse::<Region>(b"\\-2\\"), Err(Error::InvalidRegion)); |
||||
assert_eq!( |
||||
parse::<Region>(b"\\u\\"), |
||||
Err(Error::Parser(ParserError::InvalidInteger)) |
||||
); |
||||
} |
||||
|
||||
#[test] |
||||
fn parse_server_info() { |
||||
let buf = b"\ |
||||
\\protocol\\47\ |
||||
\\challenge\\12345678\ |
||||
\\players\\16\ |
||||
\\max\\32\ |
||||
\\bots\\1\ |
||||
\\invalid_field\\field_value\ |
||||
\\gamedir\\cstrike\ |
||||
\\map\\de_dust\ |
||||
\\type\\d\ |
||||
\\password\\1\ |
||||
\\os\\l\ |
||||
\\secure\\1\ |
||||
\\lan\\1\ |
||||
\\version\\1.1.2.5\ |
||||
\\region\\-1\ |
||||
\\product\\cstrike\ |
||||
\\nat\\1\ |
||||
\ntail\ |
||||
"; |
||||
|
||||
assert_eq!( |
||||
ServerInfo::from_bytes(&buf[..]), |
||||
Ok(( |
||||
Some(12345678), |
||||
ServerInfo::<&str> { |
||||
protocol: 47, |
||||
players: 16, |
||||
max: 32, |
||||
gamedir: "cstrike", |
||||
map: "de_dust", |
||||
server_type: ServerType::Dedicated, |
||||
os: Os::Linux, |
||||
version: "1.1.2.5", |
||||
region: Region::RestOfTheWorld, |
||||
product: "cstrike", |
||||
flags: ServerFlags::all(), |
||||
}, |
||||
&b"tail"[..] |
||||
)) |
||||
); |
||||
} |
||||
} |
Loading…
Reference in new issue