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