use anyhow::{Context, Result, anyhow, bail};
use std::net::IpAddr;
use url::Url;
pub(super) fn extract_domain(url: &str) -> Result<String> {
let parsed = Url::parse(url).with_context(|| format!("Failed to parse URL: {url}"))?;
let host = parsed.host_str().ok_or_else(|| anyhow!("URL has no host: {url}"))?;
if host.is_empty() {
bail!("URL has empty host: {url}");
}
Ok(host.to_string())
}
pub(crate) fn is_private_host(host: &str) -> bool {
let trimmed = host.strip_prefix('[').and_then(|s| s.strip_suffix(']')).unwrap_or(host);
if let Ok(ip) = trimmed.parse::<IpAddr>() {
return match ip {
IpAddr::V4(v4) => is_private_ipv4(v4),
IpAddr::V6(v6) => is_private_ipv6(v6),
};
}
if trimmed.eq_ignore_ascii_case("localhost") || trimmed.eq_ignore_ascii_case("localhost.localdomain") {
return true;
}
false
}
fn is_private_ipv4(v4: std::net::Ipv4Addr) -> bool {
let octets = v4.octets();
octets[0] == 127
|| octets[0] == 10
|| (octets[0] == 172 && (octets[1] & 0xf0) == 16)
|| (octets[0] == 192 && octets[1] == 168)
|| (octets[0] == 169 && octets[1] == 254)
|| octets[0] == 0
|| v4.is_broadcast()
|| (octets[0] & 0xf0) == 224
}
fn is_private_ipv6(v6: std::net::Ipv6Addr) -> bool {
let segments = v6.segments();
if v6.is_loopback()
|| v6.is_unspecified()
|| (segments[0] & 0xfe00) == 0xfc00
|| (segments[0] & 0xffc0) == 0xfe80
{
return true;
}
let is_ipv4_mapped = segments[0] == 0
&& segments[1] == 0
&& segments[2] == 0
&& segments[3] == 0
&& segments[4] == 0
&& segments[5] == 0xffff;
let is_ipv4_compat = segments[0] == 0
&& segments[1] == 0
&& segments[2] == 0
&& segments[3] == 0
&& segments[4] == 0
&& segments[5] == 0;
if is_ipv4_mapped || is_ipv4_compat {
let embedded = std::net::Ipv4Addr::new(
(segments[6] >> 8) as u8,
(segments[6] & 0xff) as u8,
(segments[7] >> 8) as u8,
(segments[7] & 0xff) as u8,
);
return is_private_ipv4(embedded);
}
false
}
pub(super) fn domain_matches_allowed(domain: &str, allowed: &str) -> bool {
let normalized_domain = domain.trim_end_matches('.').to_ascii_lowercase();
let normalized_allowed = allowed.trim_start_matches('.').trim_end_matches('.').to_ascii_lowercase();
if let Some(suffix) = normalized_allowed.strip_prefix("*.") {
if !suffix.contains('.') {
return false;
}
return normalized_domain == suffix || normalized_domain.ends_with(&format!(".{suffix}"));
}
normalized_domain == normalized_allowed || normalized_domain.ends_with(&format!(".{normalized_allowed}"))
}