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 #[cfg(feature = "http-1x")]
194 pub fn get_all_bytes_by_name<'a>(
195 &'a self,
196 key: &http_1x::HeaderName,
197 ) -> impl Iterator<Item = &'a [u8]> + 'a {
198 self.headers.get_all(key).iter().map(|v| v.as_bytes())
199 }
200
201 pub fn iter(&self) -> HeadersIter<'_> {
203 HeadersIter {
204 inner: self.headers.iter(),
205 }
206 }
207
208 pub fn iter_bytes(&self) -> impl Iterator<Item = (&str, &[u8])> {
212 self.headers.iter().map(|(k, v)| (k.as_str(), v.as_bytes()))
213 }
214
215 pub fn len(&self) -> usize {
217 self.headers.len()
218 }
219
220 pub fn is_empty(&self) -> bool {
222 self.len() == 0
223 }
224
225 pub fn contains_key(&self, key: impl AsRef<str>) -> bool {
227 self.headers.contains_key(key.as_ref())
228 }
229
230 pub fn insert(
237 &mut self,
238 key: impl AsHeaderComponent,
239 value: impl AsHeaderComponent,
240 ) -> Option<String> {
241 let key = header_name(key, false).unwrap();
242 let value = header_value(value.into_maybe_static().unwrap(), false).unwrap();
243 self.headers
244 .insert(key, value)
245 .and_then(|old_value| old_value.try_as_str().map(str::to_string))
246 }
247
248 pub fn try_insert(
254 &mut self,
255 key: impl AsHeaderComponent,
256 value: impl AsHeaderComponent,
257 ) -> Result<Option<String>, HttpError> {
258 let key = header_name(key, true)?;
259 let value = header_value(value.into_maybe_static()?, true)?;
260 Ok(self
261 .headers
262 .insert(key, value)
263 .and_then(|old_value| old_value.try_as_str().map(str::to_string)))
264 }
265
266 pub fn append(&mut self, key: impl AsHeaderComponent, value: impl AsHeaderComponent) -> bool {
271 let key = header_name(key.into_maybe_static().unwrap(), false).unwrap();
272 let value = header_value(value.into_maybe_static().unwrap(), false).unwrap();
273 self.headers.append(key, value)
274 }
275
276 pub fn try_append(
280 &mut self,
281 key: impl AsHeaderComponent,
282 value: impl AsHeaderComponent,
283 ) -> Result<bool, HttpError> {
284 let key = header_name(key.into_maybe_static()?, true)?;
285 let value = header_value(value.into_maybe_static()?, true)?;
286 Ok(self.headers.append(key, value))
287 }
288
289 pub fn remove(&mut self, key: impl AsRef<str>) -> Option<String> {
294 self.headers
295 .remove(key.as_ref())
296 .and_then(|h| h.try_as_str().map(str::to_string))
297 }
298}
299
300#[cfg(feature = "http-02x")]
301impl TryFrom<http_02x::HeaderMap> for Headers {
302 type Error = HttpError;
303
304 fn try_from(value: http_02x::HeaderMap) -> Result<Self, Self::Error> {
305 let converted: Vec<(Option<http_1x::HeaderName>, HeaderValue)> = value
315 .into_iter()
316 .map(|(k, v)| {
317 let name = k
318 .map(|n| http_1x::HeaderName::from_bytes(n.as_str().as_bytes()))
319 .transpose()
320 .map_err(HttpError::invalid_header_name)?;
321 Ok((name, HeaderValue::from_http02x(v)))
322 })
323 .collect::<Result<_, HttpError>>()?;
324 let mut headers: http_1x::HeaderMap<HeaderValue> = Default::default();
325 headers.extend(converted);
326 Ok(Headers { headers })
327 }
328}
329
330#[cfg(feature = "http-1x")]
331impl TryFrom<http_1x::HeaderMap> for Headers {
332 type Error = HttpError;
333
334 fn try_from(value: http_1x::HeaderMap) -> Result<Self, Self::Error> {
335 let mut headers: http_1x::HeaderMap<HeaderValue> = Default::default();
339 headers.extend(
340 value
341 .into_iter()
342 .map(|(k, v)| (k, HeaderValue::from_http1x(v))),
343 );
344 Ok(Headers { headers })
345 }
346}
347
348use sealed::AsHeaderComponent;
349
350mod sealed {
351 use super::*;
352 pub trait AsHeaderComponent {
354 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError>;
356
357 fn as_str(&self) -> Result<&str, HttpError>;
359
360 fn repr_as_http1x_header_name(self) -> Result<http_1x::HeaderName, Self>
362 where
363 Self: Sized,
364 {
365 Err(self)
366 }
367 }
368
369 impl AsHeaderComponent for &'static str {
370 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
371 Ok(Cow::Borrowed(self))
372 }
373
374 fn as_str(&self) -> Result<&str, HttpError> {
375 Ok(self)
376 }
377 }
378
379 impl AsHeaderComponent for String {
380 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
381 Ok(Cow::Owned(self))
382 }
383
384 fn as_str(&self) -> Result<&str, HttpError> {
385 Ok(self)
386 }
387 }
388
389 impl AsHeaderComponent for Cow<'static, str> {
390 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
391 Ok(self)
392 }
393
394 fn as_str(&self) -> Result<&str, HttpError> {
395 Ok(self.as_ref())
396 }
397 }
398
399 #[cfg(feature = "http-02x")]
400 impl AsHeaderComponent for http_02x::HeaderValue {
401 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
402 Ok(Cow::Owned(
403 std::str::from_utf8(self.as_bytes())
404 .map_err(|err| {
405 HttpError::non_utf8_header(NonUtf8Header::new(
406 self.as_bytes().to_vec(),
407 err,
408 ))
409 })?
410 .to_string(),
411 ))
412 }
413
414 fn as_str(&self) -> Result<&str, HttpError> {
415 std::str::from_utf8(self.as_bytes()).map_err(|err| {
416 HttpError::non_utf8_header(NonUtf8Header::new(self.as_bytes().to_vec(), err))
417 })
418 }
419 }
420
421 #[cfg(feature = "http-02x")]
422 impl AsHeaderComponent for http_02x::HeaderName {
423 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
424 Ok(self.to_string().into())
425 }
426
427 fn as_str(&self) -> Result<&str, HttpError> {
428 Ok(self.as_ref())
429 }
430 }
431
432 impl AsHeaderComponent for http_1x::HeaderName {
433 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
434 Ok(self.to_string().into())
435 }
436
437 fn as_str(&self) -> Result<&str, HttpError> {
438 Ok(self.as_ref())
439 }
440
441 fn repr_as_http1x_header_name(self) -> Result<http_1x::HeaderName, Self>
442 where
443 Self: Sized,
444 {
445 Ok(self)
446 }
447 }
448
449 impl AsHeaderComponent for http_1x::HeaderValue {
450 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
451 Ok(Cow::Owned(
452 std::str::from_utf8(self.as_bytes())
453 .map_err(|err| {
454 HttpError::non_utf8_header(NonUtf8Header::new(
455 self.as_bytes().to_vec(),
456 err,
457 ))
458 })?
459 .to_string(),
460 ))
461 }
462
463 fn as_str(&self) -> Result<&str, HttpError> {
464 std::str::from_utf8(self.as_bytes()).map_err(|err| {
465 HttpError::non_utf8_header(NonUtf8Header::new(self.as_bytes().to_vec(), err))
466 })
467 }
468 }
469}
470
471mod header_value {
472 use super::*;
473
474 #[derive(Debug, Clone)]
478 pub struct HeaderValue {
479 _private: Inner,
480 }
481
482 #[derive(Debug, Clone)]
483 enum Inner {
484 #[cfg(feature = "http-02x")]
485 H0(http_02x::HeaderValue),
486 H1(http_1x::HeaderValue),
487 }
488
489 impl HeaderValue {
490 #[cfg(feature = "http-02x")]
493 pub(crate) fn from_http02x(value: http_02x::HeaderValue) -> Self {
494 Self {
495 _private: Inner::H0(value),
496 }
497 }
498
499 pub(crate) fn from_http1x(value: http_1x::HeaderValue) -> Self {
501 Self {
502 _private: Inner::H1(value),
503 }
504 }
505
506 #[cfg(feature = "http-02x")]
507 pub(crate) fn into_http02x(self) -> http_02x::HeaderValue {
508 match self._private {
509 Inner::H0(v) => v,
510 Inner::H1(v) => http_02x::HeaderValue::from_maybe_shared(v).expect("unreachable"),
511 }
512 }
513
514 #[allow(dead_code)]
515 pub(crate) fn into_http1x(self) -> http_1x::HeaderValue {
516 match self._private {
517 Inner::H1(v) => v,
518 #[cfg(feature = "http-02x")]
519 Inner::H0(v) => http_1x::HeaderValue::from_maybe_shared(v).expect("unreachable"),
520 }
521 }
522 }
523
524 impl AsRef<str> for HeaderValue {
525 fn as_ref(&self) -> &str {
528 std::str::from_utf8(self.as_bytes()).expect("header value is not valid UTF-8")
529 }
530 }
531
532 impl From<HeaderValue> for String {
533 fn from(value: HeaderValue) -> Self {
534 value.as_ref().to_string()
535 }
536 }
537
538 impl HeaderValue {
539 pub fn as_str(&self) -> &str {
551 self.as_ref()
552 }
553
554 pub fn as_bytes(&self) -> &[u8] {
558 match &self._private {
559 #[cfg(feature = "http-02x")]
560 Inner::H0(v) => v.as_bytes(),
561 Inner::H1(v) => v.as_bytes(),
562 }
563 }
564
565 pub fn try_as_str(&self) -> Option<&str> {
568 std::str::from_utf8(self.as_bytes()).ok()
569 }
570 }
571
572 impl FromStr for HeaderValue {
573 type Err = HttpError;
574
575 fn from_str(s: &str) -> Result<Self, Self::Err> {
576 HeaderValue::try_from(s.to_string())
577 }
578 }
579
580 impl TryFrom<String> for HeaderValue {
581 type Error = HttpError;
582
583 fn try_from(value: String) -> Result<Self, Self::Error> {
584 Ok(HeaderValue::from_http1x(
585 http_1x::HeaderValue::try_from(value).map_err(HttpError::invalid_header_value)?,
586 ))
587 }
588 }
589}
590
591pub use header_value::HeaderValue;
592
593type MaybeStatic = Cow<'static, str>;
594
595fn header_name(
596 name: impl AsHeaderComponent,
597 panic_safe: bool,
598) -> Result<http_1x::HeaderName, HttpError> {
599 name.repr_as_http1x_header_name().or_else(|name| {
600 name.into_maybe_static().and_then(|mut cow| {
601 if cow.chars().any(|c| c.is_ascii_uppercase()) {
602 cow = Cow::Owned(cow.to_ascii_uppercase());
603 }
604 match cow {
605 Cow::Borrowed(s) if panic_safe => {
606 http_1x::HeaderName::try_from(s).map_err(HttpError::invalid_header_name)
607 }
608 Cow::Borrowed(static_s) => Ok(http_1x::HeaderName::from_static(static_s)),
609 Cow::Owned(s) => {
610 http_1x::HeaderName::try_from(s).map_err(HttpError::invalid_header_name)
611 }
612 }
613 })
614 })
615}
616
617fn header_value(value: MaybeStatic, panic_safe: bool) -> Result<HeaderValue, HttpError> {
618 let header = match value {
619 Cow::Borrowed(b) if panic_safe => {
620 http_1x::HeaderValue::try_from(b).map_err(HttpError::invalid_header_value)?
621 }
622 Cow::Borrowed(b) => http_1x::HeaderValue::from_static(b),
623 Cow::Owned(s) => {
624 http_1x::HeaderValue::try_from(s).map_err(HttpError::invalid_header_value)?
625 }
626 };
627 Ok(HeaderValue::from_http1x(header))
629}
630
631#[cfg(test)]
632mod tests {
633 use super::*;
634
635 #[test]
636 fn headers_can_be_any_string() {
637 let _: HeaderValue = "😹".parse().expect("can be any string");
638 let _: HeaderValue = "abcd".parse().expect("can be any string");
639 let _ = "a\nb"
640 .parse::<HeaderValue>()
641 .expect_err("cannot contain control characters");
642 }
643
644 #[test]
645 fn no_panic_insert_upper_case_header_name() {
646 let mut headers = Headers::new();
647 headers.insert("I-Have-Upper-Case", "foo");
648 }
649 #[test]
650 fn no_panic_append_upper_case_header_name() {
651 let mut headers = Headers::new();
652 headers.append("I-Have-Upper-Case", "foo");
653 }
654
655 #[test]
656 #[should_panic]
657 fn panic_insert_invalid_ascii_key() {
658 let mut headers = Headers::new();
659 headers.insert("💩", "foo");
660 }
661 #[test]
662 #[should_panic]
663 fn panic_insert_invalid_header_value() {
664 let mut headers = Headers::new();
665 headers.insert("foo", "💩");
666 }
667 #[test]
668 #[should_panic]
669 fn panic_append_invalid_ascii_key() {
670 let mut headers = Headers::new();
671 headers.append("💩", "foo");
672 }
673 #[test]
674 #[should_panic]
675 fn panic_append_invalid_header_value() {
676 let mut headers = Headers::new();
677 headers.append("foo", "💩");
678 }
679
680 #[test]
681 fn no_panic_try_insert_invalid_ascii_key() {
682 let mut headers = Headers::new();
683 assert!(headers.try_insert("💩", "foo").is_err());
684 }
685 #[test]
686 fn no_panic_try_insert_invalid_header_value() {
687 let mut headers = Headers::new();
688 assert!(headers
689 .try_insert(
690 "foo",
691 http_1x::HeaderValue::from_bytes(&[0xC0, 0x80]).unwrap()
693 )
694 .is_err());
695 }
696 #[test]
697 fn no_panic_try_append_invalid_ascii_key() {
698 let mut headers = Headers::new();
699 assert!(headers.try_append("💩", "foo").is_err());
700 }
701 #[test]
702 fn no_panic_try_append_invalid_header_value() {
703 let mut headers = Headers::new();
704 assert!(headers
705 .try_append(
706 "foo",
707 http_1x::HeaderValue::from_bytes(&[0xC0, 0x80]).unwrap()
709 )
710 .is_err());
711 }
712
713 #[test]
714 fn header_value_exposes_bytes_and_checked_str() {
715 let value: HeaderValue = "hello".parse().expect("valid");
716 assert_eq!(b"hello", value.as_bytes());
717 assert_eq!(Some("hello"), value.try_as_str());
718 assert_eq!("hello", value.as_str());
719 }
720
721 #[cfg(feature = "http-1x")]
722 #[test]
723 fn byte_accessors_agree_with_str_accessors() {
724 let mut map = http_1x::HeaderMap::new();
725 map.append("single", http_1x::HeaderValue::from_static("v1"));
726 map.append("multi", http_1x::HeaderValue::from_static("m1"));
727 map.append("multi", http_1x::HeaderValue::from_static("m2"));
728 let headers = Headers::try_from(map).expect("all values are valid UTF-8");
729
730 assert_eq!(Some(b"v1".as_slice()), headers.get_bytes("single"));
731 assert_eq!(
732 headers.get("single").map(str::as_bytes),
733 headers.get_bytes("single")
734 );
735
736 let all_bytes: Vec<_> = headers.get_all_bytes("multi").collect();
737 assert_eq!(vec![b"m1".as_slice(), b"m2".as_slice()], all_bytes);
738 let by_name: Vec<_> = headers
739 .get_all_bytes_by_name(&http_1x::HeaderName::from_static("multi"))
740 .collect();
741 assert_eq!(all_bytes, by_name);
742 assert_eq!(
743 0,
744 headers
745 .get_all_bytes_by_name(&http_1x::HeaderName::from_static("absent"))
746 .count()
747 );
748 let all_str: Vec<_> = headers.get_all("multi").map(str::as_bytes).collect();
749 assert_eq!(all_str, all_bytes);
750
751 assert_eq!(None, headers.get_bytes("absent"));
752
753 let mut from_bytes: Vec<_> = headers.iter_bytes().collect();
754 from_bytes.sort();
755 let mut from_str: Vec<_> = headers.iter().map(|(k, v)| (k, v.as_bytes())).collect();
756 from_str.sort();
757 assert_eq!(from_str, from_bytes);
758 }
759
760 #[cfg(feature = "http-1x")]
763 #[test]
764 fn replacing_a_non_utf8_value_does_not_panic() {
765 for replace in [
766 (|h: &mut Headers| {
767 h.insert("bad", "replacement");
768 }) as fn(&mut Headers),
769 |h: &mut Headers| {
770 h.try_insert("bad", "replacement").expect("valid");
771 },
772 ] {
773 let mut map = http_1x::HeaderMap::new();
774 map.insert("bad", non_utf8_header_value());
775 let mut headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
776 let res =
777 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| replace(&mut headers)));
778 assert!(res.is_ok(), "replacing an admitted value must not panic");
779 }
780 }
781
782 proptest::proptest! {
783 #[test]
784 fn insert_header_prop_test(input in ".*") {
785 let mut headers = Headers::new();
786 let _ = headers.try_insert(input.clone(), input);
787 }
788
789 #[test]
790 fn append_header_prop_test(input in ".*") {
791 let mut headers = Headers::new();
792 let _ = headers.try_append(input.clone(), input);
793 }
794 }
795
796 #[cfg(feature = "http-02x")]
799 #[test]
800 fn converting_an_http02x_headermap_never_panics() {
801 let name = http_02x::HeaderName::from_bytes(b"a\"b").expect("http 0.2.x accepts this");
802 let mut map = http_02x::HeaderMap::new();
803 map.insert(name, http_02x::HeaderValue::from_static("v"));
804 let res = std::panic::catch_unwind(|| Headers::try_from(map));
805 assert!(
806 res.is_ok(),
807 "TryFrom<http_02x::HeaderMap> for Headers panicked on a name that http 0.2.x \
808 considers valid but http 1.x does not; a TryFrom should return Err"
809 );
810 assert!(
811 res.unwrap().is_err(),
812 "expected an Err for a header name that http 1.x rejects"
813 );
814 }
815
816 #[cfg(any(feature = "http-1x", feature = "http-02x"))]
820 const NON_UTF8_VALUE: &[u8] = b"value-\xe9";
821
822 #[cfg(feature = "http-1x")]
823 fn non_utf8_header_value() -> http_1x::HeaderValue {
824 http_1x::HeaderValue::from_bytes(NON_UTF8_VALUE).expect("valid header octets")
825 }
826
827 #[cfg(feature = "http-1x")]
828 #[test]
829 fn non_utf8_values_are_admitted_and_readable_as_bytes() {
830 let mut map = http_1x::HeaderMap::new();
831 map.insert("ok", http_1x::HeaderValue::from_static("v"));
832 map.insert("bad", non_utf8_header_value());
833 let headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
834
835 assert!(headers.contains_key("bad"));
837 assert_eq!(Some(NON_UTF8_VALUE), headers.get_bytes("bad"));
838 assert_eq!(None, headers.get("bad"));
840
841 assert_eq!(Some("v"), headers.get("ok"));
842 }
843
844 #[cfg(feature = "http-02x")]
845 #[test]
846 fn non_utf8_values_are_admitted_from_an_http02x_headermap() {
847 let mut map = http_02x::HeaderMap::new();
848 map.insert(
849 "bad",
850 http_02x::HeaderValue::from_bytes(NON_UTF8_VALUE).expect("valid header octets"),
851 );
852 let headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
853 assert_eq!(Some(NON_UTF8_VALUE), headers.get_bytes("bad"));
854 assert_eq!(None, headers.get("bad"));
855 }
856
857 #[cfg(feature = "http-1x")]
858 #[test]
859 fn a_non_utf8_value_does_not_hide_the_other_values_of_its_header() {
860 let mut map = http_1x::HeaderMap::new();
861 map.append("multi", http_1x::HeaderValue::from_static("v1"));
862 map.append("multi", non_utf8_header_value());
863 map.append("multi", http_1x::HeaderValue::from_static("v3"));
864 let headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
865
866 assert_eq!(
867 vec!["v1", "v3"],
868 headers.get_all("multi").collect::<Vec<_>>()
869 );
870 assert_eq!(
871 vec![b"v1".as_slice(), NON_UTF8_VALUE, b"v3".as_slice()],
872 headers.get_all_bytes("multi").collect::<Vec<_>>()
873 );
874 assert_eq!(Some("v1"), headers.get("multi"));
876 }
877
878 #[cfg(feature = "http-1x")]
879 #[test]
880 fn try_get_distinguishes_unreadable_from_absent() {
881 let mut map = http_1x::HeaderMap::new();
882 map.insert("ok", http_1x::HeaderValue::from_static("v"));
883 map.insert("bad", non_utf8_header_value());
884 let headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
885
886 assert_eq!(Some(Ok("v")), headers.try_get("ok"));
887 assert_eq!(Some(Err(NON_UTF8_VALUE)), headers.try_get("bad"));
888 assert_eq!(None, headers.try_get("absent"));
889
890 assert_eq!(None, headers.get("bad"));
892 assert_eq!(None, headers.get("absent"));
893 }
894
895 #[cfg(feature = "http-1x")]
896 #[test]
897 fn iter_skips_non_utf8_values_and_iter_bytes_does_not() {
898 let mut map = http_1x::HeaderMap::new();
899 map.insert("a", non_utf8_header_value());
900 map.insert("b", http_1x::HeaderValue::from_static("v"));
901 map.insert("c", non_utf8_header_value());
902 let headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
903
904 assert_eq!(vec![("b", "v")], headers.iter().collect::<Vec<_>>());
905 assert_eq!(3, headers.iter_bytes().count());
906 assert_eq!(3, headers.len());
908 }
909
910 #[cfg(feature = "http-1x")]
911 #[test]
912 fn remove_drops_a_non_utf8_value_and_reports_none() {
913 let mut map = http_1x::HeaderMap::new();
914 map.insert("bad", non_utf8_header_value());
915 let mut headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
916
917 assert_eq!(None, headers.remove("bad"));
918 assert!(
919 !headers.contains_key("bad"),
920 "the header is removed regardless"
921 );
922 }
923
924 #[cfg(feature = "http-1x")]
925 #[test]
926 fn debug_marks_non_utf8_values_without_panicking() {
927 let mut map = http_1x::HeaderMap::new();
928 map.insert("bad", non_utf8_header_value());
929 let headers = Headers::try_from(map).expect("non-UTF-8 values are admitted");
930
931 let output = format!("{headers:?}");
932 assert!(output.contains("bad"), "{output}");
933 assert!(output.contains("non-utf8"), "{output}");
934 }
935
936 #[cfg(feature = "http-02x")]
939 #[test]
940 fn converting_an_http02x_headermap_preserves_multi_value_headers() {
941 let mut map = http_02x::HeaderMap::new();
942 map.append("multi", http_02x::HeaderValue::from_static("v1"));
943 map.append("multi", http_02x::HeaderValue::from_static("v2"));
944 let headers = Headers::try_from(map).expect("valid headers");
945 let values: Vec<_> = headers.get_all("multi").collect();
946 assert_eq!(values, vec!["v1", "v2"]);
947 }
948}
949
950#[cfg(test)]
951mod redaction_tests {
952 use super::*;
953
954 #[test]
955 fn debug_redacts_authorization() {
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("authorization"));
964 assert!(output.contains("** redacted"));
965 }
966
967 #[test]
968 fn debug_redacts_security_token() {
969 let mut headers = Headers::new();
970 headers.insert("x-amz-security-token", "IQoJb3JpZ2luSECRETTOKENMARKERzzz");
971 let output = format!("{:?}", headers);
972 assert!(!output.contains("SECRETTOKENMARKER"));
973 assert!(output.contains("x-amz-security-token"));
974 assert!(output.contains("length="));
975 }
976
977 #[test]
978 fn debug_redacts_mixed_case_header_name() {
979 let mut headers = Headers::new();
980 headers.insert(
981 "Authorization",
982 "AWS4-HMAC-SHA256 Credential=AKIAXXX/.../Signature=SECRETSIGMARKER",
983 );
984 let output = format!("{:?}", headers);
985 assert!(!output.contains("SECRETSIGMARKER"));
986 assert!(output.contains("** redacted"));
987 }
988
989 #[test]
990 fn debug_preserves_non_sensitive_headers() {
991 let mut headers = Headers::new();
992 headers.insert("host", "example.com");
993 headers.insert("x-amz-user-agent", "aws-sdk-rust/1.0");
994 let output = format!("{:?}", headers);
995 assert!(output.contains("example.com"));
996 assert!(output.contains("aws-sdk-rust/1.0"));
997 }
998
999 #[test]
1000 fn debug_handles_sse_customer_key() {
1001 let mut headers = Headers::new();
1002 headers.insert(
1003 "x-amz-server-side-encryption-customer-key",
1004 "BASE64KEYMARKER_DO_NOT_LOG",
1005 );
1006 let output = format!("{:?}", headers);
1007 assert!(!output.contains("BASE64KEYMARKER_DO_NOT_LOG"));
1008 assert!(output.contains("x-amz-server-side-encryption-customer-key"));
1009 assert!(output.contains("** redacted"));
1010 }
1011
1012 #[test]
1013 fn debug_includes_length() {
1014 let value = "exactly-twenty-chars";
1015 assert_eq!(value.len(), 20);
1016 let mut headers = Headers::new();
1017 headers.insert("authorization", value);
1018 let output = format!("{:?}", headers);
1019 assert!(output.contains("length=20"));
1020 }
1021}