Skip to main content

arbiter_proxy/
config.rs

1//! Configuration for the arbiter proxy, loaded from TOML.
2
3use serde::Deserialize;
4use std::path::Path;
5
6/// Top-level proxy configuration.
7#[derive(Debug, Clone, Deserialize)]
8pub struct ProxyConfig {
9    /// Server listen configuration.
10    pub server: ServerConfig,
11    /// Upstream target configuration.
12    pub upstream: UpstreamConfig,
13    /// Middleware pipeline configuration.
14    #[serde(default)]
15    pub middleware: MiddlewareConfig,
16    /// Audit logging configuration.
17    #[serde(default)]
18    pub audit: AuditConfig,
19}
20
21/// Audit logging configuration.
22#[derive(Debug, Clone, Deserialize)]
23pub struct AuditConfig {
24    /// Enable audit logging.
25    #[serde(default = "default_audit_enabled")]
26    pub enabled: bool,
27    /// Path to an append-only audit log file (optional).
28    #[serde(default)]
29    pub file_path: Option<String>,
30    /// Sensitive field patterns for argument redaction (overrides defaults).
31    #[serde(default)]
32    pub redaction_patterns: Vec<String>,
33}
34
35impl Default for AuditConfig {
36    fn default() -> Self {
37        Self {
38            enabled: true,
39            file_path: None,
40            redaction_patterns: Vec::new(),
41        }
42    }
43}
44
45fn default_audit_enabled() -> bool {
46    true
47}
48
49/// Server bind address and port.
50#[derive(Debug, Clone, Deserialize)]
51pub struct ServerConfig {
52    /// Listen address, e.g. "127.0.0.1".
53    #[serde(default = "default_listen_addr")]
54    pub listen_addr: String,
55    /// Listen port.
56    #[serde(default = "default_listen_port")]
57    pub listen_port: u16,
58    /// Maximum request/response body size in bytes. Default: 10 MB.
59    #[serde(default = "default_max_body_bytes")]
60    pub max_body_bytes: usize,
61    /// Timeout for upstream requests in seconds. Default: 30s.
62    #[serde(default = "default_upstream_timeout_secs")]
63    pub upstream_timeout_secs: u64,
64    /// Timeout for reading client request headers in seconds. Default: 10s.
65    #[serde(default = "default_header_read_timeout_secs")]
66    pub header_read_timeout_secs: u64,
67    /// Maximum number of concurrent connections. Default: 1024.
68    #[serde(default = "default_max_connections")]
69    pub max_connections: usize,
70}
71
72/// Upstream server to proxy requests to.
73#[derive(Debug, Clone, Deserialize)]
74pub struct UpstreamConfig {
75    /// Full base URL of the upstream, e.g. "http://127.0.0.1:8081".
76    pub url: String,
77}
78
79/// Default max body size: 10 MB.
80fn default_max_body_bytes() -> usize {
81    10 * 1024 * 1024
82}
83
84/// Default upstream request timeout: 30 seconds.
85fn default_upstream_timeout_secs() -> u64 {
86    30
87}
88
89/// Default connection header read timeout: 10 seconds.
90fn default_header_read_timeout_secs() -> u64 {
91    10
92}
93
94/// Default max concurrent connections: 1024.
95fn default_max_connections() -> usize {
96    1024
97}
98
99/// Configuration for the middleware pipeline.
100#[derive(Debug, Clone, Default, Deserialize)]
101pub struct MiddlewareConfig {
102    /// Paths to block (exact match).
103    #[serde(default)]
104    pub blocked_paths: Vec<String>,
105    /// Required headers. Requests missing any of these are rejected.
106    #[serde(default)]
107    pub required_headers: Vec<String>,
108}
109
110fn default_listen_addr() -> String {
111    "127.0.0.1".to_string()
112}
113
114fn default_listen_port() -> u16 {
115    8080
116}
117
118impl ProxyConfig {
119    /// Load configuration from a TOML file at the given path.
120    pub fn from_file(path: &Path) -> anyhow::Result<Self> {
121        let contents = std::fs::read_to_string(path)?;
122        let config: ProxyConfig = toml::from_str(&contents)?;
123        Ok(config)
124    }
125
126    /// Parse configuration from a TOML string (useful for tests).
127    pub fn parse(s: &str) -> anyhow::Result<Self> {
128        let config: ProxyConfig = toml::from_str(s)?;
129        Ok(config)
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn parse_minimal_config() {
139        let toml = r#"
140[server]
141listen_addr = "0.0.0.0"
142listen_port = 9090
143
144[upstream]
145url = "http://localhost:3000"
146"#;
147        let config = ProxyConfig::parse(toml).unwrap();
148        assert_eq!(config.server.listen_addr, "0.0.0.0");
149        assert_eq!(config.server.listen_port, 9090);
150        assert_eq!(config.upstream.url, "http://localhost:3000");
151        assert!(config.middleware.blocked_paths.is_empty());
152    }
153
154    #[test]
155    fn parse_config_with_middleware() {
156        let toml = r#"
157[server]
158listen_port = 8080
159
160[upstream]
161url = "http://backend:8081"
162
163[middleware]
164blocked_paths = ["/admin", "/secret"]
165required_headers = ["x-api-key"]
166"#;
167        let config = ProxyConfig::parse(toml).unwrap();
168        assert_eq!(config.middleware.blocked_paths.len(), 2);
169        assert_eq!(config.middleware.required_headers, vec!["x-api-key"]);
170    }
171
172    #[test]
173    fn defaults_applied() {
174        let toml = r#"
175[server]
176
177[upstream]
178url = "http://localhost:3000"
179"#;
180        let config = ProxyConfig::parse(toml).unwrap();
181        assert_eq!(config.server.listen_addr, "127.0.0.1");
182        assert_eq!(config.server.listen_port, 8080);
183    }
184}