1use std::net::IpAddr;
11
12use serde::Deserialize;
13
14use crate::query::DomainName;
15
16pub const IPV4_URL: &str = "https://data.iana.org/rdap/ipv4.json";
18
19pub const IPV6_URL: &str = "https://data.iana.org/rdap/ipv6.json";
21
22pub const DNS_URL: &str = "https://data.iana.org/rdap/dns.json";
24
25#[derive(Clone, Debug, Default, Deserialize)]
31pub struct Registry {
32 #[serde(default)]
34 services: Vec<Vec<Vec<String>>>,
35}
36
37impl Registry {
38 pub fn from_slice(bytes: &[u8]) -> Result<Self, serde_json::Error> {
44 serde_json::from_slice(bytes)
45 }
46
47 pub fn server_for_ip(&self, ip: IpAddr) -> Option<&str> {
52 let mut best: Option<(u8, &str)> = None;
53
54 for (keys, urls) in self.services() {
55 for key in keys {
56 let Some(length) = prefix_length_containing(key, ip) else {
57 continue;
58 };
59 if best.is_some_and(|(found, _)| found >= length) {
60 continue;
61 }
62 if let Some(url) = preferred(urls) {
63 best = Some((length, url));
64 }
65 }
66 }
67
68 best.map(|(_, url)| url)
69 }
70
71 pub fn server_for_domain(&self, domain: &DomainName) -> Option<&str> {
76 let name = domain.as_str();
77 let mut best: Option<(usize, &str)> = None;
78
79 for (keys, urls) in self.services() {
80 for key in keys {
81 let key = key.trim_matches('.').to_lowercase();
82 if !suffix_matches(name, &key) {
83 continue;
84 }
85 let labels = key.split('.').count();
86 if best.is_some_and(|(found, _)| found >= labels) {
87 continue;
88 }
89 if let Some(url) = preferred(urls) {
90 best = Some((labels, url));
91 }
92 }
93 }
94
95 best.map(|(_, url)| url)
96 }
97
98 fn services(&self) -> impl Iterator<Item = (&Vec<String>, &Vec<String>)> {
103 self.services
104 .iter()
105 .filter_map(|entry| Some((entry.first()?, entry.get(1)?)))
106 }
107}
108
109fn prefix_length_containing(range: &str, ip: IpAddr) -> Option<u8> {
113 let network: ipnet::IpNet = range.parse().ok()?;
114
115 network.contains(&ip).then_some(network.prefix_len())
116}
117
118fn suffix_matches(name: &str, suffix: &str) -> bool {
123 if suffix.is_empty() {
124 return false;
125 }
126 let Some(rest) = name.strip_suffix(suffix) else {
127 return false;
128 };
129 rest.is_empty() || rest.ends_with('.')
130}
131
132fn preferred(urls: &[String]) -> Option<&str> {
139 let with_scheme = |scheme: &str| urls.iter().find(|url| url.starts_with(scheme));
140
141 with_scheme("https://")
142 .or_else(|| with_scheme("http://"))
143 .map(String::as_str)
144}
145
146#[derive(Clone, Debug, Default)]
151pub struct Bootstrap {
152 pub ipv4: Registry,
154 pub ipv6: Registry,
156 pub dns: Registry,
158}
159
160impl Bootstrap {
161 pub fn server_for_ip(&self, ip: IpAddr) -> Option<&str> {
163 match ip {
164 IpAddr::V4(_) => self.ipv4.server_for_ip(ip),
165 IpAddr::V6(_) => self.ipv6.server_for_ip(ip),
166 }
167 }
168
169 pub fn server_for_domain(&self, domain: &DomainName) -> Option<&str> {
171 self.dns.server_for_domain(domain)
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 fn registry(json: &str) -> Registry {
180 Registry::from_slice(json.as_bytes()).unwrap()
181 }
182
183 const IPV4: &str = r#"{"version":"1.0","services":[
184 [["41.0.0.0/8","102.0.0.0/8"],["https://rdap.afrinic.net/rdap/"]],
185 [["1.0.0.0/8","27.0.0.0/8"],["https://rdap.apnic.net/"]],
186 [["8.0.0.0/8"],["https://rdap.arin.net/registry/","http://rdap.arin.net/registry/"]]
187 ]}"#;
188
189 const DNS: &str = r#"{"version":"1.0","services":[
190 [["com","net"],["https://rdap.verisign.com/com/v1/"]],
191 [["kg"],["http://rdap.cctld.kg/"]],
192 [["uk"],["https://rdap.nominet.uk/uk/"]],
193 [["co.uk"],["https://rdap.example.co.uk/"]]
194 ]}"#;
195
196 #[test]
197 fn finds_the_server_for_an_ipv4_address() {
198 assert_eq!(
199 registry(IPV4).server_for_ip("8.8.8.8".parse().unwrap()),
200 Some("https://rdap.arin.net/registry/")
201 );
202 }
203
204 #[test]
205 fn finds_the_server_for_a_second_registry() {
206 assert_eq!(
207 registry(IPV4).server_for_ip("27.1.2.3".parse().unwrap()),
208 Some("https://rdap.apnic.net/")
209 );
210 }
211
212 #[test]
213 fn gives_nothing_for_an_address_no_service_holds() {
214 assert_eq!(
215 registry(IPV4).server_for_ip("192.0.2.1".parse().unwrap()),
216 None
217 );
218 }
219
220 #[test]
221 fn prefers_the_https_server() {
222 let ipv4 = registry(IPV4);
224
225 let found = ipv4.server_for_ip("8.8.8.8".parse().unwrap());
226
227 assert_eq!(found, Some("https://rdap.arin.net/registry/"));
228 }
229
230 #[test]
231 fn uses_an_http_server_when_a_registry_lists_no_other() {
232 let domain = "example.kg".parse().unwrap();
233
234 assert_eq!(
235 registry(DNS).server_for_domain(&domain),
236 Some("http://rdap.cctld.kg/")
237 );
238 }
239
240 #[test]
241 fn skips_a_server_with_a_scheme_the_client_cannot_use() {
242 let listed = |urls: &[&str]| urls.iter().map(|url| (*url).to_owned()).collect::<Vec<_>>();
243
244 assert_eq!(
245 preferred(&listed(&["ftp://bad.example/", "http://usable.example/"])),
246 Some("http://usable.example/")
247 );
248 assert_eq!(
249 preferred(&listed(&[
250 "http://plain.example/",
251 "https://secure.example/"
252 ])),
253 Some("https://secure.example/")
254 );
255 assert_eq!(preferred(&listed(&["ftp://bad.example/"])), None);
256 assert_eq!(preferred(&[]), None);
257 }
258
259 #[test]
260 fn takes_the_most_specific_range() {
261 let wide_and_narrow = registry(
262 r#"{"services":[
263 [["10.0.0.0/8"],["https://wide.example/"]],
264 [["10.1.0.0/16"],["https://narrow.example/"]]
265 ]}"#,
266 );
267
268 assert_eq!(
269 wide_and_narrow.server_for_ip("10.1.2.3".parse().unwrap()),
270 Some("https://narrow.example/")
271 );
272 assert_eq!(
273 wide_and_narrow.server_for_ip("10.2.2.3".parse().unwrap()),
274 Some("https://wide.example/")
275 );
276 }
277
278 #[test]
279 fn takes_the_longest_run_of_labels() {
280 let domain = "shop.example.co.uk".parse().unwrap();
281
282 assert_eq!(
283 registry(DNS).server_for_domain(&domain),
284 Some("https://rdap.example.co.uk/")
285 );
286 }
287
288 #[test]
289 fn matches_a_suffix_on_whole_labels() {
290 let domain = "mycom".parse::<DomainName>();
292
293 assert!(domain.is_err(), "a name with no dot is not a domain");
294 assert!(!suffix_matches("mycom", "com"));
295 assert!(suffix_matches("example.com", "com"));
296 assert!(suffix_matches("com", "com"));
297 }
298
299 #[test]
300 fn finds_the_server_for_an_ipv6_address() {
301 let ipv6 = registry(
302 r#"{"services":[
303 [["2001:4200::/23","2c00::/12"],["https://rdap.afrinic.net/rdap/"]],
304 [["2001:4800::/23"],["https://rdap.arin.net/registry/"]]
305 ]}"#,
306 );
307
308 assert_eq!(
309 ipv6.server_for_ip("2c00::1".parse().unwrap()),
310 Some("https://rdap.afrinic.net/rdap/")
311 );
312 }
313
314 #[test]
315 fn an_address_of_another_family_matches_nothing() {
316 assert_eq!(
317 registry(IPV4).server_for_ip("2c00::1".parse().unwrap()),
318 None
319 );
320 }
321
322 #[test]
323 fn a_zero_length_prefix_holds_every_address() {
324 let catch_all = registry(r#"{"services":[[["0.0.0.0/0"],["https://any.example/"]]]}"#);
325
326 assert_eq!(
327 catch_all.server_for_ip("203.0.113.9".parse().unwrap()),
328 Some("https://any.example/")
329 );
330 }
331
332 #[test]
333 fn skips_a_service_in_a_shape_the_format_does_not_describe() {
334 let mixed = registry(
335 r#"{"services":[
336 [["8.0.0.0/8"]],
337 [["not-a-range"],["https://bad.example/"]],
338 [["8.0.0.0/8"],["https://good.example/"]]
339 ]}"#,
340 );
341
342 assert_eq!(
343 mixed.server_for_ip("8.8.8.8".parse().unwrap()),
344 Some("https://good.example/")
345 );
346 }
347
348 #[test]
349 fn reads_an_empty_registry() {
350 assert_eq!(
351 registry("{}").server_for_ip("8.8.8.8".parse().unwrap()),
352 None
353 );
354 }
355
356 #[test]
357 fn bootstrap_picks_the_registry_by_address_family() {
358 let bootstrap = Bootstrap {
359 ipv4: registry(IPV4),
360 ipv6: registry(r#"{"services":[[["2c00::/12"],["https://v6.example/"]]]}"#),
361 dns: registry(DNS),
362 };
363
364 assert_eq!(
365 bootstrap.server_for_ip("8.8.8.8".parse().unwrap()),
366 Some("https://rdap.arin.net/registry/")
367 );
368 assert_eq!(
369 bootstrap.server_for_ip("2c00::1".parse().unwrap()),
370 Some("https://v6.example/")
371 );
372 }
373}