use std::net::IpAddr;
use serde::Deserialize;
use crate::query::DomainName;
pub const IPV4_URL: &str = "https://data.iana.org/rdap/ipv4.json";
pub const IPV6_URL: &str = "https://data.iana.org/rdap/ipv6.json";
pub const DNS_URL: &str = "https://data.iana.org/rdap/dns.json";
#[derive(Clone, Debug, Default, Deserialize)]
pub struct Registry {
#[serde(default)]
services: Vec<Vec<Vec<String>>>,
}
impl Registry {
pub fn from_slice(bytes: &[u8]) -> Result<Self, serde_json::Error> {
serde_json::from_slice(bytes)
}
pub fn server_for_ip(&self, ip: IpAddr) -> Option<&str> {
let mut best: Option<(u8, &str)> = None;
for (keys, urls) in self.services() {
for key in keys {
let Some(length) = prefix_length_containing(key, ip) else {
continue;
};
if best.is_some_and(|(found, _)| found >= length) {
continue;
}
if let Some(url) = preferred(urls) {
best = Some((length, url));
}
}
}
best.map(|(_, url)| url)
}
pub fn server_for_domain(&self, domain: &DomainName) -> Option<&str> {
let name = domain.as_str();
let mut best: Option<(usize, &str)> = None;
for (keys, urls) in self.services() {
for key in keys {
let key = key.trim_matches('.').to_lowercase();
if !suffix_matches(name, &key) {
continue;
}
let labels = key.split('.').count();
if best.is_some_and(|(found, _)| found >= labels) {
continue;
}
if let Some(url) = preferred(urls) {
best = Some((labels, url));
}
}
}
best.map(|(_, url)| url)
}
fn services(&self) -> impl Iterator<Item = (&Vec<String>, &Vec<String>)> {
self.services
.iter()
.filter_map(|entry| Some((entry.first()?, entry.get(1)?)))
}
}
fn prefix_length_containing(range: &str, ip: IpAddr) -> Option<u8> {
let network: ipnet::IpNet = range.parse().ok()?;
network.contains(&ip).then_some(network.prefix_len())
}
fn suffix_matches(name: &str, suffix: &str) -> bool {
if suffix.is_empty() {
return false;
}
let Some(rest) = name.strip_suffix(suffix) else {
return false;
};
rest.is_empty() || rest.ends_with('.')
}
fn preferred(urls: &[String]) -> Option<&str> {
let with_scheme = |scheme: &str| urls.iter().find(|url| url.starts_with(scheme));
with_scheme("https://")
.or_else(|| with_scheme("http://"))
.map(String::as_str)
}
#[derive(Clone, Debug, Default)]
pub struct Bootstrap {
pub ipv4: Registry,
pub ipv6: Registry,
pub dns: Registry,
}
impl Bootstrap {
pub fn server_for_ip(&self, ip: IpAddr) -> Option<&str> {
match ip {
IpAddr::V4(_) => self.ipv4.server_for_ip(ip),
IpAddr::V6(_) => self.ipv6.server_for_ip(ip),
}
}
pub fn server_for_domain(&self, domain: &DomainName) -> Option<&str> {
self.dns.server_for_domain(domain)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn registry(json: &str) -> Registry {
Registry::from_slice(json.as_bytes()).unwrap()
}
const IPV4: &str = r#"{"version":"1.0","services":[
[["41.0.0.0/8","102.0.0.0/8"],["https://rdap.afrinic.net/rdap/"]],
[["1.0.0.0/8","27.0.0.0/8"],["https://rdap.apnic.net/"]],
[["8.0.0.0/8"],["https://rdap.arin.net/registry/","http://rdap.arin.net/registry/"]]
]}"#;
const DNS: &str = r#"{"version":"1.0","services":[
[["com","net"],["https://rdap.verisign.com/com/v1/"]],
[["kg"],["http://rdap.cctld.kg/"]],
[["uk"],["https://rdap.nominet.uk/uk/"]],
[["co.uk"],["https://rdap.example.co.uk/"]]
]}"#;
#[test]
fn finds_the_server_for_an_ipv4_address() {
assert_eq!(
registry(IPV4).server_for_ip("8.8.8.8".parse().unwrap()),
Some("https://rdap.arin.net/registry/")
);
}
#[test]
fn finds_the_server_for_a_second_registry() {
assert_eq!(
registry(IPV4).server_for_ip("27.1.2.3".parse().unwrap()),
Some("https://rdap.apnic.net/")
);
}
#[test]
fn gives_nothing_for_an_address_no_service_holds() {
assert_eq!(
registry(IPV4).server_for_ip("192.0.2.1".parse().unwrap()),
None
);
}
#[test]
fn prefers_the_https_server() {
let ipv4 = registry(IPV4);
let found = ipv4.server_for_ip("8.8.8.8".parse().unwrap());
assert_eq!(found, Some("https://rdap.arin.net/registry/"));
}
#[test]
fn uses_an_http_server_when_a_registry_lists_no_other() {
let domain = "example.kg".parse().unwrap();
assert_eq!(
registry(DNS).server_for_domain(&domain),
Some("http://rdap.cctld.kg/")
);
}
#[test]
fn skips_a_server_with_a_scheme_the_client_cannot_use() {
let listed = |urls: &[&str]| urls.iter().map(|url| (*url).to_owned()).collect::<Vec<_>>();
assert_eq!(
preferred(&listed(&["ftp://bad.example/", "http://usable.example/"])),
Some("http://usable.example/")
);
assert_eq!(
preferred(&listed(&[
"http://plain.example/",
"https://secure.example/"
])),
Some("https://secure.example/")
);
assert_eq!(preferred(&listed(&["ftp://bad.example/"])), None);
assert_eq!(preferred(&[]), None);
}
#[test]
fn takes_the_most_specific_range() {
let wide_and_narrow = registry(
r#"{"services":[
[["10.0.0.0/8"],["https://wide.example/"]],
[["10.1.0.0/16"],["https://narrow.example/"]]
]}"#,
);
assert_eq!(
wide_and_narrow.server_for_ip("10.1.2.3".parse().unwrap()),
Some("https://narrow.example/")
);
assert_eq!(
wide_and_narrow.server_for_ip("10.2.2.3".parse().unwrap()),
Some("https://wide.example/")
);
}
#[test]
fn takes_the_longest_run_of_labels() {
let domain = "shop.example.co.uk".parse().unwrap();
assert_eq!(
registry(DNS).server_for_domain(&domain),
Some("https://rdap.example.co.uk/")
);
}
#[test]
fn matches_a_suffix_on_whole_labels() {
let domain = "mycom".parse::<DomainName>();
assert!(domain.is_err(), "a name with no dot is not a domain");
assert!(!suffix_matches("mycom", "com"));
assert!(suffix_matches("example.com", "com"));
assert!(suffix_matches("com", "com"));
}
#[test]
fn finds_the_server_for_an_ipv6_address() {
let ipv6 = registry(
r#"{"services":[
[["2001:4200::/23","2c00::/12"],["https://rdap.afrinic.net/rdap/"]],
[["2001:4800::/23"],["https://rdap.arin.net/registry/"]]
]}"#,
);
assert_eq!(
ipv6.server_for_ip("2c00::1".parse().unwrap()),
Some("https://rdap.afrinic.net/rdap/")
);
}
#[test]
fn an_address_of_another_family_matches_nothing() {
assert_eq!(
registry(IPV4).server_for_ip("2c00::1".parse().unwrap()),
None
);
}
#[test]
fn a_zero_length_prefix_holds_every_address() {
let catch_all = registry(r#"{"services":[[["0.0.0.0/0"],["https://any.example/"]]]}"#);
assert_eq!(
catch_all.server_for_ip("203.0.113.9".parse().unwrap()),
Some("https://any.example/")
);
}
#[test]
fn skips_a_service_in_a_shape_the_format_does_not_describe() {
let mixed = registry(
r#"{"services":[
[["8.0.0.0/8"]],
[["not-a-range"],["https://bad.example/"]],
[["8.0.0.0/8"],["https://good.example/"]]
]}"#,
);
assert_eq!(
mixed.server_for_ip("8.8.8.8".parse().unwrap()),
Some("https://good.example/")
);
}
#[test]
fn reads_an_empty_registry() {
assert_eq!(
registry("{}").server_for_ip("8.8.8.8".parse().unwrap()),
None
);
}
#[test]
fn bootstrap_picks_the_registry_by_address_family() {
let bootstrap = Bootstrap {
ipv4: registry(IPV4),
ipv6: registry(r#"{"services":[[["2c00::/12"],["https://v6.example/"]]]}"#),
dns: registry(DNS),
};
assert_eq!(
bootstrap.server_for_ip("8.8.8.8".parse().unwrap()),
Some("https://rdap.arin.net/registry/")
);
assert_eq!(
bootstrap.server_for_ip("2c00::1".parse().unwrap()),
Some("https://v6.example/")
);
}
}