diff --git a/Cargo.lock b/Cargo.lock index 2b46e47..08f9b0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,7 +93,9 @@ dependencies = [ "getopts", "log", "once_cell", + "serde", "thiserror", + "toml", ] [[package]] @@ -173,6 +175,26 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "serde" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syn" version = "2.0.37" @@ -204,6 +226,15 @@ dependencies = [ "syn", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "unicode-ident" version = "1.0.12" diff --git a/Cargo.toml b/Cargo.toml index 2147485..c8613c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,8 @@ getopts = "0.2.21" log = "<0.4.19" bitflags = "2.4" fastrand = "2.0.1" +serde = { version = "1.0.188", features = ["derive"] } +toml = "0.5.11" [dependencies.chrono] version = "<0.4.27" diff --git a/config/main.toml b/config/main.toml new file mode 100644 index 0000000..8f694d1 --- /dev/null +++ b/config/main.toml @@ -0,0 +1,15 @@ +# Default config file + +[log] +# Possible values: 0-5, off, error, warn, info, debug, trace +level = "warn" + +[server] +ip = "0.0.0.0" +port = 27010 + +[server.timeout] +# Time in seconds while challenge is valid +challenge = 300 +# Time in seconds while server is valid +server = 300 diff --git a/src/cli.rs b/src/cli.rs index a4ce967..c8f2d86 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,19 +1,19 @@ // SPDX-License-Identifier: GPL-3.0-only // SPDX-FileCopyrightText: 2023 Denis Drakhnia -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::net::IpAddr; use std::process; use getopts::Options; use log::LevelFilter; use thiserror::Error; +use crate::config; + 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}\"")] @@ -24,22 +24,12 @@ pub enum Error { Options(#[from] getopts::Fail), } -#[derive(Debug)] +#[derive(Debug, Default)] 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, - ), - } - } + pub log_level: Option, + pub listen_ip: Option, + pub listen_port: Option, + pub config_path: Box, } fn print_usage(opts: Options) { @@ -52,7 +42,10 @@ fn print_version() { } pub fn parse() -> Result { - let mut cli = Cli::default(); + let mut cli = Cli { + config_path: config::DEFAULT_CONFIG_PATH.to_string().into_boxed_str(), + ..Cli::default() + }; let args: Vec<_> = std::env::args().collect(); let mut opts = Options::new(); @@ -61,10 +54,15 @@ pub fn parse() -> Result { 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()); + let ip_help = format!("listen ip [default: {}]", config::DEFAULT_MASTER_SERVER_IP); opts.optopt("i", "ip", &ip_help, "IP"); - let port_help = format!("listen port [default: {}]", cli.listen.port()); + let port_help = format!( + "listen port [default: {}]", + config::DEFAULT_MASTER_SERVER_PORT + ); opts.optopt("p", "port", &port_help, "PORT"); + let config_help = format!("config path [default: {}]", cli.config_path); + opts.optopt("c", "config", &config_help, "PATH"); let matches = opts.parse(&args[1..])?; @@ -79,38 +77,25 @@ pub fn parse() -> Result { } 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::() { - 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); - } - }, - }; + match config::parse_log_level(value.as_ref()) { + Some(level) => cli.log_level = Some(level), + None => { + 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))?); + cli.listen_ip = Some(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))?); + cli.listen_port = Some(s.parse().map_err(|_| Error::InvalidPort(s))?); + } + + if let Some(s) = matches.opt_str("config") { + cli.config_path = s.into_boxed_str(); } Ok(cli) diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..a0cc518 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,142 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2023 Denis Drakhnia + +use std::fs; +use std::io; +use std::net::{IpAddr, Ipv4Addr}; +use std::path::Path; + +use log::LevelFilter; +use serde::{de::Error as _, Deserialize, Deserializer}; +use thiserror::Error; + +pub const DEFAULT_CONFIG_PATH: &str = "config/main.toml"; + +pub const DEFAULT_MASTER_SERVER_IP: IpAddr = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)); +pub const DEFAULT_MASTER_SERVER_PORT: u16 = 27010; +pub const DEFAULT_TIMEOUT: u32 = 300; + +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + Toml(#[from] toml::de::Error), + #[error(transparent)] + Io(#[from] io::Error), +} + +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct Config { + #[serde(default)] + pub log: LogConfig, + #[serde(default)] + pub server: ServerConfig, +} + +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct LogConfig { + #[serde(default = "default_log_level")] + #[serde(deserialize_with = "deserialize_log_level")] + pub level: LevelFilter, +} + +impl Default for LogConfig { + fn default() -> Self { + Self { + level: default_log_level(), + } + } +} + +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct ServerConfig { + #[serde(default = "default_server_ip")] + pub ip: IpAddr, + #[serde(default = "default_server_port")] + pub port: u16, + #[serde(default)] + pub timeout: TimeoutConfig, +} + +impl Default for ServerConfig { + fn default() -> Self { + Self { + ip: default_server_ip(), + port: default_server_port(), + timeout: Default::default(), + } + } +} + +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct TimeoutConfig { + #[serde(default = "default_timeout")] + pub challenge: u32, + #[serde(default = "default_timeout")] + pub server: u32, +} + +impl Default for TimeoutConfig { + fn default() -> Self { + Self { + challenge: default_timeout(), + server: default_timeout(), + } + } +} + +fn default_log_level() -> LevelFilter { + LevelFilter::Warn +} + +fn default_server_ip() -> IpAddr { + DEFAULT_MASTER_SERVER_IP +} + +fn default_server_port() -> u16 { + DEFAULT_MASTER_SERVER_PORT +} + +fn default_timeout() -> u32 { + DEFAULT_TIMEOUT +} + +fn deserialize_log_level<'de, D>(de: D) -> Result +where + D: Deserializer<'de>, +{ + let s = <&str>::deserialize(de)?; + parse_log_level(s) + .ok_or_else(|| D::Error::custom(format!("Invalid value for log option: \"{}\"", s))) +} + +pub fn parse_log_level(s: &str) -> Option { + use LevelFilter as E; + + let level_filter = match s { + _ if "off".starts_with(s) => E::Off, + _ if "error".starts_with(s) => E::Error, + _ if "warn".starts_with(s) => E::Warn, + _ if "info".starts_with(s) => E::Info, + _ if "debug".starts_with(s) => E::Debug, + _ if "trace".starts_with(s) => E::Trace, + _ => match s.parse::() { + Ok(0) => E::Off, + Ok(1) => E::Error, + Ok(2) => E::Warn, + Ok(3) => E::Info, + Ok(4) => E::Debug, + Ok(5) => E::Trace, + _ => return None, + }, + }; + Some(level_filter) +} + +pub fn load>(path: P) -> Result { + let data = fs::read(path)?; + Ok(toml::de::from_slice(&data)?) +} diff --git a/src/main.rs b/src/main.rs index e0f2c91..13c0752 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod cli; mod client; +mod config; mod filter; mod logger; mod master_server; @@ -18,9 +19,26 @@ fn main() { std::process::exit(1); }); - logger::init(cli.log_level); + let mut cfg = config::load(cli.config_path.as_ref()).unwrap_or_else(|e| { + eprintln!("Failed to load config \"{}\": {}", cli.config_path, e); + std::process::exit(1); + }); + + if let Some(level) = cli.log_level { + cfg.log.level = level; + } + + if let Some(ip) = cli.listen_ip { + cfg.server.ip = ip; + } + + if let Some(port) = cli.listen_port { + cfg.server.port = port; + } + + logger::init(cfg.log.level); - if let Err(e) = master_server::run(cli.listen) { + if let Err(e) = master_server::run(cfg) { error!("{}", e); std::process::exit(1); } diff --git a/src/master_server.rs b/src/master_server.rs index d4fad23..e92554d 100644 --- a/src/master_server.rs +++ b/src/master_server.rs @@ -13,6 +13,7 @@ use log::{error, info, trace, warn}; use thiserror::Error; use crate::client::Packet; +use crate::config::{self, Config}; use crate::filter::Filter; use crate::server::Server; use crate::server_info::Region; @@ -23,13 +24,9 @@ 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; @@ -83,10 +80,12 @@ struct MasterServer { rng: Rng, cleanup_challenges: usize, cleanup_servers: usize, + timeout: config::TimeoutConfig, } impl MasterServer { - fn new(addr: SocketAddr) -> Result { + fn new(cfg: Config) -> Result { + let addr = SocketAddr::new(cfg.server.ip, cfg.server.port); info!("Listen address: {}", addr); let sock = UdpSocket::bind(addr).map_err(Error::BindSocket)?; @@ -98,6 +97,7 @@ impl MasterServer { rng: Rng::new(), cleanup_challenges: 0, cleanup_servers: 0, + timeout: cfg.server.timeout, }) } @@ -142,7 +142,7 @@ impl MasterServer { return Ok(()); } }; - if !entry.is_valid(self.now(), CHALLENGE_TIMEOUT) { + if !entry.is_valid(self.now(), self.timeout.challenge) { return Ok(()); } if challenge != entry.value { @@ -189,7 +189,7 @@ impl MasterServer { let now = self.now(); let old = self.challenges.len(); self.challenges - .retain(|_, v| v.is_valid(now, CHALLENGE_TIMEOUT)); + .retain(|_, v| v.is_valid(now, self.timeout.challenge)); let new = self.challenges.len(); if old != new { trace!("Removed {} outdated challenges", old - new); @@ -198,8 +198,7 @@ impl MasterServer { } fn add_server(&mut self, addr: SocketAddrV4, server: Server) { - let entry = Entry::new(self.now(), server); - match self.servers.insert(addr, entry) { + match self.servers.insert(addr, Entry::new(self.now(), server)) { Some(_) => trace!("{}: Updated GameServer", addr), None => trace!("{}: New GameServer", addr), } @@ -212,7 +211,8 @@ impl MasterServer { } let now = self.now(); let old = self.servers.len(); - self.servers.retain(|_, v| v.is_valid(now, SERVER_TIMEOUT)); + self.servers + .retain(|_, v| v.is_valid(now, self.timeout.server)); let new = self.servers.len(); if old != new { trace!("Removed {} outdated servers", old - new); @@ -250,7 +250,7 @@ impl MasterServer { let mut iter = self .servers .iter() - .filter(|i| i.1.is_valid(now, SERVER_TIMEOUT)) + .filter(|i| i.1.is_valid(now, self.timeout.server)) .filter(|i| i.1.matches(*i.0, region, filter)) .map(|i| i.0); @@ -287,6 +287,6 @@ impl MasterServer { } } -pub fn run(addr: SocketAddr) -> Result<(), Error> { - MasterServer::new(addr)?.run() +pub fn run(cfg: Config) -> Result<(), Error> { + MasterServer::new(cfg)?.run() }