use crate::error::{Error, Result};
pub fn require_secure_url(url: &str, field: &str) -> Result<()> {
if is_secure_url(url) {
return Ok(());
}
Err(Error::config(format!(
"{field} must be https:// or point to a loopback host \
(refusing to send credentials over plaintext HTTP)"
)))
}
#[must_use]
pub fn is_secure_url(url: &str) -> bool {
let Ok(parsed) = url::Url::parse(url.trim()) else {
return false;
};
match parsed.scheme() {
"https" => true,
"http" => match parsed.host() {
Some(url::Host::Domain(d)) => d.eq_ignore_ascii_case("localhost"),
Some(url::Host::Ipv4(ip)) => ip.is_loopback(),
Some(url::Host::Ipv6(ip)) => ip.is_loopback(),
None => false,
},
_ => false,
}
}
#[must_use]
pub fn header_looks_credential_bearing(name: &str) -> bool {
let lower = name.to_ascii_lowercase();
matches!(
lower.as_str(),
"authorization" | "cookie" | "proxy-authorization"
) || lower.starts_with("x-api")
|| lower.starts_with("x-auth")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn https_is_always_secure() {
assert!(is_secure_url("https://example.com"));
assert!(is_secure_url("HTTPS://Example.Com/path?x=1"));
assert!(is_secure_url("https://user:pass@example.com:8443/api"));
}
#[test]
fn plaintext_public_is_rejected() {
assert!(!is_secure_url("http://example.com"));
assert!(!is_secure_url("http://example.com:8080/api"));
assert!(!is_secure_url("http://user:secret@example.com"));
assert!(!is_secure_url("http://127.0.0.1@example.com"));
}
#[test]
fn backslash_authority_differential_is_rejected() {
assert!(!is_secure_url("http://evil.com\\@localhost/"));
assert!(!is_secure_url("http://evil.com\\localhost/"));
assert!(!is_secure_url("http://evil.com\\@127.0.0.1/"));
let parsed = url::Url::parse("http://evil.com\\@localhost/").unwrap();
assert_eq!(parsed.host_str(), Some("evil.com"));
}
#[test]
fn loopback_http_is_allowed() {
assert!(is_secure_url("http://localhost"));
assert!(is_secure_url("http://localhost:8000/api"));
assert!(is_secure_url("http://127.0.0.1:8000"));
assert!(is_secure_url("http://127.1.2.3"));
assert!(is_secure_url("http://[::1]"));
assert!(is_secure_url("http://[::1]:9000/x"));
}
#[test]
fn unknown_schemes_rejected() {
assert!(!is_secure_url("ftp://example.com"));
assert!(!is_secure_url("file:///etc/passwd"));
assert!(!is_secure_url(""));
assert!(!is_secure_url("example.com"));
}
#[test]
fn require_secure_url_surfaces_field_name() {
let err =
require_secure_url("http://api.example.com", "OpenAiConfig.base_url").unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("OpenAiConfig.base_url"),
"missing field: {msg}"
);
assert!(msg.contains("https"), "missing https hint: {msg}");
}
}