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)]
33pub struct Headers {
34 pub(super) headers: http_1x::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
66pub struct HeadersIter<'a> {
68 inner: http_1x::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 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)| (k, v.into_http1x())));
90 headers
91 }
92
93 #[cfg(feature = "http-02x")]
94 pub(crate) fn http0_headermap(self) -> http_02x::HeaderMap {
95 let mut headers = http_02x::HeaderMap::new();
96 headers.reserve(self.headers.len());
97 headers.extend(self.headers.into_iter().map(|(k, v)| {
98 (
99 k.map(|n| {
100 http_02x::HeaderName::from_bytes(n.as_str().as_bytes()).expect("proven valid")
101 }),
102 v.into_http02x(),
103 )
104 }));
105 headers
106 }
107
108 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 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 pub fn iter(&self) -> HeadersIter<'_> {
126 HeadersIter {
127 inner: self.headers.iter(),
128 }
129 }
130
131 pub fn len(&self) -> usize {
133 self.headers.len()
134 }
135
136 pub fn is_empty(&self) -> bool {
138 self.len() == 0
139 }
140
141 pub fn contains_key(&self, key: impl AsRef<str>) -> bool {
143 self.headers.contains_key(key.as_ref())
144 }
145
146 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 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 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 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 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 converted: Vec<(Option<http_1x::HeaderName>, HeaderValue)> = value
234 .into_iter()
235 .map(|(k, v)| {
236 let name = k
237 .map(|n| http_1x::HeaderName::from_bytes(n.as_str().as_bytes()))
238 .transpose()
239 .map_err(HttpError::invalid_header_name)?;
240 Ok((name, HeaderValue::from_http02x(v).expect("validated above")))
241 })
242 .collect::<Result<_, HttpError>>()?;
243 let mut string_safe_headers: http_1x::HeaderMap<HeaderValue> = Default::default();
244 string_safe_headers.extend(converted);
245 Ok(Headers {
246 headers: string_safe_headers,
247 })
248 }
249 }
250}
251
252#[cfg(feature = "http-1x")]
253impl TryFrom<http_1x::HeaderMap> for Headers {
254 type Error = HttpError;
255
256 fn try_from(value: http_1x::HeaderMap) -> Result<Self, Self::Error> {
257 if let Some(utf8_error) = value.iter().find_map(|(k, v)| {
258 std::str::from_utf8(v.as_bytes())
259 .err()
260 .map(|err| NonUtf8Header::new(k.as_str().to_owned(), v.as_bytes().to_vec(), err))
261 }) {
262 Err(HttpError::non_utf8_header(utf8_error))
263 } else {
264 let mut string_safe_headers: http_1x::HeaderMap<HeaderValue> = Default::default();
265 string_safe_headers.extend(
266 value
267 .into_iter()
268 .map(|(k, v)| (k, HeaderValue::from_http1x(v).expect("validated above"))),
269 );
270 Ok(Headers {
271 headers: string_safe_headers,
272 })
273 }
274 }
275}
276
277use sealed::AsHeaderComponent;
278
279mod sealed {
280 use super::*;
281 pub trait AsHeaderComponent {
283 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError>;
285
286 fn as_str(&self) -> Result<&str, HttpError>;
288
289 fn repr_as_http1x_header_name(self) -> Result<http_1x::HeaderName, Self>
291 where
292 Self: Sized,
293 {
294 Err(self)
295 }
296 }
297
298 impl AsHeaderComponent for &'static str {
299 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
300 Ok(Cow::Borrowed(self))
301 }
302
303 fn as_str(&self) -> Result<&str, HttpError> {
304 Ok(self)
305 }
306 }
307
308 impl AsHeaderComponent for String {
309 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
310 Ok(Cow::Owned(self))
311 }
312
313 fn as_str(&self) -> Result<&str, HttpError> {
314 Ok(self)
315 }
316 }
317
318 impl AsHeaderComponent for Cow<'static, str> {
319 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
320 Ok(self)
321 }
322
323 fn as_str(&self) -> Result<&str, HttpError> {
324 Ok(self.as_ref())
325 }
326 }
327
328 #[cfg(feature = "http-02x")]
329 impl AsHeaderComponent for http_02x::HeaderValue {
330 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
331 Ok(Cow::Owned(
332 std::str::from_utf8(self.as_bytes())
333 .map_err(|err| {
334 HttpError::non_utf8_header(NonUtf8Header::new_missing_name(
335 self.as_bytes().to_vec(),
336 err,
337 ))
338 })?
339 .to_string(),
340 ))
341 }
342
343 fn as_str(&self) -> Result<&str, HttpError> {
344 std::str::from_utf8(self.as_bytes()).map_err(|err| {
345 HttpError::non_utf8_header(NonUtf8Header::new_missing_name(
346 self.as_bytes().to_vec(),
347 err,
348 ))
349 })
350 }
351 }
352
353 #[cfg(feature = "http-02x")]
354 impl AsHeaderComponent for http_02x::HeaderName {
355 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
356 Ok(self.to_string().into())
357 }
358
359 fn as_str(&self) -> Result<&str, HttpError> {
360 Ok(self.as_ref())
361 }
362 }
363
364 impl AsHeaderComponent for http_1x::HeaderName {
365 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
366 Ok(self.to_string().into())
367 }
368
369 fn as_str(&self) -> Result<&str, HttpError> {
370 Ok(self.as_ref())
371 }
372
373 fn repr_as_http1x_header_name(self) -> Result<http_1x::HeaderName, Self>
374 where
375 Self: Sized,
376 {
377 Ok(self)
378 }
379 }
380
381 impl AsHeaderComponent for http_1x::HeaderValue {
382 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError> {
383 Ok(Cow::Owned(
384 std::str::from_utf8(self.as_bytes())
385 .map_err(|err| {
386 HttpError::non_utf8_header(NonUtf8Header::new_missing_name(
387 self.as_bytes().to_vec(),
388 err,
389 ))
390 })?
391 .to_string(),
392 ))
393 }
394
395 fn as_str(&self) -> Result<&str, HttpError> {
396 std::str::from_utf8(self.as_bytes()).map_err(|err| {
397 HttpError::non_utf8_header(NonUtf8Header::new_missing_name(
398 self.as_bytes().to_vec(),
399 err,
400 ))
401 })
402 }
403 }
404}
405
406mod header_value {
407 use super::*;
408
409 #[derive(Debug, Clone)]
413 pub struct HeaderValue {
414 _private: Inner,
415 }
416
417 #[derive(Debug, Clone)]
418 enum Inner {
419 #[cfg(feature = "http-02x")]
420 H0(http_02x::HeaderValue),
421 H1(http_1x::HeaderValue),
422 }
423
424 impl HeaderValue {
425 #[cfg(feature = "http-02x")]
426 pub(crate) fn from_http02x(value: http_02x::HeaderValue) -> Result<Self, HttpError> {
427 let _ = std::str::from_utf8(value.as_bytes()).map_err(|err| {
428 HttpError::non_utf8_header(NonUtf8Header::new_missing_name(
429 value.as_bytes().to_vec(),
430 err,
431 ))
432 })?;
433 Ok(Self {
434 _private: Inner::H0(value),
435 })
436 }
437
438 #[allow(dead_code)]
439 pub(crate) fn from_http1x(value: http_1x::HeaderValue) -> Result<Self, HttpError> {
440 let _ = std::str::from_utf8(value.as_bytes()).map_err(|err| {
441 HttpError::non_utf8_header(NonUtf8Header::new_missing_name(
442 value.as_bytes().to_vec(),
443 err,
444 ))
445 })?;
446 Ok(Self {
447 _private: Inner::H1(value),
448 })
449 }
450
451 #[cfg(feature = "http-02x")]
452 pub(crate) fn into_http02x(self) -> http_02x::HeaderValue {
453 match self._private {
454 Inner::H0(v) => v,
455 Inner::H1(v) => http_02x::HeaderValue::from_maybe_shared(v).expect("unreachable"),
456 }
457 }
458
459 #[allow(dead_code)]
460 pub(crate) fn into_http1x(self) -> http_1x::HeaderValue {
461 match self._private {
462 Inner::H1(v) => v,
463 #[cfg(feature = "http-02x")]
464 Inner::H0(v) => http_1x::HeaderValue::from_maybe_shared(v).expect("unreachable"),
465 }
466 }
467 }
468
469 impl AsRef<str> for HeaderValue {
470 fn as_ref(&self) -> &str {
471 let bytes = match &self._private {
472 #[cfg(feature = "http-02x")]
473 Inner::H0(v) => v.as_bytes(),
474 Inner::H1(v) => v.as_bytes(),
475 };
476 std::str::from_utf8(bytes).expect("unreachable—only strings may be stored")
477 }
478 }
479
480 impl From<HeaderValue> for String {
481 fn from(value: HeaderValue) -> Self {
482 value.as_ref().to_string()
483 }
484 }
485
486 impl HeaderValue {
487 pub fn as_str(&self) -> &str {
489 self.as_ref()
490 }
491 }
492
493 impl FromStr for HeaderValue {
494 type Err = HttpError;
495
496 fn from_str(s: &str) -> Result<Self, Self::Err> {
497 HeaderValue::try_from(s.to_string())
498 }
499 }
500
501 impl TryFrom<String> for HeaderValue {
502 type Error = HttpError;
503
504 fn try_from(value: String) -> Result<Self, Self::Error> {
505 Ok(HeaderValue::from_http1x(
506 http_1x::HeaderValue::try_from(value).map_err(HttpError::invalid_header_value)?,
507 )
508 .expect("input was a string"))
509 }
510 }
511}
512
513pub use header_value::HeaderValue;
514
515type MaybeStatic = Cow<'static, str>;
516
517fn header_name(
518 name: impl AsHeaderComponent,
519 panic_safe: bool,
520) -> Result<http_1x::HeaderName, HttpError> {
521 name.repr_as_http1x_header_name().or_else(|name| {
522 name.into_maybe_static().and_then(|mut cow| {
523 if cow.chars().any(|c| c.is_ascii_uppercase()) {
524 cow = Cow::Owned(cow.to_ascii_uppercase());
525 }
526 match cow {
527 Cow::Borrowed(s) if panic_safe => {
528 http_1x::HeaderName::try_from(s).map_err(HttpError::invalid_header_name)
529 }
530 Cow::Borrowed(static_s) => Ok(http_1x::HeaderName::from_static(static_s)),
531 Cow::Owned(s) => {
532 http_1x::HeaderName::try_from(s).map_err(HttpError::invalid_header_name)
533 }
534 }
535 })
536 })
537}
538
539fn header_value(value: MaybeStatic, panic_safe: bool) -> Result<HeaderValue, HttpError> {
540 let header = match value {
541 Cow::Borrowed(b) if panic_safe => {
542 http_1x::HeaderValue::try_from(b).map_err(HttpError::invalid_header_value)?
543 }
544 Cow::Borrowed(b) => http_1x::HeaderValue::from_static(b),
545 Cow::Owned(s) => {
546 http_1x::HeaderValue::try_from(s).map_err(HttpError::invalid_header_value)?
547 }
548 };
549 HeaderValue::from_http1x(header)
550}
551
552#[cfg(test)]
553mod tests {
554 use super::*;
555
556 #[test]
557 fn headers_can_be_any_string() {
558 let _: HeaderValue = "😹".parse().expect("can be any string");
559 let _: HeaderValue = "abcd".parse().expect("can be any string");
560 let _ = "a\nb"
561 .parse::<HeaderValue>()
562 .expect_err("cannot contain control characters");
563 }
564
565 #[test]
566 fn no_panic_insert_upper_case_header_name() {
567 let mut headers = Headers::new();
568 headers.insert("I-Have-Upper-Case", "foo");
569 }
570 #[test]
571 fn no_panic_append_upper_case_header_name() {
572 let mut headers = Headers::new();
573 headers.append("I-Have-Upper-Case", "foo");
574 }
575
576 #[test]
577 #[should_panic]
578 fn panic_insert_invalid_ascii_key() {
579 let mut headers = Headers::new();
580 headers.insert("💩", "foo");
581 }
582 #[test]
583 #[should_panic]
584 fn panic_insert_invalid_header_value() {
585 let mut headers = Headers::new();
586 headers.insert("foo", "💩");
587 }
588 #[test]
589 #[should_panic]
590 fn panic_append_invalid_ascii_key() {
591 let mut headers = Headers::new();
592 headers.append("💩", "foo");
593 }
594 #[test]
595 #[should_panic]
596 fn panic_append_invalid_header_value() {
597 let mut headers = Headers::new();
598 headers.append("foo", "💩");
599 }
600
601 #[test]
602 fn no_panic_try_insert_invalid_ascii_key() {
603 let mut headers = Headers::new();
604 assert!(headers.try_insert("💩", "foo").is_err());
605 }
606 #[test]
607 fn no_panic_try_insert_invalid_header_value() {
608 let mut headers = Headers::new();
609 assert!(headers
610 .try_insert(
611 "foo",
612 http_1x::HeaderValue::from_bytes(&[0xC0, 0x80]).unwrap()
614 )
615 .is_err());
616 }
617 #[test]
618 fn no_panic_try_append_invalid_ascii_key() {
619 let mut headers = Headers::new();
620 assert!(headers.try_append("💩", "foo").is_err());
621 }
622 #[test]
623 fn no_panic_try_append_invalid_header_value() {
624 let mut headers = Headers::new();
625 assert!(headers
626 .try_insert(
627 "foo",
628 http_1x::HeaderValue::from_bytes(&[0xC0, 0x80]).unwrap()
630 )
631 .is_err());
632 }
633
634 proptest::proptest! {
635 #[test]
636 fn insert_header_prop_test(input in ".*") {
637 let mut headers = Headers::new();
638 let _ = headers.try_insert(input.clone(), input);
639 }
640
641 #[test]
642 fn append_header_prop_test(input in ".*") {
643 let mut headers = Headers::new();
644 let _ = headers.try_append(input.clone(), input);
645 }
646 }
647
648 #[cfg(feature = "http-02x")]
651 #[test]
652 fn converting_an_http02x_headermap_never_panics() {
653 let name = http_02x::HeaderName::from_bytes(b"a\"b").expect("http 0.2.x accepts this");
654 let mut map = http_02x::HeaderMap::new();
655 map.insert(name, http_02x::HeaderValue::from_static("v"));
656 let res = std::panic::catch_unwind(|| Headers::try_from(map));
657 assert!(
658 res.is_ok(),
659 "TryFrom<http_02x::HeaderMap> for Headers panicked on a name that http 0.2.x \
660 considers valid but http 1.x does not; a TryFrom should return Err"
661 );
662 assert!(
663 res.unwrap().is_err(),
664 "expected an Err for a header name that http 1.x rejects"
665 );
666 }
667
668 #[cfg(feature = "http-02x")]
671 #[test]
672 fn converting_an_http02x_headermap_preserves_multi_value_headers() {
673 let mut map = http_02x::HeaderMap::new();
674 map.append("multi", http_02x::HeaderValue::from_static("v1"));
675 map.append("multi", http_02x::HeaderValue::from_static("v2"));
676 let headers = Headers::try_from(map).expect("valid headers");
677 let values: Vec<_> = headers.get_all("multi").collect();
678 assert_eq!(values, vec!["v1", "v2"]);
679 }
680}
681
682#[cfg(test)]
683mod redaction_tests {
684 use super::*;
685
686 #[test]
687 fn debug_redacts_authorization() {
688 let mut headers = Headers::new();
689 headers.insert(
690 "authorization",
691 "AWS4-HMAC-SHA256 Credential=AKIAXXX/.../Signature=SECRETSIGMARKER",
692 );
693 let output = format!("{:?}", headers);
694 assert!(!output.contains("SECRETSIGMARKER"));
695 assert!(output.contains("authorization"));
696 assert!(output.contains("** redacted"));
697 }
698
699 #[test]
700 fn debug_redacts_security_token() {
701 let mut headers = Headers::new();
702 headers.insert("x-amz-security-token", "IQoJb3JpZ2luSECRETTOKENMARKERzzz");
703 let output = format!("{:?}", headers);
704 assert!(!output.contains("SECRETTOKENMARKER"));
705 assert!(output.contains("x-amz-security-token"));
706 assert!(output.contains("length="));
707 }
708
709 #[test]
710 fn debug_redacts_mixed_case_header_name() {
711 let mut headers = Headers::new();
712 headers.insert(
713 "Authorization",
714 "AWS4-HMAC-SHA256 Credential=AKIAXXX/.../Signature=SECRETSIGMARKER",
715 );
716 let output = format!("{:?}", headers);
717 assert!(!output.contains("SECRETSIGMARKER"));
718 assert!(output.contains("** redacted"));
719 }
720
721 #[test]
722 fn debug_preserves_non_sensitive_headers() {
723 let mut headers = Headers::new();
724 headers.insert("host", "example.com");
725 headers.insert("x-amz-user-agent", "aws-sdk-rust/1.0");
726 let output = format!("{:?}", headers);
727 assert!(output.contains("example.com"));
728 assert!(output.contains("aws-sdk-rust/1.0"));
729 }
730
731 #[test]
732 fn debug_handles_sse_customer_key() {
733 let mut headers = Headers::new();
734 headers.insert(
735 "x-amz-server-side-encryption-customer-key",
736 "BASE64KEYMARKER_DO_NOT_LOG",
737 );
738 let output = format!("{:?}", headers);
739 assert!(!output.contains("BASE64KEYMARKER_DO_NOT_LOG"));
740 assert!(output.contains("x-amz-server-side-encryption-customer-key"));
741 assert!(output.contains("** redacted"));
742 }
743
744 #[test]
745 fn debug_includes_length() {
746 let value = "exactly-twenty-chars";
747 assert_eq!(value.len(), 20);
748 let mut headers = Headers::new();
749 headers.insert("authorization", value);
750 let output = format!("{:?}", headers);
751 assert!(output.contains("length=20"));
752 }
753}