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<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<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(&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 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 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 fn get_tag_query_urls(&self, tag: &str) -> Result<Vec<String>, RdapClientError> {
106 self.get_tag_urls(tag)
107 }
108
109 fn get_dns_urls(&self, ldh: &str) -> Result<Vec<String>, RdapClientError>;
114
115 fn get_asn_urls(&self, asn: &str) -> Result<Vec<String>, RdapClientError>;
120
121 fn get_ipv4_urls(&self, ipv4: &str) -> Result<Vec<String>, RdapClientError>;
126
127 fn get_ipv6_urls(&self, ipv6: &str) -> Result<Vec<String>, RdapClientError>;
132
133 fn get_tag_urls(&self, tag: &str) -> Result<Vec<String>, RdapClientError>;
138}
139
140pub 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
151pub 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
270pub 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
285pub 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
338pub 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 assert_eq!(actual, "https://example.com/rdap/");
622 }
623}