use std::net::IpAddr;
use crate::error::{Result, SeerError};
pub(super) fn validate_bootstrap_url(s: &str) -> Result<url::Url> {
let parsed = url::Url::parse(s)
.map_err(|e| SeerError::RdapError(format!("bad bootstrap URL {}: {}", s, e)))?;
if parsed.scheme() != "https" {
return Err(SeerError::RdapError(format!(
"bootstrap URL must be https, got {}",
parsed.scheme()
)));
}
let host = parsed
.host()
.ok_or_else(|| SeerError::RdapError(format!("bootstrap URL has no host: {}", s)))?;
match host {
url::Host::Ipv4(_) | url::Host::Ipv6(_) => {
return Err(SeerError::RdapError(format!(
"bootstrap URL must not be an IP literal: {}",
s
)));
}
url::Host::Domain(d) => {
if d.is_empty() || d.chars().any(|c| c.is_whitespace() || c.is_control()) {
return Err(SeerError::RdapError(format!(
"bootstrap URL has invalid host: {}",
s
)));
}
}
}
Ok(parsed)
}
pub(super) fn parse_asn_range(range: &str) -> Option<(u32, u32)> {
if let Some(pos) = range.find('-') {
let start = range[..pos].parse().ok()?;
let end = range[pos + 1..].parse().ok()?;
Some((start, end))
} else {
let num = range.parse().ok()?;
Some((num, num))
}
}
pub(super) fn ip_matches_prefix(prefix: &str, ip: IpAddr) -> bool {
let (addr_part, mask_part) = match prefix.split_once('/') {
Some((a, m)) => (a, Some(m)),
None => (prefix, None),
};
let (ip_value, prefix_value, width) = match (ip, addr_part.parse::<IpAddr>()) {
(IpAddr::V4(ip), Ok(IpAddr::V4(p))) => (u32::from(ip).into(), u32::from(p).into(), 32),
(IpAddr::V6(ip), Ok(IpAddr::V6(p))) => (u128::from(ip), u128::from(p), 128),
_ => return false,
};
let mask_bits: u32 = match mask_part.and_then(|s| s.parse().ok()) {
Some(bits) if bits <= width => bits,
Some(_) => return false,
None => width,
};
let mask = u128::MAX.checked_shl(width - mask_bits).unwrap_or(0);
(ip_value & mask) == (prefix_value & mask)
}
#[cfg(test)]
mod tests {
use super::*;
fn matches(prefix: &str, ip: &str) -> bool {
ip_matches_prefix(prefix, ip.parse().unwrap())
}
#[test]
fn test_ipv4_prefix_matching_partial_mask() {
assert!(matches("203.0.112.0/21", "203.0.114.1"));
assert!(!matches("203.0.112.0/21", "203.0.120.1"));
}
#[test]
fn test_ipv6_prefix_matching_partial_mask() {
assert!(matches("2001:db8::/33", "2001:db8::1"));
assert!(!matches("2001:db8::/33", "2001:db9::1"));
}
#[test]
fn test_prefix_matching_edge_masks_and_families() {
assert!(matches("0.0.0.0/0", "198.51.100.7"));
assert!(matches("::/0", "2001:db8::1"));
assert!(!matches("0.0.0.0/0", "2001:db8::1"));
assert!(!matches("::/0", "198.51.100.7"));
assert!(matches("198.51.100.7/32", "198.51.100.7"));
assert!(!matches("198.51.100.7", "198.51.100.8"));
assert!(matches("198.51.100.7/x", "198.51.100.7"));
assert!(!matches("198.51.100.7/x", "198.51.100.8"));
assert!(!matches("198.51.100.0/33", "198.51.100.7"));
assert!(!matches("2001:db8::/129", "2001:db8::1"));
assert!(!matches("not-an-ip/8", "10.0.0.1"));
}
#[test]
fn test_validate_bootstrap_url_accepts_https() {
let url = validate_bootstrap_url("https://rdap.example.com/").unwrap();
assert_eq!(url.scheme(), "https");
assert_eq!(url.host_str(), Some("rdap.example.com"));
}
#[test]
fn test_validate_bootstrap_url_rejects_http() {
let err = validate_bootstrap_url("http://rdap.example.com/").unwrap_err();
assert!(
matches!(err, SeerError::RdapError(ref s) if s.contains("https")),
"expected https-scheme error, got: {:?}",
err
);
}
#[test]
fn test_validate_bootstrap_url_rejects_ftp() {
let err = validate_bootstrap_url("ftp://rdap.example.com/").unwrap_err();
assert!(matches!(err, SeerError::RdapError(_)));
}
#[test]
fn test_validate_bootstrap_url_rejects_ip_literal_v4() {
let err = validate_bootstrap_url("https://192.0.2.1/").unwrap_err();
assert!(
matches!(err, SeerError::RdapError(ref s) if s.contains("IP literal")),
"expected IP-literal error, got: {:?}",
err
);
}
#[test]
fn test_validate_bootstrap_url_rejects_ip_literal_v6() {
let err = validate_bootstrap_url("https://[2001:db8::1]/").unwrap_err();
assert!(
matches!(err, SeerError::RdapError(ref s) if s.contains("IP literal")),
"expected IP-literal error, got: {:?}",
err
);
}
#[test]
fn test_validate_bootstrap_url_rejects_garbage() {
let err = validate_bootstrap_url("not a url").unwrap_err();
assert!(matches!(err, SeerError::RdapError(_)));
}
}