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.
limited(max: usize) -> Self47     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.
none() -> Self54     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
custom<T>(policy: T) -> Self where T: Fn(Attempt) -> Action + Send + Sync + 'static,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     /// ```
redirect(&self, attempt: Attempt) -> Action127     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 
check(&self, status: StatusCode, next: &Url, previous: &[Url]) -> ActionKind141     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 
is_default(&self) -> bool150     pub(crate) fn is_default(&self) -> bool {
151         matches!(self.inner, PolicyKind::Limit(10))
152     }
153 }
154 
155 impl Default for Policy {
default() -> Policy156     fn default() -> Policy {
157         // Keep `is_default` in sync
158         Policy::limited(10)
159     }
160 }
161 
162 impl<'a> Attempt<'a> {
163     /// Get the type of redirect.
status(&self) -> StatusCode164     pub fn status(&self) -> StatusCode {
165         self.status
166     }
167 
168     /// Get the next URL to redirect to.
url(&self) -> &Url169     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.
previous(&self) -> &[Url]174     pub fn previous(&self) -> &[Url] {
175         self.previous
176     }
177     /// Returns an action meaning reqwest should follow the next URL.
follow(self) -> Action178     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.
stop(self) -> Action187     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.
error<E: Into<Box<dyn StdError + Send + Sync>>>(self, error: E) -> Action196     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 {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result210     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
211         f.debug_tuple("Policy").field(&self.inner).finish()
212     }
213 }
214 
215 impl fmt::Debug for PolicyKind {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result216     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
217         match *self {
218             PolicyKind::Custom(..) => f.pad("Custom"),
219             PolicyKind::Limit(max) => f.debug_tuple("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 
remove_sensitive_headers(headers: &mut HeaderMap, next: &Url, previous: &[Url])234 pub(crate) fn remove_sensitive_headers(headers: &mut HeaderMap, next: &Url, previous: &[Url]) {
235     if let Some(previous) = previous.last() {
236         let cross_host = 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("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 {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result252     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253         f.write_str("too many redirects")
254     }
255 }
256 
257 impl StdError for TooManyRedirects {}
258 
259 #[test]
test_redirect_policy_limit()260 fn test_redirect_policy_limit() {
261     let policy = Policy::default();
262     let next = Url::parse("http://x.y/z").unwrap();
263     let mut previous = (0..9)
264         .map(|i| Url::parse(&format!("http://a.b/c/{}", i)).unwrap())
265         .collect::<Vec<_>>();
266 
267     match policy.check(StatusCode::FOUND, &next, &previous) {
268         ActionKind::Follow => (),
269         other => panic!("unexpected {:?}", other),
270     }
271 
272     previous.push(Url::parse("http://a.b.d/e/33").unwrap());
273 
274     match policy.check(StatusCode::FOUND, &next, &previous) {
275         ActionKind::Error(err) if err.is::<TooManyRedirects>() => (),
276         other => panic!("unexpected {:?}", other),
277     }
278 }
279 
280 #[test]
test_redirect_policy_custom()281 fn test_redirect_policy_custom() {
282     let policy = Policy::custom(|attempt| {
283         if attempt.url().host_str() == Some("foo") {
284             attempt.stop()
285         } else {
286             attempt.follow()
287         }
288     });
289 
290     let next = Url::parse("http://bar/baz").unwrap();
291     match policy.check(StatusCode::FOUND, &next, &[]) {
292         ActionKind::Follow => (),
293         other => panic!("unexpected {:?}", other),
294     }
295 
296     let next = Url::parse("http://foo/baz").unwrap();
297     match policy.check(StatusCode::FOUND, &next, &[]) {
298         ActionKind::Stop => (),
299         other => panic!("unexpected {:?}", other),
300     }
301 }
302 
303 #[test]
test_remove_sensitive_headers()304 fn test_remove_sensitive_headers() {
305     use hyper::header::{HeaderValue, ACCEPT, AUTHORIZATION, COOKIE};
306 
307     let mut headers = HeaderMap::new();
308     headers.insert(ACCEPT, HeaderValue::from_static("*/*"));
309     headers.insert(AUTHORIZATION, HeaderValue::from_static("let me in"));
310     headers.insert(COOKIE, HeaderValue::from_static("foo=bar"));
311 
312     let next = Url::parse("http://initial-domain.com/path").unwrap();
313     let mut prev = vec![Url::parse("http://initial-domain.com/new_path").unwrap()];
314     let mut filtered_headers = headers.clone();
315 
316     remove_sensitive_headers(&mut headers, &next, &prev);
317     assert_eq!(headers, filtered_headers);
318 
319     prev.push(Url::parse("http://new-domain.com/path").unwrap());
320     filtered_headers.remove(AUTHORIZATION);
321     filtered_headers.remove(COOKIE);
322 
323     remove_sensitive_headers(&mut headers, &next, &prev);
324     assert_eq!(headers, filtered_headers);
325 }
326