pub const ALWAYS_REDACTED: &[&str] = &[
"cookie",
"set-cookie",
"authorization",
"proxy-authorization",
];
pub const REDACTED_SUFFIXES: &[&str] = &["-token", "-secret"];
#[must_use]
pub fn should_redact(header_name: &str) -> bool {
let lower = header_name.to_ascii_lowercase();
if ALWAYS_REDACTED.iter().any(|h| *h == lower) {
return true;
}
REDACTED_SUFFIXES
.iter()
.any(|suffix| lower.ends_with(suffix))
}
pub const REDACTED_VALUE: &str = "[redacted]";
#[must_use]
pub fn redact_userinfo_passwords(line: &str) -> String {
let mut out = String::with_capacity(line.len());
let mut rest = line;
while let Some(pos) = rest.find("://") {
out.push_str(&rest[..pos + 3]);
let after = &rest[pos + 3..];
let auth_end = after
.find(|c: char| matches!(c, '/' | '?' | '#') || c.is_whitespace())
.unwrap_or(after.len());
let authority = &after[..auth_end];
match (authority.find('@'), authority.find(':')) {
(Some(at), Some(colon)) if colon < at => {
out.push_str(&authority[..colon]);
out.push_str(":***");
out.push_str(&authority[at..]);
}
_ => out.push_str(authority),
}
rest = &after[auth_end..];
}
out.push_str(rest);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn always_redacted_headers_match_case_insensitively() {
for h in ["Cookie", "COOKIE", "cookie", "Set-Cookie", "SET-COOKIE"] {
assert!(should_redact(h), "{h}");
}
assert!(should_redact("Authorization"));
assert!(should_redact("Proxy-Authorization"));
}
#[test]
fn suffix_match_catches_token_and_secret_headers() {
assert!(should_redact("X-Api-Token"));
assert!(should_redact("x-csrf-token"));
assert!(should_redact("X-Shared-Secret"));
assert!(should_redact("x-bearer-secret"));
}
#[test]
fn unrelated_headers_pass_through() {
for h in ["Content-Type", "User-Agent", "Accept", "X-Trace-Id"] {
assert!(!should_redact(h), "{h}");
}
}
#[test]
fn userinfo_password_is_masked_in_text() {
assert_eq!(
redact_userinfo_passwords("launch --proxy-server=http://user:pass@proxy:8080 done"),
"launch --proxy-server=http://user:***@proxy:8080 done"
);
assert_eq!(
redact_userinfo_passwords("http://user@host/x"),
"http://user@host/x"
);
assert_eq!(
redact_userinfo_passwords("connect socks5://10.0.0.5:1080 now"),
"connect socks5://10.0.0.5:1080 now"
);
assert_eq!(redact_userinfo_passwords("no url here"), "no url here");
}
}