use crate::error::Error;
use crate::headers::Headers;
use crate::method::Method;
use crate::request::Request;
use crate::response::Response;
use crate::transport;
use crate::url::Url;
pub const MAX_REDIRECTS: u8 = 10;
pub fn fetch(request: &Request) -> Result<Response, Error> {
follow(request, MAX_REDIRECTS)
}
fn follow(request: &Request, hops_remaining: u8) -> Result<Response, Error> {
let response = transport::exchange(request)?;
match redirect_target(request, &response) {
Some(next) => match hops_remaining {
0 => Err(Error::TooManyRedirects),
remaining => follow(&next, remaining - 1),
},
None => Ok(response),
}
}
fn redirect_target(request: &Request, response: &Response) -> Option<Request> {
redirect_method(response.status(), request.method())
.zip(response.headers().get("location"))
.and_then(|(next_method, location)| build_next(request, next_method, location))
}
fn redirect_method(status: u16, current: Method) -> Option<Method> {
match status {
301..=303 => Some(redirect_get_downgrade(current)),
307 | 308 => Some(current),
_other => None,
}
}
fn redirect_get_downgrade(current: Method) -> Method {
match current {
Method::Head => Method::Head,
Method::Get
| Method::Post
| Method::Put
| Method::Delete
| Method::Options
| Method::Patch => Method::Get,
}
}
fn build_next(request: &Request, method: Method, location: &str) -> Option<Request> {
let next_url = Url::parse(location).ok()?;
let cross_origin = next_url.host() != request.url().host();
let next_headers = sanitize_headers(request.headers().clone(), cross_origin, method);
let next_body = redirect_body(request, method);
Some(
request
.clone()
.with_url(next_url)
.with_method(method)
.with_headers(next_headers)
.with_body(next_body),
)
}
fn sanitize_headers(headers: Headers, cross_origin: bool, next_method: Method) -> Headers {
let without_credentials = if cross_origin {
headers.without("Cookie").without("Authorization")
} else {
headers
};
match next_method {
Method::Get | Method::Head => without_credentials.without("Content-Length"),
Method::Post | Method::Put | Method::Delete | Method::Options | Method::Patch => {
without_credentials
}
}
}
fn redirect_body(request: &Request, next_method: Method) -> Vec<u8> {
match next_method {
Method::Get | Method::Head => Vec::new(),
Method::Post | Method::Put | Method::Delete | Method::Options | Method::Patch => {
request.body().to_vec()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fail() -> Error {
Error::InvalidStatusLine {
text: "test predicate failed".to_owned(),
}
}
fn dummy_request() -> Result<Request, Error> {
Ok(Request::new(Method::Get, Url::parse("http://a.example/")?)
.with_header("Cookie", "session=abc")
.with_header("Authorization", "Bearer x")
.with_header("X-Trace", "keep-me"))
}
#[test]
fn redirect_method_303_downgrades_post_to_get() -> Result<(), Error> {
let downgraded = redirect_method(303, Method::Post);
matches!(downgraded, Some(Method::Get))
.then_some(())
.ok_or_else(fail)
}
#[test]
fn redirect_method_307_preserves_post() -> Result<(), Error> {
let preserved = redirect_method(307, Method::Post);
matches!(preserved, Some(Method::Post))
.then_some(())
.ok_or_else(fail)
}
#[test]
fn redirect_method_200_returns_none() -> Result<(), Error> {
redirect_method(200, Method::Get)
.is_none()
.then_some(())
.ok_or_else(fail)
}
#[test]
fn build_next_cross_origin_strips_cookie_and_auth() -> Result<(), Error> {
let request = dummy_request()?;
let next = build_next(&request, Method::Get, "http://b.example/other").ok_or_else(fail)?;
let has_cookie = next.headers().get("Cookie").is_some();
let has_auth = next.headers().get("Authorization").is_some();
let has_trace = next.headers().get("X-Trace").is_some();
(!has_cookie && !has_auth && has_trace)
.then_some(())
.ok_or_else(fail)
}
#[test]
fn build_next_same_origin_keeps_cookie_and_auth() -> Result<(), Error> {
let request = dummy_request()?;
let next =
build_next(&request, Method::Get, "http://a.example/elsewhere").ok_or_else(fail)?;
let has_cookie = next.headers().get("Cookie").is_some();
let has_auth = next.headers().get("Authorization").is_some();
(has_cookie && has_auth).then_some(()).ok_or_else(fail)
}
#[test]
fn build_next_get_drops_body() -> Result<(), Error> {
let request = Request::new(Method::Post, Url::parse("http://a.example/")?)
.with_body(b"hello".to_vec());
let next =
build_next(&request, Method::Get, "http://a.example/landing").ok_or_else(fail)?;
next.body().is_empty().then_some(()).ok_or_else(fail)
}
#[test]
fn build_next_307_preserves_body() -> Result<(), Error> {
let request = Request::new(Method::Post, Url::parse("http://a.example/")?)
.with_body(b"hello".to_vec());
let next =
build_next(&request, Method::Post, "http://a.example/landing").ok_or_else(fail)?;
(next.body() == b"hello").then_some(()).ok_or_else(fail)
}
#[test]
fn build_next_invalid_location_returns_none() -> Result<(), Error> {
let request = dummy_request()?;
build_next(&request, Method::Get, "not a url")
.is_none()
.then_some(())
.ok_or_else(fail)
}
}