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#[derive(Clone, Debug, Deserialize)]
15#[serde(default)]
16pub struct ServerConfig {
17 pub http_bind: SocketAddr,
19 pub grpc_bind: SocketAddr,
21 pub admin_bind: SocketAddr,
23 pub admin_allowlist: Vec<IpAddr>,
25 pub data_dir: PathBuf,
27 pub api_prefix: String,
29 pub auth_mode: AuthMode,
31 pub tls: Option<TlsConfig>,
33 #[serde(with = "humantime_serde")]
35 pub query_timeout: Duration,
36 pub max_request_size: usize,
38 pub max_response_size: usize,
40 pub max_connections: usize,
42 #[serde(with = "humantime_serde")]
44 pub session_ttl: Duration,
45 pub metrics_enabled: bool,
47 pub tracing_enabled: bool,
49 pub audit_log_enabled: bool,
51 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 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 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}