Add protocol crate

This commit is contained in:
Denis Drakhnia 2023-10-17 10:21:52 +03:00
parent a03f682180
commit 528048bcfd
24 changed files with 1968 additions and 904 deletions

40
Cargo.lock generated
View File

@ -83,21 +83,6 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "hlmaster"
version = "0.1.0"
dependencies = [
"bitflags",
"chrono",
"fastrand",
"getopts",
"log",
"once_cell",
"serde",
"thiserror",
"toml",
]
[[package]]
name = "iana-time-zone"
version = "0.1.57"
@ -388,3 +373,28 @@ name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "xash3d-master"
version = "0.1.0"
dependencies = [
"bitflags",
"chrono",
"fastrand",
"getopts",
"log",
"once_cell",
"serde",
"thiserror",
"toml",
"xash3d-protocol",
]
[[package]]
name = "xash3d-protocol"
version = "0.1.0"
dependencies = [
"bitflags",
"log",
"thiserror",
]

View File

@ -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",
]

29
master/Cargo.toml Normal file
View File

@ -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 }

View File

@ -9,8 +9,7 @@ use std::path::Path;
use log::LevelFilter;
use serde::{de::Error as _, Deserialize, Deserializer};
use thiserror::Error;
use crate::filter::Version;
use xash3d_protocol::filter::Version;
pub const DEFAULT_CONFIG_PATH: &str = "config/main.toml";

View File

@ -2,14 +2,9 @@
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
mod cli;
mod client;
mod config;
mod filter;
mod logger;
mod master_server;
mod parser;
mod server;
mod server_info;
use log::error;

287
master/src/master_server.rs Normal file
View File

@ -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()
}

12
protocol/Cargo.toml Normal file
View File

@ -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"

82
protocol/src/admin.rs Normal file
View File

@ -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));
}
}

516
protocol/src/cursor.rs Normal file
View File

@ -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(())
}
}

View File

