1use std::collections::HashMap;
2use std::net::{IpAddr, SocketAddr};
3use std::sync::Arc;
4use std::time::{Duration, Instant};
5
6use futures::StreamExt;
7use once_cell::sync::Lazy;
8use reqwest::Client;
9use serde::Deserialize;
10use tokio::sync::{Notify, RwLock};
11use tracing::{debug, info, instrument, warn};
12
13use super::bootstrap::{
14 ipv4_matches_prefix, ipv6_matches_prefix, parse_asn_range, validate_bootstrap_url,
15};
16use super::types::RdapResponse;
17use crate::error::{Result, SeerError};
18use crate::retry::{NetworkRetryClassifier, RetryClassifier, RetryExecutor, RetryPolicy};
19use crate::validation::{describe_reserved_ip, normalize_domain};
20
21const IANA_BOOTSTRAP_DNS: &str = "https://data.iana.org/rdap/dns.json";
22const IANA_BOOTSTRAP_IPV4: &str = "https://data.iana.org/rdap/ipv4.json";
23const IANA_BOOTSTRAP_IPV6: &str = "https://data.iana.org/rdap/ipv6.json";
24const IANA_BOOTSTRAP_ASN: &str = "https://data.iana.org/rdap/asn.json";
25
26const DEFAULT_TIMEOUT: Duration = Duration::from_secs(15);
31
32const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
35
36const BOOTSTRAP_TTL: Duration = Duration::from_secs(24 * 60 * 60);
38
39const BOOTSTRAP_REFRESH_MIN_INTERVAL: Duration = Duration::from_secs(60);
43
44static RDAP_HTTP_CLIENT: Lazy<Option<Client>> = Lazy::new(|| {
53 Client::builder()
54 .timeout(DEFAULT_TIMEOUT)
55 .connect_timeout(CONNECT_TIMEOUT)
56 .user_agent("Seer/1.0 (RDAP Client)")
57 .pool_max_idle_per_host(10)
58 .redirect(reqwest::redirect::Policy::none())
62 .build()
63 .ok()
64});
65
66fn rdap_http_client() -> Result<&'static Client> {
70 RDAP_HTTP_CLIENT
71 .as_ref()
72 .ok_or_else(|| SeerError::HttpError("failed to initialize HTTP client".into()))
73}
74
75static BOOTSTRAP_CACHE: Lazy<RwLock<Option<CachedBootstrap>>> = Lazy::new(|| RwLock::new(None));
77
78static BOOTSTRAP_LAST_ATTEMPT: Lazy<RwLock<Option<Instant>>> = Lazy::new(|| RwLock::new(None));
82
83static BOOTSTRAP_LOAD_NOTIFY: Lazy<Notify> = Lazy::new(Notify::new);
91
92struct CachedBootstrap {
94 data: BootstrapData,
95 loaded_at: Instant,
96}
97
98impl CachedBootstrap {
99 fn new(data: BootstrapData) -> Self {
100 Self {
101 data,
102 loaded_at: Instant::now(),
103 }
104 }
105
106 fn is_expired(&self) -> bool {
107 self.loaded_at.elapsed() > BOOTSTRAP_TTL
108 }
109
110 fn age(&self) -> Duration {
111 self.loaded_at.elapsed()
112 }
113}
114
115struct BootstrapData {
120 dns: HashMap<String, Arc<Vec<url::Url>>>,
121 ipv4: Vec<(IpRange, Arc<Vec<url::Url>>)>,
122 ipv6: Vec<(IpRange, Arc<Vec<url::Url>>)>,
123 asn: Vec<(AsnRange, Arc<Vec<url::Url>>)>,
124}
125
126#[derive(Clone)]
127struct IpRange {
128 prefix: String,
129}
130
131#[derive(Clone)]
132struct AsnRange {
133 start: u32,
134 end: u32,
135}
136
137#[derive(Deserialize)]
138struct BootstrapResponse {
139 services: Vec<Vec<serde_json::Value>>,
140}
141
142async fn wait_for_in_flight_load(
152 notified: std::pin::Pin<&mut tokio::sync::futures::Notified<'_>>,
153) -> Result<()> {
154 let _ = tokio::time::timeout(DEFAULT_TIMEOUT, notified).await;
157 let cache = BOOTSTRAP_CACHE.read().await;
158 if cache.is_some() {
159 Ok(())
160 } else {
161 Err(SeerError::RdapBootstrapError(
162 "bootstrap refresh throttled and no cache available".to_string(),
163 ))
164 }
165}
166
167#[derive(Debug, Clone)]
168pub struct RdapClient {
169 retry_policy: RetryPolicy,
170}
171
172impl Default for RdapClient {
173 fn default() -> Self {
174 Self::new()
175 }
176}
177
178impl RdapClient {
179 pub fn new() -> Self {
181 Self {
182 retry_policy: RetryPolicy::new()
189 .with_max_attempts(3)
190 .with_initial_delay(Duration::from_millis(500))
191 .with_max_delay(Duration::from_secs(5)),
192 }
193 }
194
195 pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
199 self.retry_policy = policy;
200 self
201 }
202
203 pub fn without_retries(mut self) -> Self {
205 self.retry_policy = RetryPolicy::no_retry();
206 self
207 }
208
209 async fn ensure_bootstrap(&self) -> Result<()> {
224 {
226 let cache = BOOTSTRAP_CACHE.read().await;
227 if let Some(cached) = cache.as_ref() {
228 if !cached.is_expired() {
229 return Ok(());
230 }
231 }
232 }
233
234 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
240 tokio::pin!(notified);
241
242 {
246 let last = BOOTSTRAP_LAST_ATTEMPT.read().await;
247 if let Some(ts) = *last {
248 if ts.elapsed() < BOOTSTRAP_REFRESH_MIN_INTERVAL {
249 let cache = BOOTSTRAP_CACHE.read().await;
251 if cache.is_some() {
252 return Ok(());
254 }
255 drop(cache);
258 drop(last);
259 return wait_for_in_flight_load(notified).await;
260 }
261 }
262 }
263
264 {
267 let mut last = BOOTSTRAP_LAST_ATTEMPT.write().await;
268 if let Some(ts) = *last {
270 if ts.elapsed() < BOOTSTRAP_REFRESH_MIN_INTERVAL {
271 drop(last);
272 let cache = BOOTSTRAP_CACHE.read().await;
273 if cache.is_some() {
274 return Ok(());
275 }
276 drop(cache);
277 return wait_for_in_flight_load(notified).await;
278 }
279 }
280 *last = Some(Instant::now());
281 }
282
283 debug!("Loading/refreshing RDAP bootstrap data");
287 let load_result = load_bootstrap_data_with_retry(&self.retry_policy).await;
288
289 let outcome = match load_result {
290 Ok(data) => {
291 let mut cache = BOOTSTRAP_CACHE.write().await;
292 let should_store = cache.as_ref().map(|c| c.is_expired()).unwrap_or(true);
295 if should_store {
296 *cache = Some(CachedBootstrap::new(data));
297 }
298 Ok(())
299 }
300 Err(e) => {
301 let cache = BOOTSTRAP_CACHE.read().await;
303 if let Some(cached) = cache.as_ref() {
304 debug!(
305 error = %e,
306 age_hours = cached.age().as_secs() / 3600,
307 "Bootstrap refresh failed, using stale data"
308 );
309 Ok(())
310 } else {
311 Err(e)
313 }
314 }
315 };
316
317 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
319 outcome
320 }
321
322 fn get_rdap_urls_for_domain(cache: &BootstrapData, domain: &str) -> Option<Arc<Vec<url::Url>>> {
324 let tld = domain.rsplit('.').next()?;
325 cache.dns.get(&tld.to_lowercase()).cloned()
326 }
327
328 fn get_rdap_urls_for_ip(cache: &BootstrapData, ip: &IpAddr) -> Option<Arc<Vec<url::Url>>> {
330 match ip {
331 IpAddr::V4(addr) => {
332 for (range, urls) in &cache.ipv4 {
333 if ipv4_matches_prefix(&range.prefix, addr) {
334 return Some(Arc::clone(urls));
335 }
336 }
337 }
338 IpAddr::V6(addr) => {
339 for (range, urls) in &cache.ipv6 {
340 if ipv6_matches_prefix(&range.prefix, addr) {
341 return Some(Arc::clone(urls));
342 }
343 }
344 }
345 }
346
347 None
348 }
349
350 fn get_rdap_urls_for_asn(cache: &BootstrapData, asn: u32) -> Option<Arc<Vec<url::Url>>> {
352 for (range, urls) in &cache.asn {
353 if asn >= range.start && asn <= range.end {
354 return Some(Arc::clone(urls));
355 }
356 }
357
358 None
359 }
360
361 #[instrument(skip(self), fields(domain = %domain))]
365 pub async fn lookup_domain(&self, domain: &str) -> Result<RdapResponse> {
366 self.ensure_bootstrap().await?;
367
368 let domain = normalize_domain(domain)?;
369
370 let urls = {
372 let cache_guard = BOOTSTRAP_CACHE.read().await;
373 let cache = cache_guard.as_ref().ok_or_else(|| {
374 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
375 })?;
376
377 let bases = Self::get_rdap_urls_for_domain(&cache.data, &domain).ok_or_else(|| {
378 SeerError::RdapBootstrapError(format!("no RDAP server for {}", domain))
379 })?;
380
381 build_rdap_urls(&bases, &format!("domain/{}", domain))
382 }; self.query_rdap_urls(&urls).await
385 }
386
387 #[instrument(skip(self), fields(ip = %ip))]
391 pub async fn lookup_ip(&self, ip: &str) -> Result<RdapResponse> {
392 self.ensure_bootstrap().await?;
393
394 let ip_addr: IpAddr = ip
395 .parse()
396 .map_err(|_| SeerError::InvalidIpAddress(ip.to_string()))?;
397
398 let urls = {
399 let cache_guard = BOOTSTRAP_CACHE.read().await;
400 let cache = cache_guard.as_ref().ok_or_else(|| {
401 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
402 })?;
403
404 let bases = Self::get_rdap_urls_for_ip(&cache.data, &ip_addr).ok_or_else(|| {
405 SeerError::RdapBootstrapError(format!("no RDAP server for {}", ip))
406 })?;
407
408 build_rdap_urls(&bases, &format!("ip/{}", ip))
409 };
410
411 self.query_rdap_urls(&urls).await
412 }
413
414 #[instrument(skip(self), fields(asn = %asn))]
418 pub async fn lookup_asn(&self, asn: u32) -> Result<RdapResponse> {
419 self.ensure_bootstrap().await?;
420
421 let urls = {
422 let cache_guard = BOOTSTRAP_CACHE.read().await;
423 let cache = cache_guard.as_ref().ok_or_else(|| {
424 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
425 })?;
426
427 let bases = Self::get_rdap_urls_for_asn(&cache.data, asn).ok_or_else(|| {
428 SeerError::RdapBootstrapError(format!("no RDAP server for AS{}", asn))
429 })?;
430
431 build_rdap_urls(&bases, &format!("autnum/{}", asn))
432 };
433
434 self.query_rdap_urls(&urls).await
435 }
436
437 #[instrument(skip(self), fields(tld = %tld))]
443 pub async fn get_rdap_base_url_for_tld(&self, tld: &str) -> Option<String> {
444 if self.ensure_bootstrap().await.is_err() {
445 return None;
446 }
447
448 let cache_guard = BOOTSTRAP_CACHE.read().await;
449 let cache = cache_guard.as_ref()?;
450 cache
451 .data
452 .dns
453 .get(&tld.to_lowercase())
454 .and_then(|urls| urls.first())
455 .map(|u| u.to_string())
456 }
457
458 async fn query_rdap_urls(&self, urls: &[url::Url]) -> Result<RdapResponse> {
462 if urls.is_empty() {
463 return Err(SeerError::RdapError(
464 "no candidate RDAP URLs available".to_string(),
465 ));
466 }
467
468 let mut last_error: Option<SeerError> = None;
469 for (idx, url) in urls.iter().enumerate() {
470 let url_str = url.as_str().to_string();
471 debug!(url = %url_str, candidate = idx + 1, total = urls.len(), "Querying RDAP");
472 match self.query_rdap_with_retry(&url_str).await {
473 Ok(resp) => return Ok(resp),
474 Err(e) => {
475 if urls.len() > 1 {
476 debug!(
477 url = %url_str,
478 error = %e,
479 candidate = idx + 1,
480 total = urls.len(),
481 "RDAP candidate failed, trying next",
482 );
483 }
484 last_error = Some(e);
485 }
486 }
487 }
488
489 Err(wrap_all_candidates_failed(last_error, urls.len()))
491 }
492
493 async fn query_rdap_with_retry(&self, url: &str) -> Result<RdapResponse> {
498 let classifier = NetworkRetryClassifier::new();
499 let mut attempt = 0;
500 loop {
501 match query_rdap_attempt(url).await {
502 Ok(resp) => return Ok(resp),
503 Err((err, retry_after)) => {
504 let attempts_remaining =
505 self.retry_policy.max_attempts.saturating_sub(attempt + 1);
506 if !classifier.is_retryable(&err) || attempts_remaining == 0 {
507 return Err(if attempt > 0 {
508 SeerError::RetryExhausted {
509 attempts: attempt + 1,
510 last_error: Box::new(err),
511 }
512 } else {
513 err
514 });
515 }
516 let backoff = self.retry_policy.delay_for_attempt(attempt);
517 let delay = effective_retry_delay(backoff, retry_after);
518 debug!(
519 url = %url,
520 attempt = attempt + 1,
521 max_attempts = self.retry_policy.max_attempts,
522 delay_ms = delay.as_millis(),
523 error = %err,
524 "Retrying RDAP after transient error"
525 );
526 tokio::time::sleep(delay).await;
527 attempt += 1;
528 }
529 }
530 }
531 }
532}
533
534const MAX_RDAP_RESPONSE_SIZE: usize = 10 * 1024 * 1024;
536
537const MAX_RETRY_AFTER: Duration = Duration::from_secs(5);
542
543async fn validate_url_not_reserved(url: &str) -> Result<Vec<SocketAddr>> {
550 let parsed = url::Url::parse(url)
551 .map_err(|e| SeerError::RdapError(format!("invalid URL '{}': {}", url, e)))?;
552 let host = parsed
553 .host_str()
554 .ok_or_else(|| SeerError::RdapError(format!("URL '{}' has no host", url)))?;
555 let port = parsed.port_or_known_default().unwrap_or(443);
556
557 if let Ok(ip) = host.parse::<IpAddr>() {
559 if let Some(reason) = describe_reserved_ip(&ip) {
560 return Err(SeerError::RdapError(format!(
561 "RDAP URL resolves to reserved IP {}: {} — request blocked (SSRF protection)",
562 ip, reason
563 )));
564 }
565 return Ok(vec![SocketAddr::new(ip, port)]);
566 }
567
568 let addr = format!("{}:{}", host, port);
569
570 let socket_addrs: Vec<SocketAddr> = tokio::net::lookup_host(&addr)
571 .await
572 .map_err(|e| SeerError::RdapError(format!("failed to resolve host '{}': {}", host, e)))?
573 .collect();
574
575 if socket_addrs.is_empty() {
576 return Err(SeerError::RdapError(format!(
577 "host '{}' resolved to no addresses",
578 host
579 )));
580 }
581
582 for socket_addr in &socket_addrs {
583 if let Some(reason) = describe_reserved_ip(&socket_addr.ip()) {
584 return Err(SeerError::RdapError(format!(
585 "RDAP URL resolves to reserved IP {}: {} — request blocked (SSRF protection)",
586 socket_addr.ip(),
587 reason
588 )));
589 }
590 }
591
592 Ok(socket_addrs)
593}
594
595fn parse_retry_after(value: &str) -> Option<Duration> {
600 value.trim().parse::<u64>().ok().map(Duration::from_secs)
601}
602
603fn effective_retry_delay(backoff: Duration, retry_after: Option<Duration>) -> Duration {
607 match retry_after {
608 Some(hint) => hint.min(MAX_RETRY_AFTER),
609 None => backoff,
610 }
611}
612
613async fn send_rdap_request(url: &str) -> Result<reqwest::Response> {
616 let resolved = validate_url_not_reserved(url).await?;
619
620 let parsed = url::Url::parse(url)
621 .map_err(|e| SeerError::RdapError(format!("invalid URL '{}': {}", url, e)))?;
622 let host = parsed
623 .host_str()
624 .ok_or_else(|| SeerError::RdapError(format!("URL '{}' has no host", url)))?;
625
626 let client = Client::builder()
630 .timeout(DEFAULT_TIMEOUT)
631 .connect_timeout(CONNECT_TIMEOUT)
632 .user_agent("Seer/1.0 (RDAP Client)")
633 .resolve_to_addrs(host, &resolved)
634 .redirect(reqwest::redirect::Policy::none())
643 .build()
644 .map_err(|e| SeerError::RdapError(format!("failed to build HTTP client: {}", e)))?;
645
646 client
647 .get(url)
648 .header("Accept", "application/rdap+json")
649 .send()
650 .await
651 .map_err(Into::into)
652}
653
654async fn read_and_parse_rdap_body(response: reqwest::Response, url: &str) -> Result<RdapResponse> {
657 let mut body = Vec::new();
662 let mut stream = response.bytes_stream();
663 let streamed = tokio::time::timeout(DEFAULT_TIMEOUT, async {
664 while let Some(chunk) = stream.next().await {
665 let chunk = chunk
666 .map_err(|e| SeerError::RdapError(format!("failed to read response: {}", e)))?;
667 body.extend_from_slice(&chunk);
668 if body.len() > MAX_RDAP_RESPONSE_SIZE {
669 return Err(SeerError::RdapError(format!(
670 "RDAP response exceeds {} byte limit",
671 MAX_RDAP_RESPONSE_SIZE
672 )));
673 }
674 }
675 Ok::<(), SeerError>(())
676 })
677 .await;
678
679 match streamed {
680 Ok(Ok(())) => {}
681 Ok(Err(e)) => return Err(e),
682 Err(_) => {
683 return Err(SeerError::Timeout(format!(
684 "timed out reading RDAP response body from {} after {:?}",
685 url, DEFAULT_TIMEOUT
686 )));
687 }
688 }
689
690 let rdap: RdapResponse = serde_json::from_slice(&body)?;
691 rdap.validate()?;
697 Ok(rdap)
698}
699
700async fn query_rdap_attempt(
706 url: &str,
707) -> std::result::Result<RdapResponse, (SeerError, Option<Duration>)> {
708 let response = send_rdap_request(url).await.map_err(|e| (e, None))?;
709
710 if !response.status().is_success() {
711 let status = response.status();
712 let retry_after = if status.as_u16() == 429 {
715 response
716 .headers()
717 .get(reqwest::header::RETRY_AFTER)
718 .and_then(|v| v.to_str().ok())
719 .and_then(parse_retry_after)
720 } else {
721 None
722 };
723 return Err((
724 SeerError::RdapError(format!("query failed with status {}", status)),
725 retry_after,
726 ));
727 }
728
729 read_and_parse_rdap_body(response, url)
730 .await
731 .map_err(|e| (e, None))
732}
733
734async fn load_bootstrap_data_with_retry(policy: &RetryPolicy) -> Result<BootstrapData> {
736 let executor = RetryExecutor::new(policy.clone());
737 executor.execute(load_bootstrap_data).await
738}
739
740async fn load_bootstrap_data() -> Result<BootstrapData> {
742 debug!("Loading RDAP bootstrap data from IANA");
743
744 let http = rdap_http_client()?;
748
749 let dns_future = http.get(IANA_BOOTSTRAP_DNS).send();
750 let ipv4_future = http.get(IANA_BOOTSTRAP_IPV4).send();
751 let ipv6_future = http.get(IANA_BOOTSTRAP_IPV6).send();
752 let asn_future = http.get(IANA_BOOTSTRAP_ASN).send();
753
754 let (dns_resp, ipv4_resp, ipv6_resp, asn_resp) =
757 tokio::join!(dns_future, ipv4_future, ipv6_future, asn_future);
758
759 const MAX_BOOTSTRAP_SIZE: usize = 10 * 1024 * 1024; async fn read_bootstrap(resp: reqwest::Response) -> Result<BootstrapResponse> {
763 let mut body = Vec::new();
769 let mut stream = resp.bytes_stream();
770 let streamed = tokio::time::timeout(DEFAULT_TIMEOUT, async {
771 while let Some(chunk) = stream.next().await {
772 let chunk = chunk.map_err(|e| {
773 SeerError::RdapBootstrapError(format!("failed to read body: {}", e))
774 })?;
775 body.extend_from_slice(&chunk);
776 if body.len() > MAX_BOOTSTRAP_SIZE {
777 return Err(SeerError::RdapBootstrapError(format!(
778 "bootstrap response too large (exceeds {} bytes)",
779 MAX_BOOTSTRAP_SIZE
780 )));
781 }
782 }
783 Ok::<(), SeerError>(())
784 })
785 .await;
786
787 match streamed {
788 Ok(Ok(())) => {}
789 Ok(Err(e)) => return Err(e),
790 Err(_) => {
791 return Err(SeerError::Timeout(format!(
792 "RDAP bootstrap body read timed out after {:?}",
793 DEFAULT_TIMEOUT
794 )));
795 }
796 }
797
798 serde_json::from_slice(&body).map_err(Into::into)
799 }
800
801 let dns_data = match dns_resp {
803 Ok(resp) => match read_bootstrap(resp).await {
804 Ok(data) => Some(data),
805 Err(e) => {
806 warn!(error = %e, "Failed to parse DNS bootstrap response");
807 None
808 }
809 },
810 Err(e) => {
811 warn!(error = %e, "Failed to fetch DNS bootstrap from IANA");
812 None
813 }
814 };
815 let ipv4_data = match ipv4_resp {
816 Ok(resp) => match read_bootstrap(resp).await {
817 Ok(data) => Some(data),
818 Err(e) => {
819 warn!(error = %e, "Failed to parse IPv4 bootstrap response");
820 None
821 }
822 },
823 Err(e) => {
824 warn!(error = %e, "Failed to fetch IPv4 bootstrap from IANA");
825 None
826 }
827 };
828 let ipv6_data = match ipv6_resp {
829 Ok(resp) => match read_bootstrap(resp).await {
830 Ok(data) => Some(data),
831 Err(e) => {
832 warn!(error = %e, "Failed to parse IPv6 bootstrap response");
833 None
834 }
835 },
836 Err(e) => {
837 warn!(error = %e, "Failed to fetch IPv6 bootstrap from IANA");
838 None
839 }
840 };
841 let asn_data = match asn_resp {
842 Ok(resp) => match read_bootstrap(resp).await {
843 Ok(data) => Some(data),
844 Err(e) => {
845 warn!(error = %e, "Failed to parse ASN bootstrap response");
846 None
847 }
848 },
849 Err(e) => {
850 warn!(error = %e, "Failed to fetch ASN bootstrap from IANA");
851 None
852 }
853 };
854
855 if dns_data.is_none() && ipv4_data.is_none() && ipv6_data.is_none() && asn_data.is_none() {
857 return Err(SeerError::RdapBootstrapError(
858 "all IANA bootstrap registries failed".to_string(),
859 ));
860 }
861
862 let mut dns = HashMap::new();
863 let mut ipv4 = Vec::new();
864 let mut ipv6 = Vec::new();
865 let mut asn = Vec::new();
866
867 fn collect_valid_urls(urls: &[serde_json::Value]) -> Option<Arc<Vec<url::Url>>> {
871 let mut out = Vec::new();
872 for u in urls {
873 if let Some(s) = u.as_str() {
874 match validate_bootstrap_url(s) {
875 Ok(parsed) => out.push(parsed),
876 Err(e) => {
877 debug!(url = s, error = %e, "Skipping invalid bootstrap URL");
878 }
879 }
880 }
881 }
882 if out.is_empty() {
883 None
884 } else {
885 Some(Arc::new(out))
886 }
887 }
888
889 if let Some(dns_data) = dns_data {
891 for service in dns_data.services {
892 if service.len() >= 2 {
893 if let (Some(tlds), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
894 if let Some(urls_arc) = collect_valid_urls(urls) {
895 for tld in tlds {
896 if let Some(tld_str) = tld.as_str() {
897 dns.insert(tld_str.to_lowercase(), Arc::clone(&urls_arc));
898 }
899 }
900 }
901 }
902 }
903 }
904 }
905
906 if let Some(ipv4_data) = ipv4_data {
908 for service in ipv4_data.services {
909 if service.len() >= 2 {
910 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array())
911 {
912 if let Some(urls_arc) = collect_valid_urls(urls) {
913 for prefix in prefixes {
914 if let Some(prefix_str) = prefix.as_str() {
915 ipv4.push((
916 IpRange {
917 prefix: prefix_str.to_string(),
918 },
919 Arc::clone(&urls_arc),
920 ));
921 }
922 }
923 }
924 }
925 }
926 }
927 }
928
929 if let Some(ipv6_data) = ipv6_data {
931 for service in ipv6_data.services {
932 if service.len() >= 2 {
933 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array())
934 {
935 if let Some(urls_arc) = collect_valid_urls(urls) {
936 for prefix in prefixes {
937 if let Some(prefix_str) = prefix.as_str() {
938 ipv6.push((
939 IpRange {
940 prefix: prefix_str.to_string(),
941 },
942 Arc::clone(&urls_arc),
943 ));
944 }
945 }
946 }
947 }
948 }
949 }
950 }
951
952 if let Some(asn_data) = asn_data {
954 for service in asn_data.services {
955 if service.len() >= 2 {
956 if let (Some(ranges), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
957 if let Some(urls_arc) = collect_valid_urls(urls) {
958 for range in ranges {
959 if let Some(range_str) = range.as_str() {
960 if let Some((start, end)) = parse_asn_range(range_str) {
961 asn.push((AsnRange { start, end }, Arc::clone(&urls_arc)));
962 }
963 }
964 }
965 }
966 }
967 }
968 }
969 }
970
971 info!(
972 dns_entries = dns.len(),
973 ipv4_ranges = ipv4.len(),
974 ipv6_ranges = ipv6.len(),
975 asn_ranges = asn.len(),
976 "RDAP bootstrap loaded"
977 );
978
979 Ok(BootstrapData {
980 dns,
981 ipv4,
982 ipv6,
983 asn,
984 })
985}
986
987fn wrap_all_candidates_failed(last_error: Option<SeerError>, candidate_count: usize) -> SeerError {
996 let last = last_error.unwrap_or_else(|| SeerError::RdapError("no candidates".to_string()));
997
998 if candidate_count <= 1 {
999 return last;
1000 }
1001
1002 match last {
1003 SeerError::Timeout(msg) => SeerError::Timeout(format!(
1004 "all {} RDAP candidate URLs timed out; last error: {}",
1005 candidate_count, msg
1006 )),
1007 other => SeerError::RdapError(format!(
1008 "all {} RDAP candidate URLs failed; last error: {}",
1009 candidate_count, other
1010 )),
1011 }
1012}
1013
1014fn build_rdap_urls(bases: &[url::Url], path: &str) -> Vec<url::Url> {
1016 bases
1017 .iter()
1018 .filter_map(|base| {
1019 let base_str = base.as_str();
1022 let normalized = if base_str.ends_with('/') {
1023 base_str.to_string()
1024 } else {
1025 format!("{}/", base_str)
1026 };
1027 url::Url::parse(&normalized).and_then(|u| u.join(path)).ok()
1028 })
1029 .collect()
1030}
1031
1032#[cfg(test)]
1033mod tests {
1034 use super::*;
1035
1036 #[test]
1037 fn test_default_client_has_retry_policy() {
1038 let client = RdapClient::new();
1039 assert_eq!(client.retry_policy.max_attempts, 3);
1043 }
1044
1045 #[test]
1048 fn parse_retry_after_parses_delta_seconds() {
1049 assert_eq!(parse_retry_after("5"), Some(Duration::from_secs(5)));
1050 assert_eq!(parse_retry_after(" 10 "), Some(Duration::from_secs(10)));
1051 assert_eq!(parse_retry_after("0"), Some(Duration::from_secs(0)));
1052 }
1053
1054 #[test]
1055 fn parse_retry_after_rejects_http_date_and_junk() {
1056 assert_eq!(parse_retry_after("Wed, 21 Oct 2015 07:28:00 GMT"), None);
1059 assert_eq!(parse_retry_after("soon"), None);
1060 assert_eq!(parse_retry_after(""), None);
1061 }
1062
1063 #[test]
1064 fn effective_retry_delay_prefers_capped_retry_after() {
1065 assert_eq!(
1067 effective_retry_delay(Duration::from_millis(100), Some(Duration::from_secs(5))),
1068 Duration::from_secs(5)
1069 );
1070 assert_eq!(
1072 effective_retry_delay(Duration::from_millis(100), Some(Duration::from_secs(600))),
1073 MAX_RETRY_AFTER
1074 );
1075 }
1076
1077 #[test]
1078 fn effective_retry_delay_falls_back_to_backoff() {
1079 assert_eq!(
1080 effective_retry_delay(Duration::from_millis(250), None),
1081 Duration::from_millis(250)
1082 );
1083 }
1084
1085 #[test]
1086 fn test_client_without_retries() {
1087 let client = RdapClient::new().without_retries();
1088 assert_eq!(client.retry_policy.max_attempts, 1);
1089 }
1090
1091 #[test]
1092 fn test_client_custom_retry_policy() {
1093 let policy = RetryPolicy::new().with_max_attempts(5);
1094 let client = RdapClient::new().with_retry_policy(policy);
1095 assert_eq!(client.retry_policy.max_attempts, 5);
1096 }
1097
1098 #[test]
1099 fn test_cached_bootstrap_expiration() {
1100 let data = BootstrapData {
1101 dns: HashMap::new(),
1102 ipv4: Vec::new(),
1103 ipv6: Vec::new(),
1104 asn: Vec::new(),
1105 };
1106 let cached = CachedBootstrap::new(data);
1107 assert!(!cached.is_expired());
1109 }
1110
1111 #[test]
1112 fn test_rdap_http_client_is_configured() {
1113 let client = rdap_http_client();
1116 assert!(client.is_ok(), "RDAP HTTP client builder must succeed");
1117 }
1118
1119 #[test]
1120 fn test_parse_bootstrap_empty_services() {
1121 let data = BootstrapData {
1123 dns: HashMap::new(),
1124 ipv4: Vec::new(),
1125 ipv6: Vec::new(),
1126 asn: Vec::new(),
1127 };
1128 assert!(RdapClient::get_rdap_urls_for_domain(&data, "example.com").is_none());
1130 assert!(RdapClient::get_rdap_urls_for_asn(&data, 12345).is_none());
1131 }
1132
1133 #[tokio::test]
1136 async fn test_validate_url_not_reserved_rejects_loopback_literal() {
1137 let err = validate_url_not_reserved("https://127.0.0.1/domain/example.com")
1138 .await
1139 .unwrap_err();
1140 assert!(
1141 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1142 "expected reserved-IP error, got: {:?}",
1143 err
1144 );
1145 }
1146
1147 #[tokio::test]
1148 async fn test_validate_url_not_reserved_rejects_private_ipv4_literal() {
1149 let err = validate_url_not_reserved("https://10.0.0.1/")
1150 .await
1151 .unwrap_err();
1152 assert!(
1153 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1154 "expected reserved-IP error, got: {:?}",
1155 err
1156 );
1157 }
1158
1159 #[tokio::test]
1160 async fn test_validate_url_not_reserved_rejects_ipv6_loopback_literal() {
1161 let err = validate_url_not_reserved("https://[::1]/")
1162 .await
1163 .unwrap_err();
1164 assert!(
1165 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1166 "expected reserved-IP error, got: {:?}",
1167 err
1168 );
1169 }
1170
1171 #[tokio::test]
1172 async fn test_validate_url_not_reserved_returns_resolved_addrs_for_public_literal() {
1173 let addrs = validate_url_not_reserved("https://8.8.8.8/").await.unwrap();
1176 assert_eq!(addrs.len(), 1);
1177 assert!(addrs[0].ip().is_ipv4());
1178 assert_eq!(addrs[0].port(), 443);
1179 }
1180
1181 #[test]
1184 fn test_build_rdap_urls_preserves_order_and_appends_path() {
1185 let bases = vec![
1186 url::Url::parse("https://rdap.a.example/").unwrap(),
1187 url::Url::parse("https://rdap.b.example").unwrap(), ];
1189 let built = build_rdap_urls(&bases, "domain/example.com");
1190 assert_eq!(built.len(), 2);
1191 assert_eq!(
1192 built[0].as_str(),
1193 "https://rdap.a.example/domain/example.com"
1194 );
1195 assert_eq!(
1196 built[1].as_str(),
1197 "https://rdap.b.example/domain/example.com"
1198 );
1199 }
1200
1201 #[test]
1202 fn test_build_rdap_urls_empty_input_returns_empty() {
1203 let built = build_rdap_urls(&[], "domain/example.com");
1204 assert!(built.is_empty());
1205 }
1206
1207 #[test]
1210 fn test_wrap_all_candidates_failed_preserves_timeout_variant() {
1211 let last = SeerError::Timeout("body read timed out".to_string());
1214 let wrapped = wrap_all_candidates_failed(Some(last), 3);
1215 match wrapped {
1216 SeerError::Timeout(msg) => {
1217 assert!(
1218 msg.contains("all 3 RDAP candidate URLs timed out"),
1219 "expected wrapped timeout message, got: {}",
1220 msg
1221 );
1222 assert!(
1223 msg.contains("body read timed out"),
1224 "expected original message preserved, got: {}",
1225 msg
1226 );
1227 }
1228 other => panic!(
1229 "expected SeerError::Timeout after wrapping a Timeout, got: {:?}",
1230 other
1231 ),
1232 }
1233 }
1234
1235 #[test]
1236 fn test_wrap_all_candidates_failed_wraps_non_timeout_as_rdap_error() {
1237 let last = SeerError::RdapError("500 internal error".to_string());
1238 let wrapped = wrap_all_candidates_failed(Some(last), 2);
1239 assert!(
1240 matches!(wrapped, SeerError::RdapError(ref s) if s.contains("all 2 RDAP candidate URLs failed")),
1241 "expected wrapped RdapError, got: {:?}",
1242 wrapped
1243 );
1244 }
1245
1246 #[test]
1247 fn test_wrap_all_candidates_failed_single_candidate_returns_unchanged() {
1248 let last = SeerError::Timeout("single timeout".to_string());
1251 let wrapped = wrap_all_candidates_failed(Some(last), 1);
1252 assert!(
1253 matches!(wrapped, SeerError::Timeout(ref s) if s == "single timeout"),
1254 "expected unchanged Timeout, got: {:?}",
1255 wrapped
1256 );
1257 }
1258
1259 #[test]
1260 fn test_wrap_all_candidates_failed_no_last_error_returns_placeholder() {
1261 let wrapped = wrap_all_candidates_failed(None, 0);
1262 assert!(matches!(wrapped, SeerError::RdapError(_)));
1263 }
1264
1265 static BOOTSTRAP_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
1284
1285 #[tokio::test]
1286 async fn test_bootstrap_load_notify_wakes_waiter_when_cache_populated() {
1287 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1288
1289 {
1291 let mut cache = BOOTSTRAP_CACHE.write().await;
1292 *cache = None;
1293 }
1294
1295 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1298 tokio::pin!(notified);
1299
1300 {
1302 let mut cache = BOOTSTRAP_CACHE.write().await;
1303 *cache = Some(CachedBootstrap::new(BootstrapData {
1304 dns: HashMap::new(),
1305 ipv4: Vec::new(),
1306 ipv6: Vec::new(),
1307 asn: Vec::new(),
1308 }));
1309 }
1310 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1311
1312 let result = wait_for_in_flight_load(notified).await;
1313 assert!(
1314 result.is_ok(),
1315 "expected waiter to see populated cache, got: {:?}",
1316 result
1317 );
1318
1319 {
1321 let mut cache = BOOTSTRAP_CACHE.write().await;
1322 *cache = None;
1323 }
1324 }
1325
1326 #[tokio::test]
1327 async fn test_bootstrap_load_notify_empty_cache_after_wake_returns_error() {
1328 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1329
1330 {
1332 let mut cache = BOOTSTRAP_CACHE.write().await;
1333 *cache = None;
1334 }
1335
1336 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1337 tokio::pin!(notified);
1338
1339 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1341
1342 let result = wait_for_in_flight_load(notified).await;
1343 assert!(
1344 matches!(
1345 result,
1346 Err(SeerError::RdapBootstrapError(ref s))
1347 if s.contains("throttled and no cache available")
1348 ),
1349 "expected throttled error when cache still empty after notify, got: {:?}",
1350 result
1351 );
1352 }
1353}