ureq_proto/client/
redirect.rs

1use http::uri::Scheme;
2use http::{header, Method, StatusCode, Uri};
3
4use crate::ext::{MethodExt, StatusExt};
5use crate::Error;
6
7use super::state::{Cleanup, Prepare, Redirect};
8use super::{Call, RedirectAuthHeaders};
9
10impl Call<Redirect> {
11    /// Construct a new `Call` by following the redirect.
12    ///
13    /// There are some rules when following a redirect.
14    ///
15    /// * For 307/308
16    ///     * POST/PUT results in `None`, since we do not allow redirecting a request body
17    ///     * DELETE is intentionally excluded: <https://stackoverflow.com/questions/299628>
18    ///     * All other methods retain the method in the redirect
19    /// * Other redirect (301, 302, etc)
20    ///     * HEAD results in HEAD in the redirect
21    ///     * All other methods becomes GET
22    pub fn as_new_call(
23        &mut self,
24        redirect_auth_headers: RedirectAuthHeaders,
25    ) -> Result<Option<Call<Prepare>>, Error> {
26        let header = match &self.inner.location {
27            Some(v) => v,
28            None => return Err(Error::NoLocationHeader),
29        };
30
31        let location = match header.to_str() {
32            Ok(v) => v,
33            Err(_) => {
34                return Err(Error::BadLocationHeader(
35                    String::from_utf8_lossy(header.as_bytes()).to_string(),
36                ))
37            }
38        };
39
40        // Previous request
41        let previous = &mut self.inner.request;
42
43        // Unwrap is OK, because we can't be here without having read a response.
44        let status = self.inner.status.unwrap();
45        let method = previous.method();
46
47        // A new uri by combining the base from the previous request and the new location.
48        let uri = previous.new_uri_from_location(location)?;
49
50        // Perform the redirect method differently depending on 3xx code.
51        let new_method = if status.is_redirect_retaining_status() {
52            if method.need_request_body() {
53                // only resend the request if it cannot have a body
54                return Ok(None);
55            } else if method == Method::DELETE {
56                // NOTE: DELETE is intentionally excluded: https://stackoverflow.com/questions/299628
57                return Ok(None);
58            } else {
59                method.clone()
60            }
61        } else {
62            // this is to follow how curl does it. POST, PUT etc change
63            // to GET on a redirect.
64            if matches!(*method, Method::GET | Method::HEAD) {
65                method.clone()
66            } else {
67                Method::GET
68            }
69        };
70
71        let mut request = previous.take_request();
72
73        // The calculated redirect method
74        *request.method_mut() = new_method;
75
76        let keep_auth_header = match redirect_auth_headers {
77            RedirectAuthHeaders::Never => false,
78            RedirectAuthHeaders::SameHost => can_redirect_auth_header(request.uri(), &uri),
79        };
80
81        // The redirect URI
82        *request.uri_mut() = uri;
83
84        // Mutate the original request to remove headers we cannot keep in the redirect.
85        let headers = request.headers_mut();
86        if !keep_auth_header {
87            headers.remove(header::AUTHORIZATION);
88        }
89        headers.remove(header::COOKIE);
90        headers.remove(header::CONTENT_LENGTH);
91
92        // Next state
93        let next = Call::new(request)?;
94
95        Ok(Some(next))
96    }
97
98    /// The redirect status code.
99    pub fn status(&self) -> StatusCode {
100        self.inner.status.unwrap()
101    }
102
103    /// Whether we must close the connection corresponding to the current call.
104    ///
105    /// This is used to inform connection pooling.
106    pub fn must_close_connection(&self) -> bool {
107        self.close_reason().is_some()
108    }
109
110    /// If we are closing the connection, give a reason why.
111    pub fn close_reason(&self) -> Option<&'static str> {
112        self.inner.close_reason.first().map(|s| s.explain())
113    }
114
115    /// Proceed to the cleanup state.
116    pub fn proceed(self) -> Call<Cleanup> {
117        Call::wrap(self.inner)
118    }
119}
120
121fn can_redirect_auth_header(prev: &Uri, next: &Uri) -> bool {
122    let host_prev = prev.authority().map(|a| a.host());
123    let host_next = next.authority().map(|a| a.host());
124    let scheme_prev = prev.scheme();
125    let scheme_next = next.scheme();
126    host_prev == host_next && (scheme_prev == scheme_next || scheme_next == Some(&Scheme::HTTPS))
127}