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