const MAX_REDIRECTS: usize = 10;
fn is_same_origin(previous: &reqwest::Url, next: &reqwest::Url) -> bool {
previous.scheme() == next.scheme()
&& previous.host_str() == next.host_str()
&& previous.port_or_known_default() == next.port_or_known_default()
}
fn same_origin_redirect_policy() -> reqwest::redirect::Policy {
reqwest::redirect::Policy::custom(|attempt| {
let leaves_origin = attempt
.previous()
.last()
.is_none_or(|previous| !is_same_origin(previous, attempt.url()));
let too_many_hops = attempt.previous().len() > MAX_REDIRECTS;
if leaves_origin || too_many_hops {
return attempt.stop();
}
attempt.follow()
})
}
pub(crate) fn credentialed_client_builder() -> reqwest::ClientBuilder {
let policy = same_origin_redirect_policy();
reqwest::Client::builder().redirect(policy)
}
#[cfg(test)]
mod tests {
use super::*;
fn url(value: &str) -> reqwest::Url {
reqwest::Url::parse(value).expect("test URL should parse")
}
#[test]
fn test_is_same_origin_matches_scheme_host_and_effective_port() {
let base = url("http://host:8090/v1/queries");
let sibling = url("http://host:8090/v1/queries/1");
assert!(is_same_origin(&base, &sibling));
let implicit_http = url("http://host/a");
let explicit_http = url("http://host:80/b");
assert!(is_same_origin(&implicit_http, &explicit_http));
let implicit_https = url("https://host/a");
let explicit_https = url("https://host:443/b");
assert!(is_same_origin(&implicit_https, &explicit_https));
}
#[test]
fn test_is_same_origin_rejects_a_different_origin() {
let base = url("https://runtime.example.com/v1/queries");
let other_host = url("https://attacker.example.com/v1/queries");
assert!(!is_same_origin(&base, &other_host));
let other_port = url("https://runtime.example.com:8443/v1/queries");
assert!(!is_same_origin(&base, &other_port));
}
#[test]
fn test_is_same_origin_rejects_a_scheme_downgrade_on_the_same_port() {
let secure = url("https://runtime.example.com:8443/v1/queries");
let plaintext = url("http://runtime.example.com:8443/v1/queries");
assert!(!is_same_origin(&secure, &plaintext));
}
#[test]
fn test_is_same_origin_ignores_path_query_and_fragment() {
let with_query = url("http://host:8090/v1/queries?a=1#x");
let other_path = url("http://host:8090/other?b=2#y");
assert!(is_same_origin(&with_query, &other_path));
}
}