mirror of
https://git.mentality.rip/numas13/xash3d-master.git
synced 2025-03-10 04:31:10 +00:00
Add config file
This commit is contained in:
parent
e8c0ab6531
commit
a4e10b9965
31
Cargo.lock
generated
31
Cargo.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
|
15
config/main.toml
Normal file
15
config/main.toml
Normal file
@ -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
|
79
src/cli.rs
79
src/cli.rs
@ -1,19 +1,19 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||
|
||||
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<LevelFilter>,
|
||||
pub listen_ip: Option<IpAddr>,
|
||||
pub listen_port: Option<u16>,
|
||||
pub config_path: Box<str>,
|
||||
}
|
||||
|
||||
fn print_usage(opts: Options) {
|
||||
@ -52,7 +42,10 @@ fn print_version() {
|
||||
}
|
||||
|
||||
pub fn parse() -> Result<Cli, Error> {
|
||||
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<Cli, Error> {
|
||||
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<Cli, Error> {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
},
|
||||
};
|
||||
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)
|
||||
|
142
src/config.rs
Normal file
142
src/config.rs
Normal file
@ -0,0 +1,142 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// SPDX-FileCopyrightText: 2023 Denis Drakhnia <numas13@gmail.com>
|
||||
|
||||
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<LevelFilter, D::Error>
|
||||
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<LevelFilter> {
|
||||
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::<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,
|
||||
_ => return None,
|
||||
},
|
||||
};
|
||||
Some(level_filter)
|
||||
}
|
||||
|
||||
pub fn load<P: AsRef<Path>>(path: P) -> Result<Config, Error> {
|
||||
let data = fs::read(path)?;
|
||||
Ok(toml::de::from_slice(&data)?)
|
||||
}
|
22
src/main.rs
22
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 Err(e) = master_server::run(cli.listen) {
|
||||
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(cfg) {
|
||||
error!("{}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
@ -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<Self, Error> {
|
||||
fn new(cfg: Config) -> Result<Self, Error> {
|
||||
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()
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user