From 9a636b1910f3adb45e64a09a896cb40cd1d40805 Mon Sep 17 00:00:00 2001 From: Nikolay Govorov Date: Sat, 17 Jan 2026 10:25:22 +0000 Subject: Move constants to config --- Cargo.lock | 10 ++++++ Cargo.toml | 2 ++ pkg/zorian.toml | 17 +++++++--- src/config.rs | 86 +++++++++++++++++++++++++++++++++++-------------- src/main.rs | 24 +++++++------- 5 files changed, 98 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f0a534c..2547d9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -359,6 +359,15 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +[[package]] +name = "bytesize" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" +dependencies = [ + "serde_core", +] + [[package]] name = "camino" version = "1.2.2" @@ -5517,6 +5526,7 @@ dependencies = [ "axum", "axum-extra", "bytes", + "bytesize", "cargo-deny", "cargo-llvm-cov", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 6ce0a29..37ff052 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ sqlx = { version = "0.8", features = [ "chrono", "json", ] } +bytesize = { version = "2.3", features = ["serde"] } thiserror = "2.0" tokio = { version = "1.49", features = ["full"] } toml = "0.9" @@ -70,3 +71,4 @@ uuid = { version = "1.19", features = ["v4", "v5"] } cargo-deny = "0.19" cargo-llvm-cov = "0.6" tempfile = "3.24" +url = "2.5" diff --git a/pkg/zorian.toml b/pkg/zorian.toml index 3704f52..b1a60f6 100644 --- a/pkg/zorian.toml +++ b/pkg/zorian.toml @@ -5,6 +5,19 @@ # Specify your name if you want your instance to be identified. appname="zorian" +# Path to the directory for storing state (indexes, caches, statistics). +# Must be a writable directory. +dirname="/var/lib/zorian" + +# Common server settings +[server] +shutdown_timeout = 60 # seconds +request_timeout = 30 # seconds +max_body_size = "64 MiB" +max_concurrent_requests = 512 +rate_limit_period = 10 +rate_limit_burst_size = 50 + # Listeners configuration. Each listener has an address and optional hostnames. # Empty hostnames means accept all requests (catch-all). [[listen]] @@ -14,7 +27,3 @@ hostnames = ["localhost", "127.0.0.1", "::1"] # For HTTPS, set tls_cert and tls_key to PEM file paths. # tls_crt = "/etc/zorian/cert.pem" # tls_key = "/etc/zorian/key.pem" - -# Path to the directory for storing state (indexes, caches, statistics). -# Must be a writable directory. -dirname="/var/lib/zorian" diff --git a/src/config.rs b/src/config.rs index 71bbe4f..c4daaaa 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,27 +3,19 @@ use std::fs; use std::path::{Path, PathBuf}; +use std::time::Duration; +use bytesize::ByteSize; use serde::Deserialize; use thiserror::Error; -/// When receiving a SIGINT/SIGTERM signal, we will wait for the proposed timeout before terminating workers -pub const SHUTDOWN_TIMEOUT: std::time::Duration = std::time::Duration::from_mins(1); - -/// Request timeout - maximum time to process a request (protects against Slowloris) -pub const REQUEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); - -/// Maximum request body size in bytes (64 MB) -pub const MAX_BODY_SIZE: usize = 64 * 1024 * 1024; - -/// Maximum number of concurrent requests across all clients -pub const MAX_CONCURRENT_REQUESTS: usize = 512; - -/// Rate limit: requests per second per client IP -pub const RATE_LIMIT_PER_SECOND: u64 = 10; - -/// Rate limit: burst size (max requests allowed in a burst) per client IP -pub const RATE_LIMIT_BURST_SIZE: u32 = 50; +fn deserialize_duration_secs<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let secs = u64::deserialize(deserializer)?; + Ok(Duration::from_secs(secs)) +} #[derive(Debug, Error)] pub enum ConfigError { @@ -58,6 +50,43 @@ pub enum ConfigError { TlsKeyNotFound(String, PathBuf), } +#[derive(Debug, Clone, Deserialize)] +pub struct ServerConfig { + /// When receiving a SIGINT/SIGTERM signal, we will wait for the proposed timeout before terminating workers + #[serde(deserialize_with = "deserialize_duration_secs")] + pub shutdown_timeout: Duration, + + /// Request timeout - maximum time to process a request (protects against Slowloris) + #[serde(deserialize_with = "deserialize_duration_secs")] + pub request_timeout: Duration, + + /// Maximum request body size + pub max_body_size: ByteSize, + + /// Maximum number of concurrent requests across all clients + pub max_concurrent_requests: usize, + + /// Rate limit: requests per second per client IP + #[serde(deserialize_with = "deserialize_duration_secs")] + pub rate_limit_period: Duration, + + /// Rate limit: burst size (max requests allowed in a burst) per client IP + pub rate_limit_burst_size: u32, +} + +impl Default for ServerConfig { + fn default() -> Self { + Self { + shutdown_timeout: Duration::from_secs(60), + request_timeout: Duration::from_secs(30), + max_body_size: ByteSize::mb(64), + max_concurrent_requests: 512, + rate_limit_period: Duration::from_secs(10), + rate_limit_burst_size: 50, + } + } +} + #[derive(Debug, Clone, Deserialize)] pub struct ListenerConfig { pub addr: String, @@ -91,17 +120,19 @@ impl Default for ListenerConfig { #[derive(Debug, Deserialize)] #[serde(default)] pub struct ConfigService { - listen: Vec, appname: String, dirname: PathBuf, + server: ServerConfig, + listen: Vec, } impl Default for ConfigService { fn default() -> Self { Self { - listen: vec![ListenerConfig::default()], appname: "zorian".to_string(), dirname: PathBuf::from("./.zorian-state"), + server: ServerConfig::default(), + listen: vec![ListenerConfig::default()], } } } @@ -170,27 +201,32 @@ impl ConfigService { &self.appname } - pub fn listeners(&self) -> &[ListenerConfig] { - &self.listen - } - pub fn dirname(&self) -> &Path { &self.dirname } + + pub fn server(&self) -> &ServerConfig { + &self.server + } + + pub fn listeners(&self) -> &[ListenerConfig] { + &self.listen + } } #[cfg(test)] impl ConfigService { pub fn for_test(dirname: PathBuf) -> Self { Self { + appname: "test".to_string(), + dirname, + server: ServerConfig::default(), listen: vec![ListenerConfig { addr: "127.0.0.1:0".to_string(), hostnames: Vec::new(), tls_crt: None, tls_key: None, }], - appname: "test".to_string(), - dirname, } } } diff --git a/src/main.rs b/src/main.rs index a184a7f..2b01266 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,10 +25,6 @@ use tracing::{error, info}; use tracing_subscriber::{layer::SubscriberExt, registry::LookupSpan, util::SubscriberInitExt}; use crate::backends::zig::ZigController; -use crate::config::{ - MAX_BODY_SIZE, MAX_CONCURRENT_REQUESTS, RATE_LIMIT_BURST_SIZE, RATE_LIMIT_PER_SECOND, - REQUEST_TIMEOUT, SHUTDOWN_TIMEOUT, -}; use crate::web::WebController; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -243,8 +239,8 @@ async fn main() { .on_failure(tower_http::trace::DefaultOnFailure::new().level(tracing::Level::ERROR)); let governor_config = tower_governor::governor::GovernorConfigBuilder::default() - .per_second(RATE_LIMIT_PER_SECOND) - .burst_size(RATE_LIMIT_BURST_SIZE) + .period(config.server().rate_limit_period) + .burst_size(config.server().rate_limit_burst_size) .key_extractor(ClientIpKeyExtractor) .finish() .unwrap(); @@ -263,14 +259,16 @@ async fn main() { )) .layer(tower_http::timeout::TimeoutLayer::with_status_code( http::StatusCode::REQUEST_TIMEOUT, - REQUEST_TIMEOUT, + config.server().request_timeout, + )) + .layer(tower_http::limit::RequestBodyLimitLayer::new( + config.server().max_body_size.as_u64() as usize, )) - .layer(tower_http::limit::RequestBodyLimitLayer::new(MAX_BODY_SIZE)) .layer(tower_governor::GovernorLayer::new(Arc::new( governor_config, ))) .layer(tower::limit::ConcurrencyLimitLayer::new( - MAX_CONCURRENT_REQUESTS, + config.server().max_concurrent_requests, )); let mut tasks = tokio::task::JoinSet::new(); @@ -367,7 +365,7 @@ async fn main() { drop(shutdown_tx); // broadcast // Wait for all listeners to finish with timeout - let shutdown_result = tokio::time::timeout(SHUTDOWN_TIMEOUT, async { + let shutdown_result = tokio::time::timeout(config.server().shutdown_timeout, async { while let Some(result) = tasks.join_next().await { if let Err(e) = result { error!("listener task failed: {e}"); @@ -379,7 +377,7 @@ async fn main() { if shutdown_result.is_err() { error!( "shutdown timeout after {:?}, aborting {} remaining tasks", - SHUTDOWN_TIMEOUT, + config.server().shutdown_timeout, tasks.len() ); tasks.abort_all(); @@ -488,7 +486,9 @@ where } else { raw }; - url::Host::parse(without_port).ok().map(|h| h.to_string()) + url::Host::parse(without_port) + .ok() + .map(|h| h.to_string().trim_end_matches('.').to_string()) }); let is_valid = host -- Gilti