1use crate::http::error::{HttpError, NonUtf8Header};
9use std::borrow::Cow;
10use std::fmt::Debug;
11use std::str::FromStr;
12
13const 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#[derive(Clone, Default)]
44pub struct Headers {
45 pub(super) headers: http_1x::HeaderMap<HeaderValue>,
46}
47
48impl Debug for Headers {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 let mut map = f.debug_map();
51 for (key, value) in self.headers.iter() {
52 let name = key.as_str();
53 if is_sensitive(name) {
54 map.entry(
55 &name,
56 &format_args!("** redacted (length={}) **", value.as_bytes().len()),
57 );
58 } else {
59 match value.try_as_str() {
60 Some(value) => map.entry(&name, &value),
61 None => map.entry(
62 &name,
63 &format_args!("** non-utf8 (length={}) **", value.as_bytes().len()),
64 ),
65 };
66 }
67 }
68 map.finish()
69 }
70}
71
72impl<'a> IntoIterator for &'a Headers {
73 type Item = (&'a str, &'a str);
74 type IntoIter = HeadersIter<'a>;
75
76 fn into_iter(self) -> Self::IntoIter {
77 HeadersIter {
78 inner: self.headers.iter(),
79 }
80 }
81}
82
83pub struct HeadersIter<'a> {
85 inner: http_1x::header::Iter<'a, HeaderValue>,
86}
87
88impl<'a> Iterator for HeadersIter<'a> {
89 type Item = (&'a str, &'a str);
90
91 fn next(&mut self) -> Option<Self::Item> {
92 loop {
94 let (name, value) = self.inner.next()?;
95 if let Some(value) = value.try_as_str() {
96 return Some((name.as_str(), value));
97 }
98 }
99 }
100}
101
102impl Headers {
103 pub fn new() -> Self {
105 Self::default()
106 }
107
108 #[cfg(feature = "http-1x")]
109 pub(crate) fn http1_headermap(self) -> http_1x::HeaderMap {
110 let mut headers = http_1x::HeaderMap::new();
111 headers.reserve(self.headers.len());
112 headers.extend(self.headers.into_iter().map(|(k, v)| (k, v.into_http1x())));
113 headers
114 }
115
116 #[cfg(feature = "http-02x")]
117 pub(crate) fn http0_headermap(self) -> http_02x::HeaderMap {
118 let mut headers = http_02x::HeaderMap::new();
119 headers.reserve(self.headers.len());
120 headers.extend(self.headers.into_iter().map(|(k, v)| {
121 (
122 k.map(|n| {
123 http_02x::HeaderName::from_bytes(n.as_str().as_bytes()).expect("proven valid")
124 }),
125 v.into_http02x(),
126 )
127 }));
128 headers
129 }
130
131 pub fn get(&self, key: impl AsRef<str>) -> Option<&str> {
139 self.headers.get(key.as_ref()).and_then(|v| v.try_as_str())
140 }
141
142 pub fn try_get(&self, key: impl AsRef<str>) -> Option<Result<&str, &[u8]>> {
150 self.headers
151 .get(key.as_ref())
152 .map(|value| match value.try_as_str() {
153 Some(value) => Ok(value),
154 None => Err(value.as_bytes()),
155 })
156 }
157
158 pub fn get_all(&self, key: impl AsRef<str>) -> impl Iterator<Item = &str> {
163 self.headers
164 .get_all(key.as_ref())
165 .iter()
166 .filter_map(|v| v.try_as_str())
167 }
168
169 pub fn get_bytes(&self, key: impl AsRef<str>) -> Option<&[u8]> {
175 self.headers.get(key.as_ref()).map(|v| v.as_bytes())
176 }
177
178 pub fn get_all_bytes(&self, key: impl AsRef<str>) -> impl Iterator<Item = &[u8]> {
182 self.headers
183 .get_all(key.as_ref())
184 .iter()
185 .map(|v| v.as_bytes())
186 }
187
188 pub fn iter(&self) -> HeadersIter<'_> {
190 HeadersIter {
191 inner: self.headers.iter(),
192 }
193 }
194
195 pub fn iter_bytes(&self) -> impl Iterator<Item = (&str, &[u8])> {
199 self.headers.iter().map(|(k, v)| (k.as_str(), v.as_bytes()))
200 }
201
202 pub fn len(&self) -> usize {
204 self.headers.len()
205 }
206
207 pub fn is_empty(&self) -> bool {
209 self.len() == 0
210 }
211
212 pub fn contains_key(&self, key: impl AsRef<str>) -> bool {
214 self.headers.contains_key(key.as_ref())
215 }
216
217 pub fn insert(
224 &mut self,
225 key: impl AsHeaderComponent,
226 value: impl AsHeaderComponent,
227 ) -> Option<String> {
228 let key = header_name(key, false).unwrap();
229 let value = header_value(value.into_maybe_static().unwrap(), false).unwrap();
230 self.headers
231 .insert(key, value)
232 .and_then(|old_value| old_value.try_as_str().map(str::to_string))
233 }
234
235 pub fn try_insert(
241 &mut self,
242 key: impl AsHeaderComponent,
243 value: impl AsHeaderComponent,
244 ) -> Result<Option<String>, HttpError> {
245 let key = header_name(key, true)?;
246 let value = header_value(value.into_maybe_static()?, true)?;
247 Ok(self
248 .headers
249 .insert(key, value)
250 .and_then(|old_value| old_value.try_as_str().map(str::to_string)))
251 }
252
253 pub fn append(&mut self, key: impl AsHeaderComponent, value: impl AsHeaderComponent) -> bool {
258 let key = header_name(key.into_maybe_static().unwrap(), false).unwrap();
259 let value = header_value(value.into_maybe_static().unwrap(), false).unwrap();
260 self.headers.append(key, value)
261 }
262
263 pub fn try_append(
267 &mut self,
268 key: impl AsHeaderComponent,
269 value: impl AsHeaderComponent,
270 ) -> Result<bool, HttpError> {
271 let key = header_name(key.into_maybe_static()?, true)?;
272 let value = header_value(value.into_maybe_static()?, true)?;
273 Ok(self.headers.append(key, value))
274 }
275
276 pub fn remove(&mut self, key: impl AsRef<str>) -> Option<String> {
281 self.headers
282 .remove(key.as_ref())
283 .and_then(|h| h.try_as_str().map(str::to_string))
284 }
285}
286
287#[cfg(feature = "http-02x")]
288impl TryFrom<http_02x::HeaderMap> for Headers {
289 type Error = HttpError;
290
291 fn try_from(value: http_02x::HeaderMap) -> Result<Self, Self::Error> {
292 let converted: Vec<(Option<http_1x::HeaderName>, HeaderValue)> = value
302 .into_iter()
303 .map(|(k, v)| {
304 let name = k
305 .map(|n| http_1x::HeaderName::from_bytes(n.as_str().as_bytes()))
306 .transpose()
307 .map_err(HttpError::invalid_header_name)?;
308 Ok((name, HeaderValue::from_http02x(v)))
309 })
310 .collect::<Result<_, HttpError>>()?;
311 let mut headers: http_1x::HeaderMap<HeaderValue> = Default::default();
312 headers.extend(converted);
313 Ok(Headers { headers })
314 }
315}
316
317#[cfg(feature = "http-1x")]
318impl TryFrom<http_1x::HeaderMap> for Headers {
319 type Error = HttpError;
320
321 fn try_from(value: http_1x::HeaderMap) -> Result<Self, Self::Error> {
322 let mut headers: http_1x::HeaderMap<HeaderValue> = Default::default();
326 headers.extend(
327 value
328 .into_iter()
329 .map(|(k, v)| (k, HeaderValue::from_http1x(v))),
330 );
331 Ok(Headers { headers })
332 }
333}
334
335use sealed::AsHeaderComponent;
336
337mod sealed {
338 use super::*;
339 pub trait AsHeaderComponent {
341 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError>;
343
344 fn as_str(&self) -> Result<&str, HttpError>;
346
347 fn repr_as_http1x_header_name(self) -> Result<http_1x::HeaderName, Self>
349 where
350 Self: Sized,
351 {
352 Err(self)
353 }
354 }
355
356 impl AsHeaderComponent for &'static str {
357 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
358 Ok(Cow::Borrowed(self))
359 }
360
361 fn as_str(&self) -> Result<&str, HttpError> {
362 Ok(self)
363 }
364 }
365
366 impl AsHeaderComponent for String {
367 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
368 Ok(Cow::Owned(self))
369 }
370
371 fn as_str(&self) -> Result<&str, HttpError> {
372 Ok(self)
373 }
374 }
375
376 impl AsHeaderComponent for Cow<'static, str> {
377 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
378 Ok(self)
379 }
380
381 fn as_str(&self) -> Result<&str, HttpError> {
382 Ok(self.as_ref())
383 }
384 }
385
386 #[cfg(feature = "http-02x")]
387 impl AsHeaderComponent for http_02x::HeaderValue {
388 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
389 Ok(Cow::Owned(
390 std::str::from_utf8(self.as_bytes())
391 .map_err(|err| {
392 HttpError::non_utf8_header(NonUtf8Header::new(
393 self.as_bytes().to_vec(),
394 err,
395 ))
396 })?
397 .to_string(),
398 ))
399 }
400
401 fn as_str(&self) -> Result<&str, HttpError> {
402 std::str::from_utf8(self.as_bytes()).map_err(|err| {
403 HttpError::non_utf8_header(NonUtf8Header::new(self.as_bytes().to_vec(), err))
404 })
405 }
406 }
407
408 #[cfg(feature = "http-02x")]
409 impl AsHeaderComponent for http_02x::HeaderName {
410 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
411 Ok(self.to_string().into())
412 }
413
414 fn as_str(&self) -> Result<&str, HttpError> {
415 Ok(self.as_ref())
416 }
417 }
418
419 impl AsHeaderComponent for http_1x::HeaderName {
420 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
421 Ok(self.to_string().into())
422 }
423
424 fn as_str(&self) -> Result<&str, HttpError> {
425 Ok(self.as_ref())
426 }
427
428 fn repr_as_http1x_header_name(self) -> Result<http_1x::HeaderName, Self>
429 where
430 Self: Sized,
431 {
432 Ok(self)
433 }
434 }
435
436 impl AsHeaderComponent for http_1x::HeaderValue {
437 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
438 Ok(Cow::Owned(
439 std::str::from_utf8(self.as_bytes())
440 .map_err(|err| {
441 HttpError::non_utf8_header(NonUtf8Header::new(
442 self.as_bytes().to_vec(),
443 err,
444 ))
445 })?
446 .to_string(),
447 ))
448 }
449
450 fn as_str(&self) -> Result<&str, HttpError> {
451 std::str::from_utf8(self.as_bytes()).map_err(|err| {
452 HttpError::non_utf8_header(NonUtf8Header::new(self.as_bytes().to_vec(), err))
453 })
454 }
455 }
456}
457
458mod header_value {
459 use super::*;
460
461 #[derive(Debug, Clone)]
465 pub struct HeaderValue {
466 _private: Inner,
467 }
468
469 #[derive(Debug, Clone)]
470 enum Inner {
471 #[cfg(feature = "http-02x")]
472 H0(http_02x::HeaderValue),
473 H1(http_1x::HeaderValue),
474 }
475
476 impl HeaderValue {
477 #[cfg(feature = "http-02x")]
480 pub(crate) fn from_http02x(value: http_02x::HeaderValue) -> Self {
481 Self {
482 _private: Inner::H0(value),
483 }
484 }
485
486 pub(crate) fn from_http1x(value: http_1x::HeaderValue) -> Self {
488 Self {
489 _private: Inner::H1(value),
490 }
491 }
492
493 #[cfg(feature = "http-02x")]
494 pub(crate) fn into_http02x(self) -> http_02x::HeaderValue {
495 match self._private {
496 Inner::H0(v) => v,
497 Inner::H1(v) => http_02x::HeaderValue::from_maybe_shared(v).expect("unreachable"),
498 }
499 }
500
501 #[allow(dead_code)]
502 pub(crate) fn into_http1x(self) -> http_1x::HeaderValue {
503 match self._private {
504 Inner::H1(v) => v,
505 #[cfg(feature = "http-02x")]
506 Inner::H0(v) => http_1x::HeaderValue::from_maybe_shared(v).expect("unreachable"),
507 }
508 }
509 }
510
511 impl AsRef<str> for HeaderValue {
512 fn as_ref(&self) -> &str {
515 std::str::from_utf8(self.as_bytes()).expect("header value is not valid UTF-8")
516 }
517 }
518
519 impl From<HeaderValue> for String {
520 fn from(value: HeaderValue) -> Self {
521 value.as_ref().to_string()
522 }
523 }
524
525 impl HeaderValue {
526 pub fn as_str(&self) -> &str {
538 self.as_ref()
539 }
540
541 pub fn as_bytes(&self) -> &[u8] {
545 match &self._private {
546 #[cfg(feature = "http-02x")]
547 Inner::H0(v) => v.as_bytes(),
548 Inner::H1(v) => v.as_bytes(),
549 }
550 }
551
552 pub fn try_as_str(&self) -> Option<&str> {
555 std::str::from_utf8(self.as_bytes()).ok()
556 }
557 }
558
559 impl FromStr for HeaderValue {
560 type Err = HttpError;
561
562 fn from_str(s: &str) -> Result<Self, Self::Err> {
563 HeaderValue::try_from(s.to_string())
564 }
565 }
566
567 impl TryFrom<String> for HeaderValue {
568 type Error = HttpError;
569
570 fn try_from(value: String) -> Result<Self, Self::Error> {
571 Ok(HeaderValue::from_http1x(
572 http_1x::HeaderValue::try_from(value).map_err(HttpError::invalid_header_value)?,
573 ))
574 }
575 }
576}
577
578pub use header_value::HeaderValue;
579
580type MaybeStatic = Cow<'static, str>;
581
582fn header_name(
583 name: impl AsHeaderComponent,
584 panic_safe: bool,
585) -> Result<http_1x::HeaderName, HttpError> {
586 name.repr_as_http1x_header_name().or_else(|name| {
587 name.into_maybe_static().and_then(|mut cow| {
588 if cow.chars().any(|c| c.is_ascii_uppercase()) {
589 cow = Cow::Owned(cow.to_ascii_uppercase());
590 }
591 match cow {
592 Cow::Borrowed(s) if panic_safe => {
593 http_1x::HeaderName::try_from(s).map_err(HttpError::invalid_header_name)
594 }
595 Cow::Borrowed(static_s) => Ok(http_1x::HeaderName::from_static(static_s)),
596 Cow::Owned(s) => {
597 http_1x::HeaderName::try_from(s).map_err(HttpError::invalid_header_name)
598 }
599 }
600 })
601 })
602}
603
604fn header_value(value: MaybeStatic, panic_safe: bool) -> Result<HeaderValue, HttpError> {
605 let header = match value {
606 Cow::Borrowed(b) if panic_safe => {
607 http_1x::HeaderValue::try_from(b).map_err(HttpError::invalid_header_value)?
608 }
609 Cow::Borrowed(b) => http_1x::HeaderValue::from_static(b),
610 Cow::Owned(s) => {
611 http_1x::HeaderValue::try_from(s).map_err(HttpError::invalid_header_value)?
612 }
613 };
614 Ok(HeaderValue::from_http1x(header))
616}
617
618#[cfg(test)]
619mod tests {
620 use super::*;
621
622 #[test]
623 fn headers_can_be_any_string() {
624 let _: HeaderValue = "😹".parse().expect("can be any string");
625 let _: HeaderValue = "abcd".parse().expect("can be any string");
626 let _ = "a\nb"
627 .parse::<HeaderValue>()
628 .expect_err("cannot contain control characters");
629 }
630
631 #[test]
632 fn no_panic_insert_upper_case_header_name() {
633 let mut headers = Headers::new();
634 headers.insert("I-Have-Upper-Case", "foo");
635 }
636 #[test]
637 fn no_panic_append_upper_case_header_name() {
638 let mut headers = Headers::new();
639 headers.append("I-Have-Upper-Case", "foo");
640 }
641
642 #[test]
643 #[should_panic]
644 fn panic_insert_invalid_ascii_key() {
645 let mut headers = Headers::new();
646 headers.insert("💩", "foo");
647 }
648 #[test]
649 #[should_panic]
650 fn panic_insert_invalid_header_value() {
651 let mut headers = Headers::new();
652 headers.insert("foo", "💩");
653 }
654 #[test]
655 #[should_panic]
656 fn panic_append_invalid_ascii_key() {
657 let mut headers = Headers::new();
658 headers.append("💩", "foo");
659 }
660 #[test]
661 #[should_panic]
662 fn panic_append_invalid_header_value() {
663 let mut headers = Headers::new();
664 headers.append("foo", "💩");
665 }
666
667 #[test]
668 fn no_panic_try_insert_invalid_ascii_key() {
669 let mut headers = Headers::new();
670 assert!(headers.try_insert("💩", "foo").is_err());
671 }
672 #[test]
673 fn no_panic_try_insert_invalid_header_value() {
674 let mut headers = Headers::new();
675 assert!(headers
676 .try_insert(
677 "foo",
678 http_1x::HeaderValue::from_bytes(&[0xC0, 0x80]).unwrap()
680 )
681 .is_err());
682 }
683 #[test]
684 fn no_panic_try_append_invalid_ascii_key() {
685 let mut headers = Headers::new();
686 assert!(headers.try_append("💩", "foo").is_err());
687 }
688 #[test]
689 fn no_panic_try_append_invalid_header_value() {
690 let mut headers = Headers::new();
691 assert!(headers
692 .try_append(
693 "foo",
694 http_1x::HeaderValue::from_bytes(&[0xC0, 0x80]).unwrap()
696 )
697 .is_err());
698 }
699
700 #[test]
701 fn header_value_exposes_bytes_and_checked_str() {
702 let value: HeaderValue = "hello".parse().expect("valid");
703 assert_eq!(b"hello", value.as_bytes());
704 assert_eq!(Some("hello"), value.try_as_str());
705 assert_eq!("hello", value.as_str());
706 }
707
708 #[cfg(feature = "http-1x")]
709 #[test]
710 fn byte_accessors_agree_with_str_accessors() {
711 let mut map = http_1x::HeaderMap::new();
712 map.append("single", http_1x::HeaderValue::from_static("v1"));
713 map.append("multi", http_1x::HeaderValue::from_static("m1"));
714 map.append("multi", http_1x::HeaderValue::from_static("m2"));
715 let headers = Headers::try_from(map).expect("all values are valid UTF-8");
716
717 assert_eq!(Some(b"v1".as_slice()), headers.get_bytes("single"));
718 assert_eq!(
719 headers.get("single").map(str::as_bytes),
720 headers.get_bytes("single")
721 );
722
723 let all_bytes: Vec<_> = headers.get_all_bytes("multi").collect();
724 assert_eq!(vec![b"m1".as_slice(), b"m2".as_slice()], all_bytes);
725 let all_str: Vec<_> = headers.get_all("multi").map(str::as_bytes).collect();
726 assert_eq!(all_str, all_bytes);
727
728 assert_eq!(None, headers.get_bytes("absent"));
729
730 let mut from_bytes: Vec<_> = headers.iter_bytes().collect();
731 from_bytes.sort();
732 let mut from_str: Vec<_> = headers.iter().map(|(k, v)| (k, v.as_bytes())).collect();
733 from_str.sort();
734 assert_eq!(from_str, from_bytes);
735 }
736
737 #[cfg(feature = "http-1x")]
740 #[test]
741 fn replacing_a_non_utf8_value_does_not_panic() {
742 for replace in [
743 (|h: &mut Headers| {
744 h.insert("bad", "replacement");
745 }) as fn(&mut Headers),
746 |h: &mut Headers| {
747 h.try_insert("bad", "replacement").expect("valid");
748 },
749 ] {
750 let mut map = http_1x::HeaderMap::new();
751 map.insert("bad", non_utf8_header_value());
752 let mut headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
753 let res =
754 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| replace(&mut headers)));
755 assert!(res.is_ok(), "replacing an admitted value must not panic");
756 }
757 }
758
759 proptest::proptest! {
760 #[test]
761 fn insert_header_prop_test(input in ".*") {
762 let mut headers = Headers::new();
763 let _ = headers.try_insert(input.clone(), input);
764 }
765
766 #[test]
767 fn append_header_prop_test(input in ".*") {
768 let mut headers = Headers::new();
769 let _ = headers.try_append(input.clone(), input);
770 }
771 }
772
773 #[cfg(feature = "http-02x")]
776 #[test]
777 fn converting_an_http02x_headermap_never_panics() {
778 let name = http_02x::HeaderName::from_bytes(b"a\"b").expect("http 0.2.x accepts this");
779 let mut map = http_02x::HeaderMap::new();
780 map.insert(name, http_02x::HeaderValue::from_static("v"));
781 let res = std::panic::catch_unwind(|| Headers::try_from(map));
782 assert!(
783 res.is_ok(),
784 "TryFrom<http_02x::HeaderMap> for Headers panicked on a name that http 0.2.x \
785 considers valid but http 1.x does not; a TryFrom should return Err"
786 );
787 assert!(
788 res.unwrap().is_err(),
789 "expected an Err for a header name that http 1.x rejects"
790 );
791 }
792
793 #[cfg(any(feature = "http-1x", feature = "http-02x"))]
797 const NON_UTF8_VALUE: &[u8] = b"value-\xe9";
798
799 #[cfg(feature = "http-1x")]
800 fn non_utf8_header_value() -> http_1x::HeaderValue {
801 http_1x::HeaderValue::from_bytes(NON_UTF8_VALUE).expect("valid header octets")
802 }
803
804 #[cfg(feature = "http-1x")]
805 #[test]
806 fn non_utf8_values_are_admitted_and_readable_as_bytes() {
807 let mut map = http_1x::HeaderMap::new();
808 map.insert("ok", http_1x::HeaderValue::from_static("v"));
809 map.insert("bad", non_utf8_header_value());
810 let headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
811
812 assert!(headers.contains_key("bad"));
814 assert_eq!(Some(NON_UTF8_VALUE), headers.get_bytes("bad"));
815 assert_eq!(None, headers.get("bad"));
817
818 assert_eq!(Some("v"), headers.get("ok"));
819 }
820
821 #[cfg(feature = "http-02x")]
822 #[test]
823 fn non_utf8_values_are_admitted_from_an_http02x_headermap() {
824 let mut map = http_02x::HeaderMap::new();
825 map.insert(
826 "bad",
827 http_02x::HeaderValue::from_bytes(NON_UTF8_VALUE).expect("valid header octets"),
828 );
829 let headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
830 assert_eq!(Some(NON_UTF8_VALUE), headers.get_bytes("bad"));
831 assert_eq!(None, headers.get("bad"));
832 }
833
834 #[cfg(feature = "http-1x")]
835 #[test]
836 fn a_non_utf8_value_does_not_hide_the_other_values_of_its_header() {
837 let mut map = http_1x::HeaderMap::new();
838 map.append("multi", http_1x::HeaderValue::from_static("v1"));
839 map.append("multi", non_utf8_header_value());
840 map.append("multi", http_1x::HeaderValue::from_static("v3"));
841 let headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
842
843 assert_eq!(
844 vec!["v1", "v3"],
845 headers.get_all("multi").collect::<Vec<_>>()
846 );
847 assert_eq!(
848 vec![b"v1".as_slice(), NON_UTF8_VALUE, b"v3".as_slice()],
849 headers.get_all_bytes("multi").collect::<Vec<_>>()
850 );
851 assert_eq!(Some("v1"), headers.get("multi"));
853 }
854
855 #[cfg(feature = "http-1x")]
856 #[test]
857 fn try_get_distinguishes_unreadable_from_absent() {
858 let mut map = http_1x::HeaderMap::new();
859 map.insert("ok", http_1x::HeaderValue::from_static("v"));
860 map.insert("bad", non_utf8_header_value());
861 let headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
862
863 assert_eq!(Some(Ok("v")), headers.try_get("ok"));
864 assert_eq!(Some(Err(NON_UTF8_VALUE)), headers.try_get("bad"));
865 assert_eq!(None, headers.try_get("absent"));
866
867 assert_eq!(None, headers.get("bad"));
869 assert_eq!(None, headers.get("absent"));
870 }
871
872 #[cfg(feature = "http-1x")]
873 #[test]
874 fn iter_skips_non_utf8_values_and_iter_bytes_does_not() {
875 let mut map = http_1x::HeaderMap::new();
876 map.insert("a", non_utf8_header_value());
877 map.insert("b", http_1x::HeaderValue::from_static("v"));
878 map.insert("c", non_utf8_header_value());
879 let headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
880
881 assert_eq!(vec![("b", "v")], headers.iter().collect::<Vec<_>>());
882 assert_eq!(3, headers.iter_bytes().count());
883 assert_eq!(3, headers.len());
885 }
886
887 #[cfg(feature = "http-1x")]
888 #[test]
889 fn remove_drops_a_non_utf8_value_and_reports_none() {
890 let mut map = http_1x::HeaderMap::new();
891 map.insert("bad", non_utf8_header_value());
892 let mut headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
893
894 assert_eq!(None, headers.remove("bad"));
895 assert!(
896 !headers.contains_key("bad"),
897 "the header is removed regardless"
898 );
899 }
900
901 #[cfg(feature = "http-1x")]
902 #[test]
903 fn debug_marks_non_utf8_values_without_panicking() {
904 let mut map = http_1x::HeaderMap::new();
905 map.insert("bad", non_utf8_header_value());
906 let headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
907
908 let output = format!("{headers:?}");
909 assert!(output.contains("bad"), "{output}");
910 assert!(output.contains("non-utf8"), "{output}");
911 }
912
913 #[cfg(feature = "http-02x")]
916 #[test]
917 fn converting_an_http02x_headermap_preserves_multi_value_headers() {
918 let mut map = http_02x::HeaderMap::new();
919 map.append("multi", http_02x::HeaderValue::from_static("v1"));
920 map.append("multi", http_02x::HeaderValue::from_static("v2"));
921 let headers = Headers::try_from(map).expect("valid headers");
922 let values: Vec<_> = headers.get_all("multi").collect();
923 assert_eq!(values, vec!["v1", "v2"]);
924 }
925}
926
927#[cfg(test)]
928mod redaction_tests {
929 use super::*;
930
931 #[test]
932 fn debug_redacts_authorization() {
933 let mut headers = Headers::new();
934 headers.insert(
935 "authorization",
936 "AWS4-HMAC-SHA256 Credential=AKIAXXX/.../Signature=SECRETSIGMARKER",
937 );
938 let output = format!("{:?}", headers);
939 assert!(!output.contains("SECRETSIGMARKER"));
940 assert!(output.contains("authorization"));
941 assert!(output.contains("** redacted"));
942 }
943
944 #[test]
945 fn debug_redacts_security_token() {
946 let mut headers = Headers::new();
947 headers.insert("x-amz-security-token", "IQoJb3JpZ2luSECRETTOKENMARKERzzz");
948 let output = format!("{:?}", headers);
949 assert!(!output.contains("SECRETTOKENMARKER"));
950 assert!(output.contains("x-amz-security-token"));
951 assert!(output.contains("length="));
952 }
953
954 #[test]
955 fn debug_redacts_mixed_case_header_name() {
956 let mut headers = Headers::new();
957 headers.insert(
958 "Authorization",
959 "AWS4-HMAC-SHA256 Credential=AKIAXXX/.../Signature=SECRETSIGMARKER",
960 );
961 let output = format!("{:?}", headers);
962 assert!(!output.contains("SECRETSIGMARKER"));
963 assert!(output.contains("** redacted"));
964 }
965
966 #[test]
967 fn debug_preserves_non_sensitive_headers() {
968 let mut headers = Headers::new();
969 headers.insert("host", "example.com");
970 headers.insert("x-amz-user-agent", "aws-sdk-rust/1.0");
971 let output = format!("{:?}", headers);
972 assert!(output.contains("example.com"));
973 assert!(output.contains("aws-sdk-rust/1.0"));
974 }
975
976 #[test]
977 fn debug_handles_sse_customer_key() {
978 let mut headers = Headers::new();
979 headers.insert(
980 "x-amz-server-side-encryption-customer-key",
981 "BASE64KEYMARKER_DO_NOT_LOG",
982 );
983 let output = format!("{:?}", headers);
984 assert!(!output.contains("BASE64KEYMARKER_DO_NOT_LOG"));
985 assert!(output.contains("x-amz-server-side-encryption-customer-key"));
986 assert!(output.contains("** redacted"));
987 }
988
989 #[test]
990 fn debug_includes_length() {
991 let value = "exactly-twenty-chars";
992 assert_eq!(value.len(), 20);
993 let mut headers = Headers::new();
994 headers.insert("authorization", value);
995 let output = format!("{:?}", headers);
996 assert!(output.contains("length=20"));
997 }
998}