Browse Source

Single-threaded master server

ipv6
Denis Drakhnia 12 months ago
parent
commit
81932d9e6b
  1. 366
      Cargo.lock
  2. 27
      Cargo.toml
  3. 117
      src/cli.rs
  4. 90
      src/client.rs
  5. 405
      src/filter.rs
  6. 38
      src/logger.rs
  7. 27
      src/main.rs
  8. 289
      src/master_server.rs
  9. 194
      src/parser.rs
  10. 24
      src/server.rs
  11. 334
      src/server_info.rs

366
Cargo.lock generated

@ -0,0 +1,366 @@ @@ -0,0 +1,366 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
[[package]]
name = "bumpalo"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "cc"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
dependencies = [
"libc",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5"
dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"winapi",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]]
name = "fastrand"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
[[package]]
name = "getopts"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
dependencies = [
"unicode-width",
]
[[package]]
name = "hlmaster"
version = "0.1.0"
dependencies = [
"bitflags",
"byteorder",
"chrono",
"fastrand",
"getopts",
"log",
"once_cell",
"thiserror",
]
[[package]]
name = "iana-time-zone"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "js-sys"
version = "0.3.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.148"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
[[package]]
name = "log"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de"
[[package]]
name = "num-traits"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b"
[[package]]
name = "proc-macro2"
version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "2.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-width"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
[[package]]
name = "wasm-bindgen"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"

27
Cargo.toml

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
[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"
byteorder = "1.4.3"
fastrand = "2.0.1"
[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 }

117
src/cli.rs

@ -0,0 +1,117 @@ @@ -0,0 +1,117 @@
// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::process;
use getopts::Options;
use log::LevelFilter;
use thiserror::Error;
const BIN_NAME: &str = env!("CARGO_BIN_NAME");
const PKG_NAME: &str = env!("CARGO_PKG_NAME");
const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
const DEFAULT_MASTER_SERVER_PORT: u16 = 27010;
#[derive(Error, Debug)]
pub enum Error {
#[error("Invalid ip address \"{0}\"")]
InvalidIp(String),
#[error("Invalid port number \"{0}\"")]
InvalidPort(String),
#[error(transparent)]
Options(#[from] getopts::Fail),
}
#[derive(Debug)]
pub struct Cli {
pub log_level: LevelFilter,
pub listen: SocketAddr,
}
impl Default for Cli {
fn default() -> Self {
Self {
log_level: LevelFilter::Warn,
listen: SocketAddr::new(
IpAddr::from(Ipv4Addr::new(0, 0, 0, 0)),
DEFAULT_MASTER_SERVER_PORT,
),
}
}
}
fn print_usage(opts: Options) {
let brief = format!("Usage: {} [options]", BIN_NAME);
print!("{}", opts.usage(&brief));
}
fn print_version() {
println!("{} v{}", PKG_NAME, PKG_VERSION);
}
pub fn parse() -> Result<Cli, Error> {
let mut cli = Cli::default();
let args: Vec<_> = std::env::args().collect();
let mut opts = Options::new();
opts.optflag("h", "help", "print usage help");
opts.optflag("v", "version", "print program version");
let log_help =
"logging level [default: warn(2)]\nLEVEL: 0-5, off, error, warn, info, debug, trace";
opts.optopt("l", "log", log_help, "LEVEL");
let ip_help = format!("listen ip [default: {}]", cli.listen.ip());
opts.optopt("i", "ip", &ip_help, "IP");
let port_help = format!("listen port [default: {}]", cli.listen.port());
opts.optopt("p", "port", &port_help, "PORT");
let matches = opts.parse(&args[1..])?;
if matches.opt_present("help") {
print_usage(opts);
process::exit(0);
}
if matches.opt_present("version") {
print_version();
process::exit(0);
}
if let Some(value) = matches.opt_str("log") {
use LevelFilter as E;
cli.log_level = match value.as_str() {
_ if "off".starts_with(&value) => E::Off,
_ if "error".starts_with(&value) => E::Error,
_ if "warn".starts_with(&value) => E::Warn,
_ if "info".starts_with(&value) => E::Info,
_ if "debug".starts_with(&value) => E::Debug,
_ if "trace".starts_with(&value) => E::Trace,
_ => match value.parse::<u8>() {
Ok(0) => E::Off,
Ok(1) => E::Error,
Ok(2) => E::Warn,
Ok(3) => E::Info,
Ok(4) => E::Debug,
Ok(5) => E::Trace,
_ => {
eprintln!("Invalid value for log option: \"{}\"", value);
process::exit(1);
}
},
};
}
if let Some(s) = matches.opt_str("ip") {
cli.listen
.set_ip(s.parse().map_err(|_| Error::InvalidIp(s))?);
}
if let Some(s) = matches.opt_str("port") {
cli.listen
.set_port(s.parse().map_err(|_| Error::InvalidPort(s))?);
}
Ok(cli)
}

90
src/client.rs

@ -0,0 +1,90 @@ @@ -0,0 +1,90 @@
// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
use std::fmt;
use std::io::{self, Cursor};
use std::ops::Deref;
use std::str;
use byteorder::{ReadBytesExt, LE};
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(u32, ServerInfo<&'a str>),
ServerRemove,
QueryServers(Region, Filter<'a>),
}
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, tail @ ..] => {
let challenge = Cursor::new(tail).read_u32::<LE>()?;
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)),
_ => 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))
// }

