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 allow_reserved: bool,
174}
175
176impl Default for RdapClient {
177 fn default() -> Self {
178 Self::new()
179 }
180}
181
182impl RdapClient {
183 pub fn new() -> Self {
185 Self {
186 retry_policy: RetryPolicy::new()
193 .with_max_attempts(3)
194 .with_initial_delay(Duration::from_millis(500))
195 .with_max_delay(Duration::from_secs(5)),
196 allow_reserved: false,
197 }
198 }
199
200 #[cfg(test)]
202 pub(crate) fn allowing_reserved_for_tests(mut self) -> Self {
203 self.allow_reserved = true;
204 self
205 }
206
207 pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
211 self.retry_policy = policy;
212 self
213 }
214
215 pub fn without_retries(mut self) -> Self {
217 self.retry_policy = RetryPolicy::no_retry();
218 self
219 }
220
221 async fn ensure_bootstrap(&self) -> Result<()> {
236 {
238 let cache = BOOTSTRAP_CACHE.read().await;
239 if let Some(cached) = cache.as_ref() {
240 if !cached.is_expired() {
241 return Ok(());
242 }
243 }
244 }
245
246 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
252 tokio::pin!(notified);
253
254 {
258 let last = BOOTSTRAP_LAST_ATTEMPT.read().await;
259 if let Some(ts) = *last {
260 if ts.elapsed() < BOOTSTRAP_REFRESH_MIN_INTERVAL {
261 let cache = BOOTSTRAP_CACHE.read().await;
263 if cache.is_some() {
264 return Ok(());
266 }
267 drop(cache);
270 drop(last);
271 return wait_for_in_flight_load(notified).await;
272 }
273 }
274 }
275
276 {
279 let mut last = BOOTSTRAP_LAST_ATTEMPT.write().await;
280 if let Some(ts) = *last {
282 if ts.elapsed() < BOOTSTRAP_REFRESH_MIN_INTERVAL {
283 drop(last);
284 let cache = BOOTSTRAP_CACHE.read().await;
285 if cache.is_some() {
286 return Ok(());
287 }
288 drop(cache);
289 return wait_for_in_flight_load(notified).await;
290 }
291 }
292 *last = Some(Instant::now());
293 }
294
295 debug!("Loading/refreshing RDAP bootstrap data");
299 let load_result = load_bootstrap_data_with_retry(&self.retry_policy).await;
300
301 let outcome = match load_result {
302 Ok(data) => {
303 let mut cache = BOOTSTRAP_CACHE.write().await;
304 let should_store = cache.as_ref().map(|c| c.is_expired()).unwrap_or(true);
307 if should_store {
308 *cache = Some(CachedBootstrap::new(data));
309 }
310 Ok(())
311 }
312 Err(e) => {
313 let cache = BOOTSTRAP_CACHE.read().await;
315 if let Some(cached) = cache.as_ref() {
316 debug!(
317 error = %e,
318 age_hours = cached.age().as_secs() / 3600,
319 "Bootstrap refresh failed, using stale data"
320 );
321 Ok(())
322 } else {
323 Err(e)
325 }
326 }
327 };
328
329 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
331 outcome
332 }
333
334 fn get_rdap_urls_for_domain(cache: &BootstrapData, domain: &str) -> Option<Arc<Vec<url::Url>>> {
336 let tld = domain.rsplit('.').next()?;
337 cache.dns.get(&tld.to_lowercase()).cloned()
338 }
339
340 fn get_rdap_urls_for_ip(cache: &BootstrapData, ip: &IpAddr) -> Option<Arc<Vec<url::Url>>> {
342 match ip {
343 IpAddr::V4(addr) => {
344 for (range, urls) in &cache.ipv4 {
345 if ipv4_matches_prefix(&range.prefix, addr) {
346 return Some(Arc::clone(urls));
347 }
348 }
349 }
350 IpAddr::V6(addr) => {
351 for (range, urls) in &cache.ipv6 {
352 if ipv6_matches_prefix(&range.prefix, addr) {
353 return Some(Arc::clone(urls));
354 }
355 }
356 }
357 }
358
359 None
360 }
361
362 fn get_rdap_urls_for_asn(cache: &BootstrapData, asn: u32) -> Option<Arc<Vec<url::Url>>> {
364 for (range, urls) in &cache.asn {
365 if asn >= range.start && asn <= range.end {
366 return Some(Arc::clone(urls));
367 }
368 }
369
370 None
371 }
372
373 #[instrument(skip(self), fields(domain = %domain))]
377 pub async fn lookup_domain(&self, domain: &str) -> Result<RdapResponse> {
378 self.ensure_bootstrap().await?;
379
380 let domain = normalize_domain(domain)?;
381
382 let urls = {
384 let cache_guard = BOOTSTRAP_CACHE.read().await;
385 let cache = cache_guard.as_ref().ok_or_else(|| {
386 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
387 })?;
388
389 let bases = Self::get_rdap_urls_for_domain(&cache.data, &domain).ok_or_else(|| {
390 SeerError::RdapBootstrapError(format!("no RDAP server for {}", domain))
391 })?;
392
393 build_rdap_urls(&bases, &format!("domain/{}", domain))
394 }; self.query_rdap_urls(&urls).await
397 }
398
399 #[instrument(skip(self), fields(ip = %ip))]
403 pub async fn lookup_ip(&self, ip: &str) -> Result<RdapResponse> {
404 self.ensure_bootstrap().await?;
405
406 let ip_addr: IpAddr = ip
407 .parse()
408 .map_err(|_| SeerError::InvalidIpAddress(ip.to_string()))?;
409
410 let urls = {
411 let cache_guard = BOOTSTRAP_CACHE.read().await;
412 let cache = cache_guard.as_ref().ok_or_else(|| {
413 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
414 })?;
415
416 let bases = Self::get_rdap_urls_for_ip(&cache.data, &ip_addr).ok_or_else(|| {
417 SeerError::RdapBootstrapError(format!("no RDAP server for {}", ip))
418 })?;
419
420 build_rdap_urls(&bases, &format!("ip/{}", ip))
421 };
422
423 self.query_rdap_urls(&urls).await
424 }
425
426 #[instrument(skip(self), fields(asn = %asn))]
430 pub async fn lookup_asn(&self, asn: u32) -> Result<RdapResponse> {
431 self.ensure_bootstrap().await?;
432
433 let urls = {
434 let cache_guard = BOOTSTRAP_CACHE.read().await;
435 let cache = cache_guard.as_ref().ok_or_else(|| {
436 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
437 })?;
438
439 let bases = Self::get_rdap_urls_for_asn(&cache.data, asn).ok_or_else(|| {
440 SeerError::RdapBootstrapError(format!("no RDAP server for AS{}", asn))
441 })?;
442
443 build_rdap_urls(&bases, &format!("autnum/{}", asn))
444 };
445
446 self.query_rdap_urls(&urls).await
447 }
448
449 #[instrument(skip(self), fields(tld = %tld))]
455 pub async fn get_rdap_base_url_for_tld(&self, tld: &str) -> Option<String> {
456 if self.ensure_bootstrap().await.is_err() {
457 return None;
458 }
459
460 let cache_guard = BOOTSTRAP_CACHE.read().await;
461 let cache = cache_guard.as_ref()?;
462 cache
463 .data
464 .dns
465 .get(&tld.to_lowercase())
466 .and_then(|urls| urls.first())
467 .map(|u| u.to_string())
468 }
469
470 async fn query_rdap_urls(&self, urls: &[url::Url]) -> Result<RdapResponse> {
474 if urls.is_empty() {
475 return Err(SeerError::RdapError(
476 "no candidate RDAP URLs available".to_string(),
477 ));
478 }
479
480 let mut last_error: Option<SeerError> = None;
481 for (idx, url) in urls.iter().enumerate() {
482 let url_str = url.as_str().to_string();
483 debug!(url = %url_str, candidate = idx + 1, total = urls.len(), "Querying RDAP");
484 match self.query_rdap_with_retry(&url_str).await {
485 Ok(resp) => return Ok(resp),
486 Err(e) => {
487 if urls.len() > 1 {
488 debug!(
489 url = %url_str,
490 error = %e,
491 candidate = idx + 1,
492 total = urls.len(),
493 "RDAP candidate failed, trying next",
494 );
495 }
496 last_error = Some(e);
497 }
498 }
499 }
500
501 Err(wrap_all_candidates_failed(last_error, urls.len()))
503 }
504
505 async fn query_rdap_with_retry(&self, url: &str) -> Result<RdapResponse> {
510 let classifier = NetworkRetryClassifier::new();
511 let mut attempt = 0;
512 loop {
513 match query_rdap_attempt(url, self.allow_reserved).await {
514 Ok(resp) => return Ok(resp),
515 Err((err, retry_after)) => {
516 let attempts_remaining =
517 self.retry_policy.max_attempts.saturating_sub(attempt + 1);
518 if !classifier.is_retryable(&err) || attempts_remaining == 0 {
519 return Err(if attempt > 0 {
520 SeerError::RetryExhausted {
521 attempts: attempt + 1,
522 last_error: Box::new(err),
523 }
524 } else {
525 err
526 });
527 }
528 let backoff = self.retry_policy.delay_for_attempt(attempt);
529 let delay = effective_retry_delay(backoff, retry_after);
530 debug!(
531 url = %url,
532 attempt = attempt + 1,
533 max_attempts = self.retry_policy.max_attempts,
534 delay_ms = delay.as_millis(),
535 error = %err,
536 "Retrying RDAP after transient error"
537 );
538 tokio::time::sleep(delay).await;
539 attempt += 1;
540 }
541 }
542 }
543 }
544}
545
546const MAX_RDAP_RESPONSE_SIZE: usize = 10 * 1024 * 1024;
548
549const MAX_RETRY_AFTER: Duration = Duration::from_secs(5);
554
555async fn validate_url_not_reserved(url: &str) -> Result<Vec<SocketAddr>> {
562 let parsed = url::Url::parse(url)
563 .map_err(|e| SeerError::RdapError(format!("invalid URL '{}': {}", url, e)))?;
564 let host = parsed
565 .host_str()
566 .ok_or_else(|| SeerError::RdapError(format!("URL '{}' has no host", url)))?;
567 let port = parsed.port_or_known_default().unwrap_or(443);
568
569 if let Ok(ip) = host.parse::<IpAddr>() {
571 if let Some(reason) = describe_reserved_ip(&ip) {
572 return Err(SeerError::RdapError(format!(
573 "RDAP URL resolves to reserved IP {}: {} — request blocked (SSRF protection)",
574 ip, reason
575 )));
576 }
577 return Ok(vec![SocketAddr::new(ip, port)]);
578 }
579
580 let addr = format!("{}:{}", host, port);
581
582 let socket_addrs: Vec<SocketAddr> = tokio::net::lookup_host(&addr)
583 .await
584 .map_err(|e| SeerError::RdapError(format!("failed to resolve host '{}': {}", host, e)))?
585 .collect();
586
587 if socket_addrs.is_empty() {
588 return Err(SeerError::RdapError(format!(
589 "host '{}' resolved to no addresses",
590 host
591 )));
592 }
593
594 for socket_addr in &socket_addrs {
595 if let Some(reason) = describe_reserved_ip(&socket_addr.ip()) {
596 return Err(SeerError::RdapError(format!(
597 "RDAP URL resolves to reserved IP {}: {} — request blocked (SSRF protection)",
598 socket_addr.ip(),
599 reason
600 )));
601 }
602 }
603
604 Ok(socket_addrs)
605}
606
607fn parse_retry_after(value: &str) -> Option<Duration> {
612 value.trim().parse::<u64>().ok().map(Duration::from_secs)
613}
614
615fn effective_retry_delay(backoff: Duration, retry_after: Option<Duration>) -> Duration {
619 match retry_after {
620 Some(hint) => hint.min(MAX_RETRY_AFTER),
621 None => backoff,
622 }
623}
624
625async fn send_rdap_request(url: &str, allow_reserved: bool) -> Result<reqwest::Response> {
632 if allow_reserved {
633 let client = Client::builder()
634 .timeout(DEFAULT_TIMEOUT)
635 .connect_timeout(CONNECT_TIMEOUT)
636 .user_agent("Seer/1.0 (RDAP Client)")
637 .redirect(reqwest::redirect::Policy::none())
638 .build()
639 .map_err(|e| SeerError::RdapError(format!("failed to build HTTP client: {}", e)))?;
640 return client
641 .get(url)
642 .header("Accept", "application/rdap+json")
643 .send()
644 .await
645 .map_err(Into::into);
646 }
647
648 let resolved = validate_url_not_reserved(url).await?;
651
652 let parsed = url::Url::parse(url)
653 .map_err(|e| SeerError::RdapError(format!("invalid URL '{}': {}", url, e)))?;
654 let host = parsed
655 .host_str()
656 .ok_or_else(|| SeerError::RdapError(format!("URL '{}' has no host", url)))?;
657
658 let client = Client::builder()
662 .timeout(DEFAULT_TIMEOUT)
663 .connect_timeout(CONNECT_TIMEOUT)
664 .user_agent("Seer/1.0 (RDAP Client)")
665 .resolve_to_addrs(host, &resolved)
666 .redirect(reqwest::redirect::Policy::none())
675 .build()
676 .map_err(|e| SeerError::RdapError(format!("failed to build HTTP client: {}", e)))?;
677
678 client
679 .get(url)
680 .header("Accept", "application/rdap+json")
681 .send()
682 .await
683 .map_err(Into::into)
684}
685
686async fn read_and_parse_rdap_body(response: reqwest::Response, url: &str) -> Result<RdapResponse> {
689 let mut body = Vec::new();
694 let mut stream = response.bytes_stream();
695 let streamed = tokio::time::timeout(DEFAULT_TIMEOUT, async {
696 while let Some(chunk) = stream.next().await {
697 let chunk = chunk
698 .map_err(|e| SeerError::RdapError(format!("failed to read response: {}", e)))?;
699 body.extend_from_slice(&chunk);
700 if body.len() > MAX_RDAP_RESPONSE_SIZE {
701 return Err(SeerError::RdapError(format!(
702 "RDAP response exceeds {} byte limit",
703 MAX_RDAP_RESPONSE_SIZE
704 )));
705 }
706 }
707 Ok::<(), SeerError>(())
708 })
709 .await;
710
711 match streamed {
712 Ok(Ok(())) => {}
713 Ok(Err(e)) => return Err(e),
714 Err(_) => {
715 return Err(SeerError::Timeout(format!(
716 "timed out reading RDAP response body from {} after {:?}",
717 url, DEFAULT_TIMEOUT
718 )));
719 }
720 }
721
722 let rdap: RdapResponse = serde_json::from_slice(&body)?;
723 rdap.validate()?;
729 Ok(rdap)
730}
731
732async fn query_rdap_attempt(
738 url: &str,
739 allow_reserved: bool,
740) -> std::result::Result<RdapResponse, (SeerError, Option<Duration>)> {
741 let response = send_rdap_request(url, allow_reserved)
742 .await
743 .map_err(|e| (e, None))?;
744
745 if !response.status().is_success() {
746 let status = response.status();
747 let retry_after = if status.as_u16() == 429 {
750 response
751 .headers()
752 .get(reqwest::header::RETRY_AFTER)
753 .and_then(|v| v.to_str().ok())
754 .and_then(parse_retry_after)
755 } else {
756 None
757 };
758 return Err((
759 SeerError::RdapError(format!("query failed with status {}", status)),
760 retry_after,
761 ));
762 }
763
764 read_and_parse_rdap_body(response, url)
765 .await
766 .map_err(|e| (e, None))
767}
768
769async fn load_bootstrap_data_with_retry(policy: &RetryPolicy) -> Result<BootstrapData> {
771 let executor = RetryExecutor::new(policy.clone());
772 executor.execute(load_bootstrap_data).await
773}
774
775async fn load_bootstrap_data() -> Result<BootstrapData> {
777 debug!("Loading RDAP bootstrap data from IANA");
778
779 let http = rdap_http_client()?;
783
784 let dns_future = http.get(IANA_BOOTSTRAP_DNS).send();
785 let ipv4_future = http.get(IANA_BOOTSTRAP_IPV4).send();
786 let ipv6_future = http.get(IANA_BOOTSTRAP_IPV6).send();
787 let asn_future = http.get(IANA_BOOTSTRAP_ASN).send();
788
789 let (dns_resp, ipv4_resp, ipv6_resp, asn_resp) =
792 tokio::join!(dns_future, ipv4_future, ipv6_future, asn_future);
793
794 const MAX_BOOTSTRAP_SIZE: usize = 10 * 1024 * 1024; async fn read_bootstrap(resp: reqwest::Response) -> Result<BootstrapResponse> {
798 let mut body = Vec::new();
804 let mut stream = resp.bytes_stream();
805 let streamed = tokio::time::timeout(DEFAULT_TIMEOUT, async {
806 while let Some(chunk) = stream.next().await {
807 let chunk = chunk.map_err(|e| {
808 SeerError::RdapBootstrapError(format!("failed to read body: {}", e))
809 })?;
810 body.extend_from_slice(&chunk);
811 if body.len() > MAX_BOOTSTRAP_SIZE {
812 return Err(SeerError::RdapBootstrapError(format!(
813 "bootstrap response too large (exceeds {} bytes)",
814 MAX_BOOTSTRAP_SIZE
815 )));
816 }
817 }
818 Ok::<(), SeerError>(())
819 })
820 .await;
821
822 match streamed {
823 Ok(Ok(())) => {}
824 Ok(Err(e)) => return Err(e),
825 Err(_) => {
826 return Err(SeerError::Timeout(format!(
827 "RDAP bootstrap body read timed out after {:?}",
828 DEFAULT_TIMEOUT
829 )));
830 }
831 }
832
833 serde_json::from_slice(&body).map_err(Into::into)
834 }
835
836 let dns_data = match dns_resp {
838 Ok(resp) => match read_bootstrap(resp).await {
839 Ok(data) => Some(data),
840 Err(e) => {
841 warn!(error = %e, "Failed to parse DNS bootstrap response");
842 None
843 }
844 },
845 Err(e) => {
846 warn!(error = %e, "Failed to fetch DNS bootstrap from IANA");
847 None
848 }
849 };
850 let ipv4_data = match ipv4_resp {
851 Ok(resp) => match read_bootstrap(resp).await {
852 Ok(data) => Some(data),
853 Err(e) => {
854 warn!(error = %e, "Failed to parse IPv4 bootstrap response");
855 None
856 }
857 },
858 Err(e) => {
859 warn!(error = %e, "Failed to fetch IPv4 bootstrap from IANA");
860 None
861 }
862 };
863 let ipv6_data = match ipv6_resp {
864 Ok(resp) => match read_bootstrap(resp).await {
865 Ok(data) => Some(data),
866 Err(e) => {
867 warn!(error = %e, "Failed to parse IPv6 bootstrap response");
868 None
869 }
870 },
871 Err(e) => {
872 warn!(error = %e, "Failed to fetch IPv6 bootstrap from IANA");
873 None
874 }
875 };
876 let asn_data = match asn_resp {
877 Ok(resp) => match read_bootstrap(resp).await {
878 Ok(data) => Some(data),
879 Err(e) => {
880 warn!(error = %e, "Failed to parse ASN bootstrap response");
881 None
882 }
883 },
884 Err(e) => {
885 warn!(error = %e, "Failed to fetch ASN bootstrap from IANA");
886 None
887 }
888 };
889
890 if dns_data.is_none() && ipv4_data.is_none() && ipv6_data.is_none() && asn_data.is_none() {
892 return Err(SeerError::RdapBootstrapError(
893 "all IANA bootstrap registries failed".to_string(),
894 ));
895 }
896
897 let mut dns = HashMap::new();
898 let mut ipv4 = Vec::new();
899 let mut ipv6 = Vec::new();
900 let mut asn = Vec::new();
901
902 fn collect_valid_urls(urls: &[serde_json::Value]) -> Option<Arc<Vec<url::Url>>> {
906 let mut out = Vec::new();
907 for u in urls {
908 if let Some(s) = u.as_str() {
909 match validate_bootstrap_url(s) {
910 Ok(parsed) => out.push(parsed),
911 Err(e) => {
912 debug!(url = s, error = %e, "Skipping invalid bootstrap URL");
913 }
914 }
915 }
916 }
917 if out.is_empty() {
918 None
919 } else {
920 Some(Arc::new(out))
921 }
922 }
923
924 if let Some(dns_data) = dns_data {
926 for service in dns_data.services {
927 if service.len() >= 2 {
928 if let (Some(tlds), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
929 if let Some(urls_arc) = collect_valid_urls(urls) {
930 for tld in tlds {
931 if let Some(tld_str) = tld.as_str() {
932 dns.insert(tld_str.to_lowercase(), Arc::clone(&urls_arc));
933 }
934 }
935 }
936 }
937 }
938 }
939 }
940
941 if let Some(ipv4_data) = ipv4_data {
943 for service in ipv4_data.services {
944 if service.len() >= 2 {
945 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array())
946 {
947 if let Some(urls_arc) = collect_valid_urls(urls) {
948 for prefix in prefixes {
949 if let Some(prefix_str) = prefix.as_str() {
950 ipv4.push((
951 IpRange {
952 prefix: prefix_str.to_string(),
953 },
954 Arc::clone(&urls_arc),
955 ));
956 }
957 }
958 }
959 }
960 }
961 }
962 }
963
964 if let Some(ipv6_data) = ipv6_data {
966 for service in ipv6_data.services {
967 if service.len() >= 2 {
968 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array())
969 {
970 if let Some(urls_arc) = collect_valid_urls(urls) {
971 for prefix in prefixes {
972 if let Some(prefix_str) = prefix.as_str() {
973 ipv6.push((
974 IpRange {
975 prefix: prefix_str.to_string(),
976 },
977 Arc::clone(&urls_arc),
978 ));
979 }
980 }
981 }
982 }
983 }
984 }
985 }
986
987 if let Some(asn_data) = asn_data {
989 for service in asn_data.services {
990 if service.len() >= 2 {
991 if let (Some(ranges), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
992 if let Some(urls_arc) = collect_valid_urls(urls) {
993 for range in ranges {
994 if let Some(range_str) = range.as_str() {
995 if let Some((start, end)) = parse_asn_range(range_str) {
996 asn.push((AsnRange { start, end }, Arc::clone(&urls_arc)));
997 }
998 }
999 }
1000 }
1001 }
1002 }
1003 }
1004 }
1005
1006 info!(
1007 dns_entries = dns.len(),
1008 ipv4_ranges = ipv4.len(),
1009 ipv6_ranges = ipv6.len(),
1010 asn_ranges = asn.len(),
1011 "RDAP bootstrap loaded"
1012 );
1013
1014 Ok(BootstrapData {
1015 dns,
1016 ipv4,
1017 ipv6,
1018 asn,
1019 })
1020}
1021
1022fn wrap_all_candidates_failed(last_error: Option<SeerError>, candidate_count: usize) -> SeerError {
1031 let last = last_error.unwrap_or_else(|| SeerError::RdapError("no candidates".to_string()));
1032
1033 if candidate_count <= 1 {
1034 return last;
1035 }
1036
1037 match last {
1038 SeerError::Timeout(msg) => SeerError::Timeout(format!(
1039 "all {} RDAP candidate URLs timed out; last error: {}",
1040 candidate_count, msg
1041 )),
1042 other => SeerError::RdapError(format!(
1043 "all {} RDAP candidate URLs failed; last error: {}",
1044 candidate_count, other
1045 )),
1046 }
1047}
1048
1049fn build_rdap_urls(bases: &[url::Url], path: &str) -> Vec<url::Url> {
1051 bases
1052 .iter()
1053 .filter_map(|base| {
1054 let base_str = base.as_str();
1057 let normalized = if base_str.ends_with('/') {
1058 base_str.to_string()
1059 } else {
1060 format!("{}/", base_str)
1061 };
1062 url::Url::parse(&normalized).and_then(|u| u.join(path)).ok()
1063 })
1064 .collect()
1065}
1066
1067#[cfg(test)]
1068mod tests {
1069 use super::*;
1070
1071 #[test]
1072 fn test_default_client_has_retry_policy() {
1073 let client = RdapClient::new();
1074 assert_eq!(client.retry_policy.max_attempts, 3);
1078 }
1079
1080 #[test]
1083 fn parse_retry_after_parses_delta_seconds() {
1084 assert_eq!(parse_retry_after("5"), Some(Duration::from_secs(5)));
1085 assert_eq!(parse_retry_after(" 10 "), Some(Duration::from_secs(10)));
1086 assert_eq!(parse_retry_after("0"), Some(Duration::from_secs(0)));
1087 }
1088
1089 #[test]
1090 fn parse_retry_after_rejects_http_date_and_junk() {
1091 assert_eq!(parse_retry_after("Wed, 21 Oct 2015 07:28:00 GMT"), None);
1094 assert_eq!(parse_retry_after("soon"), None);
1095 assert_eq!(parse_retry_after(""), None);
1096 }
1097
1098 #[test]
1099 fn effective_retry_delay_prefers_capped_retry_after() {
1100 assert_eq!(
1102 effective_retry_delay(Duration::from_millis(100), Some(Duration::from_secs(5))),
1103 Duration::from_secs(5)
1104 );
1105 assert_eq!(
1107 effective_retry_delay(Duration::from_millis(100), Some(Duration::from_secs(600))),
1108 MAX_RETRY_AFTER
1109 );
1110 }
1111
1112 #[test]
1113 fn effective_retry_delay_falls_back_to_backoff() {
1114 assert_eq!(
1115 effective_retry_delay(Duration::from_millis(250), None),
1116 Duration::from_millis(250)
1117 );
1118 }
1119
1120 #[test]
1121 fn test_client_without_retries() {
1122 let client = RdapClient::new().without_retries();
1123 assert_eq!(client.retry_policy.max_attempts, 1);
1124 }
1125
1126 #[test]
1127 fn test_client_custom_retry_policy() {
1128 let policy = RetryPolicy::new().with_max_attempts(5);
1129 let client = RdapClient::new().with_retry_policy(policy);
1130 assert_eq!(client.retry_policy.max_attempts, 5);
1131 }
1132
1133 #[test]
1134 fn test_cached_bootstrap_expiration() {
1135 let data = BootstrapData {
1136 dns: HashMap::new(),
1137 ipv4: Vec::new(),
1138 ipv6: Vec::new(),
1139 asn: Vec::new(),
1140 };
1141 let cached = CachedBootstrap::new(data);
1142 assert!(!cached.is_expired());
1144 }
1145
1146 #[test]
1147 fn test_rdap_http_client_is_configured() {
1148 let client = rdap_http_client();
1151 assert!(client.is_ok(), "RDAP HTTP client builder must succeed");
1152 }
1153
1154 #[test]
1155 fn test_parse_bootstrap_empty_services() {
1156 let data = BootstrapData {
1158 dns: HashMap::new(),
1159 ipv4: Vec::new(),
1160 ipv6: Vec::new(),
1161 asn: Vec::new(),
1162 };
1163 assert!(RdapClient::get_rdap_urls_for_domain(&data, "example.com").is_none());
1165 assert!(RdapClient::get_rdap_urls_for_asn(&data, 12345).is_none());
1166 }
1167
1168 #[tokio::test]
1171 async fn test_validate_url_not_reserved_rejects_loopback_literal() {
1172 let err = validate_url_not_reserved("https://127.0.0.1/domain/example.com")
1173 .await
1174 .unwrap_err();
1175 assert!(
1176 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1177 "expected reserved-IP error, got: {:?}",
1178 err
1179 );
1180 }
1181
1182 #[tokio::test]
1183 async fn test_validate_url_not_reserved_rejects_private_ipv4_literal() {
1184 let err = validate_url_not_reserved("https://10.0.0.1/")
1185 .await
1186 .unwrap_err();
1187 assert!(
1188 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1189 "expected reserved-IP error, got: {:?}",
1190 err
1191 );
1192 }
1193
1194 #[tokio::test]
1195 async fn test_validate_url_not_reserved_rejects_ipv6_loopback_literal() {
1196 let err = validate_url_not_reserved("https://[::1]/")
1197 .await
1198 .unwrap_err();
1199 assert!(
1200 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1201 "expected reserved-IP error, got: {:?}",
1202 err
1203 );
1204 }
1205
1206 #[tokio::test]
1207 async fn test_validate_url_not_reserved_returns_resolved_addrs_for_public_literal() {
1208 let addrs = validate_url_not_reserved("https://8.8.8.8/").await.unwrap();
1211 assert_eq!(addrs.len(), 1);
1212 assert!(addrs[0].ip().is_ipv4());
1213 assert_eq!(addrs[0].port(), 443);
1214 }
1215
1216 #[test]
1219 fn test_build_rdap_urls_preserves_order_and_appends_path() {
1220 let bases = vec![
1221 url::Url::parse("https://rdap.a.example/").unwrap(),
1222 url::Url::parse("https://rdap.b.example").unwrap(), ];
1224 let built = build_rdap_urls(&bases, "domain/example.com");
1225 assert_eq!(built.len(), 2);
1226 assert_eq!(
1227 built[0].as_str(),
1228 "https://rdap.a.example/domain/example.com"
1229 );
1230 assert_eq!(
1231 built[1].as_str(),
1232 "https://rdap.b.example/domain/example.com"
1233 );
1234 }
1235
1236 #[test]
1237 fn test_build_rdap_urls_empty_input_returns_empty() {
1238 let built = build_rdap_urls(&[], "domain/example.com");
1239 assert!(built.is_empty());
1240 }
1241
1242 #[test]
1245 fn test_wrap_all_candidates_failed_preserves_timeout_variant() {
1246 let last = SeerError::Timeout("body read timed out".to_string());
1249 let wrapped = wrap_all_candidates_failed(Some(last), 3);
1250 match wrapped {
1251 SeerError::Timeout(msg) => {
1252 assert!(
1253 msg.contains("all 3 RDAP candidate URLs timed out"),
1254 "expected wrapped timeout message, got: {}",
1255 msg
1256 );
1257 assert!(
1258 msg.contains("body read timed out"),
1259 "expected original message preserved, got: {}",
1260 msg
1261 );
1262 }
1263 other => panic!(
1264 "expected SeerError::Timeout after wrapping a Timeout, got: {:?}",
1265 other
1266 ),
1267 }
1268 }
1269
1270 #[test]
1271 fn test_wrap_all_candidates_failed_wraps_non_timeout_as_rdap_error() {
1272 let last = SeerError::RdapError("500 internal error".to_string());
1273 let wrapped = wrap_all_candidates_failed(Some(last), 2);
1274 assert!(
1275 matches!(wrapped, SeerError::RdapError(ref s) if s.contains("all 2 RDAP candidate URLs failed")),
1276 "expected wrapped RdapError, got: {:?}",
1277 wrapped
1278 );
1279 }
1280
1281 #[test]
1282 fn test_wrap_all_candidates_failed_single_candidate_returns_unchanged() {
1283 let last = SeerError::Timeout("single timeout".to_string());
1286 let wrapped = wrap_all_candidates_failed(Some(last), 1);
1287 assert!(
1288 matches!(wrapped, SeerError::Timeout(ref s) if s == "single timeout"),
1289 "expected unchanged Timeout, got: {:?}",
1290 wrapped
1291 );
1292 }
1293
1294 #[test]
1295 fn test_wrap_all_candidates_failed_no_last_error_returns_placeholder() {
1296 let wrapped = wrap_all_candidates_failed(None, 0);
1297 assert!(matches!(wrapped, SeerError::RdapError(_)));
1298 }
1299
1300 static BOOTSTRAP_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
1319
1320 #[tokio::test]
1321 async fn test_bootstrap_load_notify_wakes_waiter_when_cache_populated() {
1322 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1323
1324 {
1326 let mut cache = BOOTSTRAP_CACHE.write().await;
1327 *cache = None;
1328 }
1329
1330 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1333 tokio::pin!(notified);
1334
1335 {
1337 let mut cache = BOOTSTRAP_CACHE.write().await;
1338 *cache = Some(CachedBootstrap::new(BootstrapData {
1339 dns: HashMap::new(),
1340 ipv4: Vec::new(),
1341 ipv6: Vec::new(),
1342 asn: Vec::new(),
1343 }));
1344 }
1345 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1346
1347 let result = wait_for_in_flight_load(notified).await;
1348 assert!(
1349 result.is_ok(),
1350 "expected waiter to see populated cache, got: {:?}",
1351 result
1352 );
1353
1354 {
1356 let mut cache = BOOTSTRAP_CACHE.write().await;
1357 *cache = None;
1358 }
1359 }
1360
1361 #[tokio::test]
1362 async fn test_bootstrap_load_notify_empty_cache_after_wake_returns_error() {
1363 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1364
1365 {
1367 let mut cache = BOOTSTRAP_CACHE.write().await;
1368 *cache = None;
1369 }
1370
1371 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1372 tokio::pin!(notified);
1373
1374 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1376
1377 let result = wait_for_in_flight_load(notified).await;
1378 assert!(
1379 matches!(
1380 result,
1381 Err(SeerError::RdapBootstrapError(ref s))
1382 if s.contains("throttled and no cache available")
1383 ),
1384 "expected throttled error when cache still empty after notify, got: {:?}",
1385 result
1386 );
1387 }
1388
1389 use wiremock::matchers::method;
1399 use wiremock::{Mock, MockServer, ResponseTemplate};
1400
1401 #[tokio::test]
1402 async fn mock_rdap_404_is_nonretryable_typed_error() {
1403 let server = MockServer::start().await;
1404 Mock::given(method("GET"))
1405 .respond_with(ResponseTemplate::new(404))
1406 .mount(&server)
1407 .await;
1408
1409 let client = RdapClient::new()
1410 .without_retries()
1411 .allowing_reserved_for_tests();
1412 let err = client
1413 .query_rdap_with_retry(&format!("{}/domain/example.com", server.uri()))
1414 .await
1415 .unwrap_err();
1416 assert!(
1417 matches!(err, SeerError::RdapError(ref m) if m.contains("404")),
1418 "got: {err:?}"
1419 );
1420 }
1421
1422 #[tokio::test]
1423 async fn mock_rdap_429_honors_retry_after_and_succeeds() {
1424 let server = MockServer::start().await;
1425 Mock::given(method("GET"))
1428 .respond_with(ResponseTemplate::new(429).insert_header("Retry-After", "0"))
1429 .up_to_n_times(1)
1430 .mount(&server)
1431 .await;
1432 Mock::given(method("GET"))
1433 .respond_with(ResponseTemplate::new(200).set_body_raw(
1434 r#"{"objectClassName":"domain","handle":"MOCK-1"}"#,
1435 "application/rdap+json",
1436 ))
1437 .mount(&server)
1438 .await;
1439
1440 let client = RdapClient::new().allowing_reserved_for_tests();
1441 let resp = client
1442 .query_rdap_with_retry(&format!("{}/domain/example.com", server.uri()))
1443 .await
1444 .unwrap();
1445 assert_eq!(resp.handle.as_deref(), Some("MOCK-1"));
1446 }
1447
1448 #[tokio::test]
1449 async fn mock_rdap_malformed_body_is_parse_error_not_panic() {
1450 let server = MockServer::start().await;
1451 Mock::given(method("GET"))
1452 .respond_with(ResponseTemplate::new(200).set_body_raw("not json", "text/plain"))
1453 .mount(&server)
1454 .await;
1455
1456 let client = RdapClient::new()
1457 .without_retries()
1458 .allowing_reserved_for_tests();
1459 let err = client
1460 .query_rdap_with_retry(&format!("{}/domain/example.com", server.uri()))
1461 .await
1462 .unwrap_err();
1463 assert!(matches!(err, SeerError::JsonError(_)), "got: {err:?}");
1464 }
1465
1466 #[tokio::test]
1467 async fn mock_rdap_candidate_fallback_uses_second_url() {
1468 let bad = MockServer::start().await;
1469 Mock::given(method("GET"))
1470 .respond_with(ResponseTemplate::new(500))
1471 .mount(&bad)
1472 .await;
1473 let good = MockServer::start().await;
1474 Mock::given(method("GET"))
1475 .respond_with(ResponseTemplate::new(200).set_body_raw(
1476 r#"{"objectClassName":"domain","handle":"MOCK-2"}"#,
1477 "application/rdap+json",
1478 ))
1479 .mount(&good)
1480 .await;
1481
1482 let client = RdapClient::new()
1483 .without_retries()
1484 .allowing_reserved_for_tests();
1485 let urls = vec![
1486 url::Url::parse(&format!("{}/domain/example.com", bad.uri())).unwrap(),
1487 url::Url::parse(&format!("{}/domain/example.com", good.uri())).unwrap(),
1488 ];
1489 let resp = client.query_rdap_urls(&urls).await.unwrap();
1490 assert_eq!(resp.handle.as_deref(), Some("MOCK-2"));
1491 }
1492}