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_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
66pub 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 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 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 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 pub trait AsHeaderComponent {
275 fn into_maybe_static(self) -> Result<MaybeStatic, HttpError>;
277
278 fn as_str(&self) -> Result<&str, HttpError>;
280
281 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 #[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 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 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 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}