wreq-rb 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +257 -102
  3. data/ext/wreq_rb/Cargo.toml +7 -4
  4. data/ext/wreq_rb/src/client.rs +105 -15
  5. data/ext/wreq_rb/src/response.rs +28 -3
  6. data/lib/wreq-rb/version.rb +1 -1
  7. data/patches/0001-add-transfer-size-tracking.patch +11 -15
  8. data/vendor/wreq/Cargo.toml +9 -8
  9. data/vendor/wreq/README.md +5 -5
  10. data/vendor/wreq/bench/support/bench.rs +6 -2
  11. data/vendor/wreq/bench/support/client.rs +88 -1
  12. data/vendor/wreq/bench/support/exec.rs +0 -0
  13. data/vendor/wreq/bench/support/rt.rs +34 -0
  14. data/vendor/wreq/bench/support/server.rs +1 -1
  15. data/vendor/wreq/bench/support.rs +1 -15
  16. data/vendor/wreq/examples/cert_store.rs +13 -13
  17. data/vendor/wreq/examples/request_with_emulate.rs +1 -1
  18. data/vendor/wreq/examples/tcp_linger.rs +22 -0
  19. data/vendor/wreq/src/client/layer/client/pool.rs +17 -17
  20. data/vendor/wreq/src/client/layer/client.rs +2 -0
  21. data/vendor/wreq/src/client/layer/decoder.rs +71 -17
  22. data/vendor/wreq/src/client/layer/redirect/future.rs +49 -63
  23. data/vendor/wreq/src/client/layer/redirect/policy.rs +2 -26
  24. data/vendor/wreq/src/client/layer/redirect.rs +48 -60
  25. data/vendor/wreq/src/client/layer/retry.rs +12 -15
  26. data/vendor/wreq/src/client/layer/timeout/body.rs +27 -21
  27. data/vendor/wreq/src/client/layer/timeout/future.rs +33 -58
  28. data/vendor/wreq/src/client/layer/timeout.rs +8 -14
  29. data/vendor/wreq/src/client/request.rs +4 -0
  30. data/vendor/wreq/src/client.rs +53 -31
  31. data/vendor/wreq/src/conn/connector.rs +99 -129
  32. data/vendor/wreq/src/conn/http.rs +25 -18
  33. data/vendor/wreq/src/conn/net/tcp.rs +601 -107
  34. data/vendor/wreq/src/conn/proxy/socks.rs +6 -6
  35. data/vendor/wreq/src/conn/timeout.rs +166 -0
  36. data/vendor/wreq/src/conn.rs +5 -4
  37. data/vendor/wreq/src/cookie/jar.rs +1225 -0
  38. data/vendor/wreq/src/cookie/store.rs +321 -0
  39. data/vendor/wreq/src/cookie.rs +108 -612
  40. data/vendor/wreq/src/dns/resolve.rs +8 -2
  41. data/vendor/wreq/src/dns.rs +4 -4
  42. data/vendor/wreq/src/error.rs +53 -20
  43. data/vendor/wreq/src/lib.rs +1 -0
  44. data/vendor/wreq/src/proxy/matcher.rs +26 -12
  45. data/vendor/wreq/src/proxy/win.rs +39 -9
  46. data/vendor/wreq/src/redirect.rs +515 -100
  47. data/vendor/wreq/src/tls/conn.rs +3 -11
  48. data/vendor/wreq/src/tls/session.rs +7 -8
  49. data/vendor/wreq/src/tls/trust/store.rs +4 -4
  50. data/vendor/wreq/src/util.rs +23 -0
  51. data/vendor/wreq/tests/badssl.rs +72 -7
  52. data/vendor/wreq/tests/brotli.rs +1 -1
  53. data/vendor/wreq/tests/client.rs +24 -0
  54. data/vendor/wreq/tests/connector_layers.rs +8 -4
  55. data/vendor/wreq/tests/cookie.rs +59 -0
  56. data/vendor/wreq/tests/deflate.rs +1 -1
  57. data/vendor/wreq/tests/gzip.rs +53 -1
  58. data/vendor/wreq/tests/layers.rs +8 -4
  59. data/vendor/wreq/tests/redirect.rs +180 -97
  60. data/vendor/wreq/tests/timeouts.rs +47 -12
  61. data/vendor/wreq/tests/zstd.rs +1 -1
  62. metadata +8 -2
@@ -6,16 +6,16 @@
6
6
 
7
7
  use std::{borrow::Cow, error::Error as StdError, fmt, sync::Arc};
8
8
 
