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 .build()
59 .ok()
60});
61
62fn rdap_http_client() -> Result<&'static Client> {
66 RDAP_HTTP_CLIENT
67 .as_ref()
68 .ok_or_else(|| SeerError::HttpError("failed to initialize HTTP client".into()))
69}
70
71static BOOTSTRAP_CACHE: Lazy<RwLock<Option<CachedBootstrap>>> = Lazy::new(|| RwLock::new(None));
73
74static BOOTSTRAP_LAST_ATTEMPT: Lazy<RwLock<Option<Instant>>> = Lazy::new(|| RwLock::new(None));
78
79static BOOTSTRAP_LOAD_NOTIFY: Lazy<Notify> = Lazy::new(Notify::new);
87
88struct CachedBootstrap {
90 data: BootstrapData,
91 loaded_at: Instant,
92}
93
94impl CachedBootstrap {
95 fn new(data: BootstrapData) -> Self {
96 Self {
97 data,
98 loaded_at: Instant::now(),
99 }
100 }
101
102 fn is_expired(&self) -> bool {
103 self.loaded_at.elapsed() > BOOTSTRAP_TTL
104 }
105
106 fn age(&self) -> Duration {
107 self.loaded_at.elapsed()
108 }
109}
110
111struct BootstrapData {
116 dns: HashMap<String, Arc<Vec<url::Url>>>,
117 ipv4: Vec<(IpRange, Arc<Vec<url::Url>>)>,
118 ipv6: Vec<(IpRange, Arc<Vec<url::Url>>)>,
119 asn: Vec<(AsnRange, Arc<Vec<url::Url>>)>,
120}
121
122#[derive(Clone)]
123struct IpRange {
124 prefix: String,
125}
126
127#[derive(Clone)]
128struct AsnRange {
129 start: u32,
130 end: u32,
131}
132
133#[derive(Deserialize)]
134struct BootstrapResponse {
135 services: Vec<Vec<serde_json::Value>>,
136}
137
138async fn wait_for_in_flight_load(
148 notified: std::pin::Pin<&mut tokio::sync::futures::Notified<'_>>,
149) -> Result<()> {
150 let _ = tokio::time::timeout(DEFAULT_TIMEOUT, notified).await;
153 let cache = BOOTSTRAP_CACHE.read().await;
154 if cache.is_some() {
155 Ok(())
156 } else {
157 Err(SeerError::RdapBootstrapError(
158 "bootstrap refresh throttled and no cache available".to_string(),
159 ))
160 }
161}
162
163#[derive(Debug, Clone)]
164pub struct RdapClient {
165 retry_policy: RetryPolicy,
166}
167
168impl Default for RdapClient {
169 fn default() -> Self {
170 Self::new()
171 }
172}
173
174impl RdapClient {
175 pub fn new() -> Self {
177 Self {
178 retry_policy: RetryPolicy::new()
185 .with_max_attempts(3)
186 .with_initial_delay(Duration::from_millis(500))
187 .with_max_delay(Duration::from_secs(5)),
188 }
189 }
190
191 pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
195 self.retry_policy = policy;
196 self
197 }
198
199 pub fn without_retries(mut self) -> Self {
201 self.retry_policy = RetryPolicy::no_retry();
202 self
203 }
204
205 async fn ensure_bootstrap(&self) -> Result<()> {
220 {
222 let cache = BOOTSTRAP_CACHE.read().await;
223 if let Some(cached) = cache.as_ref() {
224 if !cached.is_expired() {
225 return Ok(());
226 }
227 }
228 }
229
230 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
236 tokio::pin!(notified);
237
238 {
242 let last = BOOTSTRAP_LAST_ATTEMPT.read().await;
243 if let Some(ts) = *last {
244 if ts.elapsed() < BOOTSTRAP_REFRESH_MIN_INTERVAL {
245 let cache = BOOTSTRAP_CACHE.read().await;
247 if cache.is_some() {
248 return Ok(());
250 }
251 drop(cache);
254 drop(last);
255 return wait_for_in_flight_load(notified).await;
256 }
257 }
258 }
259
260 {
263 let mut last = BOOTSTRAP_LAST_ATTEMPT.write().await;
264 if let Some(ts) = *last {
266 if ts.elapsed() < BOOTSTRAP_REFRESH_MIN_INTERVAL {
267 drop(last);
268 let cache = BOOTSTRAP_CACHE.read().await;
269 if cache.is_some() {
270 return Ok(());
271 }
272 drop(cache);
273 return wait_for_in_flight_load(notified).await;
274 }
275 }
276 *last = Some(Instant::now());
277 }
278
279 debug!("Loading/refreshing RDAP bootstrap data");
283 let load_result = load_bootstrap_data_with_retry(&self.retry_policy).await;
284
285 let outcome = match load_result {
286 Ok(data) => {
287 let mut cache = BOOTSTRAP_CACHE.write().await;
288 let should_store = cache.as_ref().map(|c| c.is_expired()).unwrap_or(true);
291 if should_store {
292 *cache = Some(CachedBootstrap::new(data));
293 }
294 Ok(())
295 }
296 Err(e) => {
297 let cache = BOOTSTRAP_CACHE.read().await;
299 if let Some(cached) = cache.as_ref() {
300 debug!(
301 error = %e,
302 age_hours = cached.age().as_secs() / 3600,
303 "Bootstrap refresh failed, using stale data"
304 );
305 Ok(())
306 } else {
307 Err(e)
309 }
310 }
311 };
312
313 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
315 outcome
316 }
317
318 fn get_rdap_urls_for_domain(cache: &BootstrapData, domain: &str) -> Option<Arc<Vec<url::Url>>> {
320 let tld = domain.rsplit('.').next()?;
321 cache.dns.get(&tld.to_lowercase()).cloned()
322 }
323
324 fn get_rdap_urls_for_ip(cache: &BootstrapData, ip: &IpAddr) -> Option<Arc<Vec<url::Url>>> {
326 match ip {
327 IpAddr::V4(addr) => {
328 for (range, urls) in &cache.ipv4 {
329 if ipv4_matches_prefix(&range.prefix, addr) {
330 return Some(Arc::clone(urls));
331 }
332 }
333 }
334 IpAddr::V6(addr) => {
335 for (range, urls) in &cache.ipv6 {
336 if ipv6_matches_prefix(&range.prefix, addr) {
337 return Some(Arc::clone(urls));
338 }
339 }
340 }
341 }
342
343 None
344 }
345
346 fn get_rdap_urls_for_asn(cache: &BootstrapData, asn: u32) -> Option<Arc<Vec<url::Url>>> {
348 for (range, urls) in &cache.asn {
349 if asn >= range.start && asn <= range.end {
350 return Some(Arc::clone(urls));
351 }
352 }
353
354 None
355 }
356
357 #[instrument(skip(self), fields(domain = %domain))]
361 pub async fn lookup_domain(&self, domain: &str) -> Result<RdapResponse> {
362 self.ensure_bootstrap().await?;
363
364 let domain = normalize_domain(domain)?;
365
366 let urls = {
368 let cache_guard = BOOTSTRAP_CACHE.read().await;
369 let cache = cache_guard.as_ref().ok_or_else(|| {
370 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
371 })?;
372
373 let bases = Self::get_rdap_urls_for_domain(&cache.data, &domain).ok_or_else(|| {
374 SeerError::RdapBootstrapError(format!("no RDAP server for {}", domain))
375 })?;
376
377 build_rdap_urls(&bases, &format!("domain/{}", domain))
378 }; self.query_rdap_urls(&urls).await
381 }
382
383 #[instrument(skip(self), fields(ip = %ip))]
387 pub async fn lookup_ip(&self, ip: &str) -> Result<RdapResponse> {
388 self.ensure_bootstrap().await?;
389
390 let ip_addr: IpAddr = ip
391 .parse()
392 .map_err(|_| SeerError::InvalidIpAddress(ip.to_string()))?;
393
394 let urls = {
395 let cache_guard = BOOTSTRAP_CACHE.read().await;
396 let cache = cache_guard.as_ref().ok_or_else(|| {
397 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
398 })?;
399
400 let bases = Self::get_rdap_urls_for_ip(&cache.data, &ip_addr).ok_or_else(|| {
401 SeerError::RdapBootstrapError(format!("no RDAP server for {}", ip))
402 })?;
403
404 build_rdap_urls(&bases, &format!("ip/{}", ip))
405 };
406
407 self.query_rdap_urls(&urls).await
408 }
409
410 #[instrument(skip(self), fields(asn = %asn))]
414 pub async fn lookup_asn(&self, asn: u32) -> Result<RdapResponse> {
415 self.ensure_bootstrap().await?;
416
417 let urls = {
418 let cache_guard = BOOTSTRAP_CACHE.read().await;
419 let cache = cache_guard.as_ref().ok_or_else(|| {
420 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
421 })?;
422
423 let bases = Self::get_rdap_urls_for_asn(&cache.data, asn).ok_or_else(|| {
424 SeerError::RdapBootstrapError(format!("no RDAP server for AS{}", asn))
425 })?;
426
427 build_rdap_urls(&bases, &format!("autnum/{}", asn))
428 };
429
430 self.query_rdap_urls(&urls).await
431 }
432
433 #[instrument(skip(self), fields(tld = %tld))]
439 pub async fn get_rdap_base_url_for_tld(&self, tld: &str) -> Option<String> {
440 if self.ensure_bootstrap().await.is_err() {
441 return None;
442 }
443
444 let cache_guard = BOOTSTRAP_CACHE.read().await;
445 let cache = cache_guard.as_ref()?;
446 cache
447 .data
448 .dns
449 .get(&tld.to_lowercase())
450 .and_then(|urls| urls.first())
451 .map(|u| u.to_string())
452 }
453
454 async fn query_rdap_urls(&self, urls: &[url::Url]) -> Result<RdapResponse> {
458 if urls.is_empty() {
459 return Err(SeerError::RdapError(
460 "no candidate RDAP URLs available".to_string(),
461 ));
462 }
463
464 let mut last_error: Option<SeerError> = None;
465 for (idx, url) in urls.iter().enumerate() {
466 let url_str = url.as_str().to_string();
467 debug!(url = %url_str, candidate = idx + 1, total = urls.len(), "Querying RDAP");
468 match self.query_rdap_with_retry(&url_str).await {
469 Ok(resp) => return Ok(resp),
470 Err(e) => {
471 if urls.len() > 1 {
472 debug!(
473 url = %url_str,
474 error = %e,
475 candidate = idx + 1,
476 total = urls.len(),
477 "RDAP candidate failed, trying next",
478 );
479 }
480 last_error = Some(e);
481 }
482 }
483 }
484
485 Err(wrap_all_candidates_failed(last_error, urls.len()))
487 }
488
489 async fn query_rdap_with_retry(&self, url: &str) -> Result<RdapResponse> {
494 let classifier = NetworkRetryClassifier::new();
495 let mut attempt = 0;
496 loop {
497 match query_rdap_attempt(url).await {
498 Ok(resp) => return Ok(resp),
499 Err((err, retry_after)) => {
500 let attempts_remaining =
501 self.retry_policy.max_attempts.saturating_sub(attempt + 1);
502 if !classifier.is_retryable(&err) || attempts_remaining == 0 {
503 return Err(if attempt > 0 {
504 SeerError::RetryExhausted {
505 attempts: attempt + 1,
506 last_error: Box::new(err),
507 }
508 } else {
509 err
510 });
511 }
512 let backoff = self.retry_policy.delay_for_attempt(attempt);
513 let delay = effective_retry_delay(backoff, retry_after);
514 debug!(
515 url = %url,
516 attempt = attempt + 1,
517 max_attempts = self.retry_policy.max_attempts,
518 delay_ms = delay.as_millis(),
519 error = %err,
520 "Retrying RDAP after transient error"
521 );
522 tokio::time::sleep(delay).await;
523 attempt += 1;
524 }
525 }
526 }
527 }
528}
529
530const MAX_RDAP_RESPONSE_SIZE: usize = 10 * 1024 * 1024;
532
533const MAX_RETRY_AFTER: Duration = Duration::from_secs(5);
538
539async fn validate_url_not_reserved(url: &str) -> Result<Vec<SocketAddr>> {
546 let parsed = url::Url::parse(url)
547 .map_err(|e| SeerError::RdapError(format!("invalid URL '{}': {}", url, e)))?;
548 let host = parsed
549 .host_str()
550 .ok_or_else(|| SeerError::RdapError(format!("URL '{}' has no host", url)))?;
551 let port = parsed.port_or_known_default().unwrap_or(443);
552
553 if let Ok(ip) = host.parse::<IpAddr>() {
555 if let Some(reason) = describe_reserved_ip(&ip) {
556 return Err(SeerError::RdapError(format!(
557 "RDAP URL resolves to reserved IP {}: {} — request blocked (SSRF protection)",
558 ip, reason
559 )));
560 }
561 return Ok(vec![SocketAddr::new(ip, port)]);
562 }
563
564 let addr = format!("{}:{}", host, port);
565
566 let socket_addrs: Vec<SocketAddr> = tokio::net::lookup_host(&addr)
567 .await
568 .map_err(|e| SeerError::RdapError(format!("failed to resolve host '{}': {}", host, e)))?
569 .collect();
570
571 if socket_addrs.is_empty() {
572 return Err(SeerError::RdapError(format!(
573 "host '{}' resolved to no addresses",
574 host
575 )));
576 }
577
578 for socket_addr in &socket_addrs {
579 if let Some(reason) = describe_reserved_ip(&socket_addr.ip()) {
580 return Err(SeerError::RdapError(format!(
581 "RDAP URL resolves to reserved IP {}: {} — request blocked (SSRF protection)",
582 socket_addr.ip(),
583 reason
584 )));
585 }
586 }
587
588 Ok(socket_addrs)
589}
590
591fn parse_retry_after(value: &str) -> Option<Duration> {
596 value.trim().parse::<u64>().ok().map(Duration::from_secs)
597}
598
599fn effective_retry_delay(backoff: Duration, retry_after: Option<Duration>) -> Duration {
603 match retry_after {
604 Some(hint) => hint.min(MAX_RETRY_AFTER),
605 None => backoff,
606 }
607}
608
609async fn send_rdap_request(url: &str) -> Result<reqwest::Response> {
612 let resolved = validate_url_not_reserved(url).await?;
615
616 let parsed = url::Url::parse(url)
617 .map_err(|e| SeerError::RdapError(format!("invalid URL '{}': {}", url, e)))?;
618 let host = parsed
619 .host_str()
620 .ok_or_else(|| SeerError::RdapError(format!("URL '{}' has no host", url)))?;
621
622 let client = Client::builder()
626 .timeout(DEFAULT_TIMEOUT)
627 .connect_timeout(CONNECT_TIMEOUT)
628 .user_agent("Seer/1.0 (RDAP Client)")
629 .resolve_to_addrs(host, &resolved)
630 .build()
631 .map_err(|e| SeerError::RdapError(format!("failed to build HTTP client: {}", e)))?;
632
633 client
634 .get(url)
635 .header("Accept", "application/rdap+json")
636 .send()
637 .await
638 .map_err(Into::into)
639}
640
641async fn read_and_parse_rdap_body(response: reqwest::Response, url: &str) -> Result<RdapResponse> {
644 let mut body = Vec::new();
649 let mut stream = response.bytes_stream();
650 let streamed = tokio::time::timeout(DEFAULT_TIMEOUT, async {
651 while let Some(chunk) = stream.next().await {
652 let chunk = chunk
653 .map_err(|e| SeerError::RdapError(format!("failed to read response: {}", e)))?;
654 body.extend_from_slice(&chunk);
655 if body.len() > MAX_RDAP_RESPONSE_SIZE {
656 return Err(SeerError::RdapError(format!(
657 "RDAP response exceeds {} byte limit",
658 MAX_RDAP_RESPONSE_SIZE
659 )));
660 }
661 }
662 Ok::<(), SeerError>(())
663 })
664 .await;
665
666 match streamed {
667 Ok(Ok(())) => {}
668 Ok(Err(e)) => return Err(e),
669 Err(_) => {
670 return Err(SeerError::Timeout(format!(
671 "timed out reading RDAP response body from {} after {:?}",
672 url, DEFAULT_TIMEOUT
673 )));
674 }
675 }
676
677 let rdap: RdapResponse = serde_json::from_slice(&body)?;
678 rdap.validate()?;
684 Ok(rdap)
685}
686
687async fn query_rdap_attempt(
693 url: &str,
694) -> std::result::Result<RdapResponse, (SeerError, Option<Duration>)> {
695 let response = send_rdap_request(url).await.map_err(|e| (e, None))?;
696
697 if !response.status().is_success() {
698 let status = response.status();
699 let retry_after = if status.as_u16() == 429 {
702 response
703 .headers()
704 .get(reqwest::header::RETRY_AFTER)
705 .and_then(|v| v.to_str().ok())
706 .and_then(parse_retry_after)
707 } else {
708 None
709 };
710 return Err((
711 SeerError::RdapError(format!("query failed with status {}", status)),
712 retry_after,
713 ));
714 }
715
716 read_and_parse_rdap_body(response, url)
717 .await
718 .map_err(|e| (e, None))
719}
720
721async fn load_bootstrap_data_with_retry(policy: &RetryPolicy) -> Result<BootstrapData> {
723 let executor = RetryExecutor::new(policy.clone());
724 executor.execute(load_bootstrap_data).await
725}
726
727async fn load_bootstrap_data() -> Result<BootstrapData> {
729 debug!("Loading RDAP bootstrap data from IANA");
730
731 let http = rdap_http_client()?;
735
736 let dns_future = http.get(IANA_BOOTSTRAP_DNS).send();
737 let ipv4_future = http.get(IANA_BOOTSTRAP_IPV4).send();
738 let ipv6_future = http.get(IANA_BOOTSTRAP_IPV6).send();
739 let asn_future = http.get(IANA_BOOTSTRAP_ASN).send();
740
741 let (dns_resp, ipv4_resp, ipv6_resp, asn_resp) =
744 tokio::join!(dns_future, ipv4_future, ipv6_future, asn_future);
745
746 const MAX_BOOTSTRAP_SIZE: usize = 10 * 1024 * 1024; async fn read_bootstrap(resp: reqwest::Response) -> Result<BootstrapResponse> {
750 let mut body = Vec::new();
756 let mut stream = resp.bytes_stream();
757 let streamed = tokio::time::timeout(DEFAULT_TIMEOUT, async {
758 while let Some(chunk) = stream.next().await {
759 let chunk = chunk.map_err(|e| {
760 SeerError::RdapBootstrapError(format!("failed to read body: {}", e))
761 })?;
762 body.extend_from_slice(&chunk);
763 if body.len() > MAX_BOOTSTRAP_SIZE {
764 return Err(SeerError::RdapBootstrapError(format!(
765 "bootstrap response too large (exceeds {} bytes)",
766 MAX_BOOTSTRAP_SIZE
767 )));
768 }
769 }
770 Ok::<(), SeerError>(())
771 })
772 .await;
773
774 match streamed {
775 Ok(Ok(())) => {}
776 Ok(Err(e)) => return Err(e),
777 Err(_) => {
778 return Err(SeerError::Timeout(format!(
779 "RDAP bootstrap body read timed out after {:?}",
780 DEFAULT_TIMEOUT
781 )));
782 }
783 }
784
785 serde_json::from_slice(&body).map_err(Into::into)
786 }
787
788 let dns_data = match dns_resp {
790 Ok(resp) => match read_bootstrap(resp).await {
791 Ok(data) => Some(data),
792 Err(e) => {
793 warn!(error = %e, "Failed to parse DNS bootstrap response");
794 None
795 }
796 },
797 Err(e) => {
798 warn!(error = %e, "Failed to fetch DNS bootstrap from IANA");
799 None
800 }
801 };
802 let ipv4_data = match ipv4_resp {
803 Ok(resp) => match read_bootstrap(resp).await {
804 Ok(data) => Some(data),
805 Err(e) => {
806 warn!(error = %e, "Failed to parse IPv4 bootstrap response");
807 None
808 }
809 },
810 Err(e) => {
811 warn!(error = %e, "Failed to fetch IPv4 bootstrap from IANA");
812 None
813 }
814 };
815 let ipv6_data = match ipv6_resp {
816 Ok(resp) => match read_bootstrap(resp).await {
817 Ok(data) => Some(data),
818 Err(e) => {
819 warn!(error = %e, "Failed to parse IPv6 bootstrap response");
820 None
821 }
822 },
823 Err(e) => {
824 warn!(error = %e, "Failed to fetch IPv6 bootstrap from IANA");
825 None
826 }
827 };
828 let asn_data = match asn_resp {
829 Ok(resp) => match read_bootstrap(resp).await {
830 Ok(data) => Some(data),
831 Err(e) => {
832 warn!(error = %e, "Failed to parse ASN bootstrap response");
833 None
834 }
835 },
836 Err(e) => {
837 warn!(error = %e, "Failed to fetch ASN bootstrap from IANA");
838 None
839 }
840 };
841
842 if dns_data.is_none() && ipv4_data.is_none() && ipv6_data.is_none() && asn_data.is_none() {
844 return Err(SeerError::RdapBootstrapError(
845 "all IANA bootstrap registries failed".to_string(),
846 ));
847 }
848
849 let mut dns = HashMap::new();
850 let mut ipv4 = Vec::new();
851 let mut ipv6 = Vec::new();
852 let mut asn = Vec::new();
853
854 fn collect_valid_urls(urls: &[serde_json::Value]) -> Option<Arc<Vec<url::Url>>> {
858 let mut out = Vec::new();
859 for u in urls {
860 if let Some(s) = u.as_str() {
861 match validate_bootstrap_url(s) {
862 Ok(parsed) => out.push(parsed),
863 Err(e) => {
864 debug!(url = s, error = %e, "Skipping invalid bootstrap URL");
865 }
866 }
867 }
868 }
869 if out.is_empty() {
870 None
871 } else {
872 Some(Arc::new(out))
873 }
874 }
875
876 if let Some(dns_data) = dns_data {
878 for service in dns_data.services {
879 if service.len() >= 2 {
880 if let (Some(tlds), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
881 if let Some(urls_arc) = collect_valid_urls(urls) {
882 for tld in tlds {
883 if let Some(tld_str) = tld.as_str() {
884 dns.insert(tld_str.to_lowercase(), Arc::clone(&urls_arc));
885 }
886 }
887 }
888 }
889 }
890 }
891 }
892
893 if let Some(ipv4_data) = ipv4_data {
895 for service in ipv4_data.services {
896 if service.len() >= 2 {
897 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array())
898 {
899 if let Some(urls_arc) = collect_valid_urls(urls) {
900 for prefix in prefixes {
901 if let Some(prefix_str) = prefix.as_str() {
902 ipv4.push((
903 IpRange {
904 prefix: prefix_str.to_string(),
905 },
906 Arc::clone(&urls_arc),
907 ));
908 }
909 }
910 }
911 }
912 }
913 }
914 }
915
916 if let Some(ipv6_data) = ipv6_data {
918 for service in ipv6_data.services {
919 if service.len() >= 2 {
920 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array())
921 {
922 if let Some(urls_arc) = collect_valid_urls(urls) {
923 for prefix in prefixes {
924 if let Some(prefix_str) = prefix.as_str() {
925 ipv6.push((
926 IpRange {
927 prefix: prefix_str.to_string(),
928 },
929 Arc::clone(&urls_arc),
930 ));
931 }
932 }
933 }
934 }
935 }
936 }
937 }
938
939 if let Some(asn_data) = asn_data {
941 for service in asn_data.services {
942 if service.len() >= 2 {
943 if let (Some(ranges), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
944 if let Some(urls_arc) = collect_valid_urls(urls) {
945 for range in ranges {
946 if let Some(range_str) = range.as_str() {
947 if let Some((start, end)) = parse_asn_range(range_str) {
948 asn.push((AsnRange { start, end }, Arc::clone(&urls_arc)));
949 }
950 }
951 }
952 }
953 }
954 }
955 }
956 }
957
958 info!(
959 dns_entries = dns.len(),
960 ipv4_ranges = ipv4.len(),
961 ipv6_ranges = ipv6.len(),
962 asn_ranges = asn.len(),
963 "RDAP bootstrap loaded"
964 );
965
966 Ok(BootstrapData {
967 dns,
968 ipv4,
969 ipv6,
970 asn,
971 })
972}
973
974fn wrap_all_candidates_failed(last_error: Option<SeerError>, candidate_count: usize) -> SeerError {
983 let last = last_error.unwrap_or_else(|| SeerError::RdapError("no candidates".to_string()));
984
985 if candidate_count <= 1 {
986 return last;
987 }
988
989 match last {
990 SeerError::Timeout(msg) => SeerError::Timeout(format!(
991 "all {} RDAP candidate URLs timed out; last error: {}",
992 candidate_count, msg
993 )),
994 other => SeerError::RdapError(format!(
995 "all {} RDAP candidate URLs failed; last error: {}",
996 candidate_count, other
997 )),
998 }
999}
1000
1001fn build_rdap_urls(bases: &[url::Url], path: &str) -> Vec<url::Url> {
1003 bases
1004 .iter()
1005 .filter_map(|base| {
1006 let base_str = base.as_str();
1009 let normalized = if base_str.ends_with('/') {
1010 base_str.to_string()
1011 } else {
1012 format!("{}/", base_str)
1013 };
1014 url::Url::parse(&normalized).and_then(|u| u.join(path)).ok()
1015 })
1016 .collect()
1017}
1018
1019#[cfg(test)]
1020mod tests {
1021 use super::*;
1022
1023 #[test]
1024 fn test_default_client_has_retry_policy() {
1025 let client = RdapClient::new();
1026 assert_eq!(client.retry_policy.max_attempts, 3);
1030 }
1031
1032 #[test]
1035 fn parse_retry_after_parses_delta_seconds() {
1036 assert_eq!(parse_retry_after("5"), Some(Duration::from_secs(5)));
1037 assert_eq!(parse_retry_after(" 10 "), Some(Duration::from_secs(10)));
1038 assert_eq!(parse_retry_after("0"), Some(Duration::from_secs(0)));
1039 }
1040
1041 #[test]
1042 fn parse_retry_after_rejects_http_date_and_junk() {
1043 assert_eq!(parse_retry_after("Wed, 21 Oct 2015 07:28:00 GMT"), None);
1046 assert_eq!(parse_retry_after("soon"), None);
1047 assert_eq!(parse_retry_after(""), None);
1048 }
1049
1050 #[test]
1051 fn effective_retry_delay_prefers_capped_retry_after() {
1052 assert_eq!(
1054 effective_retry_delay(Duration::from_millis(100), Some(Duration::from_secs(5))),
1055 Duration::from_secs(5)
1056 );
1057 assert_eq!(
1059 effective_retry_delay(Duration::from_millis(100), Some(Duration::from_secs(600))),
1060 MAX_RETRY_AFTER
1061 );
1062 }
1063
1064 #[test]
1065 fn effective_retry_delay_falls_back_to_backoff() {
1066 assert_eq!(
1067 effective_retry_delay(Duration::from_millis(250), None),
1068 Duration::from_millis(250)
1069 );
1070 }
1071
1072 #[test]
1073 fn test_client_without_retries() {
1074 let client = RdapClient::new().without_retries();
1075 assert_eq!(client.retry_policy.max_attempts, 1);
1076 }
1077
1078 #[test]
1079 fn test_client_custom_retry_policy() {
1080 let policy = RetryPolicy::new().with_max_attempts(5);
1081 let client = RdapClient::new().with_retry_policy(policy);
1082 assert_eq!(client.retry_policy.max_attempts, 5);
1083 }
1084
1085 #[test]
1086 fn test_cached_bootstrap_expiration() {
1087 let data = BootstrapData {
1088 dns: HashMap::new(),
1089 ipv4: Vec::new(),
1090 ipv6: Vec::new(),
1091 asn: Vec::new(),
1092 };
1093 let cached = CachedBootstrap::new(data);
1094 assert!(!cached.is_expired());
1096 }
1097
1098 #[test]
1099 fn test_rdap_http_client_is_configured() {
1100 let client = rdap_http_client();
1103 assert!(client.is_ok(), "RDAP HTTP client builder must succeed");
1104 }
1105
1106 #[test]
1107 fn test_parse_bootstrap_empty_services() {
1108 let data = BootstrapData {
1110 dns: HashMap::new(),
1111 ipv4: Vec::new(),
1112 ipv6: Vec::new(),
1113 asn: Vec::new(),
1114 };
1115 assert!(RdapClient::get_rdap_urls_for_domain(&data, "example.com").is_none());
1117 assert!(RdapClient::get_rdap_urls_for_asn(&data, 12345).is_none());
1118 }
1119
1120 #[tokio::test]
1123 async fn test_validate_url_not_reserved_rejects_loopback_literal() {
1124 let err = validate_url_not_reserved("https://127.0.0.1/domain/example.com")
1125 .await
1126 .unwrap_err();
1127 assert!(
1128 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1129 "expected reserved-IP error, got: {:?}",
1130 err
1131 );
1132 }
1133
1134 #[tokio::test]
1135 async fn test_validate_url_not_reserved_rejects_private_ipv4_literal() {
1136 let err = validate_url_not_reserved("https://10.0.0.1/")
1137 .await
1138 .unwrap_err();
1139 assert!(
1140 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1141 "expected reserved-IP error, got: {:?}",
1142 err
1143 );
1144 }
1145
1146 #[tokio::test]
1147 async fn test_validate_url_not_reserved_rejects_ipv6_loopback_literal() {
1148 let err = validate_url_not_reserved("https://[::1]/")
1149 .await
1150 .unwrap_err();
1151 assert!(
1152 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1153 "expected reserved-IP error, got: {:?}",
1154 err
1155 );
1156 }
1157
1158 #[tokio::test]
1159 async fn test_validate_url_not_reserved_returns_resolved_addrs_for_public_literal() {
1160 let addrs = validate_url_not_reserved("https://8.8.8.8/").await.unwrap();
1163 assert_eq!(addrs.len(), 1);
1164 assert!(addrs[0].ip().is_ipv4());
1165 assert_eq!(addrs[0].port(), 443);
1166 }
1167
1168 #[test]
1171 fn test_build_rdap_urls_preserves_order_and_appends_path() {
1172 let bases = vec![
1173 url::Url::parse("https://rdap.a.example/").unwrap(),
1174 url::Url::parse("https://rdap.b.example").unwrap(), ];
1176 let built = build_rdap_urls(&bases, "domain/example.com");
1177 assert_eq!(built.len(), 2);
1178 assert_eq!(
1179 built[0].as_str(),
1180 "https://rdap.a.example/domain/example.com"
1181 );
1182 assert_eq!(
1183 built[1].as_str(),
1184 "https://rdap.b.example/domain/example.com"
1185 );
1186 }
1187
1188 #[test]
1189 fn test_build_rdap_urls_empty_input_returns_empty() {
1190 let built = build_rdap_urls(&[], "domain/example.com");
1191 assert!(built.is_empty());
1192 }
1193
1194 #[test]
1197 fn test_wrap_all_candidates_failed_preserves_timeout_variant() {
1198 let last = SeerError::Timeout("body read timed out".to_string());
1201 let wrapped = wrap_all_candidates_failed(Some(last), 3);
1202 match wrapped {
1203 SeerError::Timeout(msg) => {
1204 assert!(
1205 msg.contains("all 3 RDAP candidate URLs timed out"),
1206 "expected wrapped timeout message, got: {}",
1207 msg
1208 );
1209 assert!(
1210 msg.contains("body read timed out"),
1211 "expected original message preserved, got: {}",
1212 msg
1213 );
1214 }
1215 other => panic!(
1216 "expected SeerError::Timeout after wrapping a Timeout, got: {:?}",
1217 other
1218 ),
1219 }
1220 }
1221
1222 #[test]
1223 fn test_wrap_all_candidates_failed_wraps_non_timeout_as_rdap_error() {
1224 let last = SeerError::RdapError("500 internal error".to_string());
1225 let wrapped = wrap_all_candidates_failed(Some(last), 2);
1226 assert!(
1227 matches!(wrapped, SeerError::RdapError(ref s) if s.contains("all 2 RDAP candidate URLs failed")),
1228 "expected wrapped RdapError, got: {:?}",
1229 wrapped
1230 );
1231 }
1232
1233 #[test]
1234 fn test_wrap_all_candidates_failed_single_candidate_returns_unchanged() {
1235 let last = SeerError::Timeout("single timeout".to_string());
1238 let wrapped = wrap_all_candidates_failed(Some(last), 1);
1239 assert!(
1240 matches!(wrapped, SeerError::Timeout(ref s) if s == "single timeout"),
1241 "expected unchanged Timeout, got: {:?}",
1242 wrapped
1243 );
1244 }
1245
1246 #[test]
1247 fn test_wrap_all_candidates_failed_no_last_error_returns_placeholder() {
1248 let wrapped = wrap_all_candidates_failed(None, 0);
1249 assert!(matches!(wrapped, SeerError::RdapError(_)));
1250 }
1251
1252 static BOOTSTRAP_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
1271
1272 #[tokio::test]
1273 async fn test_bootstrap_load_notify_wakes_waiter_when_cache_populated() {
1274 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1275
1276 {
1278 let mut cache = BOOTSTRAP_CACHE.write().await;
1279 *cache = None;
1280 }
1281
1282 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1285 tokio::pin!(notified);
1286
1287 {
1289 let mut cache = BOOTSTRAP_CACHE.write().await;
1290 *cache = Some(CachedBootstrap::new(BootstrapData {
1291 dns: HashMap::new(),
1292 ipv4: Vec::new(),
1293 ipv6: Vec::new(),
1294 asn: Vec::new(),
1295 }));
1296 }
1297 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1298
1299 let result = wait_for_in_flight_load(notified).await;
1300 assert!(
1301 result.is_ok(),
1302 "expected waiter to see populated cache, got: {:?}",
1303 result
1304 );
1305
1306 {
1308 let mut cache = BOOTSTRAP_CACHE.write().await;
1309 *cache = None;
1310 }
1311 }
1312
1313 #[tokio::test]
1314 async fn test_bootstrap_load_notify_empty_cache_after_wake_returns_error() {
1315 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1316
1317 {
1319 let mut cache = BOOTSTRAP_CACHE.write().await;
1320 *cache = None;
1321 }
1322
1323 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1324 tokio::pin!(notified);
1325
1326 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1328
1329 let result = wait_for_in_flight_load(notified).await;
1330 assert!(
1331 matches!(
1332 result,
1333 Err(SeerError::RdapBootstrapError(ref s))
1334 if s.contains("throttled and no cache available")
1335 ),
1336 "expected throttled error when cache still empty after notify, got: {:?}",
1337 result
1338 );
1339 }
1340}