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::{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::default().with_max_attempts(2),
179 }
180 }
181
182 pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
186 self.retry_policy = policy;
187 self
188 }
189
190 pub fn without_retries(mut self) -> Self {
192 self.retry_policy = RetryPolicy::no_retry();
193 self
194 }
195
196 async fn ensure_bootstrap(&self) -> Result<()> {
211 {
213 let cache = BOOTSTRAP_CACHE.read().await;
214 if let Some(cached) = cache.as_ref() {
215 if !cached.is_expired() {
216 return Ok(());
217 }
218 }
219 }
220
221 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
227 tokio::pin!(notified);
228
229 {
233 let last = BOOTSTRAP_LAST_ATTEMPT.read().await;
234 if let Some(ts) = *last {
235 if ts.elapsed() < BOOTSTRAP_REFRESH_MIN_INTERVAL {
236 let cache = BOOTSTRAP_CACHE.read().await;
238 if cache.is_some() {
239 return Ok(());
241 }
242 drop(cache);
245 drop(last);
246 return wait_for_in_flight_load(notified).await;
247 }
248 }
249 }
250
251 {
254 let mut last = BOOTSTRAP_LAST_ATTEMPT.write().await;
255 if let Some(ts) = *last {
257 if ts.elapsed() < BOOTSTRAP_REFRESH_MIN_INTERVAL {
258 drop(last);
259 let cache = BOOTSTRAP_CACHE.read().await;
260 if cache.is_some() {
261 return Ok(());
262 }
263 drop(cache);
264 return wait_for_in_flight_load(notified).await;
265 }
266 }
267 *last = Some(Instant::now());
268 }
269
270 debug!("Loading/refreshing RDAP bootstrap data");
274 let load_result = load_bootstrap_data_with_retry(&self.retry_policy).await;
275
276 let outcome = match load_result {
277 Ok(data) => {
278 let mut cache = BOOTSTRAP_CACHE.write().await;
279 let should_store = cache.as_ref().map(|c| c.is_expired()).unwrap_or(true);
282 if should_store {
283 *cache = Some(CachedBootstrap::new(data));
284 }
285 Ok(())
286 }
287 Err(e) => {
288 let cache = BOOTSTRAP_CACHE.read().await;
290 if let Some(cached) = cache.as_ref() {
291 debug!(
292 error = %e,
293 age_hours = cached.age().as_secs() / 3600,
294 "Bootstrap refresh failed, using stale data"
295 );
296 Ok(())
297 } else {
298 Err(e)
300 }
301 }
302 };
303
304 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
306 outcome
307 }
308
309 fn get_rdap_urls_for_domain(cache: &BootstrapData, domain: &str) -> Option<Arc<Vec<url::Url>>> {
311 let tld = domain.rsplit('.').next()?;
312 cache.dns.get(&tld.to_lowercase()).cloned()
313 }
314
315 fn get_rdap_urls_for_ip(cache: &BootstrapData, ip: &IpAddr) -> Option<Arc<Vec<url::Url>>> {
317 match ip {
318 IpAddr::V4(addr) => {
319 for (range, urls) in &cache.ipv4 {
320 if ipv4_matches_prefix(&range.prefix, addr) {
321 return Some(Arc::clone(urls));
322 }
323 }
324 }
325 IpAddr::V6(addr) => {
326 for (range, urls) in &cache.ipv6 {
327 if ipv6_matches_prefix(&range.prefix, addr) {
328 return Some(Arc::clone(urls));
329 }
330 }
331 }
332 }
333
334 None
335 }
336
337 fn get_rdap_urls_for_asn(cache: &BootstrapData, asn: u32) -> Option<Arc<Vec<url::Url>>> {
339 for (range, urls) in &cache.asn {
340 if asn >= range.start && asn <= range.end {
341 return Some(Arc::clone(urls));
342 }
343 }
344
345 None
346 }
347
348 #[instrument(skip(self), fields(domain = %domain))]
352 pub async fn lookup_domain(&self, domain: &str) -> Result<RdapResponse> {
353 self.ensure_bootstrap().await?;
354
355 let domain = normalize_domain(domain)?;
356
357 let urls = {
359 let cache_guard = BOOTSTRAP_CACHE.read().await;
360 let cache = cache_guard.as_ref().ok_or_else(|| {
361 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
362 })?;
363
364 let bases = Self::get_rdap_urls_for_domain(&cache.data, &domain).ok_or_else(|| {
365 SeerError::RdapBootstrapError(format!("no RDAP server for {}", domain))
366 })?;
367
368 build_rdap_urls(&bases, &format!("domain/{}", domain))
369 }; self.query_rdap_urls(&urls).await
372 }
373
374 #[instrument(skip(self), fields(ip = %ip))]
378 pub async fn lookup_ip(&self, ip: &str) -> Result<RdapResponse> {
379 self.ensure_bootstrap().await?;
380
381 let ip_addr: IpAddr = ip
382 .parse()
383 .map_err(|_| SeerError::InvalidIpAddress(ip.to_string()))?;
384
385 let urls = {
386 let cache_guard = BOOTSTRAP_CACHE.read().await;
387 let cache = cache_guard.as_ref().ok_or_else(|| {
388 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
389 })?;
390
391 let bases = Self::get_rdap_urls_for_ip(&cache.data, &ip_addr).ok_or_else(|| {
392 SeerError::RdapBootstrapError(format!("no RDAP server for {}", ip))
393 })?;
394
395 build_rdap_urls(&bases, &format!("ip/{}", ip))
396 };
397
398 self.query_rdap_urls(&urls).await
399 }
400
401 #[instrument(skip(self), fields(asn = %asn))]
405 pub async fn lookup_asn(&self, asn: u32) -> Result<RdapResponse> {
406 self.ensure_bootstrap().await?;
407
408 let urls = {
409 let cache_guard = BOOTSTRAP_CACHE.read().await;
410 let cache = cache_guard.as_ref().ok_or_else(|| {
411 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
412 })?;
413
414 let bases = Self::get_rdap_urls_for_asn(&cache.data, asn).ok_or_else(|| {
415 SeerError::RdapBootstrapError(format!("no RDAP server for AS{}", asn))
416 })?;
417
418 build_rdap_urls(&bases, &format!("autnum/{}", asn))
419 };
420
421 self.query_rdap_urls(&urls).await
422 }
423
424 #[instrument(skip(self), fields(tld = %tld))]
430 pub async fn get_rdap_base_url_for_tld(&self, tld: &str) -> Option<String> {
431 if self.ensure_bootstrap().await.is_err() {
432 return None;
433 }
434
435 let cache_guard = BOOTSTRAP_CACHE.read().await;
436 let cache = cache_guard.as_ref()?;
437 cache
438 .data
439 .dns
440 .get(&tld.to_lowercase())
441 .and_then(|urls| urls.first())
442 .map(|u| u.to_string())
443 }
444
445 async fn query_rdap_urls(&self, urls: &[url::Url]) -> Result<RdapResponse> {
449 if urls.is_empty() {
450 return Err(SeerError::RdapError(
451 "no candidate RDAP URLs available".to_string(),
452 ));
453 }
454
455 let mut last_error: Option<SeerError> = None;
456 for (idx, url) in urls.iter().enumerate() {
457 let url_str = url.as_str().to_string();
458 debug!(url = %url_str, candidate = idx + 1, total = urls.len(), "Querying RDAP");
459 match self.query_rdap_with_retry(&url_str).await {
460 Ok(resp) => return Ok(resp),
461 Err(e) => {
462 if urls.len() > 1 {
463 debug!(
464 url = %url_str,
465 error = %e,
466 candidate = idx + 1,
467 total = urls.len(),
468 "RDAP candidate failed, trying next",
469 );
470 }
471 last_error = Some(e);
472 }
473 }
474 }
475
476 Err(wrap_all_candidates_failed(last_error, urls.len()))
478 }
479
480 async fn query_rdap_with_retry(&self, url: &str) -> Result<RdapResponse> {
482 let executor = RetryExecutor::new(self.retry_policy.clone());
483 let url = url.to_string();
484
485 executor
486 .execute(|| {
487 let url = url.clone();
488 async move { query_rdap_internal(&url).await }
489 })
490 .await
491 }
492}
493
494const MAX_RDAP_RESPONSE_SIZE: usize = 10 * 1024 * 1024;
496
497async fn validate_url_not_reserved(url: &str) -> Result<Vec<SocketAddr>> {
504 let parsed = url::Url::parse(url)
505 .map_err(|e| SeerError::RdapError(format!("invalid URL '{}': {}", url, e)))?;
506 let host = parsed
507 .host_str()
508 .ok_or_else(|| SeerError::RdapError(format!("URL '{}' has no host", url)))?;
509 let port = parsed.port_or_known_default().unwrap_or(443);
510
511 if let Ok(ip) = host.parse::<IpAddr>() {
513 if let Some(reason) = describe_reserved_ip(&ip) {
514 return Err(SeerError::RdapError(format!(
515 "RDAP URL resolves to reserved IP {}: {} — request blocked (SSRF protection)",
516 ip, reason
517 )));
518 }
519 return Ok(vec![SocketAddr::new(ip, port)]);
520 }
521
522 let addr = format!("{}:{}", host, port);
523
524 let socket_addrs: Vec<SocketAddr> = tokio::net::lookup_host(&addr)
525 .await
526 .map_err(|e| SeerError::RdapError(format!("failed to resolve host '{}': {}", host, e)))?
527 .collect();
528
529 if socket_addrs.is_empty() {
530 return Err(SeerError::RdapError(format!(
531 "host '{}' resolved to no addresses",
532 host
533 )));
534 }
535
536 for socket_addr in &socket_addrs {
537 if let Some(reason) = describe_reserved_ip(&socket_addr.ip()) {
538 return Err(SeerError::RdapError(format!(
539 "RDAP URL resolves to reserved IP {}: {} — request blocked (SSRF protection)",
540 socket_addr.ip(),
541 reason
542 )));
543 }
544 }
545
546 Ok(socket_addrs)
547}
548
549async fn query_rdap_internal(url: &str) -> Result<RdapResponse> {
559 let resolved = validate_url_not_reserved(url).await?;
562
563 let parsed = url::Url::parse(url)
564 .map_err(|e| SeerError::RdapError(format!("invalid URL '{}': {}", url, e)))?;
565 let host = parsed
566 .host_str()
567 .ok_or_else(|| SeerError::RdapError(format!("URL '{}' has no host", url)))?;
568
569 let client = Client::builder()
573 .timeout(DEFAULT_TIMEOUT)
574 .connect_timeout(CONNECT_TIMEOUT)
575 .user_agent("Seer/1.0 (RDAP Client)")
576 .resolve_to_addrs(host, &resolved)
577 .build()
578 .map_err(|e| SeerError::RdapError(format!("failed to build HTTP client: {}", e)))?;
579
580 let response = client
581 .get(url)
582 .header("Accept", "application/rdap+json")
583 .send()
584 .await?;
585
586 if !response.status().is_success() {
587 return Err(SeerError::RdapError(format!(
588 "query failed with status {}",
589 response.status()
590 )));
591 }
592
593 let mut body = Vec::new();
598 let mut stream = response.bytes_stream();
599 let streamed = tokio::time::timeout(DEFAULT_TIMEOUT, async {
600 while let Some(chunk) = stream.next().await {
601 let chunk = chunk
602 .map_err(|e| SeerError::RdapError(format!("failed to read response: {}", e)))?;
603 body.extend_from_slice(&chunk);
604 if body.len() > MAX_RDAP_RESPONSE_SIZE {
605 return Err(SeerError::RdapError(format!(
606 "RDAP response exceeds {} byte limit",
607 MAX_RDAP_RESPONSE_SIZE
608 )));
609 }
610 }
611 Ok::<(), SeerError>(())
612 })
613 .await;
614
615 match streamed {
616 Ok(Ok(())) => {}
617 Ok(Err(e)) => return Err(e),
618 Err(_) => {
619 return Err(SeerError::Timeout(format!(
620 "timed out reading RDAP response body from {} after {:?}",
621 host, DEFAULT_TIMEOUT
622 )));
623 }
624 }
625
626 let rdap: RdapResponse = serde_json::from_slice(&body)?;
627 rdap.validate()?;
633 Ok(rdap)
634}
635
636async fn load_bootstrap_data_with_retry(policy: &RetryPolicy) -> Result<BootstrapData> {
638 let executor = RetryExecutor::new(policy.clone());
639 executor.execute(load_bootstrap_data).await
640}
641
642async fn load_bootstrap_data() -> Result<BootstrapData> {
644 debug!("Loading RDAP bootstrap data from IANA");
645
646 let http = rdap_http_client()?;
650
651 let dns_future = http.get(IANA_BOOTSTRAP_DNS).send();
652 let ipv4_future = http.get(IANA_BOOTSTRAP_IPV4).send();
653 let ipv6_future = http.get(IANA_BOOTSTRAP_IPV6).send();
654 let asn_future = http.get(IANA_BOOTSTRAP_ASN).send();
655
656 let (dns_resp, ipv4_resp, ipv6_resp, asn_resp) =
659 tokio::join!(dns_future, ipv4_future, ipv6_future, asn_future);
660
661 const MAX_BOOTSTRAP_SIZE: usize = 10 * 1024 * 1024; async fn read_bootstrap(resp: reqwest::Response) -> Result<BootstrapResponse> {
665 let mut body = Vec::new();
671 let mut stream = resp.bytes_stream();
672 let streamed = tokio::time::timeout(DEFAULT_TIMEOUT, async {
673 while let Some(chunk) = stream.next().await {
674 let chunk = chunk.map_err(|e| {
675 SeerError::RdapBootstrapError(format!("failed to read body: {}", e))
676 })?;
677 body.extend_from_slice(&chunk);
678 if body.len() > MAX_BOOTSTRAP_SIZE {
679 return Err(SeerError::RdapBootstrapError(format!(
680 "bootstrap response too large (exceeds {} bytes)",
681 MAX_BOOTSTRAP_SIZE
682 )));
683 }
684 }
685 Ok::<(), SeerError>(())
686 })
687 .await;
688
689 match streamed {
690 Ok(Ok(())) => {}
691 Ok(Err(e)) => return Err(e),
692 Err(_) => {
693 return Err(SeerError::Timeout(format!(
694 "RDAP bootstrap body read timed out after {:?}",
695 DEFAULT_TIMEOUT
696 )));
697 }
698 }
699
700 serde_json::from_slice(&body).map_err(Into::into)
701 }
702
703 let dns_data = match dns_resp {
705 Ok(resp) => match read_bootstrap(resp).await {
706 Ok(data) => Some(data),
707 Err(e) => {
708 warn!(error = %e, "Failed to parse DNS bootstrap response");
709 None
710 }
711 },
712 Err(e) => {
713 warn!(error = %e, "Failed to fetch DNS bootstrap from IANA");
714 None
715 }
716 };
717 let ipv4_data = match ipv4_resp {
718 Ok(resp) => match read_bootstrap(resp).await {
719 Ok(data) => Some(data),
720 Err(e) => {
721 warn!(error = %e, "Failed to parse IPv4 bootstrap response");
722 None
723 }
724 },
725 Err(e) => {
726 warn!(error = %e, "Failed to fetch IPv4 bootstrap from IANA");
727 None
728 }
729 };
730 let ipv6_data = match ipv6_resp {
731 Ok(resp) => match read_bootstrap(resp).await {
732 Ok(data) => Some(data),
733 Err(e) => {
734 warn!(error = %e, "Failed to parse IPv6 bootstrap response");
735 None
736 }
737 },
738 Err(e) => {
739 warn!(error = %e, "Failed to fetch IPv6 bootstrap from IANA");
740 None
741 }
742 };
743 let asn_data = match asn_resp {
744 Ok(resp) => match read_bootstrap(resp).await {
745 Ok(data) => Some(data),
746 Err(e) => {
747 warn!(error = %e, "Failed to parse ASN bootstrap response");
748 None
749 }
750 },
751 Err(e) => {
752 warn!(error = %e, "Failed to fetch ASN bootstrap from IANA");
753 None
754 }
755 };
756
757 if dns_data.is_none() && ipv4_data.is_none() && ipv6_data.is_none() && asn_data.is_none() {
759 return Err(SeerError::RdapBootstrapError(
760 "all IANA bootstrap registries failed".to_string(),
761 ));
762 }
763
764 let mut dns = HashMap::new();
765 let mut ipv4 = Vec::new();
766 let mut ipv6 = Vec::new();
767 let mut asn = Vec::new();
768
769 fn collect_valid_urls(urls: &[serde_json::Value]) -> Option<Arc<Vec<url::Url>>> {
773 let mut out = Vec::new();
774 for u in urls {
775 if let Some(s) = u.as_str() {
776 match validate_bootstrap_url(s) {
777 Ok(parsed) => out.push(parsed),
778 Err(e) => {
779 debug!(url = s, error = %e, "Skipping invalid bootstrap URL");
780 }
781 }
782 }
783 }
784 if out.is_empty() {
785 None
786 } else {
787 Some(Arc::new(out))
788 }
789 }
790
791 if let Some(dns_data) = dns_data {
793 for service in dns_data.services {
794 if service.len() >= 2 {
795 if let (Some(tlds), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
796 if let Some(urls_arc) = collect_valid_urls(urls) {
797 for tld in tlds {
798 if let Some(tld_str) = tld.as_str() {
799 dns.insert(tld_str.to_lowercase(), Arc::clone(&urls_arc));
800 }
801 }
802 }
803 }
804 }
805 }
806 }
807
808 if let Some(ipv4_data) = ipv4_data {
810 for service in ipv4_data.services {
811 if service.len() >= 2 {
812 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array())
813 {
814 if let Some(urls_arc) = collect_valid_urls(urls) {
815 for prefix in prefixes {
816 if let Some(prefix_str) = prefix.as_str() {
817 ipv4.push((
818 IpRange {
819 prefix: prefix_str.to_string(),
820 },
821 Arc::clone(&urls_arc),
822 ));
823 }
824 }
825 }
826 }
827 }
828 }
829 }
830
831 if let Some(ipv6_data) = ipv6_data {
833 for service in ipv6_data.services {
834 if service.len() >= 2 {
835 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array())
836 {
837 if let Some(urls_arc) = collect_valid_urls(urls) {
838 for prefix in prefixes {
839 if let Some(prefix_str) = prefix.as_str() {
840 ipv6.push((
841 IpRange {
842 prefix: prefix_str.to_string(),
843 },
844 Arc::clone(&urls_arc),
845 ));
846 }
847 }
848 }
849 }
850 }
851 }
852 }
853
854 if let Some(asn_data) = asn_data {
856 for service in asn_data.services {
857 if service.len() >= 2 {
858 if let (Some(ranges), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
859 if let Some(urls_arc) = collect_valid_urls(urls) {
860 for range in ranges {
861 if let Some(range_str) = range.as_str() {
862 if let Some((start, end)) = parse_asn_range(range_str) {
863 asn.push((AsnRange { start, end }, Arc::clone(&urls_arc)));
864 }
865 }
866 }
867 }
868 }
869 }
870 }
871 }
872
873 info!(
874 dns_entries = dns.len(),
875 ipv4_ranges = ipv4.len(),
876 ipv6_ranges = ipv6.len(),
877 asn_ranges = asn.len(),
878 "RDAP bootstrap loaded"
879 );
880
881 Ok(BootstrapData {
882 dns,
883 ipv4,
884 ipv6,
885 asn,
886 })
887}
888
889fn wrap_all_candidates_failed(last_error: Option<SeerError>, candidate_count: usize) -> SeerError {
898 let last = last_error.unwrap_or_else(|| SeerError::RdapError("no candidates".to_string()));
899
900 if candidate_count <= 1 {
901 return last;
902 }
903
904 match last {
905 SeerError::Timeout(msg) => SeerError::Timeout(format!(
906 "all {} RDAP candidate URLs timed out; last error: {}",
907 candidate_count, msg
908 )),
909 other => SeerError::RdapError(format!(
910 "all {} RDAP candidate URLs failed; last error: {}",
911 candidate_count, other
912 )),
913 }
914}
915
916fn build_rdap_urls(bases: &[url::Url], path: &str) -> Vec<url::Url> {
918 bases
919 .iter()
920 .filter_map(|base| {
921 let base_str = base.as_str();
924 let normalized = if base_str.ends_with('/') {
925 base_str.to_string()
926 } else {
927 format!("{}/", base_str)
928 };
929 url::Url::parse(&normalized).and_then(|u| u.join(path)).ok()
930 })
931 .collect()
932}
933
934#[cfg(test)]
935mod tests {
936 use super::*;
937
938 #[test]
939 fn test_default_client_has_retry_policy() {
940 let client = RdapClient::new();
941 assert_eq!(client.retry_policy.max_attempts, 2);
942 }
943
944 #[test]
945 fn test_client_without_retries() {
946 let client = RdapClient::new().without_retries();
947 assert_eq!(client.retry_policy.max_attempts, 1);
948 }
949
950 #[test]
951 fn test_client_custom_retry_policy() {
952 let policy = RetryPolicy::new().with_max_attempts(5);
953 let client = RdapClient::new().with_retry_policy(policy);
954 assert_eq!(client.retry_policy.max_attempts, 5);
955 }
956
957 #[test]
958 fn test_cached_bootstrap_expiration() {
959 let data = BootstrapData {
960 dns: HashMap::new(),
961 ipv4: Vec::new(),
962 ipv6: Vec::new(),
963 asn: Vec::new(),
964 };
965 let cached = CachedBootstrap::new(data);
966 assert!(!cached.is_expired());
968 }
969
970 #[test]
971 fn test_rdap_http_client_is_configured() {
972 let client = rdap_http_client();
975 assert!(client.is_ok(), "RDAP HTTP client builder must succeed");
976 }
977
978 #[test]
979 fn test_parse_bootstrap_empty_services() {
980 let data = BootstrapData {
982 dns: HashMap::new(),
983 ipv4: Vec::new(),
984 ipv6: Vec::new(),
985 asn: Vec::new(),
986 };
987 assert!(RdapClient::get_rdap_urls_for_domain(&data, "example.com").is_none());
989 assert!(RdapClient::get_rdap_urls_for_asn(&data, 12345).is_none());
990 }
991
992 #[tokio::test]
995 async fn test_validate_url_not_reserved_rejects_loopback_literal() {
996 let err = validate_url_not_reserved("https://127.0.0.1/domain/example.com")
997 .await
998 .unwrap_err();
999 assert!(
1000 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1001 "expected reserved-IP error, got: {:?}",
1002 err
1003 );
1004 }
1005
1006 #[tokio::test]
1007 async fn test_validate_url_not_reserved_rejects_private_ipv4_literal() {
1008 let err = validate_url_not_reserved("https://10.0.0.1/")
1009 .await
1010 .unwrap_err();
1011 assert!(
1012 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1013 "expected reserved-IP error, got: {:?}",
1014 err
1015 );
1016 }
1017
1018 #[tokio::test]
1019 async fn test_validate_url_not_reserved_rejects_ipv6_loopback_literal() {
1020 let err = validate_url_not_reserved("https://[::1]/")
1021 .await
1022 .unwrap_err();
1023 assert!(
1024 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1025 "expected reserved-IP error, got: {:?}",
1026 err
1027 );
1028 }
1029
1030 #[tokio::test]
1031 async fn test_validate_url_not_reserved_returns_resolved_addrs_for_public_literal() {
1032 let addrs = validate_url_not_reserved("https://8.8.8.8/").await.unwrap();
1035 assert_eq!(addrs.len(), 1);
1036 assert!(addrs[0].ip().is_ipv4());
1037 assert_eq!(addrs[0].port(), 443);
1038 }
1039
1040 #[test]
1043 fn test_build_rdap_urls_preserves_order_and_appends_path() {
1044 let bases = vec![
1045 url::Url::parse("https://rdap.a.example/").unwrap(),
1046 url::Url::parse("https://rdap.b.example").unwrap(), ];
1048 let built = build_rdap_urls(&bases, "domain/example.com");
1049 assert_eq!(built.len(), 2);
1050 assert_eq!(
1051 built[0].as_str(),
1052 "https://rdap.a.example/domain/example.com"
1053 );
1054 assert_eq!(
1055 built[1].as_str(),
1056 "https://rdap.b.example/domain/example.com"
1057 );
1058 }
1059
1060 #[test]
1061 fn test_build_rdap_urls_empty_input_returns_empty() {
1062 let built = build_rdap_urls(&[], "domain/example.com");
1063 assert!(built.is_empty());
1064 }
1065
1066 #[test]
1069 fn test_wrap_all_candidates_failed_preserves_timeout_variant() {
1070 let last = SeerError::Timeout("body read timed out".to_string());
1073 let wrapped = wrap_all_candidates_failed(Some(last), 3);
1074 match wrapped {
1075 SeerError::Timeout(msg) => {
1076 assert!(
1077 msg.contains("all 3 RDAP candidate URLs timed out"),
1078 "expected wrapped timeout message, got: {}",
1079 msg
1080 );
1081 assert!(
1082 msg.contains("body read timed out"),
1083 "expected original message preserved, got: {}",
1084 msg
1085 );
1086 }
1087 other => panic!(
1088 "expected SeerError::Timeout after wrapping a Timeout, got: {:?}",
1089 other
1090 ),
1091 }
1092 }
1093
1094 #[test]
1095 fn test_wrap_all_candidates_failed_wraps_non_timeout_as_rdap_error() {
1096 let last = SeerError::RdapError("500 internal error".to_string());
1097 let wrapped = wrap_all_candidates_failed(Some(last), 2);
1098 assert!(
1099 matches!(wrapped, SeerError::RdapError(ref s) if s.contains("all 2 RDAP candidate URLs failed")),
1100 "expected wrapped RdapError, got: {:?}",
1101 wrapped
1102 );
1103 }
1104
1105 #[test]
1106 fn test_wrap_all_candidates_failed_single_candidate_returns_unchanged() {
1107 let last = SeerError::Timeout("single timeout".to_string());
1110 let wrapped = wrap_all_candidates_failed(Some(last), 1);
1111 assert!(
1112 matches!(wrapped, SeerError::Timeout(ref s) if s == "single timeout"),
1113 "expected unchanged Timeout, got: {:?}",
1114 wrapped
1115 );
1116 }
1117
1118 #[test]
1119 fn test_wrap_all_candidates_failed_no_last_error_returns_placeholder() {
1120 let wrapped = wrap_all_candidates_failed(None, 0);
1121 assert!(matches!(wrapped, SeerError::RdapError(_)));
1122 }
1123
1124 static BOOTSTRAP_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
1143
1144 #[tokio::test]
1145 async fn test_bootstrap_load_notify_wakes_waiter_when_cache_populated() {
1146 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1147
1148 {
1150 let mut cache = BOOTSTRAP_CACHE.write().await;
1151 *cache = None;
1152 }
1153
1154 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1157 tokio::pin!(notified);
1158
1159 {
1161 let mut cache = BOOTSTRAP_CACHE.write().await;
1162 *cache = Some(CachedBootstrap::new(BootstrapData {
1163 dns: HashMap::new(),
1164 ipv4: Vec::new(),
1165 ipv6: Vec::new(),
1166 asn: Vec::new(),
1167 }));
1168 }
1169 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1170
1171 let result = wait_for_in_flight_load(notified).await;
1172 assert!(
1173 result.is_ok(),
1174 "expected waiter to see populated cache, got: {:?}",
1175 result
1176 );
1177
1178 {
1180 let mut cache = BOOTSTRAP_CACHE.write().await;
1181 *cache = None;
1182 }
1183 }
1184
1185 #[tokio::test]
1186 async fn test_bootstrap_load_notify_empty_cache_after_wake_returns_error() {
1187 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1188
1189 {
1191 let mut cache = BOOTSTRAP_CACHE.write().await;
1192 *cache = None;
1193 }
1194
1195 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1196 tokio::pin!(notified);
1197
1198 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1200
1201 let result = wait_for_in_flight_load(notified).await;
1202 assert!(
1203 matches!(
1204 result,
1205 Err(SeerError::RdapBootstrapError(ref s))
1206 if s.contains("throttled and no cache available")
1207 ),
1208 "expected throttled error when cache still empty after notify, got: {:?}",
1209 result
1210 );
1211 }
1212}