aws_smithy_http_server/instrumentation/sensitivity/
headers.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! A wrapper around [`HeaderMap`] to allow for sensitivity.
7
8use std::fmt::{Debug, Display, Error, Formatter};
9
10use http::{header::HeaderName, HeaderMap};
11
12use crate::instrumentation::MakeFmt;
13
14use super::Sensitive;
15
16/// Marks the sensitive data of a header pair.
17#[derive(Debug, Default, PartialEq, Eq)]
18pub struct HeaderMarker {
19    /// Set to `true` to mark the value as sensitive.
20    pub value: bool,
21    /// Set to `Some(x)` to mark `key[x..]` as sensitive.
22    pub key_suffix: Option<usize>,
23}
24
25/// A wrapper around [`&HeaderMap`](HeaderMap) which modifies the behavior of [`Debug`]. Specific parts of the
26/// [`HeaderMap`] are marked as sensitive using a closure. This accommodates the [httpPrefixHeaders trait] and
27/// [httpHeader trait].
28///
29/// The [`Debug`] implementation will respect the `unredacted-logging` flag.
30///
31/// # Example
32///
33/// ```
34/// # use aws_smithy_http_server::instrumentation::sensitivity::headers::{SensitiveHeaders, HeaderMarker};
35/// # use http::header::HeaderMap;
36/// # let headers = HeaderMap::new();
37/// // Headers with keys equal to "header-name" are sensitive
38/// let marker = |key|
39///     HeaderMarker {
40///         value: key == "header-name",
41///         key_suffix: None
42///     };
43/// let headers = SensitiveHeaders::new(&headers, marker);
44/// println!("{headers:?}");
45/// ```
46///
47/// [httpPrefixHeaders trait]: https://smithy.io/2.0/spec/http-bindings.html#httpprefixheaders-trait
48/// [httpHeader trait]: https://smithy.io/2.0/spec/http-bindings.html#httpheader-trait
49pub struct SensitiveHeaders<'a, F> {
50    headers: &'a HeaderMap,
51    marker: F,
52}
53
54impl<'a, F> SensitiveHeaders<'a, F> {
55    /// Constructs a new [`SensitiveHeaders`].
56    pub fn new(headers: &'a HeaderMap, marker: F) -> Self {
57        Self { headers, marker }
58    }
59}
60
61/// Concatenates the [`Debug`] of [`&str`](str) and ['Sensitive<&str>`](Sensitive).
62struct ThenDebug<'a>(&'a str, Sensitive<&'a str>);
63
64impl<'a> Debug for ThenDebug<'a> {
65    #[inline]
66    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
67        write!(f, "\"{}{}\"", self.0, self.1)
68    }
69}
70
71/// Allows for formatting of `Left` or `Right` variants.
72enum OrFmt<Left, Right> {
73    Left(Left),
74    Right(Right),
75}
76
77impl<Left, Right> Debug for OrFmt<Left, Right>
78where
79    Left: Debug,
80    Right: Debug,
81{
82    #[inline]
83    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
84        match self {
85            Self::Left(left) => left.fmt(f),
86            Self::Right(right) => right.fmt(f),
87        }
88    }
89}
90
91impl<Left, Right> Display for OrFmt<Left, Right>
92where
93    Left: Display,
94    Right: Display,
95{
96    #[inline]
97    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
98        match self {
99            Self::Left(left) => left.fmt(f),
100            Self::Right(right) => right.fmt(f),
101        }
102    }
103}
104
105impl<'a, F> Debug for SensitiveHeaders<'a, F>
106where
107    F: Fn(&'a HeaderName) -> HeaderMarker,
108{
109    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
110        let iter = self.headers.iter().map(|(key, value)| {
111            let HeaderMarker {
112                value: value_sensitive,
113                key_suffix,
114            } = (self.marker)(key);
115
116            let key = if let Some(key_suffix) = key_suffix {
117                let key_str = key.as_str();
118                OrFmt::Left(ThenDebug(&key_str[..key_suffix], Sensitive(&key_str[key_suffix..])))
119            } else {
120                OrFmt::Right(key)
121            };
122
123            let value = if value_sensitive {
124                OrFmt::Left(Sensitive(value))
125            } else {
126                OrFmt::Right(value)
127            };
128
129            (key, value)
130        });
131
132        f.debug_map().entries(iter).finish()
133    }
134}
135
136/// A [`MakeFmt`] producing [`SensitiveHeaders`].
137#[derive(Clone)]
138pub struct MakeHeaders<F>(pub(crate) F);
139
140impl<F> Debug for MakeHeaders<F> {
141    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
142        f.debug_tuple("MakeHeaders").field(&"...").finish()
143    }
144}
145
146impl<'a, F> MakeFmt<&'a HeaderMap> for MakeHeaders<F>
147where
148    F: Clone,
149{
150    type Target = SensitiveHeaders<'a, F>;
151
152    fn make(&self, source: &'a HeaderMap) -> Self::Target {
153        SensitiveHeaders::new(source, self.0.clone())
154    }
155}
156#[cfg(test)]
157mod tests {
158    use http::{header::HeaderName, HeaderMap, HeaderValue};
159
160    use super::*;
161
162    // This is needed because we header maps with "{redacted}" are disallowed.
163    struct TestDebugMap([(&'static str, &'static str); 4]);
164
165    impl Debug for TestDebugMap {
166        fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
167            f.debug_map().entries(self.0).finish()
168        }
169    }
170
171    const HEADER_MAP: [(&str, &str); 4] = [
172        ("name-a", "value-a"),
173        ("name-b", "value-b"),
174        ("prefix-a-x", "value-c"),
175        ("prefix-b-y", "value-d"),
176    ];
177
178    fn to_header_map<I>(values: I) -> HeaderMap
179    where
180        I: IntoIterator<Item = (&'static str, &'static str)>,
181    {
182        values
183            .into_iter()
184            .map(|(key, value)| (HeaderName::from_static(key), HeaderValue::from_static(value)))
185            .collect()
186    }
187
188    #[test]
189    fn mark_none() {
190        let original: HeaderMap = to_header_map(HEADER_MAP);
191
192        let output = SensitiveHeaders::new(&original, |_| HeaderMarker::default());
193        assert_eq!(format!("{:?}", output), format!("{:?}", original));
194    }
195
196    #[cfg(not(feature = "unredacted-logging"))]
197    const ALL_VALUES_HEADER_MAP: [(&str, &str); 4] = [
198        ("name-a", "{redacted}"),
199        ("name-b", "{redacted}"),
200        ("prefix-a-x", "{redacted}"),
201        ("prefix-b-y", "{redacted}"),
202    ];
203    #[cfg(feature = "unredacted-logging")]
204    const ALL_VALUES_HEADER_MAP: [(&str, &str); 4] = HEADER_MAP;
205
206    #[test]
207    fn mark_all_values() {
208        let original: HeaderMap = to_header_map(HEADER_MAP);
209        let expected = TestDebugMap(ALL_VALUES_HEADER_MAP);
210
211        let output = SensitiveHeaders::new(&original, |_| HeaderMarker {
212            value: true,
213            key_suffix: None,
214        });
215        assert_eq!(format!("{:?}", output), format!("{:?}", expected));
216    }
217
218    #[cfg(not(feature = "unredacted-logging"))]
219    const NAME_A_HEADER_MAP: [(&str, &str); 4] = [
220        ("name-a", "{redacted}"),
221        ("name-b", "value-b"),
222        ("prefix-a-x", "value-c"),
223        ("prefix-b-y", "value-d"),
224    ];
225    #[cfg(feature = "unredacted-logging")]
226    const NAME_A_HEADER_MAP: [(&str, &str); 4] = HEADER_MAP;
227
228    #[test]
229    fn mark_name_a_values() {
230        let original: HeaderMap = to_header_map(HEADER_MAP);
231        let expected = TestDebugMap(NAME_A_HEADER_MAP);
232
233        let output = SensitiveHeaders::new(&original, |name| HeaderMarker {
234            value: name == "name-a",
235            key_suffix: None,
236        });
237        assert_eq!(format!("{:?}", output), format!("{:?}", expected));
238    }
239
240    #[cfg(not(feature = "unredacted-logging"))]
241    const PREFIX_A_HEADER_MAP: [(&str, &str); 4] = [
242        ("name-a", "value-a"),
243        ("name-b", "value-b"),
244        ("prefix-a{redacted}", "value-c"),
245        ("prefix-b-y", "value-d"),
246    ];
247    #[cfg(feature = "unredacted-logging")]
248    const PREFIX_A_HEADER_MAP: [(&str, &str); 4] = HEADER_MAP;
249
250    #[test]
251    fn mark_prefix_a_values() {
252        let original: HeaderMap = to_header_map(HEADER_MAP);
253        let expected = TestDebugMap(PREFIX_A_HEADER_MAP);
254
255        let prefix = "prefix-a";
256        let output = SensitiveHeaders::new(&original, |name: &HeaderName| HeaderMarker {
257            value: false,
258            key_suffix: if name.as_str().starts_with(prefix) {
259                Some(prefix.len())
260            } else {
261                None
262            },
263        });
264        assert_eq!(format!("{:?}", output), format!("{:?}", expected));
265    }
266}