Skip to main content

aws_smithy_runtime_api/http/
headers.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Types for HTTP headers
7
8use crate::http::error::{HttpError, NonUtf8Header};
9use std::borrow::Cow;
10use std::fmt::Debug;
11use std::str::FromStr;
12
13/// Header names whose values must be redacted in Debug output to prevent
14/// credential / session-token / customer-key leakage via tracing.
15const DENYLIST: &[&str] = &[
16    "authorization",
17    "proxy-authorization",
18    "x-amz-security-token",
19    "cookie",
20    "set-cookie",
21    "x-amz-server-side-encryption-customer-key",
22    "x-amz-server-side-encryption-customer-key-md5",
23    "x-amz-copy-source-server-side-encryption-customer-key",
24    "x-amz-copy-source-server-side-encryption-customer-key-md5",
25];
26
27fn is_sensitive(name: &str) -> bool {
28    DENYLIST.iter().any(|d| name.eq_ignore_ascii_case(d))
29}
30
31/// An immutable view of headers
32#[derive(Clone, Default)]
33pub struct Headers {
34    pub(super) headers: http_02x::HeaderMap<HeaderValue>,
35}
36
37impl Debug for Headers {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        let mut map = f.debug_map();
40        for (key, value) in self.headers.iter() {
41            let name = key.as_str();
42            if is_sensitive(name) {
43                map.entry(
44                    &name,
45                    &format_args!("** redacted (length={}) **", value.as_ref().len()),
46                );
47            } else {
48                map.entry(&name, &value.as_ref());
49            }
50        }
51        map.finish()
52    }
53}
54
55impl<'a> IntoIterator for &'a Headers {
56    type Item = (&'a str, &'a str);
57    type IntoIter = HeadersIter<'a>;
58
59    fn into_iter(self) -> Self::IntoIter {
60        HeadersIter {
61            inner: self.headers.iter(),
62        }
63    }
64}
65
66/// An Iterator over headers
67pub struct HeadersIter<'a> {
68    inner: http_02x::header::Iter<'a, HeaderValue>,
69}
70
71impl<'a> Iterator for HeadersIter<'a> {
72    type Item = (&'a str, &'a str);
73
74    fn next(&mut self) -> Option<Self::Item> {
75        self.inner.next().map(|(k, v)| (k.as_str(), v.as_ref()))
76    }
77}
78
79impl Headers {
80    /// Create an empty header map
81    pub fn new() -> Self {
82        Self::default()
83    }
84
85    #[cfg(feature = "http-1x")]
86    pub(crate) fn http1_headermap(self) -> http_1x::HeaderMap {
87        let mut headers = http_1x::HeaderMap::new();
88        headers.reserve(self.headers.len());
89        headers.extend(self.headers.into_iter().map(|(k, v)| {
90            (
91                k.map(|n| {
92                    http_1x::HeaderName::from_bytes(n.as_str().as_bytes()).expect("proven valid")
93                }),
94                v.into_http1x(),
95            )
96        }));
97        headers
98    }
99
100    #[cfg(feature = "http-02x")]
101    pub(crate) fn http0_headermap(self) -> http_02x::HeaderMap {
102        let mut headers = http_02x::HeaderMap::new();
103        headers.reserve(self.headers.len());
104        headers.extend(self.headers.into_iter().map(|(k, v)| (k, v.into_http02x())));
105        headers
106    }
107
108    /// Returns the value for a given key
109    ///
110    /// If multiple values are associated, the first value is returned
111    /// See [HeaderMap::get](http_02x::HeaderMap::get)
112    pub fn get(&self, key: impl AsRef<str>) -> Option<&str> {
113        self.headers.get(key.as_ref()).map(|v| v.as_ref())
114    }
115
116    /// Returns all values for a given key
117    pub fn get_all(&self, key: impl AsRef<str>) -> impl Iterator<Item = &str> {
118        self.headers
119            .get_all(key.as_ref())
120            .iter()
121            .map(|v| v.as_ref())
122    }
123
124    /// Returns an iterator over the headers
125    pub fn iter(&self) -> HeadersIter<'_> {
126        HeadersIter {
127            inner: self.headers.iter(),
128        }
129    }
130
131    /// Returns the total number of **values** stored in the map
132    pub fn len(&self) -> usize {
133        self.headers.len()
134    }
135
136    /// Returns true if there are no headers
137    pub fn is_empty(&self) -> bool {
138        self.len() == 0
139    }
140
141    /// Returns true if this header is present
142    pub fn contains_key(&self, key: impl AsRef<str>) -> bool {
143        self.headers.contains_key(key.as_ref())
144    }
145
146    /// Insert a value into the headers structure.
147    ///
148    /// This will *replace* any existing value for this key. Returns the previous associated value if any.
149    ///
150    /// # Panics
151    /// If the key is not valid ASCII, or if the value is not valid UTF-8, this function will panic.
152    pub fn insert(
153        &mut self,
154        key: impl AsHeaderComponent,
155        value: impl AsHeaderComponent,
156    ) -> Option<String> {
157        let key = header_name(key, false).unwrap();
158        let value = header_value(value.into_maybe_static().unwrap(), false).unwrap();
159        self.headers
160            .insert(key, value)
161            .map(|old_value| old_value.into())
162    }
163
164    /// Insert a value into the headers structure.
165    ///
166    /// This will *replace* any existing value for this key. Returns the previous associated value if any.
167    ///
168    /// If the key is not valid ASCII, or if the value is not valid UTF-8, this function will return an error.
169    pub fn try_insert(
170        &mut self,
171        key: impl AsHeaderComponent,
172        value: impl AsHeaderComponent,
173    ) -> Result<Option<String>, HttpError> {
174        let key = header_name(key, true)?;
175        let value = header_value(value.into_maybe_static()?, true)?;
176        Ok(self
177            .headers
178            .insert(key, value)
179            .map(|old_value| old_value.into()))
180    }
181
182    /// Appends a value to a given key
183    ///
184    /// # Panics
185    /// If the key is not valid ASCII, or if the value is not valid UTF-8, this function will panic.
186    pub fn append(&mut self, key: impl AsHeaderComponent, value: impl AsHeaderComponent) -> bool {
187        let key = header_name(key.into_maybe_static().unwrap(), false).unwrap();
188        let value = header_value(value.into_maybe_static().unwrap(), false).unwrap();
189        self.headers.append(key, value)
190    }
191
192    /// Appends a value to a given key
193    ///
194    /// If the key is not valid ASCII, or if the value is not valid UTF-8, this function will return an error.
195    pub fn try_append(
196        &mut self,
197        key: impl AsHeaderComponent,
198        value: impl AsHeaderComponent,
199    ) -> Result<bool, HttpError> {
200        let key = header_name(key.into_maybe_static()?, true)?;
201        let value = header_value(value.into_maybe_static()?, true)?;
202        Ok(self.headers.append(key, value))
203    }
204
205    /// Removes all headers with a given key
206    ///
207    /// If there are multiple entries for this key, the first entry is returned
208    pub fn remove(&mut self, key: impl AsRef<str>) -> Option<String> {
209        self.headers
210            .remove(key.as_ref())
211            .map(|h| h.as_str().to_string())
212    }
213}
214
215#[cfg(feature = "http-02x")]
216impl TryFrom<http_02x::HeaderMap> for Headers {
217    type Error = HttpError;
218
219    fn try_from(value: http_02x::HeaderMap) -> Result<Self, Self::Error> {
220        if let Some(utf8_error) = value.iter().find_map(|(k, v)| {
221            std::str::from_utf8(v.as_bytes())
222                .err()
223                .map(|err| NonUtf8Header::new(k.as_str().to_owned(), v.as_bytes().to_vec(), err))
224        }) {
225            Err(HttpError::non_utf8_header(utf8_error))
226        } else {
227            let mut string_safe_headers: http_02x::HeaderMap<HeaderValue> = Default::default();
228            string_safe_headers.extend(
229                value
230                    .into_iter()
231                    .map(|(k, v)| (k, HeaderValue::from_http02x(v).expect("validated above"))),
232            );
233            Ok(Headers {
234                headers: string_safe_headers,
235            })
236        }
237    }
238}
239
240#[cfg(feature = "http-1x")]
241impl TryFrom<http_1x::HeaderMap> for Headers {
242    type Error = HttpError;
243
244    fn try_from(value: http_1x::HeaderMap) -> Result<Self, Self::Error> {
245        if let Some(utf8_error) = value.iter().find_map(|(k, v)| {
246            std::str::from_utf8(v.as_bytes())
247                .err()
248                .map(|err| NonUtf8Header::new(k.as_str().to_owned(), v.as_bytes().to_vec(), err))
249        }) {
250            Err(HttpError::non_utf8_header(utf8_error))
251        } else {
252            let mut string_safe_headers: http_02x::HeaderMap<HeaderValue> = Default::default();
253            string_safe_headers.extend(value.into_iter().map(|(k, v)| {
254                (
255                    k.map(|v| {
256                        http_02x::HeaderName::from_bytes(v.as_str().as_bytes())
257                            .expect("known valid")
258                    }),
259                    HeaderValue::from_http1x(v).expect("validated above"),
260                )
261            }));
262            Ok(Headers {
263                headers: string_safe_headers,
264            })
265        }
266    }
267}
268
269use sealed::AsHeaderComponent;
270
271mod sealed {
272    use super::*;
273    /// Trait defining things that may be converted into a header component (name or value)
274    pub trait AsHeaderComponent {
275        /// If the component can be represented as a Cow<'static, str>, return it
276        fn into_maybe_static(self) -> Result<MaybeStatic, HttpError>;
277
278        /// Return a string reference to this header
279        fn as_str(&self) -> Result<&str, HttpError>;
280
281        /// If a component is already internally represented as a `http02x::HeaderName`, return it
282        fn repr_as_http02x_header_name(self) -> Result<http_02x::HeaderName, Self>
283        where
284            Self: Sized,
285        {
286            Err(self)
287        }
288    }
289
290    impl AsHeaderComponent for &'static str {
291        fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
292            Ok(Cow::Borrowed(self))
293        }
294
295        fn as_str(&self) -> Result<&str, HttpError> {
296            Ok(self)
297        }
298    }
299
300    impl AsHeaderComponent for String {
301        fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
302            Ok(Cow::Owned(self))
303        }
304
305        fn as_str(&self) -> Result<&str, HttpError> {
306            Ok(self)
307        }
308    }
309
310    impl AsHeaderComponent for Cow<'static, str> {
311        fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
312            Ok(self)
313        }
314
315        fn as_str(&self) -> Result<&str, HttpError> {
316            Ok(self.as_ref())
317        }
318    }
319
320    impl AsHeaderComponent for http_02x::HeaderValue {
321        fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
322            Ok(Cow::Owned(
323                std::str::from_utf8(self.as_bytes())
324                    .map_err(|err| {
325                        HttpError::non_utf8_header(NonUtf8Header::new_missing_name(
326                            self.as_bytes().to_vec(),
327                            err,
328                        ))
329                    })?
330                    .to_string(),
331            ))
332        }
333
334        fn as_str(&self) -> Result<&str, HttpError> {
335            std::str::from_utf8(self.as_bytes()).map_err(|err| {
336                HttpError::non_utf8_header(NonUtf8Header::new_missing_name(
337                    self.as_bytes().to_vec(),
338                    err,
339                ))
340            })
341        }
342    }
343
344    impl AsHeaderComponent for http_02x::HeaderName {
345        fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
346            Ok(self.to_string().into())
347        }
348
349        fn as_str(&self) -> Result<&str, HttpError> {
350            Ok(self.as_ref())
351        }
352
353        fn repr_as_http02x_header_name(self) -> Result<http_02x::HeaderName, Self>
354        where
355            Self: Sized,
356        {
357            Ok(self)
358        }
359    }
360
361    #[cfg(feature = "http-1x")]
362    impl AsHeaderComponent for http_1x::HeaderName {
363        fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
364            Ok(self.to_string().into())
365        }
366
367        fn as_str(&self) -> Result<&str, HttpError> {
368            Ok(self.as_ref())
369        }
370    }
371
372    #[cfg(feature = "http-1x")]
373    impl AsHeaderComponent for http_1x::HeaderValue {
374        fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
375            Ok(Cow::Owned(
376                std::str::from_utf8(self.as_bytes())
377                    .map_err(|err| {
378                        HttpError::non_utf8_header(NonUtf8Header::new_missing_name(
379                            self.as_bytes().to_vec(),
380                            err,
381                        ))
382                    })?
383                    .to_string(),
384            ))
385        }
386
387        fn as_str(&self) -> Result<&str, HttpError> {
388            std::str::from_utf8(self.as_bytes()).map_err(|err| {
389                HttpError::non_utf8_header(NonUtf8Header::new_missing_name(
390                    self.as_bytes().to_vec(),
391                    err,
392                ))
393            })
394        }
395    }
396}
397
398mod header_value {
399    use super::*;
400
401    /// HeaderValue type
402    ///
403    /// **Note**: Unlike `HeaderValue` in `http`, this only supports UTF-8 header values
404    #[derive(Debug, Clone)]
405    pub struct HeaderValue {
406        _private: Inner,
407    }
408
409    #[derive(Debug, Clone)]
410    enum Inner {
411        H0(http_02x::HeaderValue),
412        #[allow(dead_code)]
413        H1(http_1x::HeaderValue),
414    }
415
416    impl HeaderValue {
417        #[allow(dead_code)]
418        pub(crate) fn from_http02x(value: http_02x::HeaderValue) -> Result<Self, HttpError> {
419            let _ = std::str::from_utf8(value.as_bytes()).map_err(|err| {
420                HttpError::non_utf8_header(NonUtf8Header::new_missing_name(
421                    value.as_bytes().to_vec(),
422                    err,
423                ))
424            })?;
425            Ok(Self {
426                _private: Inner::H0(value),
427            })
428        }
429
430        #[allow(dead_code)]
431        pub(crate) fn from_http1x(value: http_1x::HeaderValue) -> Result<Self, HttpError> {
432            let _ = std::str::from_utf8(value.as_bytes()).map_err(|err| {
433                HttpError::non_utf8_header(NonUtf8Header::new_missing_name(
434                    value.as_bytes().to_vec(),
435                    err,
436                ))
437            })?;
438            Ok(Self {
439                _private: Inner::H1(value),
440            })
441        }
442
443        #[allow(dead_code)]
444        pub(crate) fn into_http02x(self) -> http_02x::HeaderValue {
445            match self._private {
446                Inner::H0(v) => v,
447                Inner::H1(v) => http_02x::HeaderValue::from_maybe_shared(v).expect("unreachable"),
448            }
449        }
450
451        #[allow(dead_code)]
452        pub(crate) fn into_http1x(self) -> http_1x::HeaderValue {
453            match self._private {
454                Inner::H1(v) => v,
455                Inner::H0(v) => http_1x::HeaderValue::from_maybe_shared(v).expect("unreachable"),
456            }
457        }
458    }
459
460    impl AsRef<str> for HeaderValue {
461        fn as_ref(&self) -> &str {
462            let bytes = match &self._private {
463                Inner::H0(v) => v.as_bytes(),
464                Inner::H1(v) => v.as_bytes(),
465            };
466            std::str::from_utf8(bytes).expect("unreachable—only strings may be stored")
467        }
468    }
469
470    impl From<HeaderValue> for String {
471        fn from(value: HeaderValue) -> Self {
472            value.as_ref().to_string()
473        }
474    }
475
476    impl HeaderValue {
477        /// Returns the string representation of this header value
478        pub fn as_str(&self) -> &str {
479            self.as_ref()
480        }
481    }
482
483    impl FromStr for HeaderValue {
484        type Err = HttpError;
485
486        fn from_str(s: &str) -> Result<Self, Self::Err> {
487            HeaderValue::try_from(s.to_string())
488        }
489    }
490
491    impl TryFrom<String> for HeaderValue {
492        type Error = HttpError;
493
494        fn try_from(value: String) -> Result<Self, Self::Error> {
495            Ok(HeaderValue::from_http02x(
496                http_02x::HeaderValue::try_from(value).map_err(HttpError::invalid_header_value)?,
497            )
498            .expect("input was a string"))
499        }
500    }
501}
502
503pub use header_value::HeaderValue;
504
505type MaybeStatic = Cow<'static, str>;
506
507fn header_name(
508    name: impl AsHeaderComponent,
509    panic_safe: bool,
510) -> Result<http_02x::HeaderName, HttpError> {
511    name.repr_as_http02x_header_name().or_else(|name| {
512        name.into_maybe_static().and_then(|mut cow| {
513            if cow.chars().any(|c| c.is_ascii_uppercase()) {
514                cow = Cow::Owned(cow.to_ascii_uppercase());
515            }
516            match cow {
517                Cow::Borrowed(s) if panic_safe => {
518                    http_02x::HeaderName::try_from(s).map_err(HttpError::invalid_header_name)
519                }
520                Cow::Borrowed(static_s) => Ok(http_02x::HeaderName::from_static(static_s)),
521                Cow::Owned(s) => {
522                    http_02x::HeaderName::try_from(s).map_err(HttpError::invalid_header_name)
523                }
524            }
525        })
526    })
527}
528
529fn header_value(value: MaybeStatic, panic_safe: bool) -> Result<HeaderValue, HttpError> {
530    let header = match value {
531        Cow::Borrowed(b) if panic_safe => {
532            http_02x::HeaderValue::try_from(b).map_err(HttpError::invalid_header_value)?
533        }
534        Cow::Borrowed(b) => http_02x::HeaderValue::from_static(b),
535        Cow::Owned(s) => {
536            http_02x::HeaderValue::try_from(s).map_err(HttpError::invalid_header_value)?
537        }
538    };
539    HeaderValue::from_http02x(header)
540}
541
542#[cfg(test)]
543mod tests {
544    use super::*;
545
546    #[test]
547    fn headers_can_be_any_string() {
548        let _: HeaderValue = "😹".parse().expect("can be any string");
549        let _: HeaderValue = "abcd".parse().expect("can be any string");
550        let _ = "a\nb"
551            .parse::<HeaderValue>()
552            .expect_err("cannot contain control characters");
553    }
554
555    #[test]
556    fn no_panic_insert_upper_case_header_name() {
557        let mut headers = Headers::new();
558        headers.insert("I-Have-Upper-Case", "foo");
559    }
560    #[test]
561    fn no_panic_append_upper_case_header_name() {
562        let mut headers = Headers::new();
563        headers.append("I-Have-Upper-Case", "foo");
564    }
565
566    #[test]
567    #[should_panic]
568    fn panic_insert_invalid_ascii_key() {
569        let mut headers = Headers::new();
570        headers.insert("💩", "foo");
571    }
572    #[test]
573    #[should_panic]
574    fn panic_insert_invalid_header_value() {
575        let mut headers = Headers::new();
576        headers.insert("foo", "💩");
577    }
578    #[test]
579    #[should_panic]
580    fn panic_append_invalid_ascii_key() {
581        let mut headers = Headers::new();
582        headers.append("💩", "foo");
583    }
584    #[test]
585    #[should_panic]
586    fn panic_append_invalid_header_value() {
587        let mut headers = Headers::new();
588        headers.append("foo", "💩");
589    }
590
591    #[test]
592    fn no_panic_try_insert_invalid_ascii_key() {
593        let mut headers = Headers::new();
594        assert!(headers.try_insert("💩", "foo").is_err());
595    }
596    #[test]
597    fn no_panic_try_insert_invalid_header_value() {
598        let mut headers = Headers::new();
599        assert!(headers
600            .try_insert(
601                "foo",
602                // Valid header value with invalid UTF-8
603                http_02x::HeaderValue::from_bytes(&[0xC0, 0x80]).unwrap()
604            )
605            .is_err());
606    }
607    #[test]
608    fn no_panic_try_append_invalid_ascii_key() {
609        let mut headers = Headers::new();
610        assert!(headers.try_append("💩", "foo").is_err());
611    }
612    #[test]
613    fn no_panic_try_append_invalid_header_value() {
614        let mut headers = Headers::new();
615        assert!(headers
616            .try_insert(
617                "foo",
618                // Valid header value with invalid UTF-8
619                http_02x::HeaderValue::from_bytes(&[0xC0, 0x80]).unwrap()
620            )
621            .is_err());
622    }
623
624    proptest::proptest! {
625        #[test]
626        fn insert_header_prop_test(input in ".*") {
627            let mut headers = Headers::new();
628            let _ = headers.try_insert(input.clone(), input);
629        }
630
631        #[test]
632        fn append_header_prop_test(input in ".*") {
633            let mut headers = Headers::new();
634            let _ = headers.try_append(input.clone(), input);
635        }
636    }
637}
638
639#[cfg(test)]
640mod redaction_tests {
641    use super::*;
642
643    #[test]
644    fn debug_redacts_authorization() {
645        let mut headers = Headers::new();
646        headers.insert(
647            "authorization",
648            "AWS4-HMAC-SHA256 Credential=AKIAXXX/.../Signature=SECRETSIGMARKER",
649        );
650        let output = format!("{:?}", headers);
651        assert!(!output.contains("SECRETSIGMARKER"));
652        assert!(output.contains("authorization"));
653        assert!(output.contains("** redacted"));
654    }
655
656    #[test]
657    fn debug_redacts_security_token() {
658        let mut headers = Headers::new();
659        headers.insert("x-amz-security-token", "IQoJb3JpZ2luSECRETTOKENMARKERzzz");
660        let output = format!("{:?}", headers);
661        assert!(!output.contains("SECRETTOKENMARKER"));
662        assert!(output.contains("x-amz-security-token"));
663        assert!(output.contains("length="));
664    }
665
666    #[test]
667    fn debug_redacts_mixed_case_header_name() {
668        let mut headers = Headers::new();
669        headers.insert(
670            "Authorization",
671            "AWS4-HMAC-SHA256 Credential=AKIAXXX/.../Signature=SECRETSIGMARKER",
672        );
673        let output = format!("{:?}", headers);
674        assert!(!output.contains("SECRETSIGMARKER"));
675        assert!(output.contains("** redacted"));
676    }
677
678    #[test]
679    fn debug_preserves_non_sensitive_headers() {
680        let mut headers = Headers::new();
681        headers.insert("host", "example.com");
682        headers.insert("x-amz-user-agent", "aws-sdk-rust/1.0");
683        let output = format!("{:?}", headers);
684        assert!(output.contains("example.com"));
685        assert!(output.contains("aws-sdk-rust/1.0"));
686    }
687
688    #[test]
689    fn debug_handles_sse_customer_key() {
690        let mut headers = Headers::new();
691        headers.insert(
692            "x-amz-server-side-encryption-customer-key",
693            "BASE64KEYMARKER_DO_NOT_LOG",
694        );
695        let output = format!("{:?}", headers);
696        assert!(!output.contains("BASE64KEYMARKER_DO_NOT_LOG"));
697        assert!(output.contains("x-amz-server-side-encryption-customer-key"));
698        assert!(output.contains("** redacted"));
699    }
700
701    #[test]
702    fn debug_includes_length() {
703        let value = "exactly-twenty-chars";
704        assert_eq!(value.len(), 20);
705        let mut headers = Headers::new();
706        headers.insert("authorization", value);
707        let output = format!("{:?}", headers);
708        assert!(output.contains("length=20"));
709    }
710}