9
- use bytes::Bytes;
10
9
  use futures_util::FutureExt;
11
- use http::{HeaderMap, HeaderName, HeaderValue, StatusCode, Uri};
10
+ use http::{HeaderMap, HeaderName, StatusCode, Uri};
12
11
 
12
+ use self::referrer::Referrer;
13
13
  use crate::{
14
- client::{body::Body, layer::redirect},
14
+ client::layer::redirect,
15
15
  config::RequestConfig,
16
16
  error::{BoxError, Error},
17
17
  ext::UriExt,
18
- header::{AUTHORIZATION, COOKIE, PROXY_AUTHORIZATION, REFERER, WWW_AUTHENTICATE},
18
+ header::{AUTHORIZATION, COOKIE, PROXY_AUTHORIZATION, WWW_AUTHENTICATE},
19
19
  };
20
20
 
21
21
  /// A type that controls the policy on how to handle the following of redirects.
@@ -52,9 +52,7 @@ pub struct Attempt<'a, const PENDING: bool = true> {
52
52
 
53
53
  /// An action to perform when a redirect status code is found.
54
54
  #[derive(Debug)]
55
- pub struct Action {
56
- inner: redirect::Action,
57
- }
55
+ pub struct Action(redirect::Action);
58
56
 
59
57
  /// Redirect history information for a response.
60
58
  #[derive(Debug, Clone)]
@@ -90,17 +88,17 @@ struct TooManyRedirects;
90
88
  /// A redirect policy handler for HTTP clients.
91
89
  ///
92
90
  /// [`FollowRedirectPolicy`] manages how HTTP redirects are handled by the client,
93
- /// including the maximum number of redirects, whether to set the `Referer` header,
91
+ /// including the maximum number of redirects, `Referer` policy handling,
94
92
  /// HTTPS-only enforcement, and redirect history tracking.
95
93
  ///
96
94
  /// This type is used internally by the client to implement redirect logic according to
97
95
  /// the configured [`Policy`]. It ensures that only allowed redirects are followed,
98
- /// sensitive headers are removed when crossing hosts, and the `Referer` header is set
99
- /// when appropriate.
96
+ /// sensitive headers are removed when crossing origins, and response referrer policies
97
+ /// are applied.
100
98
  #[derive(Clone)]
101
99
  pub(crate) struct FollowRedirectPolicy {
102
100
  policy: RequestConfig<Policy>,
103
- referer: bool,
101
+ referrer: Option<Referrer>,
104
102
  uris: Vec<Uri>,
105
103
  https_only: bool,
106
104
  history: Option<Vec<HistoryEntry>>,
@@ -221,7 +219,7 @@ impl Policy {
221
219
  uri: Cow::Borrowed(next),
222
220
  previous: Cow::Borrowed(previous),
223
221
  })
224
- .inner
222
+ .0
225
223
  }
226
224
  }
227
225
 
@@ -241,9 +239,7 @@ impl<const PENDING: bool> Attempt<'_, PENDING> {
241
239
  /// Returns an action meaning wreq should follow the next URI.
242
240
  #[inline]
243
241
  pub fn follow(self) -> Action {
244
- Action {
245
- inner: redirect::Action::Follow,
246
- }
242
+ Action(redirect::Action::Follow)
247
243
  }
248
244
 
249
245
  /// Returns an action meaning wreq should not follow the next URI.
@@ -251,9 +247,7 @@ impl<const PENDING: bool> Attempt<'_, PENDING> {
251
247
  /// The 30x response will be returned as the `Ok` result.
252
248
  #[inline]
253
249
  pub fn stop(self) -> Action {
254
- Action {
255
- inner: redirect::Action::Stop,
256
- }
250
+ Action(redirect::Action::Stop)
257
251
  }
258
252
 
259
253
  /// Returns an [`Action`] failing the redirect with an error.
@@ -261,9 +255,7 @@ impl<const PENDING: bool> Attempt<'_, PENDING> {
261
255
  /// The [`Error`] will be returned for the result of the sent request.
262
256
  #[inline]
263
257
  pub fn error<E: Into<BoxError>>(self, error: E) -> Action {
264
- Action {
265
- inner: redirect::Action::Error(error.into()),
266
- }
258
+ Action(redirect::Action::Error(error.into()))
267
259
  }
268
260
  }
269
261
 
@@ -300,10 +292,10 @@ impl Attempt<'_, true> {
300
292
  uri: Cow::Owned(self.uri.into_owned()),
301
293
  previous: Cow::Owned(self.previous.into_owned()),
