Denis Drakhnia
1 year ago
11 changed files with 1911 additions and 0 deletions
@ -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 @@ |
|||||||
|
[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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
// 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