1use secrecy::{ExposeSecret, SecretString};
2use serde::{Deserialize, Serialize};
3use std::fmt;
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7pub struct RedactionPolicy {
8 #[serde(default = "default_false")]
9 pub enabled: bool,
10 #[serde(default = "default_sensitive_header_names")]
11 pub sensitive_header_names: Vec<String>,
12 #[serde(default = "default_sensitive_query_keys")]
13 pub sensitive_query_keys: Vec<String>,
14 #[serde(default = "default_false")]
15 pub redact_bodies: bool,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
19pub struct RedactionPolicyPatch {
20 #[serde(default)]
21 pub enabled: Option<bool>,
22 #[serde(default)]
23 pub sensitive_header_names: Option<Vec<String>>,
24 #[serde(default)]
25 pub sensitive_query_keys: Option<Vec<String>>,
26 #[serde(default)]
27 pub redact_bodies: Option<bool>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
31pub struct ProxyPolicyPatch {
32 #[serde(default)]
33 pub redaction: Option<RedactionPolicyPatch>,
34 #[serde(default)]
35 pub upstream: Option<UpstreamProxyConfig>,
36}
37
38#[derive(Clone, Serialize, Deserialize)]
42pub struct UpstreamProxyConfig {
43 pub proxy_url: String,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub auth: Option<UpstreamAuth>,
48 #[serde(default, skip_serializing_if = "Vec::is_empty")]
50 pub bypass_hosts: Vec<String>,
51 #[serde(default)]
53 pub fail_open: bool,
54}
55
56#[derive(Clone, Serialize, Deserialize)]
58pub struct UpstreamAuth {
59 pub username: String,
60 #[serde(
61 serialize_with = "serialize_secret",
62 deserialize_with = "deserialize_secret"
63 )]
64 pub password: SecretString,
65}
66
67fn serialize_secret<S: serde::Serializer>(_v: &SecretString, s: S) -> Result<S::Ok, S::Error> {
68 s.serialize_str("***")
69}
70fn deserialize_secret<'de, D: serde::Deserializer<'de>>(d: D) -> Result<SecretString, D::Error> {
71 let s = String::deserialize(d)?;
72 Ok(SecretString::new(s.into()))
73}
74
75impl fmt::Debug for UpstreamProxyConfig {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 f.debug_struct("UpstreamProxyConfig")
78 .field("proxy_url", &self.proxy_url)
79 .field("auth", &self.auth.as_ref().map(|_| "***"))
80 .field("bypass_hosts", &self.bypass_hosts)
81 .field("fail_open", &self.fail_open)
82 .finish()
83 }
84}
85
86impl UpstreamAuth {
87 pub fn new(username: String, password: String) -> Self {
88 Self {
89 username,
90 password: SecretString::new(password.into()),
91 }
92 }
93}
94
95impl fmt::Debug for UpstreamAuth {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 f.debug_struct("UpstreamAuth")
98 .field("username", &self.username)
99 .field("password", &"***")
100 .finish()
101 }
102}
103
104impl PartialEq for UpstreamProxyConfig {
105 fn eq(&self, other: &Self) -> bool {
106 self.proxy_url == other.proxy_url
107 && self.bypass_hosts == other.bypass_hosts
108 && self.fail_open == other.fail_open
109 && match (&self.auth, &other.auth) {
110 (Some(a), Some(b)) => {
111 a.username == b.username
112 && a.password.expose_secret() == b.password.expose_secret()
113 }
114 (None, None) => true,
115 _ => false,
116 }
117 }
118}
119
120impl Eq for UpstreamProxyConfig {}
121
122impl Default for RedactionPolicy {
123 fn default() -> Self {
124 Self {
125 enabled: false,
126 sensitive_header_names: default_sensitive_header_names(),
127 sensitive_query_keys: default_sensitive_query_keys(),
128 redact_bodies: false,
129 }
130 }
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct ProxyPolicy {
135 #[serde(default = "default_true")]
137 pub strict_http_semantics: bool,
138
139 #[serde(default = "default_false")]
141 pub allow_fallback_method: bool,
142
143 #[serde(default = "default_false")]
145 pub allow_fallback_status: bool,
146
147 #[serde(default = "default_false")]
149 pub enable_retry: bool,
150
151 #[serde(default = "default_true")]
153 pub retry_idempotent_only: bool,
154
155 #[serde(default = "default_max_retries")]
157 pub max_retries: u8,
158
159 pub sandbox_root: Option<PathBuf>,
161
162 #[serde(default = "default_max_file_bytes")]
164 pub max_local_file_bytes: usize,
165
166 #[serde(default = "default_max_body_bytes")]
168 pub max_body_size: usize,
169
170 #[serde(default = "default_rule_body_inspect_budget")]
174 pub rule_body_inspect_budget: usize,
175
176 #[serde(default = "default_request_timeout_ms")]
178 pub request_timeout_ms: u64,
179
180 #[serde(default = "default_false")]
182 pub transparent_enabled: bool,
183
184 #[serde(default = "default_true")]
186 pub transparent_require_original_dst: bool,
187
188 #[serde(default = "default_false")]
190 pub transparent_allow_host_fallback: bool,
191
192 #[serde(default = "default_true")]
194 pub transparent_reject_loopback_target: bool,
195
196 #[serde(default = "default_transparent_log_level")]
198 pub transparent_log_level: TransparentLogLevel,
199
200 #[serde(default = "default_quic_mode")]
202 pub quic_mode: QuicMode,
203
204 #[serde(default = "default_false")]
206 pub quic_downgrade_clear_cache: bool,
207
208 #[serde(default)]
209 pub redaction: RedactionPolicy,
210
211 #[serde(default, skip_serializing_if = "Option::is_none")]
213 pub upstream: Option<UpstreamProxyConfig>,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
217pub enum TransparentLogLevel {
218 Silent, Info, Debug, Trace, }
223
224#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
225pub enum QuicMode {
226 Downgrade,
228
229 Passthrough,
231
232 #[cfg(feature = "quic_mitm_experimental")]
234 ExperimentalMitm,
235}
236
237fn default_quic_mode() -> QuicMode {
238 QuicMode::Downgrade
239}
240
241impl Default for ProxyPolicy {
242 fn default() -> Self {
243 Self {
244 strict_http_semantics: true,
245 allow_fallback_method: false,
246 allow_fallback_status: false,
247 enable_retry: false,
248 retry_idempotent_only: true,
249 max_retries: 3,
250 sandbox_root: None,
251 max_local_file_bytes: 10 * 1024 * 1024, max_body_size: 10 * 1024 * 1024, rule_body_inspect_budget: 1024 * 1024, request_timeout_ms: 30_000, transparent_enabled: false,
256 transparent_require_original_dst: true,
257 transparent_allow_host_fallback: false,
258 transparent_reject_loopback_target: true,
259 transparent_log_level: TransparentLogLevel::Info,
260 quic_mode: QuicMode::Downgrade,
261 quic_downgrade_clear_cache: false,
262 redaction: RedactionPolicy::default(),
263 upstream: None,
264 }
265 }
266}
267
268impl RedactionPolicy {
269 pub fn apply_patch(&mut self, patch: RedactionPolicyPatch) {
270 if let Some(enabled) = patch.enabled {
271 self.enabled = enabled;
272 }
273 if let Some(names) = patch.sensitive_header_names {
274 self.sensitive_header_names = names;
275 }
276 if let Some(keys) = patch.sensitive_query_keys {
277 self.sensitive_query_keys = keys;
278 }
279 if let Some(redact_bodies) = patch.redact_bodies {
280 self.redact_bodies = redact_bodies;
281 }
282 }
283}
284
285impl ProxyPolicy {
286 pub fn apply_patch(&mut self, patch: ProxyPolicyPatch) {
287 if let Some(redaction_patch) = patch.redaction {
288 self.redaction.apply_patch(redaction_patch);
289 }
290 if let Some(upstream) = patch.upstream {
291 self.upstream = Some(upstream);
292 }
293 }
294}
295
296fn default_true() -> bool {
297 true
298}
299fn default_false() -> bool {
300 false
301}
302fn default_max_retries() -> u8 {
303 3
304}
305fn default_max_file_bytes() -> usize {
306 10 * 1024 * 1024
307}
308fn default_max_body_bytes() -> usize {
309 10 * 1024 * 1024
310}
311fn default_rule_body_inspect_budget() -> usize {
312 1024 * 1024 }
314fn default_request_timeout_ms() -> u64 {
315 30_000
316}
317fn default_transparent_log_level() -> TransparentLogLevel {
318 TransparentLogLevel::Info
319}
320fn default_sensitive_header_names() -> Vec<String> {
321 vec![
322 "authorization".to_string(),
323 "proxy-authorization".to_string(),
324 "cookie".to_string(),
325 "set-cookie".to_string(),
326 "x-api-key".to_string(),
327 "x-auth-token".to_string(),
328 ]
329}
330fn default_sensitive_query_keys() -> Vec<String> {
331 vec![
332 "token".to_string(),
333 "access_token".to_string(),
334 "refresh_token".to_string(),
335 "api_key".to_string(),
336 "apikey".to_string(),
337 "password".to_string(),
338 "secret".to_string(),
339 ]
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345 use secrecy::SecretString;
346
347 #[test]
348 fn upstream_proxy_config_debug_masks_password() {
349 let cfg = UpstreamProxyConfig {
350 proxy_url: "http://proxy:8080".to_string(),
351 auth: Some(UpstreamAuth {
352 username: "user".to_string(),
353 password: SecretString::new("s3cret".to_string().into()),
354 }),
355 bypass_hosts: vec!["*.local".to_string()],
356 fail_open: false,
357 };
358 let dbg = format!("{:?}", cfg);
359 assert!(dbg.contains("http://proxy:8080"));
360 assert!(dbg.contains("***"));
361 assert!(!dbg.contains("s3cret"));
362 }
363
364 #[test]
365 fn upstream_auth_debug_masks_password() {
366 let auth = UpstreamAuth {
367 username: "user".to_string(),
368 password: SecretString::new("s3cret".to_string().into()),
369 };
370 let dbg = format!("{:?}", auth);
371 assert!(dbg.contains("user"));
372 assert!(dbg.contains("***"));
373 assert!(!dbg.contains("s3cret"));
374 }
375
376 #[test]
377 fn proxy_policy_default_has_no_upstream() {
378 let policy = ProxyPolicy::default();
379 assert!(policy.upstream.is_none());
380 }
381
382 #[test]
383 fn proxy_policy_patch_applies_upstream() {
384 let mut policy = ProxyPolicy::default();
385 let upstream = UpstreamProxyConfig {
386 proxy_url: "http://corp:8080".to_string(),
387 auth: None,
388 bypass_hosts: vec![],
389 fail_open: false,
390 };
391 policy.apply_patch(ProxyPolicyPatch {
392 redaction: None,
393 upstream: Some(upstream.clone()),
394 });
395 assert_eq!(policy.upstream, Some(upstream));
396 }
397
398 #[test]
399 fn upstream_proxy_config_serde_masks_password() {
400 let cfg = UpstreamProxyConfig {
401 proxy_url: "https://secure-proxy:8443".to_string(),
402 auth: Some(UpstreamAuth {
403 username: "admin".to_string(),
404 password: SecretString::new("p@ss".to_string().into()),
405 }),
406 bypass_hosts: vec!["cidr:10.0.0.0/8".to_string(), "*.internal".to_string()],
407 fail_open: true,
408 };
409 let json = serde_json::to_string(&cfg).unwrap();
410 assert!(!json.contains("p@ss"));
412 assert!(json.contains("***"));
413 let decoded: UpstreamProxyConfig = serde_json::from_str(&json).unwrap();
415 assert_eq!(decoded.proxy_url, cfg.proxy_url);
416 assert_eq!(decoded.bypass_hosts, cfg.bypass_hosts);
417 assert_eq!(decoded.fail_open, cfg.fail_open);
418 }
419
420 #[test]
421 fn proxy_policy_serialization_hides_upstream_when_none() {
422 let policy = ProxyPolicy::default();
423 let json = serde_json::to_string(&policy).unwrap();
424 assert!(!json.contains("upstream"));
425 }
426
427 #[test]
428 fn upstream_partial_eq_compares_password() {
429 let a = UpstreamProxyConfig {
430 proxy_url: "http://p:8080".to_string(),
431 auth: Some(UpstreamAuth {
432 username: "u".to_string(),
433 password: SecretString::new("a".to_string().into()),
434 }),
435 bypass_hosts: vec![],
436 fail_open: false,
437 };
438 let b = UpstreamProxyConfig {
439 proxy_url: "http://p:8080".to_string(),
440 auth: Some(UpstreamAuth {
441 username: "u".to_string(),
442 password: SecretString::new("b".to_string().into()),
443 }),
444 bypass_hosts: vec![],
445 fail_open: false,
446 };
447 assert_ne!(a, b);
448 }
449}