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}
59
60/// Upstream server to proxy requests to.
61#[derive(Debug, Clone, Deserialize)]
62pub struct UpstreamConfig {
63    /// Full base URL of the upstream, e.g. "http://127.0.0.1:8081".
64    pub url: String,
65}
66
67/// Configuration for the middleware pipeline.
68#[derive(Debug, Clone, Default, Deserialize)]
69pub struct MiddlewareConfig {
70    /// Paths to block (exact match).
71    #[serde(default)]
72    pub blocked_paths: Vec<String>,
73    /// Required headers. Requests missing any of these are rejected.
74    #[serde(default)]
75    pub required_headers: Vec<String>,
76}
77
78fn default_listen_addr() -> String {
79    "127.0.0.1".to_string()
80}
81
82fn default_listen_port() -> u16 {
83    8080
84}
85
86impl ProxyConfig {
87    /// Load configuration from a TOML file at the given path.
88    pub fn from_file(path: &Path) -> anyhow::Result<Self> {
89        let contents = std::fs::read_to_string(path)?;
90        let config: ProxyConfig = toml::from_str(&contents)?;
91        Ok(config)
92    }
93
94    /// Parse configuration from a TOML string (useful for tests).
95    pub fn parse(s: &str) -> anyhow::Result<Self> {
96        let config: ProxyConfig = toml::from_str(s)?;
97        Ok(config)
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn parse_minimal_config() {
107        let toml = r#"
108[server]
109listen_addr = "0.0.0.0"
110listen_port = 9090
111
112[upstream]
113url = "http://localhost:3000"
114"#;
115        let config = ProxyConfig::parse(toml).unwrap();
116        assert_eq!(config.server.listen_addr, "0.0.0.0");
117        assert_eq!(config.server.listen_port, 9090);
118        assert_eq!(config.upstream.url, "http://localhost:3000");
119        assert!(config.middleware.blocked_paths.is_empty());
120    }
121
122    #[test]
123    fn parse_config_with_middleware() {
124        let toml = r#"
125[server]
126listen_port = 8080
127
128[upstream]
129url = "http://backend:8081"
130
131[middleware]
132blocked_paths = ["/admin", "/secret"]
133required_headers = ["x-api-key"]
134"#;
135        let config = ProxyConfig::parse(toml).unwrap();
136        assert_eq!(config.middleware.blocked_paths.len(), 2);
137        assert_eq!(config.middleware.required_headers, vec!["x-api-key"]);
138    }
139
140    #[test]
141    fn defaults_applied() {
142        let toml = r#"
143[server]
144
145[upstream]
146url = "http://localhost:3000"
147"#;
148        let config = ProxyConfig::parse(toml).unwrap();
149        assert_eq!(config.server.listen_addr, "127.0.0.1");
150        assert_eq!(config.server.listen_port, 8080);
151    }
152}