302
294
  };
303
- let pending = Box::pin(func(attempt).map(|action| action.inner));
304
- Action {
305
- inner: redirect::Action::Pending(pending),
306
- }
295
+
296
+ Action(redirect::Action::Pending(Box::pin(
297
+ func(attempt).map(|action| action.0),
298
+ )))
307
299
  }
308
300
  }
309
301
 
@@ -356,9 +348,9 @@ impl StdError for TooManyRedirects {}
356
348
  impl FollowRedirectPolicy {
357
349
  /// Creates a new redirect policy handler with the given [`Policy`].
358
350
  pub fn new(policy: Policy) -> Self {
359
- Self {
351
+ FollowRedirectPolicy {
360
352
  policy: RequestConfig::new(Some(policy)),
361
- referer: false,
353
+ referrer: None,
362
354
  uris: Vec::new(),
363
355
  https_only: false,
364
356
  history: None,
@@ -368,7 +360,7 @@ impl FollowRedirectPolicy {
368
360
  /// Enables or disables automatic Referer header management.
369
361
  #[inline]
370
362
  pub fn with_referer(mut self, referer: bool) -> Self {
371
- self.referer = referer;
363
+ self.referrer = referer.then(Referrer::default);
372
364
  self
373
365
  }
374
366
 
@@ -380,111 +372,505 @@ impl FollowRedirectPolicy {
380
372
  }
381
373
  }
382
374
 
383
- impl redirect::Policy<Body, BoxError> for FollowRedirectPolicy {
384
- fn redirect(&mut self, attempt: redirect::Attempt<'_>) -> Result<redirect::Action, BoxError> {
375
+ impl FollowRedirectPolicy {
376
+ pub(crate) fn redirect(
377
+ &mut self,
378
+ attempt: redirect::Attempt<'_>,
379
+ ) -> Result<redirect::Action, BoxError> {
385
380
  // Parse the next URI from the attempt.
386
381
  let previous_uri = attempt.previous;
387
382
  let next_uri = attempt.location;
388
-
389
- // Push the previous URI to the list of URLs.
390
383
  self.uris.push(previous_uri.clone());
384
+ if let Some(referrer) = &mut self.referrer {
385
+ referrer.on_redirect(attempt.headers);
386
+ }
391
387
 
392
388
  // Get policy from config
393
389
  let policy = self
394
390
  .policy
395
391
  .as_ref()
396
392
  .expect("[BUG] FollowRedirectPolicy should always have a policy set");
393
+ let action = policy.check(attempt.status, attempt.headers, next_uri, &self.uris);
397
394
 
398
- // Check if the next URI is already in the list of URLs.
399
- match policy.check(attempt.status, attempt.headers, next_uri, &self.uris) {
400
- redirect::Action::Follow => {
401
- // Validate the redirect URI scheme
402
- if !(next_uri.is_http() || next_uri.is_https()) {
403
- return Err(Error::uri_bad_scheme(next_uri.clone()).into());
404
- }
395
+ // Handle errors from the policy immediately
396
+ if let redirect::Action::Error(err) = action {
397
+ return Err(Error::redirect(err, previous_uri.clone()).into());
398
+ }
405
399
 
406
- // Check HTTPS-only policy
407
- if self.https_only && !next_uri.is_https() {
408
- return Err(Error::redirect(
409
- Error::uri_bad_scheme(next_uri.clone()),
410
- next_uri.clone(),
411
- )
412
- .into());
413
- }
400
+ // Handle follow redirect action
401
+ if matches!(&action, redirect::Action::Follow) {
402
+ // Validate the redirect URI scheme
403
+ if !(next_uri.is_http() || next_uri.is_https()) {
404
+ return Err(Error::uri_bad_scheme(next_uri.clone()).into());
405
+ }
414
406
 
415
- // Record redirect history
416
- if !matches!(policy.inner, PolicyKind::None) {
417
- self.history.get_or_insert_default().push(HistoryEntry {
418
- status: attempt.status,
419
- uri: attempt.location.clone(),
420
- previous: attempt.previous.clone(),
421
- headers: attempt.headers.clone(),
422
- });
423
- }
407
+ // Check HTTPS-only policy
408
+ if self.https_only && !next_uri.is_https() {
409
+ return Err(Error::redirect(
410
+ Error::uri_bad_scheme(next_uri.clone()),
411
+ next_uri.clone(),
412
+ )
413
+ .into());
414
+ }
424
415
 
425
- Ok(redirect::Action::Follow)
416
+ // Record redirect history
417
+ if !matches!(policy.inner, PolicyKind::None) {
418
+ self.history.get_or_insert_default().push(HistoryEntry {
419
+ status: attempt.status,
420
+ uri: attempt.location.clone(),
421
+ previous: attempt.previous.clone(),
422
+ headers: attempt.headers.clone(),
423
+ });
426
424
  }
427
- redirect::Action::Stop => Ok(redirect::Action::Stop),
428
- redirect::Action::Pending(task) => Ok(redirect::Action::Pending(task)),
429
- redirect::Action::Error(err) => Err(Error::redirect(err, previous_uri.clone()).into()),
430
425
  }
426
+
427
+ Ok(action)
431
428
  }
432
429
 
433
- fn follow_redirects(&mut self, request: &mut http::Request<Body>) -> bool {
430
+ pub(crate) fn for_request<B>(&mut self, request: &mut http::Request<B>) -> Option<Self> {
434
431
  self.policy
435
432
  .load(request.extensions_mut())
436
433
  .is_some_and(|policy| !matches!(policy.inner, PolicyKind::None))
434
+ .then(|| {
435
+ let mut policy = self.clone();
436
+ policy.referrer = policy.referrer.map(|_| Referrer::new(request.headers()));
437
+ policy
438
+ })
437
439
  }
438
440
 
439
- fn on_request(&mut self, req: &mut http::Request<Body>) {
440
- let next_url = req.uri().clone();
441
- remove_sensitive_headers(req.headers_mut(), &next_url, &self.uris);
442
- if self.referer {
443
- if let Some(previous_url) = self.uris.last() {
444
- if let Some(v) = make_referer(next_url, previous_url) {
445
- req.headers_mut().insert(REFERER, v);
446
- }
447
- }
441
+ pub(crate) fn on_request<B>(&mut self, req: &mut http::Request<B>) {
442
+ remove_sensitive_headers(req, &self.uris);
443
+ if !self.uris.is_empty()
444
+ && let Some(referrer) = &mut self.referrer
445
+ {
446
+ referrer.apply(req);
448
447
  }
449
448
  }
450
449
 
451
- fn on_response<Body>(&mut self, response: &mut http::Response<Body>) {
450
+ pub(crate) fn on_response<B>(&mut self, response: &mut http::Response<B>) {
452
451
  if let Some(history) = self.history.take() {
453
452
  response.extensions_mut().insert(History(history));
454
453
  }
455
454
  }
455
+ }
456
456
 
457
- #[inline]
458
- fn clone_body(&self, body: &Body) -> Option<Body> {
459
- body.try_clone()
457
+ fn remove_sensitive_headers<B>(req: &mut http::Request<B>, previous: &[Uri]) {
458
+ if let Some(previous) = previous.last()
459
+ && !same_origin(req.uri(), previous)
460
+ {
461
+ /// Avoid dynamic allocation of `HeaderName` by using `from_static`.
462
+ /// https://github.com/hyperium/http/blob/e9de46c9269f0a476b34a02a401212e20f639df2/src/header/map.rs#L3794
463
+ const COOKIE2: HeaderName = HeaderName::from_static("cookie2");
464
+ let headers = req.headers_mut();
465
+ headers.remove(AUTHORIZATION);
466
+ headers.remove(COOKIE);
467
+ headers.remove(COOKIE2);
468
+ headers.remove(PROXY_AUTHORIZATION);
469
+ headers.remove(WWW_AUTHENTICATE);
460
470
  }
461
471
  }
462
472
 
463
- fn make_referer(next: Uri, previous: &Uri) -> Option<HeaderValue> {
464
- if next.is_http() && previous.is_https() {
465
- return None;
466
- }
473
+ fn same_origin(left: &Uri, right: &Uri) -> bool {
474
+ let same_host = match (left.host(), right.host()) {
475
+ (Some(left), Some(right)) => left.eq_ignore_ascii_case(right),
476
+ (None, None) => true,
477
+ _ => false,
478
+ };
467
479
 
468
- let mut referer = previous.clone();
469
- referer.set_userinfo("", None);
470
- HeaderValue::from_maybe_shared(Bytes::from(referer.to_string())).ok()
480
+ same_host
481
+ && left.scheme() == right.scheme()
482
+ && left.port_or_default() == right.port_or_default()
471
483
  }
472
484
 
473
- fn remove_sensitive_headers(headers: &mut HeaderMap, next: &Uri, previous: &[Uri]) {
474
- if let Some(previous) = previous.last() {
475
- let cross_host = next.host() != previous.host()
476
- || next.port() != previous.port()
477
- || next.scheme() != previous.scheme();
478
- if cross_host {
479
- /// Avoid dynamic allocation of `HeaderName` by using `from_static`.
480
- /// https://github.com/hyperium/http/blob/e9de46c9269f0a476b34a02a401212e20f639df2/src/header/map.rs#L3794
481
- const COOKIE2: HeaderName = HeaderName::from_static("cookie2");
482
-
483
- headers.remove(AUTHORIZATION);
484
- headers.remove(COOKIE);
485
- headers.remove(COOKIE2);
486
- headers.remove(PROXY_AUTHORIZATION);
487
- headers.remove(WWW_AUTHENTICATE);
485
+ mod referrer {
486
+ use http::{
487
+ HeaderMap, HeaderValue, Uri,
488
+ header::{REFERER, REFERRER_POLICY},
489
+ uri::Scheme,
490
+ };
491
+ use url::Url;
492
+
493
+ use crate::ext::UriExt;
494
+
495
+ /// Referrer state carried across a redirect chain.
496
+ #[derive(Clone, Default)]
497
+ pub(super) struct Referrer {
498
+ source: Option<Url>,
499
+ policy: ReferrerPolicy,
500
+ }
501
+
502
+ impl Referrer {
503
+ /// Captures the caller-provided referrer without changing the initial request.
504
+ ///
505
+ /// Redirect processing updates the policy before computing the next referrer:
506
+ /// https://w3c.github.io/webappsec-referrer-policy/#integration-with-fetch
507
+ pub(super) fn new(headers: &HeaderMap) -> Self {
508
+ Self {
509
+ source: headers.get(REFERER).and_then(parse_referrer),
510
+ policy: ReferrerPolicy::default(),
511
+ }
512
+ }
513
+
514
+ /// Applies the last recognized policy from a redirect response.
515
+ ///
516
+ /// https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect
517
+ pub(super) fn on_redirect(&mut self, headers: &HeaderMap) {
518
+ match ReferrerPolicy::from(headers) {
519
+ ReferrerPolicy::None => {}
520
+ policy => self.policy = policy,
521
+ }
522
+ }
523
+
524
+ /// Computes the referrer for the next request in the redirect chain.
525
+ ///
526
+ /// https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer
527
+ pub(super) fn apply<B>(&mut self, req: &mut http::Request<B>) {
528
+ let Some(mut source) = self.source.take() else {
529
+ req.headers_mut().remove(REFERER);
530
+ return;
531
+ };
532
+ let sensitive = req
533
+ .headers_mut()
534
+ .get(REFERER)
535
+ .is_some_and(HeaderValue::is_sensitive);
536
+
537
+ let Ok(source_scheme) = source.scheme().parse::<Scheme>() else {
538
+ req.headers_mut().remove(REFERER);
539
+ return;
540
+ };
541
+
542
+ let destination = req.uri();
543
+ let same_origin = same_origin(&source, &source_scheme, destination);
544
+ let downgrade =
545
+ source_scheme == Scheme::HTTPS && destination.scheme() == Some(&Scheme::HTTP);
546
+
547
+ let strip_to_origin = match self.policy {
548
+ ReferrerPolicy::NoReferrer => {
549
+ req.headers_mut().remove(REFERER);
550
+ return;
551
+ }
552
+ ReferrerPolicy::NoReferrerWhenDowngrade if downgrade => {
553
+ req.headers_mut().remove(REFERER);
554
+ return;
555
+ }
556
+ ReferrerPolicy::SameOrigin if !same_origin => {
557
+ req.headers_mut().remove(REFERER);
558
+ return;
559
+ }
560
+ ReferrerPolicy::StrictOrigin if downgrade => {
561
+ req.headers_mut().remove(REFERER);
562
+ return;
563
+ }
564
+ ReferrerPolicy::StrictOriginWhenCrossOrigin | ReferrerPolicy::None
565
+ if !same_origin && downgrade =>
566
+ {
567
+ req.headers_mut().remove(REFERER);
568
+ return;
569
+ }
570
+ ReferrerPolicy::Origin | ReferrerPolicy::StrictOrigin => true,
571
+ ReferrerPolicy::OriginWhenCrossOrigin
572
+ | ReferrerPolicy::StrictOriginWhenCrossOrigin
573
+ | ReferrerPolicy::None => !same_origin,
574
+ ReferrerPolicy::NoReferrerWhenDowngrade
575
+ | ReferrerPolicy::SameOrigin
576
+ | ReferrerPolicy::UnsafeUrl => false,
577
+ };
578
+
579
+ if strip_to_origin {
580
+ strip_to_origin_url(&mut source);
581
+ }
582
+
583
+ match HeaderValue::try_from(source.as_str()) {
584
+ Ok(mut value) => {
585
+ value.set_sensitive(sensitive);
586
+ req.headers_mut().insert(REFERER, value);
587
+ self.source = Some(source);
588
+ }
589
+ Err(_) => {
590
+ req.headers_mut().remove(REFERER);
591
+ }
592
+ }
593
+ }
594
+ }
595
+
596
+ /// Referrer policies defined by the Referrer Policy specification.
597
+ #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
598
+ enum ReferrerPolicy {
599
+ None,
600
+ NoReferrer,
601
+ NoReferrerWhenDowngrade,
602
+ SameOrigin,
603
+ Origin,
604
+ StrictOrigin,
605
+ OriginWhenCrossOrigin,
606
+ #[default]
607
+ StrictOriginWhenCrossOrigin,
608
+ UnsafeUrl,
609
+ }
610
+
611
+ /// Parses all policy tokens and keeps the last recognized value.
612
+ ///
613
+ /// https://w3c.github.io/webappsec-referrer-policy/#parse-referrer-policy-from-header
614
+ impl From<&HeaderMap> for ReferrerPolicy {
615
+ fn from(headers: &HeaderMap) -> Self {
616
+ let mut policy = Self::None;
617
+
618
+ for value in headers.get_all(REFERRER_POLICY) {
619
+ let Ok(value) = value.to_str() else {
620
+ continue;
621
+ };
622
+
623
+ for token in value.split(',') {
624
+ let parsed = Self::from(token.trim());
625
+ if parsed != Self::None {
626
+ policy = parsed;
627
+ }
628
+ }
629
+ }
630
+
631
+ policy
632
+ }
633
+ }
634
+
635
+ impl From<&str> for ReferrerPolicy {
636
+ fn from(token: &str) -> Self {
637
+ const TOKENS: &[(&str, ReferrerPolicy)] = &[
638
+ ("no-referrer", ReferrerPolicy::NoReferrer),
639
+ (
640
+ "no-referrer-when-downgrade",
641
+ ReferrerPolicy::NoReferrerWhenDowngrade,
642
+ ),
643
+ ("same-origin", ReferrerPolicy::SameOrigin),
644
+ ("origin", ReferrerPolicy::Origin),
645
+ ("strict-origin", ReferrerPolicy::StrictOrigin),
646
+ (
647
+ "origin-when-cross-origin",
648
+ ReferrerPolicy::OriginWhenCrossOrigin,
649
+ ),
650
+ (
651
+ "strict-origin-when-cross-origin",
652
+ ReferrerPolicy::StrictOriginWhenCrossOrigin,
653
+ ),
654
+ ("unsafe-url", ReferrerPolicy::UnsafeUrl),
655
+ ];
656
+
657
+ match TOKENS
658
+ .iter()
659
+ .find(|(name, _)| token.eq_ignore_ascii_case(name))
660
+ {
661
+ Some((_, policy)) => *policy,
662
+ None => Self::None,
663
+ }
664
+ }
665
+ }
666
+
667
+ /// Parses an HTTP(S) referrer and strips credentials and fragments.
668
+ ///
669
+ /// `Referer` field syntax:
670
+ /// https://www.rfc-editor.org/rfc/rfc9110.html#section-10.1.3
671
+ ///
672
+ /// Referrer Policy URL stripping:
673
+ /// https://w3c.github.io/webappsec-referrer-policy/#strip-url
674
+ fn parse_referrer(value: &HeaderValue) -> Option<Url> {
675
+ let mut source = Url::parse(value.to_str().ok()?).ok()?;
676
+ let scheme = source.scheme().parse::<Scheme>().ok()?;
677
+ if scheme != Scheme::HTTP && scheme != Scheme::HTTPS {
678
+ return None;
679
+ }
680
+
681
+ source.set_username("").ok()?;
682
+ source.set_password(None).ok()?;
683
+ source.set_fragment(None);
684
+
685
+ if source.as_str().len() > 4096 {
686
+ strip_to_origin_url(&mut source);
687
+ }
688
+
689
+ Some(source)
690
+ }
691
+
692
+ fn strip_to_origin_url(url: &mut Url) {
693
+ url.set_path("");
694
+ url.set_query(None);
695
+ url.set_fragment(None);
696
+ }
697
+
698
+ fn same_origin(source: &Url, source_scheme: &Scheme, destination: &Uri) -> bool {
699
+ let same_host = match (source.host_str(), destination.host()) {
700
+ (Some(source), Some(destination)) => source.eq_ignore_ascii_case(destination),
701
+ (None, None) => true,
702
+ _ => false,
703
+ };
704
+
705
+ same_host
706
+ && destination.scheme() == Some(source_scheme)
707
+ && source.port_or_known_default() == Some(destination.port_or_default())
708
+ }
709
+
710
+ #[cfg(test)]
711
+ mod tests {
712
+ use super::*;
713
+
714
+ fn headers_with_referrer(value: &'static str) -> HeaderMap {
715
+ let mut headers = HeaderMap::new();
716
+ headers.insert(REFERER, HeaderValue::from_static(value));
717
+ headers
718
+ }
719
+
720
+ fn request(headers: HeaderMap, destination: &'static str) -> http::Request<()> {
721
+ let mut req = http::Request::new(());
722
+ *req.headers_mut() = headers;
723
+ *req.uri_mut() = Uri::from_static(destination);
724
+ req
725
+ }
726
+
727
+ fn apply(
728
+ source: &'static str,
729
+ destination: &'static str,
730
+ policy: Option<&'static str>,
731
+ ) -> Option<HeaderValue> {
732
+ let headers = headers_with_referrer(source);
733
+ let mut referrer = Referrer::new(&headers);
734
+
735
+ if let Some(policy) = policy {
736
+ let mut response_headers = HeaderMap::new();
737
+ response_headers.insert(REFERRER_POLICY, HeaderValue::from_static(policy));
738
+ referrer.on_redirect(&response_headers);
739
+ }
740
+
741
+ let mut req = request(headers, destination);
742
+ referrer.apply(&mut req);
743
+ req.headers().get(REFERER).cloned()
744
+ }
745
+
746
+ fn header_str(value: &Option<HeaderValue>) -> Option<&str> {
747
+ value.as_ref().and_then(|value| value.to_str().ok())
748
+ }
749
+
750
+ #[test]
751
+ fn applies_referrer_policy() {
752
+ let mut headers = HeaderMap::new();
753
+ headers.append(
754
+ REFERRER_POLICY,
755
+ HeaderValue::from_static("same-origin, future-policy"),
756
+ );
757
+ headers.append(REFERRER_POLICY, HeaderValue::from_static("ORIGIN, unknown"));
758
+
759
+ assert_eq!(ReferrerPolicy::from(&headers), ReferrerPolicy::Origin);
760
+ assert_eq!(ReferrerPolicy::from("future-policy"), ReferrerPolicy::None);
761
+
762
+ let cases = [
763
+ (
764
+ "default same-origin",
765
+ "https://user:pass@example.com/source?q=1#fragment",
766
+ "https://example.com/target",
767
+ None,
768
+ Some("https://example.com/source?q=1"),
769
+ ),
770
+ (
771
+ "default cross-origin",
772
+ "https://example.com/source?q=1",
773
+ "https://other.example/target",
774
+ None,
775
+ Some("https://example.com/"),
776
+ ),
777
+ (
778
+ "default downgrade",
779
+ "https://example.com/source",
780
+ "http://example.com/target",
781
+ None,
782
+ None,
783
+ ),
784
+ (
785
+ "no-referrer",
786
+ "https://example.com/source",
787
+ "https://other.example/target",
788
+ Some("no-referrer"),
789
+ None,
790
+ ),
791
+ (
792
+ "no-referrer-when-downgrade",
793
+ "https://example.com/source",
794
+ "https://other.example/target",
795
+ Some("no-referrer-when-downgrade"),
796
+ Some("https://example.com/source"),
797
+ ),
798
+ (
799
+ "same-origin",
800
+ "https://example.com/source",
801
+ "https://other.example/target",
802
+ Some("same-origin"),
803
+ None,
804
+ ),
805
+ (
806
+ "origin",
807
+ "https://example.com/source",
808
+ "https://other.example/target",
809
+ Some("origin"),
810
+ Some("https://example.com/"),
811
+ ),
812
+ (
813
+ "strict-origin",
814
+ "https://example.com/source",
815
+ "https://other.example/target",
816
+ Some("strict-origin"),
817
+ Some("https://example.com/"),
818
+ ),
819
+ (
820
+ "origin-when-cross-origin",
821
+ "https://example.com/source",
822
+ "https://other.example/target",
823
+ Some("origin-when-cross-origin"),
824
+ Some("https://example.com/"),
825
+ ),
826
+ (
827
+ "strict-origin-when-cross-origin",
828
+ "https://example.com/source",
829
+ "https://other.example/target",
830
+ Some("strict-origin-when-cross-origin"),
831
+ Some("https://example.com/"),
832
+ ),
833
+ (
834
+ "unsafe-url",
835
+ "https://example.com/source",
836
+ "https://other.example/target",
837
+ Some("unsafe-url"),
838
+ Some("https://example.com/source"),
839
+ ),
840
+ ];
841
+
842
+ for (name, source, destination, policy, expected) in cases {
843
+ let actual = apply(source, destination, policy);
844
+ assert_eq!(header_str(&actual), expected, "case: {name}");
845
+ }
846
+
847
+ let mut value = HeaderValue::from_static("https://example.com/private");
848
+ value.set_sensitive(true);
849
+ let mut headers = HeaderMap::new();
850
+ headers.insert(REFERER, value);
851
+ let mut referrer = Referrer::new(&headers);
852
+
853
+ let mut req = request(headers, "https://other.example/");
854
+ referrer.apply(&mut req);
855
+ assert!(req.headers()[REFERER].is_sensitive());
856
+
857
+ let headers = headers_with_referrer("https://example.com/source");
858
+ let mut referrer = Referrer::new(&headers);
859
+ let mut req = request(headers, "https://example.com/first");
860
+
861
+ let mut response_headers = HeaderMap::new();
862
+ response_headers.insert(REFERRER_POLICY, HeaderValue::from_static("no-referrer"));
863
+ referrer.on_redirect(&response_headers);
864
+
865
+ referrer.apply(&mut req);
866
+
867
+ response_headers.insert(REFERRER_POLICY, HeaderValue::from_static("unsafe-url"));
868
+ referrer.on_redirect(&response_headers);
869
+
870
+ *req.uri_mut() = Uri::from_static("https://example.com/second");
871
+ referrer.apply(&mut req);
872
+
873
+ assert_eq!(req.headers().get(REFERER), None);
488
874
  }
489
875
  }
490
876
  }
@@ -562,14 +948,43 @@ mod tests {
562
948
  let mut prev = vec![Uri::try_from("http://initial-domain.com/new_path").unwrap()];
563
949
  let mut filtered_headers = headers.clone();
564
950
 
565
- remove_sensitive_headers(&mut headers, &next, &prev);
566
- assert_eq!(headers, filtered_headers);
951
+ let mut req = http::Request::new(());
952
+ *req.headers_mut() = headers;
953
+ *req.uri_mut() = next;
954
+
955
+ remove_sensitive_headers(&mut req, &prev);
956
+ assert_eq!(req.headers(), &filtered_headers);
567
957
 
568
958
  prev.push(Uri::try_from("http://new-domain.com/path").unwrap());
569
959
  filtered_headers.remove(AUTHORIZATION);
570
960
  filtered_headers.remove(COOKIE);
571
961
 
572
- remove_sensitive_headers(&mut headers, &next, &prev);
573
- assert_eq!(headers, filtered_headers);
962
+ remove_sensitive_headers(&mut req, &prev);
963
+ assert_eq!(req.headers(), &filtered_headers);
964
+
965
+ let mut default_port_headers = HeaderMap::new();
966
+ default_port_headers.insert(AUTHORIZATION, HeaderValue::from_static("let me in"));
967
+
968
+ let next = Uri::from_static("http://EXAMPLE.com:80/next");
969
+ let previous = vec![Uri::from_static("http://example.com/previous")];
970
+
971
+ let mut req = http::Request::new(());
972
+ *req.headers_mut() = default_port_headers;
973
+ *req.uri_mut() = next;
974
+
975
+ remove_sensitive_headers(&mut req, &previous);
976
+ assert_eq!(
977
+ req.headers().get(AUTHORIZATION),
978
+ Some(&HeaderValue::from_static("let me in"))
979
+ );
980
+
981
+ req.headers_mut()
982
+ .insert(COOKIE, HeaderValue::from_static("foo=bar"));
983
+ *req.uri_mut() = Uri::from_static("http://example.com:8443/next");
984
+ let previous = [Uri::from_static("https://example.com:8443/previous")];
985
+
986
+ remove_sensitive_headers(&mut req, &previous);
987
+ assert_eq!(req.headers().get(AUTHORIZATION), None);
988
+ assert_eq!(req.headers().get(COOKIE), None);
574
989
  }
575
990
  }