405
src/filter.rs

@ -0,0 +1,405 @@ @@ -0,0 +1,405 @@
// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
use std::net::SocketAddrV4;
use bitflags::bitflags;
use log::{debug, log_enabled, Level};
use crate::parser::{Error as ParserError, ParseValue, Parser};
use crate::server::Server;
use crate::server_info::{Os, ServerFlags, ServerInfo, ServerType};
bitflags! {
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct FilterFlags: u16 {
/// Servers running dedicated
const DEDICATED = 1 << 0;
/// Servers that are spectator proxies
const PROXY = 1 << 1;
/// Servers using anti-cheat technology (VAC, but potentially others as well)
const SECURE = 1 << 2;
/// Servers running on a Linux platform
const LINUX = 1 << 3;
/// Servers that are not password protected
const PASSWORD = 1 << 4;
/// Servers that are not empty
const NOT_EMPTY = 1 << 5;
/// Servers that are not full
const FULL = 1 << 6;
/// Servers that are empty
const NOPLAYERS = 1 << 7;
/// Servers that are whitelisted
const WHITE = 1 << 8;
/// Servers that are behind NAT
const NAT = 1 << 9;
/// Servers that are LAN
const LAN = 1 << 11;
/// Servers that has bots
const BOTS = 1 << 12;
}
}
impl<T> From<&ServerInfo<T>> for FilterFlags {
fn from(info: &ServerInfo<T>) -> Self {
let mut flags = Self::empty();
flags.set(Self::DEDICATED, info.server_type == ServerType::Dedicated);
flags.set(Self::PROXY, info.server_type == ServerType::Proxy);
flags.set(Self::SECURE, info.flags.contains(ServerFlags::SECURE));
flags.set(Self::LINUX, info.os == Os::Linux);
flags.set(Self::PASSWORD, info.flags.contains(ServerFlags::PASSWORD));
flags.set(Self::NOT_EMPTY, info.players > 0); // XXX: and not full?
flags.set(Self::FULL, info.players >= info.max);
flags.set(Self::NOPLAYERS, info.players == 0);
flags.set(Self::NAT, info.flags.contains(ServerFlags::NAT));
flags.set(Self::LAN, info.flags.contains(ServerFlags::LAN));
flags.set(Self::BOTS, info.flags.contains(ServerFlags::BOTS));
flags
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Filter<'a> {
// A special filter, specifies that servers matching any of the following [x] conditions should not be returned
// TODO: \nor\[x]
// A special filter, specifies that servers matching all of the following [x] conditions should not be returned
// TODO: \nand\[x]
/// Servers running the specified modification (ex. cstrike)
pub gamedir: Option<&'a str>,
/// Servers running the specified map (ex. cs_italy)
pub map: Option<&'a str>,
/// Servers with all of the given tag(s) in sv_tags
pub gametype: Option<&'a str>,
/// Servers with all of the given tag(s) in their 'hidden' tags (L4D2)
pub gamedata: Option<&'a str>,
/// Servers with any of the given tag(s) in their 'hidden' tags (L4D2)
pub gamedataor: Option<&'a str>,
/// Servers with their hostname matching [hostname] (can use * as a wildcard)
pub name_match: Option<&'a str>,
/// Servers running version [version] (can use * as a wildcard)
pub version_match: Option<&'a str>,
/// Return only servers on the specified IP address (port supported and optional)
pub gameaddr: Option<SocketAddrV4>,
/// Servers that are running game [appid]
pub appid: Option<u32>,
/// Servers that are NOT running game [appid] (This was introduced to block Left 4 Dead games from the Steam Server Browser)
pub napp: Option<u32>,
/// Return only one server for each unique IP address matched
pub collapse_addr_hash: bool,
/// Client version.
pub clver: Option<&'a str>,
pub flags: FilterFlags,
pub flags_mask: FilterFlags,
}
impl Filter<'_> {
pub fn insert_flag(&mut self, flag: FilterFlags, value: bool) {
self.flags.set(flag, value);
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.version_match.map_or(false, |i| &*server.version != i) {
return false;
}
if let Some(a) = self.gameaddr {
if addr.ip() != a.ip() {
return false;
}
if a.port() != 0 && addr.port() != a.port() {
return false;
}
}
true
}
}
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> {
let mut filter = Self::default();
loop {
let name = match p.parse_bytes() {
Ok(s) => s,
Err(ParserError::End) => 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"linux" => filter.insert_flag(FilterFlags::LINUX, p.parse()?),
b"password" => filter.insert_flag(FilterFlags::PASSWORD, p.parse()?),
b"proxy" => filter.insert_flag(FilterFlags::PROXY, p.parse()?),
b"appid" => filter.appid = Some(p.parse()?),
b"napp" => filter.napp = Some(p.parse()?),
b"noplayers" => filter.insert_flag(FilterFlags::NOPLAYERS, p.parse()?),
b"white" => filter.insert_flag(FilterFlags::WHITE, p.parse()?),
b"gametype" => filter.gametype = Some(p.parse()?),
b"gamedata" => filter.gamedata = Some(p.parse()?),
b"gamedataor" => filter.gamedataor = Some(p.parse()?),
b"name_match" => filter.name_match = Some(p.parse()?),
b"version_match" => filter.version_match = Some(p.parse()?),
b"collapse_addr_hash" => filter.collapse_addr_hash = p.parse()?,
b"gameaddr" => {
let s = p.parse::<&str>()?;
if let Ok(addr) = s.parse() {
filter.gameaddr = Some(addr);
} else if let Ok(ip) = s.parse() {
filter.gameaddr = Some(SocketAddrV4::new(ip, 0));
}
}
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()?),
_ => {
// 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);
}
}
}
}
Ok(filter)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::Ipv4Addr;
macro_rules! tests {
($($name:ident$(($($predefined_f:ident: $predefined_v:expr),+ $(,)?))? {
$($src:expr => {
$($field:ident: $value:expr),* $(,)?
})+
})+) => {
$(#[test]
fn $name() {
let predefined = Filter {
$($($predefined_f: $predefined_v,)+)?
.. Filter::default()
};
$(assert_eq!(
Filter::from_bytes($src),
Ok(Filter {
$($field: $value,)*
..predefined
})
);)+
})+
};
}
tests! {
parse_gamedir {
b"\\gamedir\\valve" => {
gamedir: Some("valve"),
}
}
parse_map {
b"\\map\\crossfire" => {
map: Some("crossfire"),
}
}
parse_appid {
b"\\appid\\70" => {
appid: Some(70),
}
}
parse_napp {
b"\\napp\\70" => {
napp: Some(70),
}
}
parse_gametype {
b"\\gametype\\a,b,c,d" => {
gametype: Some("a,b,c,d"),
}
}
parse_gamedata {
b"\\gamedata\\a,b,c,d" => {
gamedata: Some("a,b,c,d"),
}
}
parse_gamedataor {
b"\\gamedataor\\a,b,c,d" => {
gamedataor: Some("a,b,c,d"),
}
}
parse_name_match {
b"\\name_match\\localhost" => {
name_match: Some("localhost"),
}
}
parse_version_match {
b"\\version_match\\1.2.3.4" => {
version_match: Some("1.2.3.4"),
}
}
parse_collapse_addr_hash {
b"\\collapse_addr_hash\\1" => {
collapse_addr_hash: true,
}
}
parse_gameaddr {
b"\\gameaddr\\192.168.1.100" => {
gameaddr: Some(SocketAddrV4::new(Ipv4Addr::new(192, 168, 1, 100), 0)),
}
b"\\gameaddr\\192.168.1.100:27015" => {
gameaddr: Some(SocketAddrV4::new(Ipv4Addr::new(192, 168, 1, 100), 27015)),
}
}
parse_clver {
b"\\clver\\0.20" => {
clver: Some("0.20"),
}
}
parse_dedicated(flags_mask: FilterFlags::DEDICATED) {
b"\\dedicated\\0" => {}
b"\\dedicated\\1" => {
flags: FilterFlags::DEDICATED,
}
}
parse_secure(flags_mask: FilterFlags::SECURE) {
b"\\secure\\0" => {}
b"\\secure\\1" => {
flags: FilterFlags::SECURE,
}
}
parse_linux(flags_mask: FilterFlags::LINUX) {
b"\\linux\\0" => {}
b"\\linux\\1" => {
flags: FilterFlags::LINUX,
}
}
parse_password(flags_mask: FilterFlags::PASSWORD) {
b"\\password\\0" => {}
b"\\password\\1" => {
flags: FilterFlags::PASSWORD,
}
}
parse_empty(flags_mask: FilterFlags::NOT_EMPTY) {
b"\\empty\\0" => {}
b"\\empty\\1" => {
flags: FilterFlags::NOT_EMPTY,
}
}
parse_full(flags_mask: FilterFlags::FULL) {
b"\\full\\0" => {}
b"\\full\\1" => {
flags: FilterFlags::FULL,
}
}
parse_proxy(flags_mask: FilterFlags::PROXY) {
b"\\proxy\\0" => {}
b"\\proxy\\1" => {
flags: FilterFlags::PROXY,
}
}
parse_noplayers(flags_mask: FilterFlags::NOPLAYERS) {
b"\\noplayers\\0" => {}
b"\\noplayers\\1" => {
flags: FilterFlags::NOPLAYERS,
}
}
parse_white(flags_mask: FilterFlags::WHITE) {
b"\\white\\0" => {}
b"\\white\\1" => {
flags: FilterFlags::WHITE,
}
}
parse_nat(flags_mask: FilterFlags::NAT) {
b"\\nat\\0" => {}
b"\\nat\\1" => {
flags: FilterFlags::NAT,
}
}
parse_lan(flags_mask: FilterFlags::LAN) {
b"\\lan\\0" => {}
b"\\lan\\1" => {
flags: FilterFlags::LAN,
}
}
parse_bots(flags_mask: FilterFlags::BOTS) {
b"\\bots\\0" => {}
b"\\bots\\1" => {
flags: FilterFlags::BOTS,
}
}
parse_all {
b"\
\\appid\\70\
\\bots\\1\
\\clver\\0.20\
\\collapse_addr_hash\\1\
\\dedicated\\1\
\\empty\\1\
\\full\\1\
\\gameaddr\\192.168.1.100\
\\gamedata\\a,b,c,d\
\\gamedataor\\a,b,c,d\
\\gamedir\\valve\
\\gametype\\a,b,c,d\
\\lan\\1\
\\linux\\1\
\\map\\crossfire\
\\name_match\\localhost\
\\napp\\60\
\\nat\\1\
\\noplayers\\1\
\\password\\1\
\\proxy\\1\
\\secure\\1\
\\version_match\\1.2.3.4\
\\white\\1\
" => {
gamedir: Some("valve"),
map: Some("crossfire"),
appid: Some(70),
napp: Some(60),
gametype: Some("a,b,c,d"),
gamedata: Some("a,b,c,d"),
gamedataor: Some("a,b,c,d"),
name_match: Some("localhost"),
version_match: Some("1.2.3.4"),
collapse_addr_hash: true,
gameaddr: Some(SocketAddrV4::new(Ipv4Addr::new(192, 168, 1, 100), 0)),
clver: Some("0.20"),
flags: FilterFlags::all(),
flags_mask: FilterFlags::all(),
}
}
}
}

