use std::fmt;
use std::net::IpAddr;
use reqwest::Url;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct InsecureTransportError {
pub origin: String,
pub redirect: bool,
}
impl fmt::Display for InsecureTransportError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let what = if self.redirect { "a redirect" } else { "the request" };
write!(
f,
"refusing to send the bearer credential with {what} to {}: use https://, or http:// only on a loopback host (localhost, 127.0.0.1, [::1])",
self.origin
)
}
}
impl std::error::Error for InsecureTransportError {}
pub fn is_loopback_host(host: &str) -> bool {
let name = host.trim_start_matches('[').trim_end_matches(']');
if name.eq_ignore_ascii_case("localhost") {
return true;
}
name.parse::<IpAddr>().is_ok_and(|ip| ip.is_loopback())
}
pub fn is_trusted_destination(url: &Url) -> bool {
match url.scheme() {
"https" => true,
"http" => url.host_str().is_some_and(is_loopback_host),
_ => false,
}
}
pub(crate) fn origin_of(url: &Url) -> String {
let host = url.host_str().unwrap_or("");
match url.port() {
Some(port) => format!("{}://{host}:{port}", url.scheme()),
None => format!("{}://{host}", url.scheme()),
}
}
pub(crate) fn assert_credential_destination(url: &Url, redirect: bool) -> Result<(), InsecureTransportError> {
if is_trusted_destination(url) {
return Ok(());
}
Err(InsecureTransportError {
origin: origin_of(url),
redirect,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn url(s: &str) -> Url {
Url::parse(s).unwrap()
}
#[test]
fn https_is_trusted_anywhere() {
assert!(is_trusted_destination(&url("https://api.0xinsider.com")));
assert!(is_trusted_destination(&url("https://example.com:8443/x")));
}
#[test]
fn http_is_trusted_only_on_loopback() {
assert!(is_trusted_destination(&url("http://localhost:8080")));
assert!(is_trusted_destination(&url("http://127.0.0.1")));
assert!(is_trusted_destination(&url("http://127.8.9.10")));
assert!(is_trusted_destination(&url("http://[::1]:3000")));
assert!(!is_trusted_destination(&url("http://api.0xinsider.com")));
assert!(!is_trusted_destination(&url("http://10.0.0.1")));
assert!(!is_trusted_destination(&url("http://localhost.evil.com")));
}
#[test]
fn other_schemes_are_refused() {
assert!(!is_trusted_destination(&url("ftp://localhost")));
}
#[test]
fn the_error_names_the_origin_never_a_token() {
let error = assert_credential_destination(&url("http://proxy.example:8080/api?key=x"), true).unwrap_err();
assert_eq!(error.origin, "http://proxy.example:8080");
assert!(error.to_string().contains("a redirect"));
assert!(!error.to_string().contains("key=x"));
}
}