use std::net::IpAddr; use url::Url; use crate::UnfurlError; pub fn is_public_ip(ip: &IpAddr) -> bool { match ip { IpAddr::V4(v4) => { !v4.is_loopback() && !v4.is_private() && !v4.is_link_local() && !v4.is_broadcast() && !v4.is_documentation() && !v4.is_unspecified() && !(v4.octets()[0] == 100 && (v4.octets()[1] & 0b1100_0000) == 0b0100_0000) } IpAddr::V6(v6) => { !v6.is_loopback() && !v6.is_unspecified() && !v6.is_multicast() && { let segments = v6.segments(); !(segments[0] == 0x0064 && segments[1] == 0xff9b) && (segments[0] & 0xfe00) != 0xfc00 && (segments[0] & 0xffc0) != 0xfe80 } && match v6.to_ipv4_mapped() { Some(v4) => is_public_ip(&IpAddr::V4(v4)), None => true, } } } } pub fn parse(raw: &str) -> Result { let trimmed = raw.trim(); if trimmed.is_empty() || trimmed.len() > 8192 { return Err(UnfurlError::InvalidUrl("url is empty or too long".into())); } if trimmed.bytes().any(|byte| byte < 0x20 || byte == 0x7f) { return Err(UnfurlError::InvalidUrl("url contains control bytes".into())); } let url = Url::parse(trimmed).map_err(|error| UnfurlError::InvalidUrl(error.to_string()))?; validate_static(&url)?; Ok(url) } pub fn validate_static(url: &Url) -> Result<(), UnfurlError> { if !matches!(url.scheme(), "http" | "https") { return Err(UnfurlError::BlockedUrl(format!( "scheme {} is not allowed", url.scheme() ))); } if !url.username().is_empty() || url.password().is_some() { return Err(UnfurlError::BlockedUrl( "credentials in url are not allowed".into(), )); } let host = url .host_str() .ok_or_else(|| UnfurlError::InvalidUrl("host is required".into()))?; if let Ok(ip) = host.parse::() && !is_public_ip(&ip) { return Err(UnfurlError::BlockedUrl( "url points at a private or loopback address".into(), )); } Ok(()) } pub async fn resolve_public( host: &str, port: u16, ) -> Result, UnfurlError> { let addrs = tokio::net::lookup_host((host, port)) .await .map_err(|error| UnfurlError::Fetch(format!("dns lookup failed: {error}")))? .collect::>(); if addrs.is_empty() { return Err(UnfurlError::Fetch( "dns resolution returned no address".into(), )); } if addrs.iter().any(|addr| !is_public_ip(&addr.ip())) { return Err(UnfurlError::BlockedUrl( "url resolves to a private or loopback address".into(), )); } Ok(addrs) }