agent_first_http/shared/
redact.rs1pub const ALWAYS_REDACTED: &[&str] = &[
11 "cookie",
12 "set-cookie",
13 "authorization",
14 "proxy-authorization",
15];
16
17pub const REDACTED_SUFFIXES: &[&str] = &["-token", "-secret"];
21
22#[must_use]
25pub fn should_redact(header_name: &str) -> bool {
26 let lower = header_name.to_ascii_lowercase();
27 if ALWAYS_REDACTED.iter().any(|h| *h == lower) {
28 return true;
29 }
30 REDACTED_SUFFIXES
31 .iter()
32 .any(|suffix| lower.ends_with(suffix))
33}
34
35pub const REDACTED_VALUE: &str = "[redacted]";
37
38#[must_use]
48pub fn redact_userinfo_passwords(line: &str) -> String {
49 let mut out = String::with_capacity(line.len());
50 let mut rest = line;
51 while let Some(pos) = rest.find("://") {
52 out.push_str(&rest[..pos + 3]);
54 let after = &rest[pos + 3..];
55 let auth_end = after
57 .find(|c: char| matches!(c, '/' | '?' | '#') || c.is_whitespace())
58 .unwrap_or(after.len());
59 let authority = &after[..auth_end];
60 match (authority.find('@'), authority.find(':')) {
61 (Some(at), Some(colon)) if colon < at => {
62 out.push_str(&authority[..colon]);
63 out.push_str(":***");
64 out.push_str(&authority[at..]);
65 }
66 _ => out.push_str(authority),
67 }
68 rest = &after[auth_end..];
69 }
70 out.push_str(rest);
71 out
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn always_redacted_headers_match_case_insensitively() {
80 for h in ["Cookie", "COOKIE", "cookie", "Set-Cookie", "SET-COOKIE"] {
81 assert!(should_redact(h), "{h}");
82 }
83 assert!(should_redact("Authorization"));
84 assert!(should_redact("Proxy-Authorization"));
85 }
86
87 #[test]
88 fn suffix_match_catches_token_and_secret_headers() {
89 assert!(should_redact("X-Api-Token"));
90 assert!(should_redact("x-csrf-token"));
91 assert!(should_redact("X-Shared-Secret"));
92 assert!(should_redact("x-bearer-secret"));
93 }
94
95 #[test]
96 fn unrelated_headers_pass_through() {
97 for h in ["Content-Type", "User-Agent", "Accept", "X-Trace-Id"] {
98 assert!(!should_redact(h), "{h}");
99 }
100 }
101
102 #[test]
103 fn userinfo_password_is_masked_in_text() {
104 assert_eq!(
105 redact_userinfo_passwords("launch --proxy-server=http://user:pass@proxy:8080 done"),
106 "launch --proxy-server=http://user:***@proxy:8080 done"
107 );
108 assert_eq!(
110 redact_userinfo_passwords("http://user@host/x"),
111 "http://user@host/x"
112 );
113 assert_eq!(
114 redact_userinfo_passwords("connect socks5://10.0.0.5:1080 now"),
115 "connect socks5://10.0.0.5:1080 now"
116 );
117 assert_eq!(redact_userinfo_passwords("no url here"), "no url here");
118 }
119}