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.
- checksums.yaml +4 -4
- data/Cargo.lock +257 -102
- data/ext/wreq_rb/Cargo.toml +7 -4
- data/ext/wreq_rb/src/client.rs +105 -15
- data/ext/wreq_rb/src/response.rs +28 -3
- data/lib/wreq-rb/version.rb +1 -1
- data/patches/0001-add-transfer-size-tracking.patch +11 -15
- data/vendor/wreq/Cargo.toml +9 -8
- data/vendor/wreq/README.md +5 -5
- data/vendor/wreq/bench/support/bench.rs +6 -2
- data/vendor/wreq/bench/support/client.rs +88 -1
- data/vendor/wreq/bench/support/exec.rs +0 -0
- data/vendor/wreq/bench/support/rt.rs +34 -0
- data/vendor/wreq/bench/support/server.rs +1 -1
- data/vendor/wreq/bench/support.rs +1 -15
- data/vendor/wreq/examples/cert_store.rs +13 -13
- data/vendor/wreq/examples/request_with_emulate.rs +1 -1
- data/vendor/wreq/examples/tcp_linger.rs +22 -0
- data/vendor/wreq/src/client/layer/client/pool.rs +17 -17
- data/vendor/wreq/src/client/layer/client.rs +2 -0
- data/vendor/wreq/src/client/layer/decoder.rs +71 -17
- data/vendor/wreq/src/client/layer/redirect/future.rs +49 -63
- data/vendor/wreq/src/client/layer/redirect/policy.rs +2 -26
- data/vendor/wreq/src/client/layer/redirect.rs +48 -60
- data/vendor/wreq/src/client/layer/retry.rs +12 -15
- data/vendor/wreq/src/client/layer/timeout/body.rs +27 -21
- data/vendor/wreq/src/client/layer/timeout/future.rs +33 -58
- data/vendor/wreq/src/client/layer/timeout.rs +8 -14
- data/vendor/wreq/src/client/request.rs +4 -0
- data/vendor/wreq/src/client.rs +53 -31
- data/vendor/wreq/src/conn/connector.rs +99 -129
- data/vendor/wreq/src/conn/http.rs +25 -18
- data/vendor/wreq/src/conn/net/tcp.rs +601 -107
- data/vendor/wreq/src/conn/proxy/socks.rs +6 -6
- data/vendor/wreq/src/conn/timeout.rs +166 -0
- data/vendor/wreq/src/conn.rs +5 -4
- data/vendor/wreq/src/cookie/jar.rs +1225 -0
- data/vendor/wreq/src/cookie/store.rs +321 -0
- data/vendor/wreq/src/cookie.rs +108 -612
- data/vendor/wreq/src/dns/resolve.rs +8 -2
- data/vendor/wreq/src/dns.rs +4 -4
- data/vendor/wreq/src/error.rs +53 -20
- data/vendor/wreq/src/lib.rs +1 -0
- data/vendor/wreq/src/proxy/matcher.rs +26 -12
- data/vendor/wreq/src/proxy/win.rs +39 -9
- data/vendor/wreq/src/redirect.rs +515 -100
- data/vendor/wreq/src/tls/conn.rs +3 -11
- data/vendor/wreq/src/tls/session.rs +7 -8
- data/vendor/wreq/src/tls/trust/store.rs +4 -4
- data/vendor/wreq/src/util.rs +23 -0
- data/vendor/wreq/tests/badssl.rs +72 -7
- data/vendor/wreq/tests/brotli.rs +1 -1
- data/vendor/wreq/tests/client.rs +24 -0
- data/vendor/wreq/tests/connector_layers.rs +8 -4
- data/vendor/wreq/tests/cookie.rs +59 -0
- data/vendor/wreq/tests/deflate.rs +1 -1
- data/vendor/wreq/tests/gzip.rs +53 -1
- data/vendor/wreq/tests/layers.rs +8 -4
- data/vendor/wreq/tests/redirect.rs +180 -97
- data/vendor/wreq/tests/timeouts.rs +47 -12
- data/vendor/wreq/tests/zstd.rs +1 -1
- metadata +8 -2
data/vendor/wreq/src/redirect.rs
CHANGED
|
@@ -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,
|
|
10
|
+
use http::{HeaderMap, HeaderName, StatusCode, Uri};
|
|
12
11
|
|
|
12
|
+
use self::referrer::Referrer;
|
|
13
13
|
use crate::{
|
|
14
|
-
client::
|
|
14
|
+
client::layer::redirect,
|
|
15
15
|
config::RequestConfig,
|
|
16
16
|
error::{BoxError, Error},
|
|
17
17
|
ext::UriExt,
|
|
18
|
-
header::{AUTHORIZATION, COOKIE, PROXY_AUTHORIZATION,
|
|
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,
|
|
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
|
|
99
|
-
///
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
304
|
-
Action
|
|
305
|
-
|
|
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
|
-
|
|
351
|
+
FollowRedirectPolicy {
|
|
360
352
|
policy: RequestConfig::new(Some(policy)),
|
|
361
|
-
|
|
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.
|
|
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
|
|
384
|
-
|
|
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
|
-
//
|
|
399
|
-
|
|
400
|
-
redirect
|
|
401
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
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
|
|
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<
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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<
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
480
|
+
same_host
|
|
481
|
+
&& left.scheme() == right.scheme()
|
|
482
|
+
&& left.port_or_default() == right.port_or_default()
|
|
471
483
|
}
|
|
472
484
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
566
|
-
|
|
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
|
|
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
|
}
|