aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to 'crates/tesor/src/config.rs')
-rw-r--r--crates/tesor/src/config.rs627+0 −627
1 files changed, 0 insertions, 627 deletions
diff --git a/crates/tesor/src/config.rs b/crates/tesor/src/config.rs
deleted file mode 100644
--- a/crates/tesor/src/config.rs
+++ /dev/null
@@ -1,627 +0,0 @@
-// SPDX-FileCopyrightText: 2026 Nikolay Govorov
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-use std::collections::HashMap;
-use std::fs;
-use std::net::SocketAddr;
-use std::path::{Path, PathBuf};
-use std::time::Duration;
-
-use bytesize::ByteSize;
-use serde::Deserialize;
-use thiserror::Error;
-use tracing::info;
-
-use base::serde::{deserialize_duration, deserialize_listener_addr};
-use repos::{GoConfig, ZigConfig};
-
-#[derive(Debug, Error)]
-pub enum ConfigError {
- #[error("failed to read config file: {0}")]
- Io(#[from] std::io::Error),
-
- #[error("failed to parse config file: {0}")]
- Parse(#[from] toml::de::Error),
-
- #[error("appname '{0}' contains invalid characters (only a-z, A-Z, 0-9, -, _ allowed)")]
- InvalidAppname(String),
-
- #[error("dirname '{0}' does not exist")]
- DirNotFound(PathBuf),
-
- #[error("dirname '{0}' is not a directory")]
- NotADirectory(PathBuf),
-
- #[error("dirname '{0}' is not writable: {1}")]
- NotWritable(PathBuf, std::io::Error),
-
- #[error("listener '{0}': tls_crt is set but tls_key is missing")]
- TlsKeyMissing(SocketAddr),
-
- #[error("listener '{0}': tls_key is set but tls_crt is missing")]
- TlsCrtMissing(SocketAddr),
-
- #[error("listener '{0}': TLS crtificate file not found: {1}")]
- TlsCrtNotFound(SocketAddr, PathBuf),
-
- #[error("listener '{0}': TLS key file not found: {1}")]
- TlsKeyNotFound(SocketAddr, 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")]
- pub shutdown_timeout: Duration,
-
- /// Request timeout - maximum time to process a request (protects against Slowloris)
- #[serde(deserialize_with = "deserialize_duration")]
- 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")]
- 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 {
- #[serde(deserialize_with = "deserialize_listener_addr")]
- pub addr: SocketAddr,
-
- /// Hostnames to accept for this listener. Empty means accept all.
- pub hostnames: Vec<String>,
-
- /// Path to TLS certificate file (PEM format). If set, tls_key must also be set.
- pub tls_crt: Option<PathBuf>,
-
- /// Path to TLS private key file (PEM format). If set, tls_crt must also be set.
- pub tls_key: Option<PathBuf>,
-}
-
-impl Default for ListenerConfig {
- fn default() -> Self {
- Self {
- addr: "127.0.0.1:2025".parse().unwrap(),
- hostnames: vec![
- String::from("[::1]"),
- String::from("127.0.0.1"),
- String::from("localhost"),
- ],
- tls_crt: None,
- tls_key: None,
- }
- }
-}
-
-#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
-#[serde(rename_all = "lowercase")]
-pub enum LogLevel {
- Trace,
- Debug,
- Info,
- Warning,
- Error,
-}
-
-#[derive(Debug, Clone, Copy, Deserialize, Default, PartialEq, Eq)]
-#[serde(rename_all = "lowercase")]
-pub enum StdoutFormat {
- #[default]
- Pretty,
- Json,
-}
-
-#[derive(Debug, Clone, Deserialize)]
-#[serde(default)]
-pub struct StdoutConfig {
- /// Enables sending logs to the stdout
- pub enabled: bool,
-
- /// Controls which logs will be sent to stdout
- pub log_level: LogLevel,
-
- /// Controls the format of logs in stdout
- pub log_format: StdoutFormat,
-}
-
-impl Default for StdoutConfig {
- fn default() -> Self {
- Self {
- enabled: true,
- log_level: LogLevel::Info,
- log_format: StdoutFormat::Pretty,
- }
- }
-}
-
-#[derive(Debug, Clone, Deserialize)]
-#[serde(default)]
-pub struct OtelcolConfig {
- /// Enables sending telemetry to the otlp collector
- pub enabled: bool,
-
- /// Send logs to OTLP at this level (None = disabled)
- pub logs: bool,
-
- /// Send traces to OTLP
- pub traces: bool,
-
- /// Send traces to OTLP
- pub metrics: bool,
-
- /// OTLP endpoint (grpc:// or http://)
- pub endpoint: String,
-
- /// Export timeout in seconds
- #[serde(deserialize_with = "deserialize_duration")]
- pub timeout: Duration,
-
- /// Controls which logs will be sent to otlp
- pub log_level: LogLevel,
-
- /// Path to CA certificate for TLS (required for grpcs://)
- pub tls_ca: Option<PathBuf>,
-
- /// Path to client certificate for mTLS
- pub tls_crt: Option<PathBuf>,
-
- /// Path to client key for mTLS
- pub tls_key: Option<PathBuf>,
-
- /// HTTP headers for authentication
- pub headers: HashMap<String, String>,
-}
-
-impl Default for OtelcolConfig {
- fn default() -> Self {
- Self {
- enabled: true,
- logs: true,
- traces: true,
- metrics: true,
- timeout: Duration::from_secs(10),
- endpoint: "http://localhost:4317".into(),
- log_level: LogLevel::Info,
- tls_ca: None,
- tls_crt: None,
- tls_key: None,
- headers: HashMap::new(),
- }
- }
-}
-
-#[derive(Debug, Clone, Deserialize, Default)]
-#[serde(default)]
-pub struct TelemetryConfig {
- pub stdout: StdoutConfig,
- pub otelcol: Option<OtelcolConfig>,
-}
-
-#[derive(Debug, Clone, Deserialize, Default)]
-#[serde(default)]
-pub struct BackendsConfig {
- pub go: GoConfig,
- pub zig: ZigConfig,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(default)]
-pub struct ConfigService {
- appname: String,
- dirname: PathBuf,
- listen: Vec<ListenerConfig>,
- server: ServerConfig,
- telemetry: TelemetryConfig,
- backends: BackendsConfig,
-}
-
-impl Default for ConfigService {
- fn default() -> Self {
- Self {
- appname: "tesor".to_string(),
- dirname: PathBuf::from("./.tesor-state"),
- listen: vec![ListenerConfig::default()],
- server: ServerConfig::default(),
- telemetry: TelemetryConfig::default(),
- backends: BackendsConfig::default(),
- }
- }
-}
-
-impl ConfigService {
- pub fn load(config_path: Option<PathBuf>) -> Result<Self, ConfigError> {
- let config = match config_path {
- Some(path) => {
- info!("use config file from {}", path.to_str().unwrap());
-
- let content = fs::read_to_string(path)?;
- toml::from_str(&content)?
- }
- None => {
- info!("configuration file path not provided");
- Self::default()
- }
- };
-
- config.validate()
- }
-
- fn validate(self) -> Result<Self, ConfigError> {
- let mut chars = self.appname.chars();
- if !chars.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') {
- return Err(ConfigError::InvalidAppname(self.appname.clone()));
- }
-
- let metadata = fs::metadata(&self.dirname).map_err(|e| {
- if e.kind() == std::io::ErrorKind::NotFound {
- ConfigError::DirNotFound(self.dirname.clone())
- } else {
- ConfigError::Io(e)
- }
- })?;
-
- if !metadata.is_dir() {
- return Err(ConfigError::NotADirectory(self.dirname.clone()));
- }
-
- let testfile = self.dirname.join(".health");
- fs::write(&testfile, std::process::id().to_string())
- .map_err(|e| ConfigError::NotWritable(self.dirname.clone(), e))?;
- fs::remove_file(&testfile)?;
-
- // Validate listener configuration
- for listener in &self.listen {
- match (&listener.tls_crt, &listener.tls_key) {
- (Some(_crt), None) => {
- return Err(ConfigError::TlsKeyMissing(listener.addr));
- }
- (None, Some(_)) => {
- return Err(ConfigError::TlsCrtMissing(listener.addr));
- }
- (Some(crt), Some(key)) => {
- if !crt.exists() {
- return Err(ConfigError::TlsCrtNotFound(listener.addr, crt.clone()));
- }
- if !key.exists() {
- return Err(ConfigError::TlsKeyNotFound(listener.addr, key.clone()));
- }
- }
- (None, None) => {}
- }
- }
-
- Ok(self)
- }
-
- pub fn appname(&self) -> &str {
- &self.appname
- }
-
- pub fn dirname(&self) -> &Path {
- &self.dirname
- }
-
- pub fn server(&self) -> &ServerConfig {
- &self.server
- }
-
- pub fn listeners(&self) -> &[ListenerConfig] {
- &self.listen
- }
-
- pub fn telemetry(&self) -> &TelemetryConfig {
- &self.telemetry
- }
-
- pub fn backends(&self) -> &BackendsConfig {
- &self.backends
- }
-}
-
-#[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".parse().unwrap(),
- hostnames: Vec::new(),
- tls_crt: None,
- tls_key: None,
- }],
- telemetry: TelemetryConfig::default(),
- backends: BackendsConfig::default(),
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use tempfile::TempDir;
-
- fn base_config(dir: PathBuf) -> ConfigService {
- ConfigService {
- appname: "valid-name".to_string(),
- dirname: dir,
- server: ServerConfig::default(),
- listen: vec![ListenerConfig {
- addr: "127.0.0.1:0".parse().unwrap(),
- hostnames: Vec::new(),
- tls_crt: None,
- tls_key: None,
- }],
- telemetry: TelemetryConfig::default(),
- backends: BackendsConfig::default(),
- }
- }
-
- fn write_temp_file(dir: &Path, name: &str) -> PathBuf {
- let path = dir.join(name);
- std::fs::write(&path, "x").unwrap();
- path
- }
-
- mod defaults_tests {
- use super::*;
-
- #[test]
- fn test_for_test_config() {
- let dir = TempDir::new().unwrap();
- let cfg = ConfigService::for_test(dir.path().to_path_buf());
- assert_eq!(cfg.appname(), "test");
- assert_eq!(cfg.dirname(), dir.path());
- }
-
- #[test]
- fn test_listener_default() {
- let cfg = ListenerConfig::default();
- assert!(cfg.hostnames.contains(&"localhost".to_string()));
- }
- }
-
- mod load_tests {
- use super::*;
-
- #[test]
- fn test_load_none_defaults() {
- let temp = TempDir::new().unwrap();
- let state = temp.path().join(".tesor-state");
- std::fs::create_dir_all(&state).unwrap();
- let cwd = std::env::current_dir().unwrap();
- std::env::set_current_dir(temp.path()).unwrap();
-
- let cfg = ConfigService::load(None).unwrap();
- assert_eq!(cfg.appname(), "tesor");
-
- std::env::set_current_dir(cwd).unwrap();
- }
-
- #[test]
- fn test_load_some_and_parse_error() {
- let dir = TempDir::new().unwrap();
- let good = dir.path().join("good.toml");
- let bad = dir.path().join("bad.toml");
- let missing = dir.path().join("missing.toml");
- let state = dir.path().join("state");
- std::fs::create_dir_all(&state).unwrap();
-
- std::fs::write(
- &good,
- format!(
- "appname = \"ok\"\n\
- dirname = \"{}\"\n\
- listen = []\n\
- [server]\n\
- shutdown_timeout = 1\n\
- request_timeout = 1\n\
- max_body_size = \"1 MB\"\n\
- max_concurrent_requests = 1\n\
- rate_limit_period = 1\n\
- rate_limit_burst_size = 1\n\
- [telemetry]\n\
- [telemetry.stdout]\n\
- enabled = true\n\
- log_level = \"info\"\n\
- log_format = \"pretty\"\n\
- [backends]\n\
- [backends.go]\n\
- [backends.zig]\n",
- state.display()
- ),
- )
- .unwrap();
-
- std::fs::write(&bad, "not = [valid").unwrap();
-
- let cfg = ConfigService::load(Some(good)).unwrap();
- assert_eq!(cfg.appname(), "ok");
-
- let err = ConfigService::load(Some(missing)).unwrap_err();
- assert!(err.to_string().contains("failed to read"));
-
- let err = ConfigService::load(Some(bad)).unwrap_err();
- assert!(err.to_string().contains("failed to parse"));
- }
- }
-
- mod validation_tests {
- use super::*;
-
- #[test]
- fn test_validate_invalid_appname() {
- let dir = TempDir::new().unwrap();
- let mut cfg = base_config(dir.path().to_path_buf());
- cfg.appname = "bad name!".to_string();
- let err = cfg.validate().unwrap_err();
- assert!(err.to_string().contains("appname 'bad name!'"));
- }
-
- #[test]
- fn test_validate_dir_not_found() {
- let temp = TempDir::new().unwrap();
- let missing = temp.path().join("missing-dir");
- let cfg = base_config(missing);
- let err = cfg.validate().unwrap_err();
- assert!(err.to_string().contains("does not exist"));
- }
-
- #[test]
- fn test_validate_not_a_directory() {
- let dir = TempDir::new().unwrap();
- let file_path = dir.path().join("file");
- std::fs::write(&file_path, "x").unwrap();
- let cfg = base_config(file_path);
- let err = cfg.validate().unwrap_err();
- assert!(err.to_string().contains("is not a directory"));
- }
-
- #[cfg(unix)]
- #[test]
- fn test_validate_metadata_io_error() {
- use std::os::unix::fs::PermissionsExt;
-
- let dir = TempDir::new().unwrap();
- let child = dir.path().join("child");
- std::fs::create_dir_all(&child).unwrap();
-
- let mut perms = std::fs::metadata(dir.path()).unwrap().permissions();
- perms.set_mode(0o000);
- std::fs::set_permissions(dir.path(), perms).unwrap();
-
- let cfg = base_config(child.clone());
- let err = cfg.validate().unwrap_err();
- assert!(err.to_string().contains("failed to read"));
-
- let mut perms = std::fs::metadata(dir.path()).unwrap().permissions();
- perms.set_mode(0o700);
- std::fs::set_permissions(dir.path(), perms).unwrap();
- }
-
- #[cfg(unix)]
- #[test]
- fn test_validate_not_writable() {
- use std::os::unix::fs::PermissionsExt;
-
- let dir = TempDir::new().unwrap();
- let mut perms = std::fs::metadata(dir.path()).unwrap().permissions();
- perms.set_mode(0o400);
- std::fs::set_permissions(dir.path(), perms).unwrap();
-
- let cfg = base_config(dir.path().to_path_buf());
- let err = cfg.validate().unwrap_err();
- assert!(err.to_string().contains("not writable"));
- }
-
- #[test]
- fn test_validate_tls_key_missing() {
- let dir = TempDir::new().unwrap();
- let mut cfg = base_config(dir.path().to_path_buf());
- cfg.listen = vec![ListenerConfig {
- addr: "127.0.0.1:0".parse().unwrap(),
- hostnames: Vec::new(),
- tls_crt: Some(PathBuf::from("/tmp/does-not-matter.crt")),
- tls_key: None,
- }];
- let err = cfg.validate().unwrap_err();
- assert!(err.to_string().contains("tls_key is missing"));
- }
-
- #[test]
- fn test_validate_tls_crt_missing() {
- let dir = TempDir::new().unwrap();
- let mut cfg = base_config(dir.path().to_path_buf());
- cfg.listen = vec![ListenerConfig {
- addr: "127.0.0.1:0".parse().unwrap(),
- hostnames: Vec::new(),
- tls_crt: None,
- tls_key: Some(PathBuf::from("/tmp/does-not-matter.key")),
- }];
- let err = cfg.validate().unwrap_err();
- assert!(err.to_string().contains("tls_crt is missing"));
- }
-
- #[test]
- fn test_validate_tls_files_not_found() {
- let dir = TempDir::new().unwrap();
- let mut cfg = base_config(dir.path().to_path_buf());
- cfg.listen = vec![ListenerConfig {
- addr: "127.0.0.1:0".parse().unwrap(),
- hostnames: Vec::new(),
- tls_crt: Some(PathBuf::from("/tmp/missing.crt")),
- tls_key: Some(PathBuf::from("/tmp/missing.key")),
- }];
- let err = cfg.validate().unwrap_err();
- assert!(err.to_string().contains("TLS crtificate file not found"));
- }
-
- #[test]
- fn test_validate_tls_key_not_found() {
- let dir = TempDir::new().unwrap();
- let mut cfg = base_config(dir.path().to_path_buf());
- let crt = write_temp_file(dir.path(), "cert.pem");
- cfg.listen = vec![ListenerConfig {
- addr: "127.0.0.1:0".parse().unwrap(),
- hostnames: Vec::new(),
- tls_crt: Some(crt),
- tls_key: Some(dir.path().join("missing.key")),
- }];
- let err = cfg.validate().unwrap_err();
- assert!(err.to_string().contains("TLS key file not found"));
- }
-
- #[test]
- fn test_validate_tls_files_exist() {
- let dir = TempDir::new().unwrap();
- let mut cfg = base_config(dir.path().to_path_buf());
- let crt = write_temp_file(dir.path(), "cert.pem");
- let key = write_temp_file(dir.path(), "key.pem");
- cfg.listen = vec![ListenerConfig {
- addr: "127.0.0.1:0".parse().unwrap(),
- hostnames: Vec::new(),
- tls_crt: Some(crt),
- tls_key: Some(key),
- }];
- cfg.validate().unwrap();
- }
-
- #[test]
- fn test_validate_ok_and_getters() {
- let dir = TempDir::new().unwrap();
- let mut cfg = base_config(dir.path().to_path_buf());
- cfg.listen = vec![ListenerConfig::default()];
-
- let cfg = cfg.validate().unwrap();
- assert_eq!(cfg.appname(), "valid-name");
- assert_eq!(cfg.dirname(), dir.path());
- let _ = cfg.server();
- let _ = cfg.listeners();
- let _ = cfg.telemetry();
- let _ = cfg.backends();
- }
- }
-}