Skip to main content

icann_rdap_client/iana/
bootstrap.rs

1//! Does RDAP query bootstrapping.
2
3use std::sync::{Arc, RwLock};
4
5use icann_rdap_common::{
6    httpdata::HttpData,
7    iana::{
8        get_preferred_url, BootstrapRegistry, BootstrapRegistryError, IanaRegistry,
9        IanaRegistryType,
10    },
11};
12
13use crate::{http::Client, iana::iana_request::iana_request, rdap::QueryType, RdapClientError};
14
15const SECONDS_IN_WEEK: i64 = 604800;
16
17/// Defines a trait for things that store bootstrap registries.
18pub trait BootstrapStore: Send + Sync {
19    /// Called when store is checked to see if it has a valid bootstrap registry.
20    ///
21    /// This method should return false (i.e. `Ok(false)``) if the registry doesn't
22    /// exist in the store or if the registry in the store is out-of-date (such as
23    /// the cache control data indicates it is old).
24    fn has_bootstrap_registry(&self, reg_type: &IanaRegistryType) -> Result<bool, RdapClientError>;
25
26    /// Puts a registry into the bootstrap registry store.
27    fn put_bootstrap_registry(
28        &self,
29        reg_type: &IanaRegistryType,
30        registry: IanaRegistry,
31        http_data: HttpData,
32    ) -> Result<(), RdapClientError>;
33
34    /// Get the urls for a domain or nameserver (which are domain names) query type.
35    ///
36    /// The default method should be good enough for most trait implementations.
37    fn get_domain_query_urls(
38        &self,
39        query_type: &QueryType,
40    ) -> Result<Option<Vec<String>>, RdapClientError> {
41        let domain_name = match query_type {
42            QueryType::Domain(domain) => domain.to_ascii(),
43            QueryType::Nameserver(ns) => ns.to_ascii(),
44            _ => panic!("invalid domain query type"),
45        };
46        self.get_dns_urls(domain_name)
47    }
48
49    /// Get the urls for an autnum query type.
50    ///
51    /// The default method should be good enough for most trait implementations.
52    fn get_autnum_query_urls(
53        &self,
54        query_type: &QueryType,
55    ) -> Result<Option<Vec<String>>, RdapClientError> {
56        let QueryType::AsNumber(asn) = query_type else {
57            panic!("invalid query type")
58        };
59        self.get_asn_urls(asn.to_string().as_str())
60    }
61
62    /// Get the urls for an IPv4 query type.
63    ///
64    /// The default method should be good enough for most trait implementations.
65    fn get_ipv4_query_urls(
66        &self,
67        query_type: &QueryType,
68    ) -> Result<Option<Vec<String>>, RdapClientError> {
69        let ip = match query_type {
70            QueryType::IpV4Addr(addr) => format!("{addr}/32"),
71            QueryType::IpV4Cidr(cidr) => cidr.to_string(),
72            _ => panic!("non ip query for ip bootstrap"),
73        };
74        self.get_ipv4_urls(&ip)
75    }
76
77    /// Get the urls for an IPv6 query type.
78    ///
79    /// The default method should be good enough for most trait implementations.
80    fn get_ipv6_query_urls(
81        &self,
82        query_type: &QueryType,
83    ) -> Result<Option<Vec<String>>, RdapClientError> {
84        let ip = match query_type {
85            QueryType::IpV6Addr(addr) => format!("{addr}/128"),
86            QueryType::IpV6Cidr(cidr) => cidr.to_string(),
87            _ => panic!("non ip query for ip bootstrap"),
88        };
89        self.get_ipv6_urls(&ip)
90    }
91
92    /// Get the urls for an entity handle query type.
93    ///
94    /// The default method should be good enough for most trait implementations.
95    fn get_entity_handle_query_urls(
96        &self,
97        query_type: &QueryType,
98    ) -> Result<Option<Vec<String>>, RdapClientError> {
99        let QueryType::Entity(handle) = query_type else {
100            panic!("non entity handle for bootstrap")
101        };
102        let handle_split = handle
103            .rsplit_once('-')
104            .ok_or(BootstrapRegistryError::InvalidBootstrapInput)?;
105        self.get_tag_query_urls(handle_split.1)
106    }
107
108    /// Get the urls for an object tag query type.
109    ///
110    /// The default method should be good enough for most trait implementations.
111    fn get_tag_query_urls(&self, tag: &str) -> Result<Option<Vec<String>>, RdapClientError> {
112        self.get_tag_urls(tag)
113    }
114
115    /// Get the URLs associated with the IANA RDAP DNS bootstrap.
116    ///
117    /// Returns [None] if no URLs were found for the given key.
118    /// Implementations should implement the logic to pull the [icann_rdap_common::iana::IanaRegistry]
119    /// and ultimately call its [icann_rdap_common::iana::IanaRegistry::get_dns_bootstrap_urls] method.
120    fn get_dns_urls(&self, ldh: &str) -> Result<Option<Vec<String>>, RdapClientError>;
121
122    /// Get the URLs associated with the IANA RDAP ASN bootstrap.
123    ///
124    /// Returns [None] if no URLs were found for the given key.
125    /// Implementations should implement the logic to pull the [icann_rdap_common::iana::IanaRegistry]
126    /// and ultimately call its [icann_rdap_common::iana::IanaRegistry::get_asn_bootstrap_urls] method.
127    fn get_asn_urls(&self, asn: &str) -> Result<Option<Vec<String>>, RdapClientError>;
128
129    /// Get the URLs associated with the IANA RDAP IPv4 bootstrap.
130    ///
131    /// Returns [None] if no URLs were found for the given key.
132    /// Implementations should implement the logic to pull the [icann_rdap_common::iana::IanaRegistry]
133    /// and ultimately call its [icann_rdap_common::iana::IanaRegistry::get_ipv4_bootstrap_urls] method.
134    fn get_ipv4_urls(&self, ipv4: &str) -> Result<Option<Vec<String>>, RdapClientError>;
135
136    /// Get the URLs associated with the IANA RDAP IPv6 bootstrap.
137    ///
138    /// Returns [None] if no URLs were found for the given key.
139    /// Implementations should implement the logic to pull the [icann_rdap_common::iana::IanaRegistry]
140    /// and ultimately call its [icann_rdap_common::iana::IanaRegistry::get_ipv6_bootstrap_urls] method.
141    fn get_ipv6_urls(&self, ipv6: &str) -> Result<Option<Vec<String>>, RdapClientError>;
142
143    /// Get the URLs associated with the IANA RDAP Object Tags bootstrap.
144    ///
145    /// Returns [None] if no URLs were found for the given key.
146    /// Implementations should implement the logic to pull the [icann_rdap_common::iana::IanaRegistry]
147    /// and ultimately call its [icann_rdap_common::iana::IanaRegistry::get_tag_bootstrap_urls] method.
148    fn get_tag_urls(&self, tag: &str) -> Result<Option<Vec<String>>, RdapClientError>;
149}
150
151/// A trait to find the preferred URL from a bootstrap service.
152pub trait PreferredUrl {
153    fn preferred_url(self) -> Result<String, RdapClientError>;
154}
155
156impl PreferredUrl for Vec<String> {
157    fn preferred_url(self) -> Result<String, RdapClientError> {
158        Ok(get_preferred_url(self)?)
159    }
160}
161
162impl PreferredUrl for Option<Vec<String>> {
163    fn preferred_url(self) -> Result<String, RdapClientError> {
164        match self {
165            Some(vec) => Ok(get_preferred_url(vec)?),
166            None => Err(RdapClientError::BootstrapUnavailable),
167        }
168    }
169}
170
171/// A bootstrap registry store backed by memory.
172///
173/// This implementation of [BootstrapStore] keeps registries in memory. Every new instance starts with
174/// no registries in memory. They are added and maintained over time by calls to [MemoryBootstrapStore::put_bootstrap_registry()] by the
175/// machinery of [crate::rdap::request::rdap_bootstrapped_request()] and [crate::iana::bootstrap::qtype_to_bootstrap_url()].
176///
177/// Ideally, this should be kept in the same scope as [reqwest::Client].
178pub struct MemoryBootstrapStore {
179    ipv4: Arc<RwLock<Option<(IanaRegistry, HttpData)>>>,
180    ipv6: Arc<RwLock<Option<(IanaRegistry, HttpData)>>>,
181    autnum: Arc<RwLock<Option<(IanaRegistry, HttpData)>>>,
182    dns: Arc<RwLock<Option<(IanaRegistry, HttpData)>>>,
183    tag: Arc<RwLock<Option<(IanaRegistry, HttpData)>>>,
184}
185
186unsafe impl Send for MemoryBootstrapStore {}
187unsafe impl Sync for MemoryBootstrapStore {}
188
189impl Default for MemoryBootstrapStore {
190    fn default() -> Self {
191        Self::new()
192    }
193}
194
195impl MemoryBootstrapStore {
196    pub fn new() -> Self {
197        Self {
198            ipv4: <_>::default(),
199            ipv6: <_>::default(),
200            autnum: <_>::default(),
201            dns: <_>::default(),
202            tag: <_>::default(),
203        }
204    }
205}
206
207impl BootstrapStore for MemoryBootstrapStore {
208    fn has_bootstrap_registry(&self, reg_type: &IanaRegistryType) -> Result<bool, RdapClientError> {
209        Ok(match reg_type {
210            IanaRegistryType::RdapBootstrapDns => self.dns.read()?.registry_has_not_expired(),
211            IanaRegistryType::RdapBootstrapAsn => self.autnum.read()?.registry_has_not_expired(),
212            IanaRegistryType::RdapBootstrapIpv4 => self.ipv4.read()?.registry_has_not_expired(),
213            IanaRegistryType::RdapBootstrapIpv6 => self.ipv6.read()?.registry_has_not_expired(),
214            IanaRegistryType::RdapObjectTags => self.tag.read()?.registry_has_not_expired(),
215        })
216    }
217
218    fn put_bootstrap_registry(
219        &self,
220        reg_type: &IanaRegistryType,
221        registry: IanaRegistry,
222        http_data: HttpData,
223    ) -> Result<(), RdapClientError> {
224        match reg_type {
225            IanaRegistryType::RdapBootstrapDns => {
226                let mut g = self.dns.write()?;
227                *g = Some((registry, http_data));
228            }
229            IanaRegistryType::RdapBootstrapAsn => {
230                let mut g = self.autnum.write()?;
231                *g = Some((registry, http_data));
232            }
233            IanaRegistryType::RdapBootstrapIpv4 => {
234                let mut g = self.ipv4.write()?;
235                *g = Some((registry, http_data));
236            }
237            IanaRegistryType::RdapBootstrapIpv6 => {
238                let mut g = self.ipv6.write()?;
239                *g = Some((registry, http_data));
240            }
241            IanaRegistryType::RdapObjectTags => {
242                let mut g = self.tag.write()?;
243                *g = Some((registry, http_data));
244            }
245        };
246        Ok(())
247    }
248
249    fn get_dns_urls(&self, ldh: &str) -> Result<Option<Vec<String>>, RdapClientError> {
250        if let Some((iana, _http_data)) = self.dns.read()?.as_ref() {
251            Ok(iana.get_dns_bootstrap_urls(ldh)?)
252        } else {
253            Err(RdapClientError::BootstrapUnavailable)
254        }
255    }
256
257    fn get_asn_urls(&self, asn: &str) -> Result<Option<Vec<String>>, RdapClientError> {
258        if let Some((iana, _http_data)) = self.autnum.read()?.as_ref() {
259            Ok(iana.get_asn_bootstrap_urls(asn)?)
260        } else {
261            Err(RdapClientError::BootstrapUnavailable)
262        }
263    }
264
265    fn get_ipv4_urls(&self, ipv4: &str) -> Result<Option<Vec<String>>, RdapClientError> {
266        if let Some((iana, _http_data)) = self.ipv4.read()?.as_ref() {
267            Ok(iana.get_ipv4_bootstrap_urls(ipv4)?)
268        } else {
269            Err(RdapClientError::BootstrapUnavailable)
270        }
271    }
272
273    fn get_ipv6_urls(&self, ipv6: &str) -> Result<Option<Vec<String>>, RdapClientError> {
274        if let Some((iana, _http_data)) = self.ipv6.read()?.as_ref() {
275            Ok(iana.get_ipv6_bootstrap_urls(ipv6)?)
276        } else {
277            Err(RdapClientError::BootstrapUnavailable)
278        }
279    }
280
281    fn get_tag_urls(&self, tag: &str) -> Result<Option<Vec<String>>, RdapClientError> {
282        if let Some((iana, _http_data)) = self.tag.read()?.as_ref() {
283            Ok(iana.get_tag_bootstrap_urls(tag)?)
284        } else {
285            Err(RdapClientError::BootstrapUnavailable)
286        }
287    }
288}
289
290/// Trait to determine if a bootstrap registry is past its expiration (i.e. needs to be rechecked).
291pub trait RegistryHasNotExpired {
292    fn registry_has_not_expired(&self) -> bool;
293}
294
295impl RegistryHasNotExpired for Option<(IanaRegistry, HttpData)> {
296    fn registry_has_not_expired(&self) -> bool {
297        if let Some((_iana, http_data)) = self {
298            !http_data.is_expired(SECONDS_IN_WEEK)
299        } else {
300            false
301        }
302    }
303}
304
305/// Given a [QueryType], it will get the bootstrap URL.
306pub async fn qtype_to_bootstrap_url<F>(
307    client: &Client,
308    store: &dyn BootstrapStore,
309    query_type: &QueryType,
310    callback: F,
311) -> Result<String, RdapClientError>
312where
313    F: FnOnce(&IanaRegistryType),
314{
315    match query_type {
316        QueryType::IpV4Addr(_) | QueryType::IpV4Cidr(_) => {
317            fetch_bootstrap(
318                &IanaRegistryType::RdapBootstrapIpv4,
319                client,
320                store,
321                callback,
322            )
323            .await?;
324            Ok(store.get_ipv4_query_urls(query_type)?.preferred_url()?)
325        }
326        QueryType::IpV6Addr(_) | QueryType::IpV6Cidr(_) => {
327            fetch_bootstrap(
328                &IanaRegistryType::RdapBootstrapIpv6,
329                client,
330                store,
331                callback,
332            )
333            .await?;
334            Ok(store.get_ipv6_query_urls(query_type)?.preferred_url()?)
335        }
336        QueryType::AsNumber(_) => {
337            fetch_bootstrap(&IanaRegistryType::RdapBootstrapAsn, client, store, callback).await?;
338            Ok(store.get_autnum_query_urls(query_type)?.preferred_url()?)
339        }
340        QueryType::Domain(_) => {
341            fetch_bootstrap(&IanaRegistryType::RdapBootstrapDns, client, store, callback).await?;
342            Ok(store.get_domain_query_urls(query_type)?.preferred_url()?)
343        }
344        QueryType::Entity(_) => {
345            fetch_bootstrap(&IanaRegistryType::RdapObjectTags, client, store, callback).await?;
346            Ok(store
347                .get_entity_handle_query_urls(query_type)?
348                .preferred_url()?)
349        }
350        QueryType::Nameserver(_) => {
351            fetch_bootstrap(&IanaRegistryType::RdapBootstrapDns, client, store, callback).await?;
352            Ok(store.get_domain_query_urls(query_type)?.preferred_url()?)
353        }
354        _ => Err(RdapClientError::BootstrapUnavailable),
355    }
356}
357
358/// Fetches a bootstrap registry for a [BootstrapStore].
359pub async fn fetch_bootstrap<F>(
360    reg_type: &IanaRegistryType,
361    client: &Client,
362    store: &dyn BootstrapStore,
363    callback: F,
364) -> Result<(), RdapClientError>
365where
366    F: FnOnce(&IanaRegistryType),
367{
368    if !store.has_bootstrap_registry(reg_type)? {
369        callback(reg_type);
370        let iana_resp = iana_request(reg_type.clone(), client).await?;
371        store.put_bootstrap_registry(reg_type, iana_resp.registry, iana_resp.http_data)?;
372    }
373    Ok(())
374}
375
376#[cfg(test)]
377#[allow(non_snake_case)]
378mod test {
379    use icann_rdap_common::{
380        httpdata::HttpData,
381        iana::{IanaRegistry, IanaRegistryType},
382    };
383
384    use crate::{iana::bootstrap::PreferredUrl, rdap::QueryType};
385
386    use super::{BootstrapStore, MemoryBootstrapStore};
387
388    #[test]
389    fn GIVEN_membootstrap_with_dns_WHEN_get_domain_query_url_THEN_correct_url() {
390        // GIVEN
391        let mem = MemoryBootstrapStore::new();
392        let bootstrap = r#"
393            {
394                "version": "1.0",
395                "publication": "2024-01-07T10:11:12Z",
396                "description": "Some text",
397                "services": [
398                  [
399                    ["net", "com"],
400                    [
401                      "https://registry.example.com/myrdap/"
402                    ]
403                  ],
404                  [
405                    ["org", "mytld"],
406                    [
407                      "https://example.org/"
408                    ]
409                  ]
410                ]
411            }
412        "#;
413        let iana =
414            serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse domain bootstrap");
415        mem.put_bootstrap_registry(
416            &IanaRegistryType::RdapBootstrapDns,
417            iana,
418            HttpData::example().build(),
419        )
420        .expect("put iana registry");
421
422        // WHEN
423        let actual = mem
424            .get_domain_query_urls(&QueryType::domain("example.org").expect("invalid domain name"))
425            .expect("get bootstrap url")
426            .preferred_url()
427            .expect("preferred url");
428
429        // THEN
430        assert_eq!(actual, "https://example.org/")
431    }
432
433    #[test]
434    fn GIVEN_membootstrap_with_autnum_WHEN_get_autnum_query_url_THEN_correct_url() {
435        // GIVEN
436        let mem = MemoryBootstrapStore::new();
437        let bootstrap = r#"
438            {
439                "version": "1.0",
440                "publication": "2024-01-07T10:11:12Z",
441                "description": "RDAP Bootstrap file for example registries.",
442                "services": [
443                  [
444                    ["64496-64496"],
445                    [
446                      "https://rir3.example.com/myrdap/"
447                    ]
448                  ],
449                  [
450                    ["64497-64510", "65536-65551"],
451                    [
452                      "https://example.org/"
453                    ]
454                  ],
455                  [
456                    ["64512-65534"],
457                    [
458                      "http://example.net/rdaprir2/",
459                      "https://example.net/rdaprir2/"
460                    ]
461                  ]
462                ]
463            }
464        "#;
465        let iana =
466            serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
467        mem.put_bootstrap_registry(
468            &IanaRegistryType::RdapBootstrapAsn,
469            iana,
470            HttpData::example().build(),
471        )
472        .expect("put iana registry");
473
474        // WHEN
475        let actual = mem
476            .get_autnum_query_urls(&QueryType::autnum("as64512").expect("invalid autnum"))
477            .expect("get bootstrap url")
478            .preferred_url()
479            .expect("preferred url");
480
481        // THEN
482        assert_eq!(actual, "https://example.net/rdaprir2/");
483    }
484
485    #[test]
486    fn GIVEN_membootstrap_with_ipv4_THEN_get_ipv4_query_urls_THEN_correct_url() {
487        // GIVEN
488        let mem = MemoryBootstrapStore::new();
489        let bootstrap = r#"
490            {
491                "version": "1.0",
492                "publication": "2024-01-07T10:11:12Z",
493                "description": "RDAP Bootstrap file for example registries.",
494                "services": [
495                  [
496                    ["198.51.100.0/24", "192.0.0.0/8"],
497                    [
498                      "https://rir1.example.com/myrdap/"
499                    ]
500                  ],
501                  [
502                    ["203.0.113.0/24", "192.0.2.0/24"],
503                    [
504                      "https://example.org/"
505                    ]
506                  ],
507                  [
508                    ["203.0.113.0/28"],
509                    [
510                      "https://example.net/rdaprir2/",
511                      "http://example.net/rdaprir2/"
512                    ]
513                  ]
514                ]
515            }
516        "#;
517        let iana =
518            serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
519        mem.put_bootstrap_registry(
520            &IanaRegistryType::RdapBootstrapIpv4,
521            iana,
522            HttpData::example().build(),
523        )
524        .expect("put iana registry");
525
526        // WHEN
527        let actual = mem
528            .get_ipv4_query_urls(&QueryType::ipv4("198.51.100.1").expect("invalid IP address"))
529            .expect("get bootstrap url")
530            .preferred_url()
531            .expect("preferred url");
532
533        // THEN
534        assert_eq!(actual, "https://rir1.example.com/myrdap/");
535    }
536
537    #[test]
538    fn GIVEN_membootstrap_with_ipv6_THEN_get_ipv6_query_urls_THEN_correct_url() {
539        // GIVEN
540        let mem = MemoryBootstrapStore::new();
541        let bootstrap = r#"
542            {
543                "version": "1.0",
544                "publication": "2024-01-07T10:11:12Z",
545                "description": "RDAP Bootstrap file for example registries.",
546                "services": [
547                  [
548                    ["2001:db8::/34"],
549                    [
550                      "https://rir2.example.com/myrdap/"
551                    ]
552                  ],
553                  [
554                    ["2001:db8:4000::/36", "2001:db8:ffff::/48"],
555                    [
556                      "https://example.org/"
557                    ]
558                  ],
559                  [
560                    ["2001:db8:1000::/36"],
561                    [
562                      "https://example.net/rdaprir2/",
563                      "http://example.net/rdaprir2/"
564                    ]
565                  ]
566                ]
567            }
568        "#;
569        let iana =
570            serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
571        mem.put_bootstrap_registry(
572            &IanaRegistryType::RdapBootstrapIpv6,
573            iana,
574            HttpData::example().build(),
575        )
576        .expect("put iana registry");
577
578        // WHEN
579        let actual = mem
580            .get_ipv6_query_urls(&QueryType::ipv6("2001:db8::1").expect("invalid IP address"))
581            .expect("get bootstrap url")
582            .preferred_url()
583            .expect("preferred url");
584
585        // THEN
586        assert_eq!(actual, "https://rir2.example.com/myrdap/");
587    }
588
589    #[test]
590    fn GIVEN_membootstrap_with_tag_THEN_get_tag_query_urls_THEN_correct_url() {
591        // GIVEN
592        let mem = MemoryBootstrapStore::new();
593        let bootstrap = r#"
594            {
595              "version": "1.0",
596              "publication": "YYYY-MM-DDTHH:MM:SSZ",
597              "description": "RDAP bootstrap file for service provider object tags",
598              "services": [
599                [
600                  ["contact@example.com"],
601                  ["YYYY"],
602                  [
603                    "https://example.com/rdap/"
604                  ]
605                ],
606                [
607                  ["contact@example.org"],
608                  ["ZZ54"],
609                  [
610                    "http://rdap.example.org/"
611                  ]
612                ],
613                [
614                  ["contact@example.net"],
615                  ["1754"],
616                  [
617                    "https://example.net/rdap/",
618                    "http://example.net/rdap/"
619                  ]
620                ]
621              ]
622             }
623        "#;
624        let iana =
625            serde_json::from_str::<IanaRegistry>(bootstrap).expect("cannot parse autnum bootstrap");
626        mem.put_bootstrap_registry(
627            &IanaRegistryType::RdapObjectTags,
628            iana,
629            HttpData::example().build(),
630        )
631        .expect("put iana registry");
632
633        // WHEN
634        let actual = mem
635            .get_entity_handle_query_urls(&QueryType::Entity("foo-YYYY".to_string()))
636            .expect("get bootstrap url")
637            .preferred_url()
638            .expect("preferred url");
639
640        // THEN
641        assert_eq!(actual, "https://example.com/rdap/");
642    }
643}