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
7use std::error::Error as StdError;
8use std::fmt;
9
10use crate::header::{HeaderMap, AUTHORIZATION, COOKIE, PROXY_AUTHORIZATION, WWW_AUTHENTICATE};
11use hyper::StatusCode;
12
13use 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.
24pub 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)]
31pub 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)]
39pub struct Action {
40 inner: ActionKind,
41}
42
43impl 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
155impl Default for Policy {
156 fn default() -> Policy {
157 // Keep `is_default` in sync
158 Policy::limited(max:10)
159 }
160}
161
162impl<'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
203enum PolicyKind {
204 Custom(Box<dyn Fn(Attempt) -> Action + Send + Sync + 'static>),
205 Limit(usize),
206 None,
207}
208
209impl 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
215impl 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)]
228pub(crate) enum ActionKind {
229 Follow,
230 Stop,
231 Error(Box<dyn StdError + Send + Sync>),
232}
233
234pub(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)]
249struct TooManyRedirects;
250
251impl 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
257impl StdError for TooManyRedirects {}
258
259#[test]
260fn 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]
281fn 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]
293fn 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]
316fn 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