1 | //! Redirect Handling |
2 | //! |
3 | //! By default, a `Client` will automatically handle HTTP redirects, having a |
4 | //! maximum redirect chain of 10 hops. To customize this behavior, a |
5 | //! `redirect::Policy` can be used with a `ClientBuilder`. |
6 | |
7 | use std::error::Error as StdError; |
8 | use std::fmt; |
9 | |
10 | use crate::header::{HeaderMap, AUTHORIZATION, COOKIE, PROXY_AUTHORIZATION, WWW_AUTHENTICATE}; |
11 | use hyper::StatusCode; |
12 | |
13 | use crate::Url; |
14 | |
15 | /// A type that controls the policy on how to handle the following of redirects. |
16 | /// |
17 | /// The default value will catch redirect loops, and has a maximum of 10 |
18 | /// redirects it will follow in a chain before returning an error. |
19 | /// |
20 | /// - `limited` can be used have the same as the default behavior, but adjust |
21 | /// the allowed maximum redirect hops in a chain. |
22 | /// - `none` can be used to disable all redirect behavior. |
23 | /// - `custom` can be used to create a customized policy. |
24 | pub struct Policy { |
25 | inner: PolicyKind, |
26 | } |
27 | |
28 | /// A type that holds information on the next request and previous requests |
29 | /// in redirect chain. |
30 | #[derive (Debug)] |
31 | pub struct Attempt<'a> { |
32 | status: StatusCode, |
33 | next: &'a Url, |
34 | previous: &'a [Url], |
35 | } |
36 | |
37 | /// An action to perform when a redirect status code is found. |
38 | #[derive (Debug)] |
39 | pub struct Action { |
40 | inner: ActionKind, |
41 | } |
42 | |
43 | impl Policy { |
44 | /// Create a `Policy` with a maximum number of redirects. |
45 | /// |
46 | /// An `Error` will be returned if the max is reached. |
47 | pub fn limited(max: usize) -> Self { |
48 | Self { |
49 | inner: PolicyKind::Limit(max), |
50 | } |
51 | } |
52 | |
53 | /// Create a `Policy` that does not follow any redirect. |
54 | pub fn none() -> Self { |
55 | Self { |
56 | inner: PolicyKind::None, |
57 | } |
58 | } |
59 | |
60 | /// Create a custom `Policy` using the passed function. |
61 | /// |
62 | /// # Note |
63 | /// |
64 | /// The default `Policy` handles a maximum loop |
65 | /// chain, but the custom variant does not do that for you automatically. |
66 | /// The custom policy should have some way of handling those. |
67 | /// |
68 | /// Information on the next request and previous requests can be found |
69 | /// on the [`Attempt`] argument passed to the closure. |
70 | /// |
71 | /// Actions can be conveniently created from methods on the |
72 | /// [`Attempt`]. |
73 | /// |
74 | /// # Example |
75 | /// |
76 | /// ```rust |
77 | /// # use reqwest::{Error, redirect}; |
78 | /// # |
79 | /// # fn run() -> Result<(), Error> { |
80 | /// let custom = redirect::Policy::custom(|attempt| { |
81 | /// if attempt.previous().len() > 5 { |
82 | /// attempt.error("too many redirects" ) |
83 | /// } else if attempt.url().host_str() == Some("example.domain" ) { |
84 | /// // prevent redirects to 'example.domain' |
85 | /// attempt.stop() |
86 | /// } else { |
87 | /// attempt.follow() |
88 | /// } |
89 | /// }); |
90 | /// let client = reqwest::Client::builder() |
91 | /// .redirect(custom) |
92 | /// .build()?; |
93 | /// # Ok(()) |
94 | /// # } |
95 | /// ``` |
96 | /// |
97 | /// [`Attempt`]: struct.Attempt.html |
98 | pub fn custom<T>(policy: T) -> Self |
99 | where |
100 | T: Fn(Attempt) -> Action + Send + Sync + 'static, |
101 | { |
102 | Self { |
103 | inner: PolicyKind::Custom(Box::new(policy)), |
104 | } |
105 | } |
106 | |
107 | /// Apply this policy to a given [`Attempt`] to produce a [`Action`]. |
108 | /// |
109 | /// # Note |
110 | /// |
111 | /// This method can be used together with `Policy::custom()` |
112 | /// to construct one `Policy` that wraps another. |
113 | /// |
114 | /// # Example |
115 | /// |
116 | /// ```rust |
117 | /// # use reqwest::{Error, redirect}; |
118 | /// # |
119 | /// # fn run() -> Result<(), Error> { |
120 | /// let custom = redirect::Policy::custom(|attempt| { |
121 | /// eprintln!("{}, Location: {:?}" , attempt.status(), attempt.url()); |
122 | /// redirect::Policy::default().redirect(attempt) |
123 | /// }); |
124 | /// # Ok(()) |
125 | /// # } |
126 | /// ``` |
127 | pub fn redirect(&self, attempt: Attempt) -> Action { |
128 | match self.inner { |
129 | PolicyKind::Custom(ref custom) => custom(attempt), |
130 | PolicyKind::Limit(max) => { |
131 | if attempt.previous.len() >= max { |
132 | attempt.error(TooManyRedirects) |
133 | } else { |
134 | attempt.follow() |
135 | } |
136 | } |
137 | PolicyKind::None => attempt.stop(), |
138 | } |
139 | } |
140 | |
141 | pub(crate) fn check(&self, status: StatusCode, next: &Url, previous: &[Url]) -> ActionKind { |
142 | self.redirect(Attempt { |
143 | status, |
144 | next, |
145 | previous, |
146 | }) |
147 | .inner |
148 | } |
149 | |
150 | pub(crate) fn is_default(&self) -> bool { |
151 | matches!(self.inner, PolicyKind::Limit(10)) |
152 | } |
153 | } |
154 | |
155 | impl Default for Policy { |
156 | fn default() -> Policy { |
157 | // Keep `is_default` in sync |
158 | Policy::limited(max:10) |
159 | } |
160 | } |
161 | |
162 | impl<'a> Attempt<'a> { |
163 | /// Get the type of redirect. |
164 | pub fn status(&self) -> StatusCode { |
165 | self.status |
166 | } |
167 | |
168 | /// Get the next URL to redirect to. |
169 | pub fn url(&self) -> &Url { |
170 | self.next |
171 | } |
172 | |
173 | /// Get the list of previous URLs that have already been requested in this chain. |
174 | pub fn previous(&self) -> &[Url] { |
175 | self.previous |
176 | } |
177 | /// Returns an action meaning reqwest should follow the next URL. |
178 | pub fn follow(self) -> Action { |
179 | Action { |
180 | inner: ActionKind::Follow, |
181 | } |
182 | } |
183 | |
184 | /// Returns an action meaning reqwest should not follow the next URL. |
185 | /// |
186 | /// The 30x response will be returned as the `Ok` result. |
187 | pub fn stop(self) -> Action { |
188 | Action { |
189 | inner: ActionKind::Stop, |
190 | } |
191 | } |
192 | |
193 | /// Returns an action failing the redirect with an error. |
194 | /// |
195 | /// The `Error` will be returned for the result of the sent request. |
196 | pub fn error<E: Into<Box<dyn StdError + Send + Sync>>>(self, error: E) -> Action { |
197 | Action { |
198 | inner: ActionKind::Error(error.into()), |
199 | } |
200 | } |
201 | } |
202 | |
203 | enum PolicyKind { |
204 | Custom(Box<dyn Fn(Attempt) -> Action + Send + Sync + 'static>), |
205 | Limit(usize), |
206 | None, |
207 | } |
208 | |
209 | impl fmt::Debug for Policy { |
210 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
211 | f.debug_tuple(name:"Policy" ).field(&self.inner).finish() |
212 | } |
213 | } |
214 | |
215 | impl fmt::Debug for PolicyKind { |
216 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
217 | match *self { |
218 | PolicyKind::Custom(..) => f.pad("Custom" ), |
219 | PolicyKind::Limit(max: usize) => f.debug_tuple(name:"Limit" ).field(&max).finish(), |
220 | PolicyKind::None => f.pad("None" ), |
221 | } |
222 | } |
223 | } |
224 | |
225 | // pub(crate) |
226 | |
227 | #[derive (Debug)] |
228 | pub(crate) enum ActionKind { |
229 | Follow, |
230 | Stop, |
231 | Error(Box<dyn StdError + Send + Sync>), |
232 | } |
233 | |
234 | pub(crate) fn remove_sensitive_headers(headers: &mut HeaderMap, next: &Url, previous: &[Url]) { |
235 | if let Some(previous: &Url) = previous.last() { |
236 | let cross_host: bool = next.host_str() != previous.host_str() |
237 | || next.port_or_known_default() != previous.port_or_known_default(); |
238 | if cross_host { |
239 | headers.remove(AUTHORIZATION); |
240 | headers.remove(COOKIE); |
241 | headers.remove(key:"cookie2" ); |
242 | headers.remove(PROXY_AUTHORIZATION); |
243 | headers.remove(WWW_AUTHENTICATE); |
244 | } |
245 | } |
246 | } |
247 | |
248 | #[derive (Debug)] |
249 | struct TooManyRedirects; |
250 | |
251 | impl fmt::Display for TooManyRedirects { |
252 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
253 | f.write_str(data:"too many redirects" ) |
254 | } |
255 | } |
256 | |
257 | impl StdError for TooManyRedirects {} |
258 | |
259 | #[test ] |
260 | fn test_redirect_policy_limit() { |
261 | let policy: Policy = Policy::default(); |
262 | let next: Url = Url::parse(input:"http://x.y/z" ).unwrap(); |
263 | let mut previous: Vec = (0..9) |
264 | .map(|i: i32| Url::parse(&format!("http://a.b/c/ {i}" )).unwrap()) |
265 | .collect::<Vec<_>>(); |
266 | |
267 | match policy.check(status:StatusCode::FOUND, &next, &previous) { |
268 | ActionKind::Follow => (), |
269 | other: ActionKind => panic!("unexpected {other:?}" ), |
270 | } |
271 | |
272 | previous.push(Url::parse(input:"http://a.b.d/e/33" ).unwrap()); |
273 | |
274 | match policy.check(status:StatusCode::FOUND, &next, &previous) { |
275 | ActionKind::Error(err: Box) if err.is::<TooManyRedirects>() => (), |
276 | other: ActionKind => panic!("unexpected {other:?}" ), |
277 | } |
278 | } |
279 | |
280 | #[test ] |
281 | fn test_redirect_policy_limit_to_0() { |
282 | let policy: Policy = Policy::limited(max:0); |
283 | let next: Url = Url::parse(input:"http://x.y/z" ).unwrap(); |
284 | let previous: Vec = vec![Url::parse("http://a.b/c" ).unwrap()]; |
285 | |
286 | match policy.check(status:StatusCode::FOUND, &next, &previous) { |
287 | ActionKind::Error(err: Box) if err.is::<TooManyRedirects>() => (), |
288 | other: ActionKind => panic!("unexpected {other:?}" ), |
289 | } |
290 | } |
291 | |
292 | #[test ] |
293 | fn test_redirect_policy_custom() { |
294 | let policy: Policy = Policy::custom(|attempt: Attempt<'_>| { |
295 | if attempt.url().host_str() == Some("foo" ) { |
296 | attempt.stop() |
297 | } else { |
298 | attempt.follow() |
299 | } |
300 | }); |
301 | |
302 | let next: Url = Url::parse(input:"http://bar/baz" ).unwrap(); |
303 | match policy.check(status:StatusCode::FOUND, &next, &[]) { |
304 | ActionKind::Follow => (), |
305 | other: ActionKind => panic!("unexpected {other:?}" ), |
306 | } |
307 | |
308 | let next: Url = Url::parse(input:"http://foo/baz" ).unwrap(); |
309 | match policy.check(status:StatusCode::FOUND, &next, &[]) { |
310 | ActionKind::Stop => (), |
311 | other: ActionKind => panic!("unexpected {other:?}" ), |
312 | } |
313 | } |
314 | |
315 | #[test ] |
316 | fn test_remove_sensitive_headers() { |
317 | use hyper::header::{HeaderValue, ACCEPT, AUTHORIZATION, COOKIE}; |
318 | |
319 | let mut headers: HeaderMap = HeaderMap::new(); |
320 | headers.insert(ACCEPT, val:HeaderValue::from_static(src:"*/*" )); |
321 | headers.insert(AUTHORIZATION, val:HeaderValue::from_static(src:"let me in" )); |
322 | headers.insert(COOKIE, val:HeaderValue::from_static(src:"foo=bar" )); |
323 | |
324 | let next: Url = Url::parse(input:"http://initial-domain.com/path" ).unwrap(); |
325 | let mut prev: Vec = vec![Url::parse("http://initial-domain.com/new_path" ).unwrap()]; |
326 | let mut filtered_headers: HeaderMap = headers.clone(); |
327 | |
328 | remove_sensitive_headers(&mut headers, &next, &prev); |
329 | assert_eq!(headers, filtered_headers); |
330 | |
331 | prev.push(Url::parse(input:"http://new-domain.com/path" ).unwrap()); |
332 | filtered_headers.remove(AUTHORIZATION); |
333 | filtered_headers.remove(COOKIE); |
334 | |
335 | remove_sensitive_headers(&mut headers, &next, &prev); |
336 | assert_eq!(headers, filtered_headers); |
337 | } |
338 | |