main
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<Url, UnfurlError> {
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::<IpAddr>()
&& !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<Vec<std::net::SocketAddr>, UnfurlError> {
let addrs = tokio::net::lookup_host((host, port))
.await
.map_err(|error| UnfurlError::Fetch(format!("dns lookup failed: {error}")))?
.collect::<Vec<_>>();
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)
}