1use serde::Deserialize;
4use std::path::Path;
5
6#[derive(Debug, Clone, Deserialize)]
8pub struct ProxyConfig {
9 pub server: ServerConfig,
11 pub upstream: UpstreamConfig,
13 #[serde(default)]
15 pub middleware: MiddlewareConfig,
16 #[serde(default)]
18 pub audit: AuditConfig,
19}
20
21#[derive(Debug, Clone, Deserialize)]
23pub struct AuditConfig {
24 #[serde(default = "default_audit_enabled")]
26 pub enabled: bool,
27 #[serde(default)]
29 pub file_path: Option<String>,
30 #[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#[derive(Debug, Clone, Deserialize)]
51pub struct ServerConfig {
52 #[serde(default = "default_listen_addr")]
54 pub listen_addr: String,
55 #[serde(default = "default_listen_port")]
57 pub listen_port: u16,
58 #[serde(default = "default_max_body_bytes")]
60 pub max_body_bytes: usize,
61 #[serde(default = "default_upstream_timeout_secs")]
63 pub upstream_timeout_secs: u64,
64 #[serde(default = "default_header_read_timeout_secs")]
66 pub header_read_timeout_secs: u64,
67 #[serde(default = "default_max_connections")]
69 pub max_connections: usize,
70}
71
72#[derive(Debug, Clone, Deserialize)]
74pub struct UpstreamConfig {
75 pub url: String,
77}
78
79fn default_max_body_bytes() -> usize {
81 10 * 1024 * 1024
82}
83
84fn default_upstream_timeout_secs() -> u64 {
86 30
87}
88
89fn default_header_read_timeout_secs() -> u64 {
91 10
92}
93
94fn default_max_connections() -> usize {
96 1024
97}
98
99#[derive(Debug, Clone, Default, Deserialize)]
101pub struct MiddlewareConfig {
102 #[serde(default)]
104 pub blocked_paths: Vec<String>,
105 #[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 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 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}