wreq-rb 0.3.0
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 +7 -0
- data/Cargo.lock +2688 -0
- data/Cargo.toml +6 -0
- data/README.md +179 -0
- data/ext/wreq_rb/Cargo.toml +39 -0
- data/ext/wreq_rb/extconf.rb +22 -0
- data/ext/wreq_rb/src/client.rs +565 -0
- data/ext/wreq_rb/src/error.rs +25 -0
- data/ext/wreq_rb/src/lib.rs +20 -0
- data/ext/wreq_rb/src/response.rs +132 -0
- data/lib/wreq-rb/version.rb +5 -0
- data/lib/wreq-rb.rb +17 -0
- data/patches/0001-add-transfer-size-tracking.patch +292 -0
- data/vendor/wreq/Cargo.toml +306 -0
- data/vendor/wreq/LICENSE +202 -0
- data/vendor/wreq/README.md +122 -0
- data/vendor/wreq/examples/cert_store.rs +77 -0
- data/vendor/wreq/examples/connect_via_lower_priority_tokio_runtime.rs +258 -0
- data/vendor/wreq/examples/emulation.rs +118 -0
- data/vendor/wreq/examples/form.rs +14 -0
- data/vendor/wreq/examples/http1_websocket.rs +37 -0
- data/vendor/wreq/examples/http2_websocket.rs +45 -0
- data/vendor/wreq/examples/json_dynamic.rs +41 -0
- data/vendor/wreq/examples/json_typed.rs +47 -0
- data/vendor/wreq/examples/keylog.rs +16 -0
- data/vendor/wreq/examples/request_with_emulation.rs +115 -0
- data/vendor/wreq/examples/request_with_interface.rs +37 -0
- data/vendor/wreq/examples/request_with_local_address.rs +16 -0
- data/vendor/wreq/examples/request_with_proxy.rs +13 -0
- data/vendor/wreq/examples/request_with_redirect.rs +22 -0
- data/vendor/wreq/examples/request_with_version.rs +15 -0
- data/vendor/wreq/examples/tor_socks.rs +24 -0
- data/vendor/wreq/examples/unix_socket.rs +33 -0
- data/vendor/wreq/src/client/body.rs +304 -0
- data/vendor/wreq/src/client/conn/conn.rs +231 -0
- data/vendor/wreq/src/client/conn/connector.rs +549 -0
- data/vendor/wreq/src/client/conn/http.rs +1023 -0
- data/vendor/wreq/src/client/conn/proxy/socks.rs +233 -0
- data/vendor/wreq/src/client/conn/proxy/tunnel.rs +260 -0
- data/vendor/wreq/src/client/conn/proxy.rs +39 -0
- data/vendor/wreq/src/client/conn/tls_info.rs +98 -0
- data/vendor/wreq/src/client/conn/uds.rs +44 -0
- data/vendor/wreq/src/client/conn/verbose.rs +149 -0
- data/vendor/wreq/src/client/conn.rs +323 -0
- data/vendor/wreq/src/client/core/body/incoming.rs +485 -0
- data/vendor/wreq/src/client/core/body/length.rs +118 -0
- data/vendor/wreq/src/client/core/body.rs +34 -0
- data/vendor/wreq/src/client/core/common/buf.rs +149 -0
- data/vendor/wreq/src/client/core/common/rewind.rs +141 -0
- data/vendor/wreq/src/client/core/common/watch.rs +76 -0
- data/vendor/wreq/src/client/core/common.rs +3 -0
- data/vendor/wreq/src/client/core/conn/http1.rs +342 -0
- data/vendor/wreq/src/client/core/conn/http2.rs +307 -0
- data/vendor/wreq/src/client/core/conn.rs +11 -0
- data/vendor/wreq/src/client/core/dispatch.rs +299 -0
- data/vendor/wreq/src/client/core/error.rs +435 -0
- data/vendor/wreq/src/client/core/ext.rs +201 -0
- data/vendor/wreq/src/client/core/http1.rs +178 -0
- data/vendor/wreq/src/client/core/http2.rs +483 -0
- data/vendor/wreq/src/client/core/proto/h1/conn.rs +988 -0
- data/vendor/wreq/src/client/core/proto/h1/decode.rs +1170 -0
- data/vendor/wreq/src/client/core/proto/h1/dispatch.rs +684 -0
- data/vendor/wreq/src/client/core/proto/h1/encode.rs +580 -0
- data/vendor/wreq/src/client/core/proto/h1/io.rs +879 -0
- data/vendor/wreq/src/client/core/proto/h1/role.rs +694 -0
- data/vendor/wreq/src/client/core/proto/h1.rs +104 -0
- data/vendor/wreq/src/client/core/proto/h2/client.rs +650 -0
- data/vendor/wreq/src/client/core/proto/h2/ping.rs +539 -0
- data/vendor/wreq/src/client/core/proto/h2.rs +379 -0
- data/vendor/wreq/src/client/core/proto/headers.rs +138 -0
- data/vendor/wreq/src/client/core/proto.rs +58 -0
- data/vendor/wreq/src/client/core/rt/bounds.rs +57 -0
- data/vendor/wreq/src/client/core/rt/timer.rs +150 -0
- data/vendor/wreq/src/client/core/rt/tokio.rs +99 -0
- data/vendor/wreq/src/client/core/rt.rs +25 -0
- data/vendor/wreq/src/client/core/upgrade.rs +267 -0
- data/vendor/wreq/src/client/core.rs +16 -0
- data/vendor/wreq/src/client/emulation.rs +161 -0
- data/vendor/wreq/src/client/http/client/error.rs +142 -0
- data/vendor/wreq/src/client/http/client/exec.rs +29 -0
- data/vendor/wreq/src/client/http/client/extra.rs +77 -0
- data/vendor/wreq/src/client/http/client/lazy.rs +79 -0
- data/vendor/wreq/src/client/http/client/pool.rs +1105 -0
- data/vendor/wreq/src/client/http/client/util.rs +104 -0
- data/vendor/wreq/src/client/http/client.rs +1003 -0
- data/vendor/wreq/src/client/http/future.rs +99 -0
- data/vendor/wreq/src/client/http.rs +1629 -0
- data/vendor/wreq/src/client/layer/config/options.rs +156 -0
- data/vendor/wreq/src/client/layer/config.rs +116 -0
- data/vendor/wreq/src/client/layer/cookie.rs +161 -0
- data/vendor/wreq/src/client/layer/decoder.rs +139 -0
- data/vendor/wreq/src/client/layer/redirect/future.rs +270 -0
- data/vendor/wreq/src/client/layer/redirect/policy.rs +63 -0
- data/vendor/wreq/src/client/layer/redirect.rs +145 -0
- data/vendor/wreq/src/client/layer/retry/classify.rs +105 -0
- data/vendor/wreq/src/client/layer/retry/scope.rs +51 -0
- data/vendor/wreq/src/client/layer/retry.rs +151 -0
- data/vendor/wreq/src/client/layer/timeout/body.rs +233 -0
- data/vendor/wreq/src/client/layer/timeout/future.rs +90 -0
- data/vendor/wreq/src/client/layer/timeout.rs +177 -0
- data/vendor/wreq/src/client/layer.rs +15 -0
- data/vendor/wreq/src/client/multipart.rs +717 -0
- data/vendor/wreq/src/client/request.rs +818 -0
- data/vendor/wreq/src/client/response.rs +534 -0
- data/vendor/wreq/src/client/ws/json.rs +99 -0
- data/vendor/wreq/src/client/ws/message.rs +453 -0
- data/vendor/wreq/src/client/ws.rs +714 -0
- data/vendor/wreq/src/client.rs +27 -0
- data/vendor/wreq/src/config.rs +140 -0
- data/vendor/wreq/src/cookie.rs +579 -0
- data/vendor/wreq/src/dns/gai.rs +249 -0
- data/vendor/wreq/src/dns/hickory.rs +78 -0
- data/vendor/wreq/src/dns/resolve.rs +180 -0
- data/vendor/wreq/src/dns.rs +69 -0
- data/vendor/wreq/src/error.rs +502 -0
- data/vendor/wreq/src/ext.rs +398 -0
- data/vendor/wreq/src/hash.rs +143 -0
- data/vendor/wreq/src/header.rs +506 -0
- data/vendor/wreq/src/into_uri.rs +187 -0
- data/vendor/wreq/src/lib.rs +586 -0
- data/vendor/wreq/src/proxy/mac.rs +82 -0
- data/vendor/wreq/src/proxy/matcher.rs +806 -0
- data/vendor/wreq/src/proxy/uds.rs +66 -0
- data/vendor/wreq/src/proxy/win.rs +31 -0
- data/vendor/wreq/src/proxy.rs +569 -0
- data/vendor/wreq/src/redirect.rs +575 -0
- data/vendor/wreq/src/retry.rs +198 -0
- data/vendor/wreq/src/sync.rs +129 -0
- data/vendor/wreq/src/tls/conn/cache.rs +123 -0
- data/vendor/wreq/src/tls/conn/cert_compression.rs +125 -0
- data/vendor/wreq/src/tls/conn/ext.rs +82 -0
- data/vendor/wreq/src/tls/conn/macros.rs +34 -0
- data/vendor/wreq/src/tls/conn/service.rs +138 -0
- data/vendor/wreq/src/tls/conn.rs +681 -0
- data/vendor/wreq/src/tls/keylog/handle.rs +64 -0
- data/vendor/wreq/src/tls/keylog.rs +99 -0
- data/vendor/wreq/src/tls/options.rs +464 -0
- data/vendor/wreq/src/tls/x509/identity.rs +122 -0
- data/vendor/wreq/src/tls/x509/parser.rs +71 -0
- data/vendor/wreq/src/tls/x509/store.rs +228 -0
- data/vendor/wreq/src/tls/x509.rs +68 -0
- data/vendor/wreq/src/tls.rs +154 -0
- data/vendor/wreq/src/trace.rs +55 -0
- data/vendor/wreq/src/util.rs +122 -0
- data/vendor/wreq/tests/badssl.rs +228 -0
- data/vendor/wreq/tests/brotli.rs +350 -0
- data/vendor/wreq/tests/client.rs +1098 -0
- data/vendor/wreq/tests/connector_layers.rs +227 -0
- data/vendor/wreq/tests/cookie.rs +306 -0
- data/vendor/wreq/tests/deflate.rs +347 -0
- data/vendor/wreq/tests/emulation.rs +260 -0
- data/vendor/wreq/tests/gzip.rs +347 -0
- data/vendor/wreq/tests/layers.rs +261 -0
- data/vendor/wreq/tests/multipart.rs +165 -0
- data/vendor/wreq/tests/proxy.rs +438 -0
- data/vendor/wreq/tests/redirect.rs +629 -0
- data/vendor/wreq/tests/retry.rs +135 -0
- data/vendor/wreq/tests/support/delay_server.rs +117 -0
- data/vendor/wreq/tests/support/error.rs +16 -0
- data/vendor/wreq/tests/support/layer.rs +183 -0
- data/vendor/wreq/tests/support/mod.rs +9 -0
- data/vendor/wreq/tests/support/server.rs +232 -0
- data/vendor/wreq/tests/timeouts.rs +281 -0
- data/vendor/wreq/tests/unix_socket.rs +135 -0
- data/vendor/wreq/tests/upgrade.rs +98 -0
- data/vendor/wreq/tests/zstd.rs +559 -0
- metadata +225 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
//! Redirect Handling
|
|
2
|
+
//!
|
|
3
|
+
//! By default, a `Client` does not follow HTTP redirects. To enable automatic
|
|
4
|
+
//! redirect handling with a maximum redirect chain of 10 hops, use a [`Policy`]
|
|
5
|
+
//! with [`ClientBuilder::redirect()`](crate::ClientBuilder::redirect).
|
|
6
|
+
|
|
7
|
+
use std::{borrow::Cow, error::Error as StdError, fmt, sync::Arc};
|
|
8
|
+
|
|
9
|
+
use bytes::Bytes;
|
|
10
|
+
use futures_util::FutureExt;
|
|
11
|
+
use http::{HeaderMap, HeaderName, HeaderValue, StatusCode, Uri};
|
|
12
|
+
|
|
13
|
+
use crate::{
|
|
14
|
+
client::{Body, layer::redirect},
|
|
15
|
+
config::RequestConfig,
|
|
16
|
+
error::{BoxError, Error},
|
|
17
|
+
ext::UriExt,
|
|
18
|
+
header::{AUTHORIZATION, COOKIE, PROXY_AUTHORIZATION, REFERER, WWW_AUTHENTICATE},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/// A type that controls the policy on how to handle the following of redirects.
|
|
22
|
+
///
|
|
23
|
+
/// The default value will catch redirect loops, and has a maximum of 10
|
|
24
|
+
/// redirects it will follow in a chain before returning an error.
|
|
25
|
+
///
|
|
26
|
+
/// - `limited` can be used have the same as the default behavior, but adjust the allowed maximum
|
|
27
|
+
/// redirect hops in a chain.
|
|
28
|
+
/// - `none` can be used to disable all redirect behavior.
|
|
29
|
+
/// - `custom` can be used to create a customized policy.
|
|
30
|
+
#[derive(Debug, Clone)]
|
|
31
|
+
pub struct Policy {
|
|
32
|
+
inner: PolicyKind,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// A type that holds information on the next request and previous requests
|
|
36
|
+
/// in redirect chain.
|
|
37
|
+
#[derive(Debug)]
|
|
38
|
+
#[non_exhaustive]
|
|
39
|
+
pub struct Attempt<'a, const PENDING: bool = true> {
|
|
40
|
+
/// The status code of the redirect response.
|
|
41
|
+
pub status: StatusCode,
|
|
42
|
+
|
|
43
|
+
/// The headers of the redirect response.
|
|
44
|
+
pub headers: Cow<'a, HeaderMap>,
|
|
45
|
+
|
|
46
|
+
/// The URI to redirect to.
|
|
47
|
+
pub uri: Cow<'a, Uri>,
|
|
48
|
+
|
|
49
|
+
/// The list of previous URIs that have already been requested in this chain.
|
|
50
|
+
pub previous: Cow<'a, [Uri]>,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// An action to perform when a redirect status code is found.
|
|
54
|
+
#[derive(Debug)]
|
|
55
|
+
pub struct Action {
|
|
56
|
+
inner: redirect::Action,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Redirect history information for a response.
|
|
60
|
+
#[derive(Debug, Clone)]
|
|
61
|
+
pub struct History(Vec<HistoryEntry>);
|
|
62
|
+
|
|
63
|
+
/// An entry in the redirect history.
|
|
64
|
+
#[derive(Debug, Clone)]
|
|
65
|
+
#[non_exhaustive]
|
|
66
|
+
pub struct HistoryEntry {
|
|
67
|
+
/// The status code of the redirect response.
|
|
68
|
+
pub status: StatusCode,
|
|
69
|
+
|
|
70
|
+
/// The URI of the redirect response.
|
|
71
|
+
pub uri: Uri,
|
|
72
|
+
|
|
73
|
+
/// The previous URI before the redirect response.
|
|
74
|
+
pub previous: Uri,
|
|
75
|
+
|
|
76
|
+
/// The headers of the redirect response.
|
|
77
|
+
pub headers: HeaderMap,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#[derive(Clone)]
|
|
81
|
+
enum PolicyKind {
|
|
82
|
+
Custom(Arc<dyn Fn(Attempt) -> Action + Send + Sync + 'static>),
|
|
83
|
+
Limit(usize),
|
|
84
|
+
None,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[derive(Debug)]
|
|
88
|
+
struct TooManyRedirects;
|
|
89
|
+
|
|
90
|
+
/// A redirect policy handler for HTTP clients.
|
|
91
|
+
///
|
|
92
|
+
/// [`FollowRedirectPolicy`] manages how HTTP redirects are handled by the client,
|
|
93
|
+
/// including the maximum number of redirects, whether to set the `Referer` header,
|
|
94
|
+
/// HTTPS-only enforcement, and redirect history tracking.
|
|
95
|
+
///
|
|
96
|
+
/// This type is used internally by the client to implement redirect logic according to
|
|
97
|
+
/// 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.
|
|
100
|
+
#[derive(Clone)]
|
|
101
|
+
pub(crate) struct FollowRedirectPolicy {
|
|
102
|
+
policy: RequestConfig<Policy>,
|
|
103
|
+
referer: bool,
|
|
104
|
+
uris: Vec<Uri>,
|
|
105
|
+
https_only: bool,
|
|
106
|
+
history: Option<Vec<HistoryEntry>>,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ===== impl Policy =====
|
|
110
|
+
|
|
111
|
+
impl Policy {
|
|
112
|
+
/// Create a [`Policy`] with a maximum number of redirects.
|
|
113
|
+
///
|
|
114
|
+
/// An [`Error`] will be returned if the max is reached.
|
|
115
|
+
#[inline]
|
|
116
|
+
pub fn limited(max: usize) -> Self {
|
|
117
|
+
Self {
|
|
118
|
+
inner: PolicyKind::Limit(max),
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// Create a [`Policy`] that does not follow any redirect.
|
|
123
|
+
#[inline]
|
|
124
|
+
pub fn none() -> Self {
|
|
125
|
+
Self {
|
|
126
|
+
inner: PolicyKind::None,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Create a custom [`Policy`] using the passed function.
|
|
131
|
+
///
|
|
132
|
+
/// # Note
|
|
133
|
+
///
|
|
134
|
+
/// The default [`Policy`] handles a maximum loop
|
|
135
|
+
/// chain, but the custom variant does not do that for you automatically.
|
|
136
|
+
/// The custom policy should have some way of handling those.
|
|
137
|
+
///
|
|
138
|
+
/// Information on the next request and previous requests can be found
|
|
139
|
+
/// on the [`Attempt`] argument passed to the closure.
|
|
140
|
+
///
|
|
141
|
+
/// Actions can be conveniently created from methods on the
|
|
142
|
+
/// [`Attempt`].
|
|
143
|
+
///
|
|
144
|
+
/// # Example
|
|
145
|
+
///
|
|
146
|
+
/// ```rust
|
|
147
|
+
/// # use wreq::{Error, redirect};
|
|
148
|
+
/// #
|
|
149
|
+
/// # fn run() -> Result<(), Error> {
|
|
150
|
+
/// let custom = redirect::Policy::custom(|attempt| {
|
|
151
|
+
/// if attempt.previous.len() > 5 {
|
|
152
|
+
/// attempt.error("too many redirects")
|
|
153
|
+
/// } else if attempt.uri() == "example.domain" {
|
|
154
|
+
/// // prevent redirects to 'example.domain'
|
|
155
|
+
/// attempt.stop()
|
|
156
|
+
/// } else {
|
|
157
|
+
/// attempt.follow()
|
|
158
|
+
/// }
|
|
159
|
+
/// });
|
|
160
|
+
/// let client = wreq::Client::builder().redirect(custom).build()?;
|
|
161
|
+
/// # Ok(())
|
|
162
|
+
/// # }
|
|
163
|
+
/// ```
|
|
164
|
+
#[inline]
|
|
165
|
+
pub fn custom<T>(policy: T) -> Self
|
|
166
|
+
where
|
|
167
|
+
T: Fn(Attempt) -> Action + Send + Sync + 'static,
|
|
168
|
+
{
|
|
169
|
+
Self {
|
|
170
|
+
inner: PolicyKind::Custom(Arc::new(policy)),
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// Apply this policy to a given [`Attempt`] to produce a [`Action`].
|
|
175
|
+
///
|
|
176
|
+
/// # Note
|
|
177
|
+
///
|
|
178
|
+
/// This method can be used together with [`Policy::custom()`]
|
|
179
|
+
/// to construct one [`Policy`] that wraps another.
|
|
180
|
+
///
|
|
181
|
+
/// # Example
|
|
182
|
+
///
|
|
183
|
+
/// ```rust
|
|
184
|
+
/// # use wreq::{Error, redirect};
|
|
185
|
+
/// #
|
|
186
|
+
/// # fn run() -> Result<(), Error> {
|
|
187
|
+
/// let custom = redirect::Policy::custom(|attempt| {
|
|
188
|
+
/// eprintln!("{}, Location: {:?}", attempt.status(), attempt.uri());
|
|
189
|
+
/// redirect::Policy::default().redirect(attempt)
|
|
190
|
+
/// });
|
|
191
|
+
/// # Ok(())
|
|
192
|
+
/// # }
|
|
193
|
+
/// ```
|
|
194
|
+
pub fn redirect(&self, attempt: Attempt) -> Action {
|
|
195
|
+
match self.inner {
|
|
196
|
+
PolicyKind::Custom(ref custom) => custom(attempt),
|
|
197
|
+
PolicyKind::Limit(max) => {
|
|
198
|
+
// The first URI in the previous is the initial URI and not a redirection. It needs
|
|
199
|
+
// to be excluded.
|
|
200
|
+
if attempt.previous.len() > max {
|
|
201
|
+
attempt.error(TooManyRedirects)
|
|
202
|
+
} else {
|
|
203
|
+
attempt.follow()
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
PolicyKind::None => attempt.stop(),
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#[inline]
|
|
211
|
+
fn check(
|
|
212
|
+
&self,
|
|
213
|
+
status: StatusCode,
|
|
214
|
+
headers: &HeaderMap,
|
|
215
|
+
next: &Uri,
|
|
216
|
+
previous: &[Uri],
|
|
217
|
+
) -> redirect::Action {
|
|
218
|
+
self.redirect(Attempt {
|
|
219
|
+
status,
|
|
220
|
+
headers: Cow::Borrowed(headers),
|
|
221
|
+
uri: Cow::Borrowed(next),
|
|
222
|
+
previous: Cow::Borrowed(previous),
|
|
223
|
+
})
|
|
224
|
+
.inner
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
impl Default for Policy {
|
|
229
|
+
#[inline]
|
|
230
|
+
fn default() -> Policy {
|
|
231
|
+
// Keep `is_default` in sync
|
|
232
|
+
Policy::limited(10)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
impl_request_config_value!(Policy);
|
|
237
|
+
|
|
238
|
+
// ===== impl Attempt =====
|
|
239
|
+
|
|
240
|
+
impl<const PENDING: bool> Attempt<'_, PENDING> {
|
|
241
|
+
/// Returns an action meaning wreq should follow the next URI.
|
|
242
|
+
#[inline]
|
|
243
|
+
pub fn follow(self) -> Action {
|
|
244
|
+
Action {
|
|
245
|
+
inner: redirect::Action::Follow,
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/// Returns an action meaning wreq should not follow the next URI.
|
|
250
|
+
///
|
|
251
|
+
/// The 30x response will be returned as the `Ok` result.
|
|
252
|
+
#[inline]
|
|
253
|
+
pub fn stop(self) -> Action {
|
|
254
|
+
Action {
|
|
255
|
+
inner: redirect::Action::Stop,
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/// Returns an [`Action`] failing the redirect with an error.
|
|
260
|
+
///
|
|
261
|
+
/// The [`Error`] will be returned for the result of the sent request.
|
|
262
|
+
#[inline]
|
|
263
|
+
pub fn error<E: Into<BoxError>>(self, error: E) -> Action {
|
|
264
|
+
Action {
|
|
265
|
+
inner: redirect::Action::Error(error.into()),
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
impl Attempt<'_, true> {
|
|
271
|
+
/// Returns an action meaning wreq should perform the redirect asynchronously.
|
|
272
|
+
///
|
|
273
|
+
/// The provided async closure receives an owned [`Attempt<'static>`] and should
|
|
274
|
+
/// return an [`Action`] to determine the final redirect behavior.
|
|
275
|
+
///
|
|
276
|
+
/// # Example
|
|
277
|
+
///
|
|
278
|
+
/// ```rust
|
|
279
|
+
/// # use wreq::redirect;
|
|
280
|
+
/// #
|
|
281
|
+
/// let policy = redirect::Policy::custom(|attempt| {
|
|
282
|
+
/// attempt.pending(|attempt| async move {
|
|
283
|
+
/// // Perform some async operation
|
|
284
|
+
/// if attempt.uri().host() == Some("trusted.domain") {
|
|
285
|
+
/// attempt.follow()
|
|
286
|
+
/// } else {
|
|
287
|
+
/// attempt.stop()
|
|
288
|
+
/// }
|
|
289
|
+
/// })
|
|
290
|
+
/// });
|
|
291
|
+
/// ```
|
|
292
|
+
pub fn pending<F, Fut>(self, func: F) -> Action
|
|
293
|
+
where
|
|
294
|
+
F: FnOnce(Attempt<'static, false>) -> Fut + Send + 'static,
|
|
295
|
+
Fut: Future<Output = Action> + Send + 'static,
|
|
296
|
+
{
|
|
297
|
+
let attempt = Attempt {
|
|
298
|
+
status: self.status,
|
|
299
|
+
headers: Cow::Owned(self.headers.into_owned()),
|
|
300
|
+
uri: Cow::Owned(self.uri.into_owned()),
|
|
301
|
+
previous: Cow::Owned(self.previous.into_owned()),
|
|
302
|
+
};
|
|
303
|
+
let pending = Box::pin(func(attempt).map(|action| action.inner));
|
|
304
|
+
Action {
|
|
305
|
+
inner: redirect::Action::Pending(pending),
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ===== impl History =====
|
|
311
|
+
|
|
312
|
+
impl IntoIterator for History {
|
|
313
|
+
type Item = HistoryEntry;
|
|
314
|
+
type IntoIter = std::vec::IntoIter<HistoryEntry>;
|
|
315
|
+
|
|
316
|
+
#[inline]
|
|
317
|
+
fn into_iter(self) -> Self::IntoIter {
|
|
318
|
+
self.0.into_iter()
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
impl<'a> IntoIterator for &'a History {
|
|
323
|
+
type Item = &'a HistoryEntry;
|
|
324
|
+
type IntoIter = std::slice::Iter<'a, HistoryEntry>;
|
|
325
|
+
|
|
326
|
+
#[inline]
|
|
327
|
+
fn into_iter(self) -> Self::IntoIter {
|
|
328
|
+
self.0.iter()
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ===== impl PolicyKind =====
|
|
333
|
+
|
|
334
|
+
impl fmt::Debug for PolicyKind {
|
|
335
|
+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
336
|
+
match *self {
|
|
337
|
+
PolicyKind::Custom(..) => f.pad("Custom"),
|
|
338
|
+
PolicyKind::Limit(max) => f.debug_tuple("Limit").field(&max).finish(),
|
|
339
|
+
PolicyKind::None => f.pad("None"),
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ===== impl TooManyRedirects =====
|
|
345
|
+
|
|
346
|
+
impl fmt::Display for TooManyRedirects {
|
|
347
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
348
|
+
f.write_str("too many redirects")
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
impl StdError for TooManyRedirects {}
|
|
353
|
+
|
|
354
|
+
// ===== impl FollowRedirectPolicy =====
|
|
355
|
+
|
|
356
|
+
impl FollowRedirectPolicy {
|
|
357
|
+
/// Creates a new redirect policy handler with the given [`Policy`].
|
|
358
|
+
pub fn new(policy: Policy) -> Self {
|
|
359
|
+
Self {
|
|
360
|
+
policy: RequestConfig::new(Some(policy)),
|
|
361
|
+
referer: false,
|
|
362
|
+
uris: Vec::new(),
|
|
363
|
+
https_only: false,
|
|
364
|
+
history: None,
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/// Enables or disables automatic Referer header management.
|
|
369
|
+
#[inline]
|
|
370
|
+
pub fn with_referer(mut self, referer: bool) -> Self {
|
|
371
|
+
self.referer = referer;
|
|
372
|
+
self
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/// Enables or disables HTTPS-only redirect enforcement.
|
|
376
|
+
#[inline]
|
|
377
|
+
pub fn with_https_only(mut self, https_only: bool) -> Self {
|
|
378
|
+
self.https_only = https_only;
|
|
379
|
+
self
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
impl redirect::Policy<Body, BoxError> for FollowRedirectPolicy {
|
|
384
|
+
fn redirect(&mut self, attempt: redirect::Attempt<'_>) -> Result<redirect::Action, BoxError> {
|
|
385
|
+
// Parse the next URI from the attempt.
|
|
386
|
+
let previous_uri = attempt.previous;
|
|
387
|
+
let next_uri = attempt.location;
|
|
388
|
+
|
|
389
|
+
// Push the previous URI to the list of URLs.
|
|
390
|
+
self.uris.push(previous_uri.clone());
|
|
391
|
+
|
|
392
|
+
// Get policy from config
|
|
393
|
+
let policy = self
|
|
394
|
+
.policy
|
|
395
|
+
.as_ref()
|
|
396
|
+
.expect("[BUG] FollowRedirectPolicy should always have a policy set");
|
|
397
|
+
|
|
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
|
+
}
|
|
405
|
+
|
|
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
|
+
}
|
|
414
|
+
|
|
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
|
+
}
|
|
424
|
+
|
|
425
|
+
Ok(redirect::Action::Follow)
|
|
426
|
+
}
|
|
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
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
fn follow_redirects(&mut self, request: &mut http::Request<Body>) -> bool {
|
|
434
|
+
self.policy
|
|
435
|
+
.load(request.extensions_mut())
|
|
436
|
+
.is_some_and(|policy| !matches!(policy.inner, PolicyKind::None))
|
|
437
|
+
}
|
|
438
|
+
|
|
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
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
fn on_response<Body>(&mut self, response: &mut http::Response<Body>) {
|
|
452
|
+
if let Some(history) = self.history.take() {
|
|
453
|
+
response.extensions_mut().insert(History(history));
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
#[inline]
|
|
458
|
+
fn clone_body(&self, body: &Body) -> Option<Body> {
|
|
459
|
+
body.try_clone()
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
fn make_referer(next: Uri, previous: &Uri) -> Option<HeaderValue> {
|
|
464
|
+
if next.is_http() && previous.is_https() {
|
|
465
|
+
return None;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
let mut referer = previous.clone();
|
|
469
|
+
referer.set_userinfo("", None);
|
|
470
|
+
HeaderValue::from_maybe_shared(Bytes::from(referer.to_string())).ok()
|
|
471
|
+
}
|
|
472
|
+
|
|
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);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
#[cfg(test)]
|
|
493
|
+
mod tests {
|
|
494
|
+
use super::*;
|
|
495
|
+
|
|
496
|
+
#[test]
|
|
497
|
+
fn test_redirect_policy_limit() {
|
|
498
|
+
let policy = Policy::default();
|
|
499
|
+
let next = Uri::try_from("http://x.y/z").unwrap();
|
|
500
|
+
let mut previous = (0..=9)
|
|
501
|
+
.map(|i| Uri::try_from(&format!("http://a.b/c/{i}")).unwrap())
|
|
502
|
+
.collect::<Vec<_>>();
|
|
503
|
+
|
|
504
|
+
match policy.check(StatusCode::FOUND, &HeaderMap::new(), &next, &previous) {
|
|
505
|
+
redirect::Action::Follow => (),
|
|
506
|
+
other => panic!("unexpected {other:?}"),
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
previous.push(Uri::try_from("http://a.b.d/e/33").unwrap());
|
|
510
|
+
|
|
511
|
+
match policy.check(StatusCode::FOUND, &HeaderMap::new(), &next, &previous) {
|
|
512
|
+
redirect::Action::Error(err) if err.is::<TooManyRedirects>() => (),
|
|
513
|
+
other => panic!("unexpected {other:?}"),
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
#[test]
|
|
518
|
+
fn test_redirect_policy_limit_to_0() {
|
|
519
|
+
let policy = Policy::limited(0);
|
|
520
|
+
let next = Uri::try_from("http://x.y/z").unwrap();
|
|
521
|
+
let previous = vec![Uri::try_from("http://a.b/c").unwrap()];
|
|
522
|
+
|
|
523
|
+
match policy.check(StatusCode::FOUND, &HeaderMap::new(), &next, &previous) {
|
|
524
|
+
redirect::Action::Error(err) if err.is::<TooManyRedirects>() => (),
|
|
525
|
+
other => panic!("unexpected {other:?}"),
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
#[test]
|
|
530
|
+
fn test_redirect_policy_custom() {
|
|
531
|
+
let policy = Policy::custom(|attempt| {
|
|
532
|
+
if attempt.uri.host() == Some("foo") {
|
|
533
|
+
attempt.stop()
|
|
534
|
+
} else {
|
|
535
|
+
attempt.follow()
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
let next = Uri::try_from("http://bar/baz").unwrap();
|
|
540
|
+
match policy.check(StatusCode::FOUND, &HeaderMap::new(), &next, &[]) {
|
|
541
|
+
redirect::Action::Follow => (),
|
|
542
|
+
other => panic!("unexpected {other:?}"),
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
let next = Uri::try_from("http://foo/baz").unwrap();
|
|
546
|
+
match policy.check(StatusCode::FOUND, &HeaderMap::new(), &next, &[]) {
|
|
547
|
+
redirect::Action::Stop => (),
|
|
548
|
+
other => panic!("unexpected {other:?}"),
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
#[test]
|
|
553
|
+
fn test_remove_sensitive_headers() {
|
|
554
|
+
use http::header::{ACCEPT, AUTHORIZATION, COOKIE, HeaderValue};
|
|
555
|
+
|
|
556
|
+
let mut headers = HeaderMap::new();
|
|
557
|
+
headers.insert(ACCEPT, HeaderValue::from_static("*/*"));
|
|
558
|
+
headers.insert(AUTHORIZATION, HeaderValue::from_static("let me in"));
|
|
559
|
+
headers.insert(COOKIE, HeaderValue::from_static("foo=bar"));
|
|
560
|
+
|
|
561
|
+
let next = Uri::try_from("http://initial-domain.com/path").unwrap();
|
|
562
|
+
let mut prev = vec![Uri::try_from("http://initial-domain.com/new_path").unwrap()];
|
|
563
|
+
let mut filtered_headers = headers.clone();
|
|
564
|
+
|
|
565
|
+
remove_sensitive_headers(&mut headers, &next, &prev);
|
|
566
|
+
assert_eq!(headers, filtered_headers);
|
|
567
|
+
|
|
568
|
+
prev.push(Uri::try_from("http://new-domain.com/path").unwrap());
|
|
569
|
+
filtered_headers.remove(AUTHORIZATION);
|
|
570
|
+
filtered_headers.remove(COOKIE);
|
|
571
|
+
|
|
572
|
+
remove_sensitive_headers(&mut headers, &next, &prev);
|
|
573
|
+
assert_eq!(headers, filtered_headers);
|
|
574
|
+
}
|
|
575
|
+
}
|