aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat
-rw-r--r--Cargo.lock10+10 −0
-rw-r--r--Cargo.toml2+2 −0
-rw-r--r--pkg/zorian.toml17+13 −4
-rw-r--r--src/config.rs86+61 −25
-rw-r--r--src/main.rs24+12 −12
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
@@ -360,6 +360,15 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -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<Duration, D::Error>
+where
+ D: serde::Deserializer<'de>,
+{
+ let secs = u64::deserialize(deserializer)?;
+ Ok(Duration::from_secs(secs))
+}
#[derive(Debug, Error)]
pub enum ConfigError {
@@ -59,6 +51,43 @@ pub enum ConfigError {
}
#[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<ListenerConfig>,
appname: String,
dirname: PathBuf,
+ server: ServerConfig,
+ listen: Vec<ListenerConfig>,
}
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