1use url::Url;
4
5pub fn redact_url_credentials_for_logs(input: &str) -> String {
11 if let Ok(mut u) = Url::parse(input) {
12 let has_user = !u.username().is_empty();
13 let has_pass = u.password().is_some();
14 if has_user || has_pass {
15 let _ = u.set_username("");
16 let _ = u.set_password(None);
17 return u.to_string();
18 }
19 }
23 heuristic_redact_userinfo(input)
24}
25
26pub fn redact_url_if_echoed_in_text(err_text: &str, raw_connection_url: &str) -> String {
32 let redacted = redact_url_credentials_for_logs(raw_connection_url);
33 if raw_connection_url == redacted || !err_text.contains(raw_connection_url) {
34 return err_text.to_string();
35 }
36 err_text.replace(raw_connection_url, &redacted)
37}
38
39fn heuristic_redact_userinfo(s: &str) -> String {
40 let Some(pos) = s.find("://") else {
41 return s.to_string();
42 };
43 let after_scheme = &s[pos + 3..];
44 let Some(at_rel) = after_scheme.find('@') else {
45 return s.to_string();
46 };
47 format!("{}<redacted>{}", &s[..pos + 3], &after_scheme[at_rel + 1..])
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55
56 #[test]
57 fn leaves_bare_nats_url_unchanged() {
58 assert_eq!(
59 redact_url_credentials_for_logs("nats://127.0.0.1:4222"),
60 "nats://127.0.0.1:4222"
61 );
62 }
63
64 #[test]
65 fn strips_user_password_nats() {
66 let out = redact_url_credentials_for_logs("nats://alice:secret@broker.internal:4222");
67 assert!(!out.contains("alice"), "{out}");
68 assert!(!out.contains("secret"), "{out}");
69 assert!(out.contains("broker.internal"), "{out}");
70 }
71
72 #[test]
73 fn heuristic_when_not_parseable() {
74 let out = heuristic_redact_userinfo("nats://x:y@host:1/extra");
75 assert_eq!(out, "nats://<redacted>host:1/extra");
76 }
77
78 #[test]
79 fn redacts_echoed_url_in_error_blob() {
80 let raw = "nats://alice:hunter2@10.0.0.5:4222";
81 let err = format!("connection refused: {raw} (try again)");
82 let out = redact_url_if_echoed_in_text(&err, raw);
83 assert!(!out.contains("alice"), "{out}");
84 assert!(!out.contains("hunter2"), "{out}");
85 assert!(out.contains("10.0.0.5"), "{out}");
86 }
87
88 #[test]
89 fn leaves_error_unchanged_when_url_not_echoed() {
90 let err = "IO error: connection refused";
91 assert_eq!(
92 redact_url_if_echoed_in_text(err, "nats://u:p@h:1"),
93 err.to_string()
94 );
95 }
96}