38
src/logger.rs

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
use log::{LevelFilter, Metadata, Record};
struct Logger;
impl Logger {}
impl log::Log for Logger {
fn enabled(&self, _metadata: &Metadata) -> bool {
true
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
#[cfg(not(feature = "logtime"))]
println!("{} - {}", record.level(), record.args());
#[cfg(feature = "logtime")]
{
let dt = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
println!("[{}] {} - {}", dt, record.level(), record.args());
}
}
}
fn flush(&self) {}
}
static LOGGER: Logger = Logger;
pub fn init(level_filter: LevelFilter) {
if let Err(e) = log::set_logger(&LOGGER) {
eprintln!("Failed to initialize logger: {}", e);
}
log::set_max_level(level_filter);
}

27
src/main.rs

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
mod cli;
mod client;
mod filter;
mod logger;
mod master_server;
mod parser;
mod server;
mod server_info;
use log::error;
fn main() {
let cli = cli::parse().unwrap_or_else(|e| {
eprintln!("{}", e);
std::process::exit(1);
});
logger::init(cli.log_level);
if let Err(e) = master_server::run(cli.listen) {
error!("{}", e);
std::process::exit(1);
}
}

289
src/master_server.rs

@ -0,0 +1,289 @@ @@ -0,0 +1,289 @@
// 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::filter::Filter;
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";
/// Time in seconds while server is valid.
const SERVER_TIMEOUT: u32 = 300;
/// How many cleanup calls should be skipped before removing outdated servers.
const SERVER_CLEANUP_MAX: usize = 100;
/// Time in seconds while challenge is valid.
const CHALLENGE_TIMEOUT: u32 = 300;
/// 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(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,
start_time: Instant,
challenges: HashMap<SocketAddrV4, Entry<u32>>,
servers: HashMap<SocketAddrV4, Entry<Server>>,
rng: Rng,
cleanup_challenges: usize,
cleanup_servers: usize,
}
impl MasterServer {
fn new(addr: SocketAddr) -> Result<Self, Error> {
info!("Listen address: {}", addr);
let sock = UdpSocket::bind(addr).map_err(Error::BindSocket)?;
Ok(Self {
sock,
start_time: Instant::now(),
challenges: Default::default(),
servers: Default::default(),
rng: Rng::new(),
cleanup_challenges: 0,
cleanup_servers: 0,
})
}
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 = Packet::decode(s)?;
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 entry = match self.challenges.get(&from) {
Some(e) => e,
None => {
trace!("{}: Challenge does not exists", from);
return Ok(());
}
};
if !entry.is_valid(self.now(), CHALLENGE_TIMEOUT) {
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 => {
// XXX: anyone can delete server from the list?
self.servers.remove(&from);
}
Packet::QueryServers(region, filter) => match Filter::from_bytes(&filter) {
Ok(filter) => self.send_server_list(from, region, &filter)?,
_ => {
warn!("{}: Invalid filter: {:?}", from, filter);
return Ok(());
}
},
}
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, CHALLENGE_TIMEOUT));
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) {
let entry = Entry::new(self.now(), server);
match self.servers.insert(addr, entry) {
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, SERVER_TIMEOUT));
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: ToSocketAddrs>(
&self,
to: A,
region: Region,
filter: &Filter,
) -> Result<(), io::Error> {
let now = self.now();
let mut iter = self
.servers
.iter()
.filter(|i| i.1.is_valid(now, SERVER_TIMEOUT))
.filter(|i| i.1.matches(*i.0, region, filter))
.map(|i| i.0);
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(addr: SocketAddr) -> Result<(), Error> {
MasterServer::new(addr)?.run()
}

194
src/parser.rs

@ -0,0 +1,194 @@ @@ -0,0 +1,194 @@
// SPDX-License-Identifier: GPL-3.0-only
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
use std::str;
use thiserror::Error;
#[derive(Copy, Clone, Error, Debug, PartialEq, Eq)]
pub enum Error {
#[error("End of map")]
End,
#[error("Invalid map")]
InvalidMap,
#[error("Invalid string")]
InvalidString,
#[error("Invalid boolean")]
InvalidBool,
#[error("Invalid integer")]
InvalidInteger,
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
pub struct Parser<'a> {
cur: &'a [u8],
}
impl<'a> Parser<'a> {
pub fn new(cur: &'a [u8]) -> Self {
Self { cur }
}
pub fn parse_bytes(&mut self) -> Result<&'a [u8]> {
match self.cur.split_first() {
Some((b'\\', tail)) => {
let pos = tail
.iter()
.position(|&c| c == b'\\' || c == b'\n')
.unwrap_or(tail.len());
let (head, tail) = tail.split_at(pos);
self.cur = tail;
Ok(head)
}
Some((b'\n', _)) | None => Err(Error::End),
_ => Err(Error::InvalidMap),
}
}
pub fn parse<T: ParseValue<'a>>(&mut self) -> Result<T, T::Err> {
T::parse(self)
}
pub fn end(self) -> &'a [u8] {
self.cur
}
}
pub trait ParseValue<'a>: Sized {
type Err: From<Error>;
fn parse(p: &mut Parser<'a>) -> Result<Self, Self::Err>;
}
impl<'a> ParseValue<'a> for &'a [u8] {
type Err = Error;
fn parse(p: &mut Parser<'a>) -> Result<Self, Self::Err> {
p.parse_bytes()
}
}
impl<'a> ParseValue<'a> for &'a str {
type Err = Error;
fn parse(p: &mut Parser<'a>) -> Result<Self, Self::Err> {
p.parse_bytes()
.and_then(|s| str::from_utf8(s).map_err(|_| Error::InvalidString))
}
}
impl ParseValue<'_> for String {
type Err = Error;
fn parse(p: &mut Parser) -> Result<Self, Self::Err> {
p.parse::<&str>().map(|s| s.to_string())
}
}
impl ParseValue<'_> for Box<str> {
type Err = Error;
fn parse(p: &mut Parser) -> Result<Self, Self::Err> {
p.parse::<String>().map(|s| s.into_boxed_str())
}
}
impl ParseValue<'_> for bool {
type Err = Error;
fn parse(p: &mut Parser) -> Result<Self, Self::Err> {
p.parse_bytes().and_then(|s| match s {
b"0" => Ok(false),
b"1" => Ok(true),
_ => Err(Error::InvalidBool),
})
}
}
macro_rules! impl_parse_int {
($($t:ty : $f:ty),+ $(,)?) => (
$(impl ParseValue<'_> for $t {
type Err = Error;
fn parse(p: &mut Parser) -> Result<Self, Self::Err> {
p.parse::<&str>().and_then(|s| {
s.parse::<$t>()
.or_else(|_| s.parse::<$f>().map(|i| i as $t))
.map_err(|_| Error::InvalidInteger)
})
}
})+
);
}
impl_parse_int! {
i8 :u8,
i16:u16,
i32:u32,
i64:u64,
u8 :i8,
u16:i16,
u32:i32,
u64:i64,
}
#[cfg(test)]
pub(crate) fn parse<'a, T: ParseValue<'a>>(s: &'a [u8]) -> Result<T, T::Err> {
Parser::new(s).parse()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_end() {
assert_eq!(parse::<&[u8]>(b"\\abc"), Ok(&b"abc"[..]));
assert_eq!(parse::<&[u8]>(b"\\abc\\"), Ok(&b"abc"[..]));
assert_eq!(parse::<&[u8]>(b"\\abc\n"), Ok(&b"abc"[..]));
}
#[test]
fn parse_empty() {
assert_eq!(parse::<&[u8]>(b""), Err(Error::End));
assert_eq!(parse::<&[u8]>(b"\n"), Err(Error::End));
assert_eq!(parse::<&[u8]>(b"\\"), Ok(&b""[..]));
assert_eq!(parse::<&[u8]>(b"\\\\"), Ok(&b""[..]));
assert_eq!(parse::<&[u8]>(b"\\\n"), Ok(&b""[..]));
}
#[test]
fn parse_str() {
assert_eq!(parse::<&str>(b"\\abc\n"), Ok("abc"));
assert_eq!(parse::<&str>(b"\\abc\0\n"), Ok("abc\0"));
assert_eq!(parse::<&str>(b"\\abc\x80\\n"), Err(Error::InvalidString));
}
#[test]
fn parse_bool() {
assert_eq!(parse::<bool>(b"\\0\n"), Ok(false));
assert_eq!(parse::<bool>(b"\\1\n"), Ok(true));
assert_eq!(parse::<bool>(b"\\2\n"), Err(Error::InvalidBool));
assert_eq!(parse::<bool>(b"\\00\n"), Err(Error::InvalidBool));
assert_eq!(parse::<bool>(b"\\true\n"), Err(Error::InvalidBool));
assert_eq!(parse::<bool>(b"\\false\n"), Err(Error::InvalidBool));
}
#[test]
fn parse_int() {
assert_eq!(parse::<u8>(b"\\0\n"), Ok(0));
assert_eq!(parse::<u8>(b"\\255\n"), Ok(255));
assert_eq!(parse::<u8>(b"\\-1\n"), Ok(255));
assert_eq!(parse::<u8>(b"\\256\n"), Err(Error::InvalidInteger));
assert_eq!(parse::<u8>(b"\\0xff\n"), Err(Error::InvalidInteger));
assert_eq!(parse::<i8>(b"\\-1\n"), Ok(-1));
assert_eq!(parse::<i8>(b"\\-128\n"), Ok(-128));
assert_eq!(parse::<i8>(b"\\255\n"), Ok(-1));
assert_eq!(parse::<i8>(b"\\128\n"), Ok(-128));
assert_eq!(parse::<i8>(b"\\-129\n"), Err(Error::InvalidInteger));
assert_eq!(parse::<i8>(b"\\0xff\n"), Err(Error::InvalidInteger));
}
}

24
src/server.rs

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
// 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 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(),
flags: FilterFlags::from(info),
region: info.region,
}
}
}

334
src/server_info.rs

@ -0,0 +1,334 @@ @@ -0,0 +1,334 @@
// 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("Missing challenge in ServerInfo")]
MissingChallenge,
#[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<(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 (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);
}
}
}
}
match challenge {
Some(challenge) => Ok((challenge, info)),
None => Err(Error::MissingChallenge),
}
}
}
#[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((
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…
Cancel
Save