Skip to main content

origin_http/
headers.rs

1use std::fmt;
2
3/// Header names whose values must never reach a log line.
4const SENSITIVE: &[&str] = &[
5    "authorization",
6    "proxy-authorization",
7    "cookie",
8    "set-cookie",
9    "x-api-key",
10    "x-auth-token",
11];
12
13/// Case-insensitive header collection.
14///
15/// Stored as a list rather than a map because HTTP allows repeated headers and the
16/// order occasionally matters.
17#[derive(Clone, Default, PartialEq, Eq)]
18pub struct Headers(Vec<(String, String)>);
19
20impl Headers {
21    pub fn new() -> Self {
22        Self::default()
23    }
24
25    pub fn insert(&mut self, name: impl Into<String>, value: impl Into<String>) {
26        let name = name.into().to_ascii_lowercase();
27        self.0.retain(|(existing, _)| existing != &name);
28        self.0.push((name, value.into()));
29    }
30
31    pub fn append(&mut self, name: impl Into<String>, value: impl Into<String>) {
32        self.0
33            .push((name.into().to_ascii_lowercase(), value.into()));
34    }
35
36    pub fn get(&self, name: &str) -> Option<&str> {
37        let name = name.to_ascii_lowercase();
38        self.0
39            .iter()
40            .find(|(existing, _)| existing == &name)
41            .map(|(_, value)| value.as_str())
42    }
43
44    /// Parse a header value as a number, ignoring anything unparseable.
45    ///
46    /// A malformed rate-limit header is not worth failing a request over.
47    pub fn get_u64(&self, name: &str) -> Option<u64> {
48        self.get(name)?.trim().parse().ok()
49    }
50
51    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
52        self.0
53            .iter()
54            .map(|(name, value)| (name.as_str(), value.as_str()))
55    }
56
57    pub fn is_empty(&self) -> bool {
58        self.0.is_empty()
59    }
60
61    pub fn len(&self) -> usize {
62        self.0.len()
63    }
64}
65
66impl<K: Into<String>, V: Into<String>> FromIterator<(K, V)> for Headers {
67    fn from_iter<I: IntoIterator<Item = (K, V)>>(entries: I) -> Self {
68        let mut headers = Self::new();
69        for (name, value) in entries {
70            headers.append(name, value);
71        }
72        headers
73    }
74}
75
76/// A body's length, standing in for its content in `Debug` output.
77///
78/// `HttpRequest` and `HttpResponse` bodies can carry OAuth codes, refresh tokens or
79/// client secrets; nothing at this layer can tell safe content from sensitive content,
80/// so the body is never printed, only its size.
81pub(crate) struct RedactedBody(pub(crate) usize);
82
83impl fmt::Debug for RedactedBody {
84    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85        write!(f, "<{} bytes, redacted>", self.0)
86    }
87}
88
89/// Redacting `Debug`.
90///
91/// A `tracing::debug!(?headers, ...)` anywhere in the stack would otherwise print
92/// bearer tokens into the log file.
93impl fmt::Debug for Headers {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        let mut map = f.debug_map();
96        for (name, value) in self.iter() {
97            if SENSITIVE.contains(&name) {
98                map.entry(&name, &"***");
99            } else {
100                map.entry(&name, &value);
101            }
102        }
103        map.finish()
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn lookup_ignores_case() {
113        let mut headers = Headers::new();
114        headers.insert("Content-Type", "application/json");
115
116        assert_eq!(headers.get("content-type"), Some("application/json"));
117        assert_eq!(headers.get("CONTENT-TYPE"), Some("application/json"));
118    }
119
120    #[test]
121    fn insert_replaces_while_append_keeps_both() {
122        let mut headers = Headers::new();
123        headers.insert("x-test", "a");
124        headers.insert("x-test", "b");
125        assert_eq!(headers.len(), 1);
126
127        headers.append("x-test", "c");
128        assert_eq!(headers.len(), 2);
129    }
130
131    #[test]
132    fn debug_output_redacts_credentials() {
133        let mut headers = Headers::new();
134        headers.insert("Authorization", "Bearer ghp_supersecret");
135        headers.insert("Accept", "application/json");
136
137        let rendered = format!("{headers:?}");
138
139        assert!(!rendered.contains("ghp_supersecret"), "got: {rendered}");
140        assert!(rendered.contains("application/json"));
141    }
142
143    #[test]
144    fn a_malformed_numeric_header_is_ignored_rather_than_fatal() {
145        let headers = Headers::from_iter([("x-ratelimit-remaining", "unknown")]);
146        assert_eq!(headers.get_u64("x-ratelimit-remaining"), None);
147    }
148}