use url::Url;
use crate::error::Error;
use crate::http::{HeaderMap, HeaderValue};
const SENSITIVE_HEADERS: &[&str] = &[
"authorization",
"proxy-authorization",
"cookie",
"set-cookie",
"x-csrf-token",
];
pub fn require_secure_endpoint(url: &Url) -> Result<(), Error> {
if url.scheme() == "https" || (url.scheme() == "http" && is_localhost(url)) {
Ok(())
} else {
Err(Error::usage(format!(
"{} must use HTTPS",
url.origin().ascii_serialization()
)))
}
}
pub fn is_localhost(url: &Url) -> bool {
match url.host_str() {
Some(host) => {
let host = host
.trim_start_matches('[')
.trim_end_matches(']')
.to_ascii_lowercase();
host == "localhost"
|| host == "127.0.0.1"
|| host == "::1"
|| host.ends_with(".localhost")
}
None => false,
}
}
pub fn is_same_origin(a: &Url, b: &Url) -> bool {
a.scheme().eq_ignore_ascii_case(b.scheme())
&& a.host_str()
.unwrap_or_default()
.eq_ignore_ascii_case(b.host_str().unwrap_or_default())
&& a.port_or_known_default() == b.port_or_known_default()
}
pub fn redact_headers(headers: &HeaderMap) -> HeaderMap {
let mut redacted = headers.clone();
for name in SENSITIVE_HEADERS {
if redacted.contains_key(*name) {
redacted.insert(*name, HeaderValue::from_static("[REDACTED]"));
}
}
redacted
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn localhost_may_use_plain_http() {
assert!(require_secure_endpoint(&Url::parse("http://localhost:3000").unwrap()).is_ok());
assert!(require_secure_endpoint(&Url::parse("http://127.0.0.1:8080/x").unwrap()).is_ok());
assert!(require_secure_endpoint(&Url::parse("http://app.localhost").unwrap()).is_ok());
assert!(require_secure_endpoint(&Url::parse("https://fizzy.do").unwrap()).is_ok());
assert!(require_secure_endpoint(&Url::parse("http://evil.example.com").unwrap()).is_err());
let error = require_secure_endpoint(
&Url::parse("http://user:s3cret@evil.example.com/x?t=s3cret").unwrap(),
)
.unwrap_err();
assert!(!error.to_string().contains("s3cret"), "{error}");
}
#[test]
fn same_origin_ignores_default_ports_and_case() {
let a = Url::parse("https://Fizzy.DO/999/boards").unwrap();
assert!(is_same_origin(
&a,
&Url::parse("HTTPS://fizzy.do:443/other").unwrap()
));
assert!(!is_same_origin(
&a,
&Url::parse("http://fizzy.do/other").unwrap()
));
assert!(!is_same_origin(
&a,
&Url::parse("https://evil.example.com/boards").unwrap()
));
}
#[test]
fn credentials_are_redacted() {
let mut headers = HeaderMap::new();
headers.insert("Authorization", HeaderValue::from_static("Bearer secret"));
headers.insert("Cookie", HeaderValue::from_static("session_token=secret"));
headers.insert(
"Proxy-Authorization",
HeaderValue::from_static("Basic secret"),
);
headers.insert("Accept", HeaderValue::from_static("application/json"));
let redacted = redact_headers(&headers);
assert_eq!(redacted["authorization"], "[REDACTED]");
assert_eq!(redacted["cookie"], "[REDACTED]");
assert_eq!(redacted["proxy-authorization"], "[REDACTED]");
assert_eq!(redacted["accept"], "application/json");
}
}