1use 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
17pub trait BootstrapStore: Send + Sync {
19 fn has_bootstrap_registry(&self, reg_type: &IanaRegistryType) -> Result<bool, RdapClientError>;
25
26 fn put_bootstrap_registry(
28 &self,
29 reg_type: &IanaRegistryType,
30 registry: IanaRegistry,
31 http_data: HttpData,
32 ) -> Result<(), RdapClientError>;
33
34 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 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 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 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 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 fn get_tag_query_urls(&self, tag: &str) -> Result<Option<Vec<String>>, RdapClientError> {
112 self.get_tag_urls(tag)
113 }
114
115 fn get_dns_urls(&self, ldh: &str) -> Result<Option<Vec<String>>, RdapClientError>;
121
122 fn get_asn_urls(&self, asn: &str) -> Result<Option<Vec<String>>, RdapClientError>;
128
129 fn get_ipv4_urls(&self, ipv4: &str) -> Result<Option<Vec<String>>, RdapClientError>;
135
136 fn get_ipv6_urls(&self, ipv6: &str) -> Result<Option<Vec<String>>, RdapClientError>;
142
143 fn get_tag_urls(&self, tag: &str) -> Result<Option<Vec<String>>, RdapClientError>;
149}
150
151pub 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
171pub 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
290pub 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
305pub 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
358pub 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(actual, "https://example.com/rdap/");
642 }
643}