use url::{Host, Url};
pub fn validate_url(url: &str) -> Result<Url, String> {
let parsed = Url::parse(url).map_err(|e| format!("invalid URL: {e}"))?;
match parsed.scheme() {
"http" | "https" => {}
"file" => return Err("file:// URLs are not allowed".to_string()),
other => return Err(format!("unsupported scheme: {other}")),
}
match parsed.host() {
None => return Err("URL has no host".to_string()),
Some(Host::Domain(host)) => {
let lower = host.to_ascii_lowercase();
if lower == "localhost" || lower == "localhost.localdomain" {
return Err("requests to localhost are not allowed".to_string());
}
}
Some(Host::Ipv4(v4)) => {
if v4.is_private() || v4.is_loopback() || v4.is_link_local() || v4.is_broadcast() {
return Err("requests to private/internal IP addresses are not allowed".to_string());
}
if v4.is_multicast() {
return Err("requests to multicast addresses are not allowed".to_string());
}
}
Some(Host::Ipv6(v6)) => {
if v6.is_loopback() || v6.is_multicast() {
return Err("requests to internal IPv6 addresses are not allowed".to_string());
}
if (v6.segments()[0] & 0xfe00) == 0xfc00 {
return Err("requests to private IPv6 addresses are not allowed".to_string());
}
}
}
Ok(parsed)
}