Skip to main content

alopex_server/
config.rs

1use std::net::{IpAddr, SocketAddr};
2use std::path::Path;
3use std::path::PathBuf;
4use std::time::Duration;
5
6use serde::Deserialize;
7
8use crate::audit::AuditLogOutput;
9use crate::auth::AuthMode;
10use crate::error::{Result, ServerError};
11use crate::tls::TlsConfig;
12
13/// Server configuration options.
14#[derive(Clone, Debug, Deserialize)]
15#[serde(default)]
16pub struct ServerConfig {
17    /// HTTP bind address.
18    pub http_bind: SocketAddr,
19    /// gRPC bind address.
20    pub grpc_bind: SocketAddr,
21    /// Admin bind address.
22    pub admin_bind: SocketAddr,
23    /// Allowlist for admin API when non-loopback.
24    pub admin_allowlist: Vec<IpAddr>,
25    /// Data directory for storage.
26    pub data_dir: PathBuf,
27    /// API prefix for HTTP routes.
28    pub api_prefix: String,
29    /// Authentication mode.
30    pub auth_mode: AuthMode,
31    /// TLS configuration (optional).
32    pub tls: Option<TlsConfig>,
33    /// Query timeout.
34    #[serde(with = "humantime_serde")]
35    pub query_timeout: Duration,
36    /// Max request size in bytes.
37    pub max_request_size: usize,
38    /// Max response size in bytes.
39    pub max_response_size: usize,
40    /// Max concurrent connections.
41    pub max_connections: usize,
42    /// Session TTL.
43    #[serde(with = "humantime_serde")]
44    pub session_ttl: Duration,
45    /// Enable Prometheus metrics.
46    pub metrics_enabled: bool,
47    /// Enable tracing.
48    pub tracing_enabled: bool,
49    /// Enable audit logging.
50    pub audit_log_enabled: bool,
51    /// Audit log output.
52    pub audit_log_output: AuditLogOutput,
53}
54
55impl Default for ServerConfig {
56    fn default() -> Self {
57        Self {
58            http_bind: "127.0.0.1:8080".parse().unwrap(),
59            grpc_bind: "127.0.0.1:9090".parse().unwrap(),
60            admin_bind: "127.0.0.1:8081".parse().unwrap(),
61            admin_allowlist: Vec::new(),
62            data_dir: PathBuf::from("./data"),
63            api_prefix: String::new(),
64            auth_mode: AuthMode::None,
65            tls: None,
66            query_timeout: Duration::from_secs(30),
67            max_request_size: 100 * 1024 * 1024,
68            max_response_size: 100 * 1024 * 1024,
69            max_connections: 1000,
70            session_ttl: Duration::from_secs(300),
71            metrics_enabled: true,
72            tracing_enabled: true,
73            audit_log_enabled: true,
74            audit_log_output: AuditLogOutput::Stdout,
75        }
76    }
77}
78
79impl ServerConfig {
80    /// Load config from TOML and environment variables.
81    ///
82    /// Environment variables use `ALOPEX__` prefix with `__` separators.
83    pub fn load(path: Option<&Path>) -> Result<Self> {
84        let mut builder = config::Config::builder();
85        if let Some(path) = path {
86            builder = builder.add_source(config::File::from(path).required(false));
87        } else {
88            builder = builder.add_source(config::File::with_name("alopex").required(false));
89        }
90        builder = builder.add_source(config::Environment::with_prefix("ALOPEX").separator("__"));
91        let mut config: ServerConfig = builder
92            .build()
93            .map_err(|err| ServerError::InvalidConfig(err.to_string()))?
94            .try_deserialize()
95            .map_err(|err| ServerError::InvalidConfig(err.to_string()))?;
96        config.normalize()?;
97        Ok(config)
98    }
99
100    /// Validate config invariants.
101    pub fn validate(&self) -> Result<()> {
102        if !self.admin_bind.ip().is_loopback() && self.admin_allowlist.is_empty() {
103            return Err(ServerError::InvalidConfig(
104                "admin_allowlist is required for non-loopback admin_bind".into(),
105            ));
106        }
107        if !self.api_prefix.is_empty() && !self.api_prefix.starts_with('/') {
108            return Err(ServerError::InvalidConfig(
109                "api_prefix must start with '/' or be empty".into(),
110            ));
111        }
112        if self.max_response_size == 0 {
113            return Err(ServerError::InvalidConfig(
114                "max_response_size must be greater than 0".into(),
115            ));
116        }
117        if self.max_request_size == 0 {
118            return Err(ServerError::InvalidConfig(
119                "max_request_size must be greater than 0".into(),
120            ));
121        }
122        if self.max_connections == 0 {
123            return Err(ServerError::InvalidConfig(
124                "max_connections must be greater than 0".into(),
125            ));
126        }
127        Ok(())
128    }
129
130    fn normalize(&mut self) -> Result<()> {
131        if self.api_prefix == "/" {
132            self.api_prefix.clear();
133        } else if self.api_prefix.ends_with('/') {
134            while self.api_prefix.ends_with('/') {
135                self.api_prefix.pop();
136            }
137        }
138        self.validate()
139    }
140}