Single-threaded master server
This commit is contained in:
parent
53c4605576
commit
81932d9e6b
|
@ -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