Denis Drakhnia
1 year ago
11 changed files with 1911 additions and 0 deletions
@ -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" |
@ -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 } |
@ -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) |
||||
} |
@ -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))
|
||||
// }
|
@ -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(), |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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() |
||||
} |
@ -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)); |
||||
} |
||||
} |
@ -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, |
||||
} |
||||
} |
||||
} |
@ -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…
Reference in new issue