@ -34,11 +34,12 @@ use std::num::ParseIntError;
use std::str::FromStr;
use bitflags::bitflags;
use log::{debug, log_enabled, Level};
use log::debug;
use crate::parser::{Error as ParserError, ParseValue, Parser};
use crate::server::Server;
use crate::server_info::{ServerFlags, ServerInfo, ServerType};
use crate::cursor::{Cursor, GetKeyValue, PutKeyValue};
use crate::server::{ServerAdd, ServerFlags, ServerType};
use crate::types::Str;
use crate::{Error, ServerInfo};
bitflags! {
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
@ -64,8 +65,8 @@ bitflags! {
}
}
impl<T> From<&ServerInfo<T>> for FilterFlags {
fn from(info: &ServerInfo<T>) -> Self {
impl<T> From<&ServerAdd<T>> for FilterFlags {
fn from(info: &ServerAdd<T>) -> Self {
let mut flags = Self::empty();
flags.set(Self::DEDICATED, info.server_type == ServerType::Dedicated);
@ -115,24 +116,31 @@ impl FromStr for Version {
}
}
impl ParseValue<'_> for Version {
type Err = ParserError;
impl GetKeyValue<'_> for Version {
fn get_key_value(cur: &mut Cursor) -> Result<Self, Error> {
Self::from_str(cur.get_key_value()?).map_err(|_| Error::InvalidPacket)
}
}
fn parse(p: &mut Parser<'_>) -> Result<Self, Self::Err> {
let s = p.parse::<&str>()?;
let v = s.parse()?;
Ok(v)
impl PutKeyValue for Version {
fn put_key_value<'a, 'b>(
&self,
cur: &'b mut crate::cursor::CursorMut<'a>,
) -> Result<&'b mut crate::cursor::CursorMut<'a>, Error> {
cur.put_key_value(self.major)?
.put_u8(b'.')?
.put_key_value(self.minor)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Filter<'a> {
/// Servers running the specified modification (ex. cstrike)
pub gamedir: Option<&'a str>,
pub gamedir: &'a [u8],
/// Servers running the specified map (ex. cs_italy)
pub map: Option<&'a str>,
pub map: &'a [u8],
/// Client version.
pub clver: Option<Version>,
pub clver: Version,
pub flags: FilterFlags,
pub flags_mask: FilterFlags,
@ -144,62 +152,47 @@ impl Filter<'_> {
self.flags_mask.insert(flag);
}
pub fn matches(&self, _addr: SocketAddrV4, server: &Server) -> bool {
if (server.flags & self.flags_mask) != self.flags {
return false;
}
if self.gamedir.map_or(false, |i| &*server.gamedir != i) {
return false;
}
if self.map.map_or(false, |i| &*server.map != i) {
return false;
}
true
pub fn matches(&self, _addr: SocketAddrV4, info: &ServerInfo) -> bool {
!((info.flags & self.flags_mask) != self.flags
|| (!self.gamedir.is_empty() && self.gamedir != &*info.gamedir)
|| (!self.map.is_empty() && self.map != &*info.map))
}
}
impl<'a> Filter<'a> {
pub fn from_bytes(src: &'a [u8]) -> Result<Self, ParserError> {
let mut parser = Parser::new(src);
let filter = parser.parse()?;
Ok(filter)
}
}
impl<'a> ParseValue<'a> for Filter<'a> {
type Err = ParserError;
fn parse(p: &mut Parser<'a>) -> Result<Self, Self::Err> {
pub fn from_bytes(src: &'a [u8]) -> Result<Self, Error> {
let mut cur = Cursor::new(src);
let mut filter = Self::default();
loop {
let name = match p.parse_bytes() {
let key = match cur.get_key_raw().map(Str) {
Ok(s) => s,
Err(ParserError::End) => break,
Err(Error::UnexpectedEnd) => break,
Err(e) => return Err(e),
};
match name {
b"dedicated" => filter.insert_flag(FilterFlags::DEDICATED, p.parse()?),
b"secure" => filter.insert_flag(FilterFlags::SECURE, p.parse()?),
b"gamedir" => filter.gamedir = Some(p.parse()?),
b"map" => filter.map = Some(p.parse()?),
b"empty" => filter.insert_flag(FilterFlags::NOT_EMPTY, p.parse()?),
b"full" => filter.insert_flag(FilterFlags::FULL, p.parse()?),
b"password" => filter.insert_flag(FilterFlags::PASSWORD, p.parse()?),
b"noplayers" => filter.insert_flag(FilterFlags::NOPLAYERS, p.parse()?),
b"clver" => filter.clver = Some(p.parse()?),
b"nat" => filter.insert_flag(FilterFlags::NAT, p.parse()?),
b"lan" => filter.insert_flag(FilterFlags::LAN, p.parse()?),
b"bots" => filter.insert_flag(FilterFlags::BOTS, p.parse()?),
match *key {
b"dedicated" => filter.insert_flag(FilterFlags::DEDICATED, cur.get_key_value()?),
b"secure" => filter.insert_flag(FilterFlags::SECURE, cur.get_key_value()?),
b"gamedir" => filter.gamedir = cur.get_key_value()?,
b"map" => filter.map = cur.get_key_value()?,
b"empty" => filter.insert_flag(FilterFlags::NOT_EMPTY, cur.get_key_value()?),
b"full" => filter.insert_flag(FilterFlags::FULL, cur.get_key_value()?),
b"password" => filter.insert_flag(FilterFlags::PASSWORD, cur.get_key_value()?),
b"noplayers" => filter.insert_flag(FilterFlags::NOPLAYERS, cur.get_key_value()?),
b"clver" => {
filter.clver = cur
.get_key_value::<&str>()?
.parse()
.map_err(|_| Error::InvalidPacket)?
}
b"nat" => filter.insert_flag(FilterFlags::NAT, cur.get_key_value()?),
b"lan" => filter.insert_flag(FilterFlags::LAN, cur.get_key_value()?),
b"bots" => filter.insert_flag(FilterFlags::BOTS, cur.get_key_value()?),
_ => {
// 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 Filter field \"{}\" = \"{}\"", name, value);
}
let value = Str(cur.get_key_value_raw()?);
debug!("Invalid Filter field \"{}\" = \"{}\"", key, value);
}
}
}
@ -208,8 +201,43 @@ impl<'a> ParseValue<'a> for Filter<'a> {
}
}
impl fmt::Display for &Filter<'_> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
macro_rules! display_flag {
($n:expr, $f:expr) => {
if self.flags_mask.contains($f) {
let flag = if self.flags.contains($f) { '1' } else { '0' };
write!(fmt, "\\{}\\{}", $n, flag)?;
}
};
}
display_flag!("dedicated", FilterFlags::DEDICATED);
display_flag!("secure", FilterFlags::SECURE);
if !self.gamedir.is_empty() {
write!(fmt, "\\gamedir\\{}", Str(self.gamedir))?;
}
display_flag!("secure", FilterFlags::SECURE);
if !self.map.is_empty() {
write!(fmt, "\\map\\{}", Str(self.map))?;
}
display_flag!("empty", FilterFlags::NOT_EMPTY);
display_flag!("full", FilterFlags::FULL);
display_flag!("password", FilterFlags::PASSWORD);
display_flag!("noplayers", FilterFlags::NOPLAYERS);
write!(fmt, "\\clver\\{}", self.clver)?;
display_flag!("nat", FilterFlags::NAT);
display_flag!("lan", FilterFlags::LAN);
display_flag!("bots", FilterFlags::BOTS);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::super::cursor::CursorMut;
use super::super::types::Str;
use super::*;
macro_rules! tests {
@ -238,17 +266,17 @@ mod tests {
tests! {
parse_gamedir {
b"\\gamedir\\valve" => {
gamedir: Some("valve"),
gamedir: &b"valve"[..],
}
}
parse_map {
b"\\map\\crossfire" => {
map: Some("crossfire"),
map: &b"crossfire"[..],
}
}
parse_clver {
b"\\clver\\0.20" => {
clver: Some(Version::new(0, 20)),
clver: Version::new(0, 20),
}
}
parse_dedicated(flags_mask: FilterFlags::DEDICATED) {
@ -321,9 +349,9 @@ mod tests {
\\password\\1\
\\secure\\1\
" => {
gamedir: Some("valve"),
map: Some("crossfire"),
clver: Some(Version::new(0, 20)),
gamedir: &b"valve"[..],
map: &b"crossfire"[..],
clver: Version::new(0, 20),
flags: FilterFlags::all(),
flags_mask: FilterFlags::all(),
}
@ -334,8 +362,14 @@ mod tests {
($($addr:expr => $info:expr $(=> $func:expr)?)+) => (
[$({
let addr = $addr.parse::<SocketAddrV4>().unwrap();
let (_, info, _) = ServerInfo::<&str>::from_bytes($info).unwrap();
let server = Server::new(&info);
let mut buf = [0; 512];
let n = CursorMut::new(&mut buf)
.put_bytes(ServerAdd::HEADER).unwrap()
.put_key("challenge", 0).unwrap()
.put_bytes($info).unwrap()
.pos();
let p = ServerAdd::<Str<&[u8]>>::decode(&buf[..n]).unwrap();
let server = ServerInfo::new(&p);
$(
let mut server = server;
let func: fn(&mut Server) = $func;

128
protocol/src/game.rs Normal file
View File

@ -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));
}
}

28
protocol/src/lib.rs Normal file
View File

@ -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,
}

170
protocol/src/master.rs Normal file
View File

@ -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));
}
}

506
protocol/src/server.rs Normal file
View File

@ -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));
}
}

View File

@ -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,
}
}
}

51
protocol/src/types.rs Normal file
View File

@ -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
}
}

View File

@ -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))
// }

View File

@ -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()
}

View File

@ -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,
}
}
}

View File

@ -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"[..]
))
);
}
}