Skip to main content

apollo_redaction/strategy/
url.rs

1use crate::RedactionStrategy;
2use std::fmt;
3use url::Url;
4
5/// Redact URLs, removing the scheme, hostname, port, auth, query values, and fragments.
6///
7/// ```rust
8/// use apollo_redaction::Redacted;
9/// use apollo_redaction::strategy::UrlHostRedactor;
10///
11/// fn url(input: &str) -> String {
12///     Redacted::<_, UrlHostRedactor>::new(input).to_string()
13/// }
14///
15/// assert_eq!(url("https://example.com/api/secret"), "****/api/secret");
16/// assert_eq!(url("https://example.com"), "****");
17/// assert_eq!(url("postgres://user:pass@host/db"), "****/db");
18/// assert_eq!(url("https://example.com/api?key=secret"), "****/api?key=****");
19/// assert_eq!(url("https://example.com/page#section"), "****/page#****");
20/// ```
21#[derive(Default, Clone)]
22pub struct UrlHostRedactor;
23
24impl<T: AsRef<str>> RedactionStrategy<T> for UrlHostRedactor {
25    fn display(&self, value: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        redact_url(value.as_ref(), f)
27    }
28}
29
30fn redact_url(input: &str, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31    let Ok(parsed) = Url::parse(input) else {
32        return write!(f, "[REDACTED: invalid URL]");
33    };
34
35    write!(f, "****")?;
36
37    // Write path (the url crate returns "/" for URLs with authority even when no path)
38    let path = parsed.path();
39    if path != "/" {
40        write!(f, "{}", path)?;
41    }
42
43    // Write redacted query parameters
44    if parsed.query().is_some() {
45        write!(f, "?")?;
46        let mut first = true;
47        for (key, _value) in parsed.query_pairs() {
48            if !first {
49                write!(f, "&")?;
50            }
51            first = false;
52            write!(f, "{}=****", key)?;
53        }
54    }
55
56    // Write redacted fragment
57    if parsed.fragment().is_some() {
58        write!(f, "#****")?;
59    }
60
61    Ok(())
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use crate::Redacted;
68
69    fn url(input: &str) -> String {
70        Redacted::<_, UrlHostRedactor>::new(input).to_string()
71    }
72
73    #[test]
74    fn test_https_with_path() {
75        assert_eq!(url("https://example.com/api/secret"), "****/api/secret");
76    }
77
78    #[test]
79    fn test_no_path() {
80        assert_eq!(url("https://example.com"), "****");
81    }
82
83    #[test]
84    fn test_with_credentials() {
85        assert_eq!(url("postgres://user:pass@host/db"), "****/db");
86    }
87
88    #[test]
89    fn test_invalid_url() {
90        assert_eq!(url("not a url"), "[REDACTED: invalid URL]");
91    }
92
93    #[test]
94    fn test_with_query_string() {
95        assert_eq!(
96            url("https://example.com/api?key=secret"),
97            "****/api?key=****"
98        );
99    }
100
101    #[test]
102    fn test_with_multiple_query_params() {
103        assert_eq!(
104            url("https://example.com/api?key=secret&token=abc123"),
105            "****/api?key=****&token=****"
106        );
107    }
108
109    #[test]
110    fn test_with_query_flag_no_value() {
111        assert_eq!(
112            url("https://example.com/api?debug&key=secret"),
113            "****/api?debug=****&key=****"
114        );
115    }
116
117    #[test]
118    fn test_with_fragment() {
119        assert_eq!(url("https://example.com/page#section"), "****/page#****");
120    }
121
122    #[test]
123    fn test_with_query_and_fragment() {
124        assert_eq!(
125            url("https://example.com/api?key=secret#section"),
126            "****/api?key=****#****"
127        );
128    }
129
130    #[test]
131    fn test_query_without_path() {
132        assert_eq!(url("https://example.com?key=secret"), "****?key=****");
133    }
134
135    #[test]
136    fn test_fragment_without_path() {
137        assert_eq!(url("https://example.com#section"), "****#****");
138    }
139}