2026-02-05 04:04:08 -08:00
|
|
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
|
|
|
|
|
|
|
|
|
use crate::error::Error;
|
|
|
|
|
use tokio::net::lookup_host;
|
2026-04-15 04:38:29 -07:00
|
|
|
use url::{Host, Url};
|
2026-02-05 04:04:08 -08:00
|
|
|
|
|
|
|
|
// TODO: Use is_global() once stabilized
|
|
|
|
|
// https://doc.rust-lang.org/std/net/enum.IpAddr.html#method.is_global
|
2026-04-15 04:38:29 -07:00
|
|
|
pub(crate) async fn validate_ip(url: &Url) -> Result<(), Error> {
|
|
|
|
|
let mut ip = vec![];
|
|
|
|
|
let host = url
|
|
|
|
|
.host()
|
|
|
|
|
.ok_or(Error::UrlVerificationError("Url must have a domain"))?;
|
|
|
|
|
match host {
|
|
|
|
|
Host::Domain(domain) => ip.extend(
|
|
|
|
|
lookup_host((domain.to_owned(), 80))
|
|
|
|
|
.await?
|
|
|
|
|
.map(|s| s.ip().to_canonical()),
|
|
|
|
|
),
|
|
|
|
|
Host::Ipv4(ipv4) => ip.push(ipv4.into()),
|
|
|
|
|
Host::Ipv6(ipv6) => ip.push(ipv6.into()),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let invalid_ip = ip.into_iter().any(|addr| match addr {
|
2026-02-05 04:04:08 -08:00
|
|
|
IpAddr::V4(addr) => v4_is_invalid(addr),
|
|
|
|
|
IpAddr::V6(addr) => v6_is_invalid(addr),
|
2026-04-15 04:38:29 -07:00
|
|
|
});
|
|
|
|
|
if invalid_ip {
|
|
|
|
|
return Err(Error::DomainResolveError(host.to_string()));
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
2026-02-05 04:04:08 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn v4_is_invalid(v4: Ipv4Addr) -> bool {
|
|
|
|
|
v4.is_private()
|
|
|
|
|
|| v4.is_loopback()
|
|
|
|
|
|| v4.is_link_local()
|
|
|
|
|
|| v4.is_multicast()
|
|
|
|
|
|| v4.is_documentation()
|
2026-03-16 03:11:01 -07:00
|
|
|
|| v4.is_unspecified()
|
|
|
|
|
|| v4.is_broadcast()
|
2026-02-05 04:04:08 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn v6_is_invalid(v6: Ipv6Addr) -> bool {
|
|
|
|
|
v6.is_loopback()
|
|
|
|
|
|| v6.is_multicast()
|
|
|
|
|
|| v6.is_unique_local()
|
|
|
|
|
|| v6.is_unicast_link_local()
|
|
|
|
|
|| v6.is_unspecified()
|
|
|
|
|
|| v6_is_documentation(v6)
|
|
|
|
|
|| v6.to_ipv4_mapped().is_some_and(v4_is_invalid)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn v6_is_documentation(v6: std::net::Ipv6Addr) -> bool {
|
|
|
|
|
matches!(
|
|
|
|
|
v6.segments(),
|
|
|
|
|
[0x2001, 0xdb8, ..] | [0x3fff, 0..=0x0fff, ..]
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
#[allow(clippy::unwrap_used)]
|
|
|
|
|
mod test {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_is_valid_ip() -> Result<(), Error> {
|
2026-04-15 04:38:29 -07:00
|
|
|
assert!(validate_ip(&Url::parse("http://example.com")?)
|
|
|
|
|
.await
|
|
|
|
|
.is_ok());
|
|
|
|
|
assert!(validate_ip(&Url::parse("http://172.66.147.243")?)
|
|
|
|
|
.await
|
|
|
|
|
.is_ok());
|
|
|
|
|
assert!(validate_ip(&Url::parse("http://localhost")?).await.is_err());
|
|
|
|
|
assert!(validate_ip(&Url::parse("http://127.0.0.1")?).await.is_err());
|
2026-02-05 04:04:08 -08:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|