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.
Files changed (167) hide show
  1. checksums.yaml +7 -0
  2. data/Cargo.lock +2688 -0
  3. data/Cargo.toml +6 -0
  4. data/README.md +179 -0
  5. data/ext/wreq_rb/Cargo.toml +39 -0
  6. data/ext/wreq_rb/extconf.rb +22 -0
  7. data/ext/wreq_rb/src/client.rs +565 -0
  8. data/ext/wreq_rb/src/error.rs +25 -0
  9. data/ext/wreq_rb/src/lib.rs +20 -0
  10. data/ext/wreq_rb/src/response.rs +132 -0
  11. data/lib/wreq-rb/version.rb +5 -0
  12. data/lib/wreq-rb.rb +17 -0
  13. data/patches/0001-add-transfer-size-tracking.patch +292 -0
  14. data/vendor/wreq/Cargo.toml +306 -0
  15. data/vendor/wreq/LICENSE +202 -0
  16. data/vendor/wreq/README.md +122 -0
  17. data/vendor/wreq/examples/cert_store.rs +77 -0
  18. data/vendor/wreq/examples/connect_via_lower_priority_tokio_runtime.rs +258 -0
  19. data/vendor/wreq/examples/emulation.rs +118 -0
  20. data/vendor/wreq/examples/form.rs +14 -0
  21. data/vendor/wreq/examples/http1_websocket.rs +37 -0
  22. data/vendor/wreq/examples/http2_websocket.rs +45 -0
  23. data/vendor/wreq/examples/json_dynamic.rs +41 -0
  24. data/vendor/wreq/examples/json_typed.rs +47 -0
  25. data/vendor/wreq/examples/keylog.rs +16 -0
  26. data/vendor/wreq/examples/request_with_emulation.rs +115 -0
  27. data/vendor/wreq/examples/request_with_interface.rs +37 -0
  28. data/vendor/wreq/examples/request_with_local_address.rs +16 -0
  29. data/vendor/wreq/examples/request_with_proxy.rs +13 -0
  30. data/vendor/wreq/examples/request_with_redirect.rs +22 -0
  31. data/vendor/wreq/examples/request_with_version.rs +15 -0
  32. data/vendor/wreq/examples/tor_socks.rs +24 -0
  33. data/vendor/wreq/examples/unix_socket.rs +33 -0
  34. data/vendor/wreq/src/client/body.rs +304 -0
  35. data/vendor/wreq/src/client/conn/conn.rs +231 -0
  36. data/vendor/wreq/src/client/conn/connector.rs +549 -0
  37. data/vendor/wreq/src/client/conn/http.rs +1023 -0
  38. data/vendor/wreq/src/client/conn/proxy/socks.rs +233 -0
  39. data/vendor/wreq/src/client/conn/proxy/tunnel.rs +260 -0
  40. data/vendor/wreq/src/client/conn/proxy.rs +39 -0
  41. data/vendor/wreq/src/client/conn/tls_info.rs +98 -0
  42. data/vendor/wreq/src/client/conn/uds.rs +44 -0
  43. data/vendor/wreq/src/client/conn/verbose.rs +149 -0
  44. data/vendor/wreq/src/client/conn.rs +323 -0
  45. data/vendor/wreq/src/client/core/body/incoming.rs +485 -0
  46. data/vendor/wreq/src/client/core/body/length.rs +118 -0
  47. data/vendor/wreq/src/client/core/body.rs +34 -0
  48. data/vendor/wreq/src/client/core/common/buf.rs +149 -0
  49. data/vendor/wreq/src/client/core/common/rewind.rs +141 -0
  50. data/vendor/wreq/src/client/core/common/watch.rs +76 -0
  51. data/vendor/wreq/src/client/core/common.rs +3 -0
  52. data/vendor/wreq/src/client/core/conn/http1.rs +342 -0
  53. data/vendor/wreq/src/client/core/conn/http2.rs +307 -0
  54. data/vendor/wreq/src/client/core/conn.rs +11 -0
  55. data/vendor/wreq/src/client/core/dispatch.rs +299 -0
  56. data/vendor/wreq/src/client/core/error.rs +435 -0
  57. data/vendor/wreq/src/client/core/ext.rs +201 -0
  58. data/vendor/wreq/src/client/core/http1.rs +178 -0
  59. data/vendor/wreq/src/client/core/http2.rs +483 -0
  60. data/vendor/wreq/src/client/core/proto/h1/conn.rs +988 -0
  61. data/vendor/wreq/src/client/core/proto/h1/decode.rs +1170 -0
  62. data/vendor/wreq/src/client/core/proto/h1/dispatch.rs +684 -0
  63. data/vendor/wreq/src/client/core/proto/h1/encode.rs +580 -0
  64. data/vendor/wreq/src/client/core/proto/h1/io.rs +879 -0
  65. data/vendor/wreq/src/client/core/proto/h1/role.rs +694 -0
  66. data/vendor/wreq/src/client/core/proto/h1.rs +104 -0
  67. data/vendor/wreq/src/client/core/proto/h2/client.rs +650 -0
  68. data/vendor/wreq/src/client/core/proto/h2/ping.rs +539 -0
  69. data/vendor/wreq/src/client/core/proto/h2.rs +379 -0
  70. data/vendor/wreq/src/client/core/proto/headers.rs +138 -0
  71. data/vendor/wreq/src/client/core/proto.rs +58 -0
  72. data/vendor/wreq/src/client/core/rt/bounds.rs +57 -0
  73. data/vendor/wreq/src/client/core/rt/timer.rs +150 -0
  74. data/vendor/wreq/src/client/core/rt/tokio.rs +99 -0
  75. data/vendor/wreq/src/client/core/rt.rs +25 -0
  76. data/vendor/wreq/src/client/core/upgrade.rs +267 -0
  77. data/vendor/wreq/src/client/core.rs +16 -0
  78. data/vendor/wreq/src/client/emulation.rs +161 -0
  79. data/vendor/wreq/src/client/http/client/error.rs +142 -0
  80. data/vendor/wreq/src/client/http/client/exec.rs +29 -0
  81. data/vendor/wreq/src/client/http/client/extra.rs +77 -0
  82. data/vendor/wreq/src/client/http/client/lazy.rs +79 -0
  83. data/vendor/wreq/src/client/http/client/pool.rs +1105 -0
  84. data/vendor/wreq/src/client/http/client/util.rs +104 -0
  85. data/vendor/wreq/src/client/http/client.rs +1003 -0
  86. data/vendor/wreq/src/client/http/future.rs +99 -0
  87. data/vendor/wreq/src/client/http.rs +1629 -0
  88. data/vendor/wreq/src/client/layer/config/options.rs +156 -0
  89. data/vendor/wreq/src/client/layer/config.rs +116 -0
  90. data/vendor/wreq/src/client/layer/cookie.rs +161 -0
  91. data/vendor/wreq/src/client/layer/decoder.rs +139 -0
  92. data/vendor/wreq/src/client/layer/redirect/future.rs +270 -0
  93. data/vendor/wreq/src/client/layer/redirect/policy.rs +63 -0
  94. data/vendor/wreq/src/client/layer/redirect.rs +145 -0
  95. data/vendor/wreq/src/client/layer/retry/classify.rs +105 -0
  96. data/vendor/wreq/src/client/layer/retry/scope.rs +51 -0
  97. data/vendor/wreq/src/client/layer/retry.rs +151 -0
  98. data/vendor/wreq/src/client/layer/timeout/body.rs +233 -0
  99. data/vendor/wreq/src/client/layer/timeout/future.rs +90 -0
  100. data/vendor/wreq/src/client/layer/timeout.rs +177 -0
  101. data/vendor/wreq/src/client/layer.rs +15 -0
  102. data/vendor/wreq/src/client/multipart.rs +717 -0
  103. data/vendor/wreq/src/client/request.rs +818 -0
  104. data/vendor/wreq/src/client/response.rs +534 -0
  105. data/vendor/wreq/src/client/ws/json.rs +99 -0
  106. data/vendor/wreq/src/client/ws/message.rs +453 -0
  107. data/vendor/wreq/src/client/ws.rs +714 -0
  108. data/vendor/wreq/src/client.rs +27 -0
  109. data/vendor/wreq/src/config.rs +140 -0
  110. data/vendor/wreq/src/cookie.rs +579 -0
  111. data/vendor/wreq/src/dns/gai.rs +249 -0
  112. data/vendor/wreq/src/dns/hickory.rs +78 -0
  113. data/vendor/wreq/src/dns/resolve.rs +180 -0
  114. data/vendor/wreq/src/dns.rs +69 -0
  115. data/vendor/wreq/src/error.rs +502 -0
  116. data/vendor/wreq/src/ext.rs +398 -0
  117. data/vendor/wreq/src/hash.rs +143 -0
  118. data/vendor/wreq/src/header.rs +506 -0
  119. data/vendor/wreq/src/into_uri.rs +187 -0
  120. data/vendor/wreq/src/lib.rs +586 -0
  121. data/vendor/wreq/src/proxy/mac.rs +82 -0
  122. data/vendor/wreq/src/proxy/matcher.rs +806 -0
  123. data/vendor/wreq/src/proxy/uds.rs +66 -0
  124. data/vendor/wreq/src/proxy/win.rs +31 -0
  125. data/vendor/wreq/src/proxy.rs +569 -0
  126. data/vendor/wreq/src/redirect.rs +575 -0
  127. data/vendor/wreq/src/retry.rs +198 -0
  128. data/vendor/wreq/src/sync.rs +129 -0
  129. data/vendor/wreq/src/tls/conn/cache.rs +123 -0
  130. data/vendor/wreq/src/tls/conn/cert_compression.rs +125 -0
  131. data/vendor/wreq/src/tls/conn/ext.rs +82 -0
  132. data/vendor/wreq/src/tls/conn/macros.rs +34 -0
  133. data/vendor/wreq/src/tls/conn/service.rs +138 -0
  134. data/vendor/wreq/src/tls/conn.rs +681 -0
  135. data/vendor/wreq/src/tls/keylog/handle.rs +64 -0
  136. data/vendor/wreq/src/tls/keylog.rs +99 -0
  137. data/vendor/wreq/src/tls/options.rs +464 -0
  138. data/vendor/wreq/src/tls/x509/identity.rs +122 -0
  139. data/vendor/wreq/src/tls/x509/parser.rs +71 -0
  140. data/vendor/wreq/src/tls/x509/store.rs +228 -0
  141. data/vendor/wreq/src/tls/x509.rs +68 -0
  142. data/vendor/wreq/src/tls.rs +154 -0
  143. data/vendor/wreq/src/trace.rs +55 -0
  144. data/vendor/wreq/src/util.rs +122 -0
  145. data/vendor/wreq/tests/badssl.rs +228 -0
  146. data/vendor/wreq/tests/brotli.rs +350 -0
  147. data/vendor/wreq/tests/client.rs +1098 -0
  148. data/vendor/wreq/tests/connector_layers.rs +227 -0
  149. data/vendor/wreq/tests/cookie.rs +306 -0
  150. data/vendor/wreq/tests/deflate.rs +347 -0
  151. data/vendor/wreq/tests/emulation.rs +260 -0
  152. data/vendor/wreq/tests/gzip.rs +347 -0
  153. data/vendor/wreq/tests/layers.rs +261 -0
  154. data/vendor/wreq/tests/multipart.rs +165 -0
  155. data/vendor/wreq/tests/proxy.rs +438 -0
  156. data/vendor/wreq/tests/redirect.rs +629 -0
  157. data/vendor/wreq/tests/retry.rs +135 -0
  158. data/vendor/wreq/tests/support/delay_server.rs +117 -0
  159. data/vendor/wreq/tests/support/error.rs +16 -0
  160. data/vendor/wreq/tests/support/layer.rs +183 -0
  161. data/vendor/wreq/tests/support/mod.rs +9 -0
  162. data/vendor/wreq/tests/support/server.rs +232 -0
  163. data/vendor/wreq/tests/timeouts.rs +281 -0
  164. data/vendor/wreq/tests/unix_socket.rs +135 -0
  165. data/vendor/wreq/tests/upgrade.rs +98 -0
  166. data/vendor/wreq/tests/zstd.rs +559 -0
  167. 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
+ }