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 timeout: Duration,
172 allow_reserved: bool,
176}
177
178impl Default for RdapClient {
179 fn default() -> Self {
180 Self::new()
181 }
182}
183
184impl RdapClient {
185 pub fn new() -> Self {
187 Self {
188 retry_policy: RetryPolicy::new()
195 .with_max_attempts(3)
196 .with_initial_delay(Duration::from_millis(500))
197 .with_max_delay(Duration::from_secs(5)),
198 timeout: DEFAULT_TIMEOUT,
199 allow_reserved: false,
200 }
201 }
202
203 pub fn with_timeout(mut self, timeout: Duration) -> Self {
205 self.timeout = timeout;
206 self
207 }
208
209 #[cfg(test)]
211 pub(crate) fn allowing_reserved_for_tests(mut self) -> Self {
212 self.allow_reserved = true;
213 self
214 }
215
216 pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
220 self.retry_policy = policy;
221 self
222 }
223
224 pub fn without_retries(mut self) -> Self {
226 self.retry_policy = RetryPolicy::no_retry();
227 self
228 }
229
230 async fn ensure_bootstrap(&self) -> Result<()> {
245 {
247 let cache = BOOTSTRAP_CACHE.read().await;
248 if let Some(cached) = cache.as_ref() {
249 if !cached.is_expired() {
250 return Ok(());
251 }
252 }
253 }
254
255 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
261 tokio::pin!(notified);
262
263 {
267 let last = BOOTSTRAP_LAST_ATTEMPT.read().await;
268 if let Some(ts) = *last {
269 if ts.elapsed() < BOOTSTRAP_REFRESH_MIN_INTERVAL {
270 let cache = BOOTSTRAP_CACHE.read().await;
272 if cache.is_some() {
273 return Ok(());
275 }
276 drop(cache);
279 drop(last);
280 return wait_for_in_flight_load(notified).await;
281 }
282 }
283 }
284
285 {
288 let mut last = BOOTSTRAP_LAST_ATTEMPT.write().await;
289 if let Some(ts) = *last {
291 if ts.elapsed() < BOOTSTRAP_REFRESH_MIN_INTERVAL {
292 drop(last);
293 let cache = BOOTSTRAP_CACHE.read().await;
294 if cache.is_some() {
295 return Ok(());
296 }
297 drop(cache);
298 return wait_for_in_flight_load(notified).await;
299 }
300 }
301 *last = Some(Instant::now());
302 }
303
304 debug!("Loading/refreshing RDAP bootstrap data");
308 let load_result = load_bootstrap_data_with_retry(&self.retry_policy).await;
309
310 let outcome = match load_result {
311 Ok(data) => {
312 let mut cache = BOOTSTRAP_CACHE.write().await;
313 let should_store = cache.as_ref().map(|c| c.is_expired()).unwrap_or(true);
316 if should_store {
317 *cache = Some(CachedBootstrap::new(data));
318 }
319 Ok(())
320 }
321 Err(e) => {
322 let cache = BOOTSTRAP_CACHE.read().await;
324 if let Some(cached) = cache.as_ref() {
325 debug!(
326 error = %e,
327 age_hours = cached.age().as_secs() / 3600,
328 "Bootstrap refresh failed, using stale data"
329 );
330 Ok(())
331 } else {
332 Err(e)
334 }
335 }
336 };
337
338 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
340 outcome
341 }
342
343 fn get_rdap_urls_for_domain(cache: &BootstrapData, domain: &str) -> Option<Arc<Vec<url::Url>>> {
345 let tld = domain.rsplit('.').next()?;
346 cache.dns.get(&tld.to_lowercase()).cloned()
347 }
348
349 fn get_rdap_urls_for_ip(cache: &BootstrapData, ip: &IpAddr) -> Option<Arc<Vec<url::Url>>> {
351 match ip {
352 IpAddr::V4(addr) => {
353 for (range, urls) in &cache.ipv4 {
354 if ipv4_matches_prefix(&range.prefix, addr) {
355 return Some(Arc::clone(urls));
356 }
357 }
358 }
359 IpAddr::V6(addr) => {
360 for (range, urls) in &cache.ipv6 {
361 if ipv6_matches_prefix(&range.prefix, addr) {
362 return Some(Arc::clone(urls));
363 }
364 }
365 }
366 }
367
368 None
369 }
370
371 fn get_rdap_urls_for_asn(cache: &BootstrapData, asn: u32) -> Option<Arc<Vec<url::Url>>> {
373 for (range, urls) in &cache.asn {
374 if asn >= range.start && asn <= range.end {
375 return Some(Arc::clone(urls));
376 }
377 }
378
379 None
380 }
381
382 #[instrument(skip(self), fields(domain = %domain))]
386 pub async fn lookup_domain(&self, domain: &str) -> Result<RdapResponse> {
387 self.ensure_bootstrap().await?;
388
389 let domain = normalize_domain(domain)?;
390
391 let urls = {
393 let cache_guard = BOOTSTRAP_CACHE.read().await;
394 let cache = cache_guard.as_ref().ok_or_else(|| {
395 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
396 })?;
397
398 let bases = Self::get_rdap_urls_for_domain(&cache.data, &domain).ok_or_else(|| {
399 SeerError::RdapBootstrapError(format!("no RDAP server for {}", domain))
400 })?;
401
402 build_rdap_urls(&bases, &format!("domain/{}", domain))
403 }; self.query_rdap_urls(&urls).await
406 }
407
408 #[instrument(skip(self), fields(ip = %ip))]
412 pub async fn lookup_ip(&self, ip: &str) -> Result<RdapResponse> {
413 self.ensure_bootstrap().await?;
414
415 let ip_addr: IpAddr = ip
416 .parse()
417 .map_err(|_| SeerError::InvalidIpAddress(ip.to_string()))?;
418
419 let urls = {
420 let cache_guard = BOOTSTRAP_CACHE.read().await;
421 let cache = cache_guard.as_ref().ok_or_else(|| {
422 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
423 })?;
424
425 let bases = Self::get_rdap_urls_for_ip(&cache.data, &ip_addr).ok_or_else(|| {
426 SeerError::RdapBootstrapError(format!("no RDAP server for {}", ip))
427 })?;
428
429 build_rdap_urls(&bases, &format!("ip/{}", ip))
430 };
431
432 self.query_rdap_urls(&urls).await
433 }
434
435 #[instrument(skip(self), fields(asn = %asn))]
439 pub async fn lookup_asn(&self, asn: u32) -> Result<RdapResponse> {
440 self.ensure_bootstrap().await?;
441
442 let urls = {
443 let cache_guard = BOOTSTRAP_CACHE.read().await;
444 let cache = cache_guard.as_ref().ok_or_else(|| {
445 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
446 })?;
447
448 let bases = Self::get_rdap_urls_for_asn(&cache.data, asn).ok_or_else(|| {
449 SeerError::RdapBootstrapError(format!("no RDAP server for AS{}", asn))
450 })?;
451
452 build_rdap_urls(&bases, &format!("autnum/{}", asn))
453 };
454
455 self.query_rdap_urls(&urls).await
456 }
457
458 #[instrument(skip(self), fields(tld = %tld))]
464 pub async fn get_rdap_base_url_for_tld(&self, tld: &str) -> Option<String> {
465 if self.ensure_bootstrap().await.is_err() {
466 return None;
467 }
468
469 let cache_guard = BOOTSTRAP_CACHE.read().await;
470 let cache = cache_guard.as_ref()?;
471 let lower = tld.to_lowercase();
475 let key = crate::validation::domain_to_ascii(&lower).unwrap_or(lower);
476 cache
477 .data
478 .dns
479 .get(&key)
480 .and_then(|urls| urls.first())
481 .map(|u| u.to_string())
482 }
483
484 async fn query_rdap_urls(&self, urls: &[url::Url]) -> Result<RdapResponse> {
488 if urls.is_empty() {
489 return Err(SeerError::RdapError(
490 "no candidate RDAP URLs available".to_string(),
491 ));
492 }
493
494 let mut last_error: Option<SeerError> = None;
495 let mut not_found_error: Option<SeerError> = None;
501 for (idx, url) in urls.iter().enumerate() {
502 let url_str = url.as_str().to_string();
503 debug!(url = %url_str, candidate = idx + 1, total = urls.len(), "Querying RDAP");
504 match self.query_rdap_with_retry(&url_str).await {
505 Ok(resp) => return Ok(resp),
506 Err(e) => {
507 if urls.len() > 1 {
508 debug!(
509 url = %url_str,
510 error = %e,
511 candidate = idx + 1,
512 total = urls.len(),
513 "RDAP candidate failed, trying next",
514 );
515 }
516 if not_found_error.is_none() && crate::rdap::rdap_error_is_404(&e) {
517 not_found_error = Some(e);
518 } else {
519 last_error = Some(e);
520 }
521 }
522 }
523 }
524
525 Err(wrap_all_candidates_failed(
528 not_found_error.or(last_error),
529 urls.len(),
530 ))
531 }
532
533 async fn query_rdap_with_retry(&self, url: &str) -> Result<RdapResponse> {
538 let classifier = NetworkRetryClassifier::new();
539 let mut attempt = 0;
540 loop {
541 match query_rdap_attempt(url, self.timeout, self.allow_reserved).await {
542 Ok(resp) => return Ok(resp),
543 Err((err, retry_after)) => {
544 let attempts_remaining =
545 self.retry_policy.max_attempts.saturating_sub(attempt + 1);
546 if !classifier.is_retryable(&err) || attempts_remaining == 0 {
547 return Err(if attempt > 0 {
548 SeerError::RetryExhausted {
549 attempts: attempt + 1,
550 last_error: Box::new(err),
551 }
552 } else {
553 err
554 });
555 }
556 let backoff = self.retry_policy.delay_for_attempt(attempt);
557 let delay = effective_retry_delay(backoff, retry_after);
558 debug!(
559 url = %url,
560 attempt = attempt + 1,
561 max_attempts = self.retry_policy.max_attempts,
562 delay_ms = delay.as_millis(),
563 error = %err,
564 "Retrying RDAP after transient error"
565 );
566 tokio::time::sleep(delay).await;
567 attempt += 1;
568 }
569 }
570 }
571 }
572}
573
574const MAX_RDAP_RESPONSE_SIZE: usize = 10 * 1024 * 1024;
576
577const MAX_RETRY_AFTER: Duration = Duration::from_secs(5);
582
583async fn validate_url_not_reserved(url: &str) -> Result<Vec<SocketAddr>> {
590 let parsed = url::Url::parse(url)
591 .map_err(|e| SeerError::RdapError(format!("invalid URL '{}': {}", url, e)))?;
592 if parsed.scheme() != "https" {
598 return Err(SeerError::RdapError(format!(
599 "RDAP URL '{}' is not https — request blocked (downgrade/SSRF protection)",
600 url
601 )));
602 }
603 let host = parsed
604 .host_str()
605 .ok_or_else(|| SeerError::RdapError(format!("URL '{}' has no host", url)))?;
606 let port = parsed.port_or_known_default().unwrap_or(443);
607
608 if let Ok(ip) = host.parse::<IpAddr>() {
610 if let Some(reason) = describe_reserved_ip(&ip) {
611 return Err(SeerError::RdapError(format!(
612 "RDAP URL resolves to reserved IP {}: {} — request blocked (SSRF protection)",
613 ip, reason
614 )));
615 }
616 return Ok(vec![SocketAddr::new(ip, port)]);
617 }
618
619 let addr = format!("{}:{}", host, port);
620
621 let socket_addrs: Vec<SocketAddr> = tokio::net::lookup_host(&addr)
622 .await
623 .map_err(|e| SeerError::RdapError(format!("failed to resolve host '{}': {}", host, e)))?
624 .collect();
625
626 if socket_addrs.is_empty() {
627 return Err(SeerError::RdapError(format!(
628 "host '{}' resolved to no addresses",
629 host
630 )));
631 }
632
633 for socket_addr in &socket_addrs {
634 if let Some(reason) = describe_reserved_ip(&socket_addr.ip()) {
635 return Err(SeerError::RdapError(format!(
636 "RDAP URL resolves to reserved IP {}: {} — request blocked (SSRF protection)",
637 socket_addr.ip(),
638 reason
639 )));
640 }
641 }
642
643 Ok(socket_addrs)
644}
645
646fn parse_retry_after(value: &str) -> Option<Duration> {
651 value.trim().parse::<u64>().ok().map(Duration::from_secs)
652}
653
654fn effective_retry_delay(backoff: Duration, retry_after: Option<Duration>) -> Duration {
658 match retry_after {
659 Some(hint) => hint.min(MAX_RETRY_AFTER),
660 None => backoff,
661 }
662}
663
664async fn send_rdap_request(
671 url: &str,
672 timeout: Duration,
673 allow_reserved: bool,
674) -> Result<reqwest::Response> {
675 let connect_timeout = CONNECT_TIMEOUT.min(timeout);
678 if allow_reserved {
679 let client = Client::builder()
680 .timeout(timeout)
681 .connect_timeout(connect_timeout)
682 .user_agent("Seer/1.0 (RDAP Client)")
683 .redirect(reqwest::redirect::Policy::none())
684 .build()
685 .map_err(|e| SeerError::RdapError(format!("failed to build HTTP client: {}", e)))?;
686 return client
687 .get(url)
688 .header("Accept", "application/rdap+json")
689 .send()
690 .await
691 .map_err(Into::into);
692 }
693
694 let resolved = validate_url_not_reserved(url).await?;
697
698 let parsed = url::Url::parse(url)
699 .map_err(|e| SeerError::RdapError(format!("invalid URL '{}': {}", url, e)))?;
700 let host = parsed
701 .host_str()
702 .ok_or_else(|| SeerError::RdapError(format!("URL '{}' has no host", url)))?;
703
704 let client = Client::builder()
708 .timeout(timeout)
709 .connect_timeout(connect_timeout)
710 .user_agent("Seer/1.0 (RDAP Client)")
711 .resolve_to_addrs(host, &resolved)
712 .redirect(reqwest::redirect::Policy::none())
721 .build()
722 .map_err(|e| SeerError::RdapError(format!("failed to build HTTP client: {}", e)))?;
723
724 client
725 .get(url)
726 .header("Accept", "application/rdap+json")
727 .send()
728 .await
729 .map_err(Into::into)
730}
731
732async fn read_and_parse_rdap_body(response: reqwest::Response, url: &str) -> Result<RdapResponse> {
735 let mut body = Vec::new();
740 let mut stream = response.bytes_stream();
741 let streamed = tokio::time::timeout(DEFAULT_TIMEOUT, async {
742 while let Some(chunk) = stream.next().await {
743 let chunk = chunk
744 .map_err(|e| SeerError::RdapError(format!("failed to read response: {}", e)))?;
745 body.extend_from_slice(&chunk);
746 if body.len() > MAX_RDAP_RESPONSE_SIZE {
747 return Err(SeerError::RdapError(format!(
748 "RDAP response exceeds {} byte limit",
749 MAX_RDAP_RESPONSE_SIZE
750 )));
751 }
752 }
753 Ok::<(), SeerError>(())
754 })
755 .await;
756
757 match streamed {
758 Ok(Ok(())) => {}
759 Ok(Err(e)) => return Err(e),
760 Err(_) => {
761 return Err(SeerError::Timeout(format!(
762 "timed out reading RDAP response body from {} after {:?}",
763 url, DEFAULT_TIMEOUT
764 )));
765 }
766 }
767
768 let rdap: RdapResponse = serde_json::from_slice(&body)?;
769 rdap.validate()?;
775 Ok(rdap)
776}
777
778async fn query_rdap_attempt(
784 url: &str,
785 timeout: Duration,
786 allow_reserved: bool,
787) -> std::result::Result<RdapResponse, (SeerError, Option<Duration>)> {
788 let response = send_rdap_request(url, timeout, allow_reserved)
789 .await
790 .map_err(|e| (e, None))?;
791
792 if !response.status().is_success() {
793 let status = response.status();
794 let retry_after = if status.as_u16() == 429 {
797 response
798 .headers()
799 .get(reqwest::header::RETRY_AFTER)
800 .and_then(|v| v.to_str().ok())
801 .and_then(parse_retry_after)
802 } else {
803 None
804 };
805 return Err((
806 SeerError::RdapError(format!("query failed with status {}", status)),
807 retry_after,
808 ));
809 }
810
811 read_and_parse_rdap_body(response, url)
812 .await
813 .map_err(|e| (e, None))
814}
815
816async fn load_bootstrap_data_with_retry(policy: &RetryPolicy) -> Result<BootstrapData> {
818 let executor = RetryExecutor::new(policy.clone());
819 executor.execute(load_bootstrap_data).await
820}
821
822async fn load_bootstrap_data() -> Result<BootstrapData> {
824 debug!("Loading RDAP bootstrap data from IANA");
825
826 let http = rdap_http_client()?;
831
832 let dns_future = http.get(IANA_BOOTSTRAP_DNS).send();
833 let ipv4_future = http.get(IANA_BOOTSTRAP_IPV4).send();
834 let ipv6_future = http.get(IANA_BOOTSTRAP_IPV6).send();
835 let asn_future = http.get(IANA_BOOTSTRAP_ASN).send();
836
837 let (dns_resp, ipv4_resp, ipv6_resp, asn_resp) =
840 tokio::join!(dns_future, ipv4_future, ipv6_future, asn_future);
841
842 const MAX_BOOTSTRAP_SIZE: usize = 10 * 1024 * 1024; async fn read_bootstrap(resp: reqwest::Response) -> Result<BootstrapResponse> {
846 let mut body = Vec::new();
852 let mut stream = resp.bytes_stream();
853 let streamed = tokio::time::timeout(DEFAULT_TIMEOUT, async {
854 while let Some(chunk) = stream.next().await {
855 let chunk = chunk.map_err(|e| {
856 SeerError::RdapBootstrapError(format!("failed to read body: {}", e))
857 })?;
858 body.extend_from_slice(&chunk);
859 if body.len() > MAX_BOOTSTRAP_SIZE {
860 return Err(SeerError::RdapBootstrapError(format!(
861 "bootstrap response too large (exceeds {} bytes)",
862 MAX_BOOTSTRAP_SIZE
863 )));
864 }
865 }
866 Ok::<(), SeerError>(())
867 })
868 .await;
869
870 match streamed {
871 Ok(Ok(())) => {}
872 Ok(Err(e)) => return Err(e),
873 Err(_) => {
874 return Err(SeerError::Timeout(format!(
875 "RDAP bootstrap body read timed out after {:?}",
876 DEFAULT_TIMEOUT
877 )));
878 }
879 }
880
881 serde_json::from_slice(&body).map_err(Into::into)
882 }
883
884 let dns_data = match dns_resp {
886 Ok(resp) => match read_bootstrap(resp).await {
887 Ok(data) => Some(data),
888 Err(e) => {
889 warn!(error = %e, "Failed to parse DNS bootstrap response");
890 None
891 }
892 },
893 Err(e) => {
894 warn!(error = %e, "Failed to fetch DNS bootstrap from IANA");
895 None
896 }
897 };
898 let ipv4_data = match ipv4_resp {
899 Ok(resp) => match read_bootstrap(resp).await {
900 Ok(data) => Some(data),
901 Err(e) => {
902 warn!(error = %e, "Failed to parse IPv4 bootstrap response");
903 None
904 }
905 },
906 Err(e) => {
907 warn!(error = %e, "Failed to fetch IPv4 bootstrap from IANA");
908 None
909 }
910 };
911 let ipv6_data = match ipv6_resp {
912 Ok(resp) => match read_bootstrap(resp).await {
913 Ok(data) => Some(data),
914 Err(e) => {
915 warn!(error = %e, "Failed to parse IPv6 bootstrap response");
916 None
917 }
918 },
919 Err(e) => {
920 warn!(error = %e, "Failed to fetch IPv6 bootstrap from IANA");
921 None
922 }
923 };
924 let asn_data = match asn_resp {
925 Ok(resp) => match read_bootstrap(resp).await {
926 Ok(data) => Some(data),
927 Err(e) => {
928 warn!(error = %e, "Failed to parse ASN bootstrap response");
929 None
930 }
931 },
932 Err(e) => {
933 warn!(error = %e, "Failed to fetch ASN bootstrap from IANA");
934 None
935 }
936 };
937
938 if dns_data.is_none() && ipv4_data.is_none() && ipv6_data.is_none() && asn_data.is_none() {
940 return Err(SeerError::RdapBootstrapError(
941 "all IANA bootstrap registries failed".to_string(),
942 ));
943 }
944
945 let mut dns = HashMap::new();
946 let mut ipv4 = Vec::new();
947 let mut ipv6 = Vec::new();
948 let mut asn = Vec::new();
949
950 fn collect_valid_urls(urls: &[serde_json::Value]) -> Option<Arc<Vec<url::Url>>> {
954 let mut out = Vec::new();
955 for u in urls {
956 if let Some(s) = u.as_str() {
957 match validate_bootstrap_url(s) {
958 Ok(parsed) => out.push(parsed),
959 Err(e) => {
960 debug!(url = s, error = %e, "Skipping invalid bootstrap URL");
961 }
962 }
963 }
964 }
965 if out.is_empty() {
966 None
967 } else {
968 Some(Arc::new(out))
969 }
970 }
971
972 if let Some(dns_data) = dns_data {
974 for service in dns_data.services {
975 if service.len() >= 2 {
976 if let (Some(tlds), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
977 if let Some(urls_arc) = collect_valid_urls(urls) {
978 for tld in tlds {
979 if let Some(tld_str) = tld.as_str() {
980 dns.insert(tld_str.to_lowercase(), Arc::clone(&urls_arc));
981 }
982 }
983 }
984 }
985 }
986 }
987 }
988
989 if let Some(ipv4_data) = ipv4_data {
991 for service in ipv4_data.services {
992 if service.len() >= 2 {
993 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array())
994 {
995 if let Some(urls_arc) = collect_valid_urls(urls) {
996 for prefix in prefixes {
997 if let Some(prefix_str) = prefix.as_str() {
998 ipv4.push((
999 IpRange {
1000 prefix: prefix_str.to_string(),
1001 },
1002 Arc::clone(&urls_arc),
1003 ));
1004 }
1005 }
1006 }
1007 }
1008 }
1009 }
1010 }
1011
1012 if let Some(ipv6_data) = ipv6_data {
1014 for service in ipv6_data.services {
1015 if service.len() >= 2 {
1016 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array())
1017 {
1018 if let Some(urls_arc) = collect_valid_urls(urls) {
1019 for prefix in prefixes {
1020 if let Some(prefix_str) = prefix.as_str() {
1021 ipv6.push((
1022 IpRange {
1023 prefix: prefix_str.to_string(),
1024 },
1025 Arc::clone(&urls_arc),
1026 ));
1027 }
1028 }
1029 }
1030 }
1031 }
1032 }
1033 }
1034
1035 if let Some(asn_data) = asn_data {
1037 for service in asn_data.services {
1038 if service.len() >= 2 {
1039 if let (Some(ranges), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
1040 if let Some(urls_arc) = collect_valid_urls(urls) {
1041 for range in ranges {
1042 if let Some(range_str) = range.as_str() {
1043 if let Some((start, end)) = parse_asn_range(range_str) {
1044 asn.push((AsnRange { start, end }, Arc::clone(&urls_arc)));
1045 }
1046 }
1047 }
1048 }
1049 }
1050 }
1051 }
1052 }
1053
1054 info!(
1055 dns_entries = dns.len(),
1056 ipv4_ranges = ipv4.len(),
1057 ipv6_ranges = ipv6.len(),
1058 asn_ranges = asn.len(),
1059 "RDAP bootstrap loaded"
1060 );
1061
1062 Ok(BootstrapData {
1063 dns,
1064 ipv4,
1065 ipv6,
1066 asn,
1067 })
1068}
1069
1070fn wrap_all_candidates_failed(last_error: Option<SeerError>, candidate_count: usize) -> SeerError {
1079 let last = last_error.unwrap_or_else(|| SeerError::RdapError("no candidates".to_string()));
1080
1081 if candidate_count <= 1 {
1082 return last;
1083 }
1084
1085 match last {
1086 SeerError::Timeout(msg) => SeerError::Timeout(format!(
1087 "all {} RDAP candidate URLs timed out; last error: {}",
1088 candidate_count, msg
1089 )),
1090 other => SeerError::RdapError(format!(
1091 "all {} RDAP candidate URLs failed; last error: {}",
1092 candidate_count, other
1093 )),
1094 }
1095}
1096
1097fn build_rdap_urls(bases: &[url::Url], path: &str) -> Vec<url::Url> {
1099 bases
1100 .iter()
1101 .filter_map(|base| {
1102 let base_str = base.as_str();
1105 let normalized = if base_str.ends_with('/') {
1106 base_str.to_string()
1107 } else {
1108 format!("{}/", base_str)
1109 };
1110 url::Url::parse(&normalized).and_then(|u| u.join(path)).ok()
1111 })
1112 .collect()
1113}
1114
1115#[cfg(test)]
1116mod tests {
1117 use super::*;
1118
1119 #[test]
1120 fn test_default_client_has_retry_policy() {
1121 let client = RdapClient::new();
1122 assert_eq!(client.retry_policy.max_attempts, 3);
1126 }
1127
1128 #[test]
1131 fn parse_retry_after_parses_delta_seconds() {
1132 assert_eq!(parse_retry_after("5"), Some(Duration::from_secs(5)));
1133 assert_eq!(parse_retry_after(" 10 "), Some(Duration::from_secs(10)));
1134 assert_eq!(parse_retry_after("0"), Some(Duration::from_secs(0)));
1135 }
1136
1137 #[test]
1138 fn parse_retry_after_rejects_http_date_and_junk() {
1139 assert_eq!(parse_retry_after("Wed, 21 Oct 2015 07:28:00 GMT"), None);
1142 assert_eq!(parse_retry_after("soon"), None);
1143 assert_eq!(parse_retry_after(""), None);
1144 }
1145
1146 #[test]
1147 fn effective_retry_delay_prefers_capped_retry_after() {
1148 assert_eq!(
1150 effective_retry_delay(Duration::from_millis(100), Some(Duration::from_secs(5))),
1151 Duration::from_secs(5)
1152 );
1153 assert_eq!(
1155 effective_retry_delay(Duration::from_millis(100), Some(Duration::from_secs(600))),
1156 MAX_RETRY_AFTER
1157 );
1158 }
1159
1160 #[test]
1161 fn effective_retry_delay_falls_back_to_backoff() {
1162 assert_eq!(
1163 effective_retry_delay(Duration::from_millis(250), None),
1164 Duration::from_millis(250)
1165 );
1166 }
1167
1168 #[test]
1169 fn test_client_without_retries() {
1170 let client = RdapClient::new().without_retries();
1171 assert_eq!(client.retry_policy.max_attempts, 1);
1172 }
1173
1174 #[test]
1175 fn test_client_custom_retry_policy() {
1176 let policy = RetryPolicy::new().with_max_attempts(5);
1177 let client = RdapClient::new().with_retry_policy(policy);
1178 assert_eq!(client.retry_policy.max_attempts, 5);
1179 }
1180
1181 #[test]
1182 fn test_cached_bootstrap_expiration() {
1183 let data = BootstrapData {
1184 dns: HashMap::new(),
1185 ipv4: Vec::new(),
1186 ipv6: Vec::new(),
1187 asn: Vec::new(),
1188 };
1189 let cached = CachedBootstrap::new(data);
1190 assert!(!cached.is_expired());
1192 }
1193
1194 #[test]
1195 fn test_rdap_http_client_is_configured() {
1196 let client = rdap_http_client();
1199 assert!(client.is_ok(), "RDAP HTTP client builder must succeed");
1200 }
1201
1202 #[test]
1203 fn test_parse_bootstrap_empty_services() {
1204 let data = BootstrapData {
1206 dns: HashMap::new(),
1207 ipv4: Vec::new(),
1208 ipv6: Vec::new(),
1209 asn: Vec::new(),
1210 };
1211 assert!(RdapClient::get_rdap_urls_for_domain(&data, "example.com").is_none());
1213 assert!(RdapClient::get_rdap_urls_for_asn(&data, 12345).is_none());
1214 }
1215
1216 #[tokio::test]
1219 async fn test_validate_url_not_reserved_rejects_loopback_literal() {
1220 let err = validate_url_not_reserved("https://127.0.0.1/domain/example.com")
1221 .await
1222 .unwrap_err();
1223 assert!(
1224 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1225 "expected reserved-IP error, got: {:?}",
1226 err
1227 );
1228 }
1229
1230 #[tokio::test]
1231 async fn test_validate_url_not_reserved_rejects_private_ipv4_literal() {
1232 let err = validate_url_not_reserved("https://10.0.0.1/")
1233 .await
1234 .unwrap_err();
1235 assert!(
1236 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1237 "expected reserved-IP error, got: {:?}",
1238 err
1239 );
1240 }
1241
1242 #[tokio::test]
1243 async fn test_validate_url_not_reserved_rejects_non_https_scheme() {
1244 let err = validate_url_not_reserved("http://93.184.216.34/domain/example.com")
1248 .await
1249 .unwrap_err();
1250 assert!(
1251 matches!(err, SeerError::RdapError(ref s) if s.contains("not https")),
1252 "expected non-https rejection, got: {:?}",
1253 err
1254 );
1255 }
1256
1257 #[tokio::test]
1258 async fn test_validate_url_not_reserved_rejects_ipv6_loopback_literal() {
1259 let err = validate_url_not_reserved("https://[::1]/")
1260 .await
1261 .unwrap_err();
1262 assert!(
1263 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1264 "expected reserved-IP error, got: {:?}",
1265 err
1266 );
1267 }
1268
1269 #[tokio::test]
1270 async fn test_validate_url_not_reserved_returns_resolved_addrs_for_public_literal() {
1271 let addrs = validate_url_not_reserved("https://8.8.8.8/").await.unwrap();
1274 assert_eq!(addrs.len(), 1);
1275 assert!(addrs[0].ip().is_ipv4());
1276 assert_eq!(addrs[0].port(), 443);
1277 }
1278
1279 #[test]
1282 fn test_build_rdap_urls_preserves_order_and_appends_path() {
1283 let bases = vec![
1284 url::Url::parse("https://rdap.a.example/").unwrap(),
1285 url::Url::parse("https://rdap.b.example").unwrap(), ];
1287 let built = build_rdap_urls(&bases, "domain/example.com");
1288 assert_eq!(built.len(), 2);
1289 assert_eq!(
1290 built[0].as_str(),
1291 "https://rdap.a.example/domain/example.com"
1292 );
1293 assert_eq!(
1294 built[1].as_str(),
1295 "https://rdap.b.example/domain/example.com"
1296 );
1297 }
1298
1299 #[test]
1300 fn test_build_rdap_urls_empty_input_returns_empty() {
1301 let built = build_rdap_urls(&[], "domain/example.com");
1302 assert!(built.is_empty());
1303 }
1304
1305 #[test]
1308 fn test_wrap_all_candidates_failed_preserves_timeout_variant() {
1309 let last = SeerError::Timeout("body read timed out".to_string());
1312 let wrapped = wrap_all_candidates_failed(Some(last), 3);
1313 match wrapped {
1314 SeerError::Timeout(msg) => {
1315 assert!(
1316 msg.contains("all 3 RDAP candidate URLs timed out"),
1317 "expected wrapped timeout message, got: {}",
1318 msg
1319 );
1320 assert!(
1321 msg.contains("body read timed out"),
1322 "expected original message preserved, got: {}",
1323 msg
1324 );
1325 }
1326 other => panic!(
1327 "expected SeerError::Timeout after wrapping a Timeout, got: {:?}",
1328 other
1329 ),
1330 }
1331 }
1332
1333 #[test]
1334 fn test_wrap_all_candidates_failed_wraps_non_timeout_as_rdap_error() {
1335 let last = SeerError::RdapError("500 internal error".to_string());
1336 let wrapped = wrap_all_candidates_failed(Some(last), 2);
1337 assert!(
1338 matches!(wrapped, SeerError::RdapError(ref s) if s.contains("all 2 RDAP candidate URLs failed")),
1339 "expected wrapped RdapError, got: {:?}",
1340 wrapped
1341 );
1342 }
1343
1344 #[test]
1345 fn test_wrap_all_candidates_failed_single_candidate_returns_unchanged() {
1346 let last = SeerError::Timeout("single timeout".to_string());
1349 let wrapped = wrap_all_candidates_failed(Some(last), 1);
1350 assert!(
1351 matches!(wrapped, SeerError::Timeout(ref s) if s == "single timeout"),
1352 "expected unchanged Timeout, got: {:?}",
1353 wrapped
1354 );
1355 }
1356
1357 #[test]
1358 fn test_wrap_all_candidates_failed_no_last_error_returns_placeholder() {
1359 let wrapped = wrap_all_candidates_failed(None, 0);
1360 assert!(matches!(wrapped, SeerError::RdapError(_)));
1361 }
1362
1363 static BOOTSTRAP_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
1382
1383 #[tokio::test]
1384 async fn test_bootstrap_load_notify_wakes_waiter_when_cache_populated() {
1385 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1386
1387 {
1389 let mut cache = BOOTSTRAP_CACHE.write().await;
1390 *cache = None;
1391 }
1392
1393 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1396 tokio::pin!(notified);
1397
1398 {
1400 let mut cache = BOOTSTRAP_CACHE.write().await;
1401 *cache = Some(CachedBootstrap::new(BootstrapData {
1402 dns: HashMap::new(),
1403 ipv4: Vec::new(),
1404 ipv6: Vec::new(),
1405 asn: Vec::new(),
1406 }));
1407 }
1408 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1409
1410 let result = wait_for_in_flight_load(notified).await;
1411 assert!(
1412 result.is_ok(),
1413 "expected waiter to see populated cache, got: {:?}",
1414 result
1415 );
1416
1417 {
1419 let mut cache = BOOTSTRAP_CACHE.write().await;
1420 *cache = None;
1421 }
1422 }
1423
1424 #[tokio::test]
1425 async fn test_bootstrap_load_notify_empty_cache_after_wake_returns_error() {
1426 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1427
1428 {
1430 let mut cache = BOOTSTRAP_CACHE.write().await;
1431 *cache = None;
1432 }
1433
1434 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1435 tokio::pin!(notified);
1436
1437 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1439
1440 let result = wait_for_in_flight_load(notified).await;
1441 assert!(
1442 matches!(
1443 result,
1444 Err(SeerError::RdapBootstrapError(ref s))
1445 if s.contains("throttled and no cache available")
1446 ),
1447 "expected throttled error when cache still empty after notify, got: {:?}",
1448 result
1449 );
1450 }
1451
1452 use wiremock::matchers::method;
1462 use wiremock::{Mock, MockServer, ResponseTemplate};
1463
1464 #[tokio::test]
1465 async fn mock_rdap_404_is_nonretryable_typed_error() {
1466 let server = MockServer::start().await;
1467 Mock::given(method("GET"))
1468 .respond_with(ResponseTemplate::new(404))
1469 .mount(&server)
1470 .await;
1471
1472 let client = RdapClient::new()
1473 .without_retries()
1474 .allowing_reserved_for_tests();
1475 let err = client
1476 .query_rdap_with_retry(&format!("{}/domain/example.com", server.uri()))
1477 .await
1478 .unwrap_err();
1479 assert!(
1480 matches!(err, SeerError::RdapError(ref m) if m.contains("404")),
1481 "got: {err:?}"
1482 );
1483 }
1484
1485 #[tokio::test]
1486 async fn mock_rdap_429_honors_retry_after_and_succeeds() {
1487 let server = MockServer::start().await;
1488 Mock::given(method("GET"))
1491 .respond_with(ResponseTemplate::new(429).insert_header("Retry-After", "0"))
1492 .up_to_n_times(1)
1493 .mount(&server)
1494 .await;
1495 Mock::given(method("GET"))
1496 .respond_with(ResponseTemplate::new(200).set_body_raw(
1497 r#"{"objectClassName":"domain","handle":"MOCK-1"}"#,
1498 "application/rdap+json",
1499 ))
1500 .mount(&server)
1501 .await;
1502
1503 let client = RdapClient::new().allowing_reserved_for_tests();
1504 let resp = client
1505 .query_rdap_with_retry(&format!("{}/domain/example.com", server.uri()))
1506 .await
1507 .unwrap();
1508 assert_eq!(resp.handle.as_deref(), Some("MOCK-1"));
1509 }
1510
1511 #[tokio::test]
1512 async fn mock_rdap_malformed_body_is_parse_error_not_panic() {
1513 let server = MockServer::start().await;
1514 Mock::given(method("GET"))
1515 .respond_with(ResponseTemplate::new(200).set_body_raw("not json", "text/plain"))
1516 .mount(&server)
1517 .await;
1518
1519 let client = RdapClient::new()
1520 .without_retries()
1521 .allowing_reserved_for_tests();
1522 let err = client
1523 .query_rdap_with_retry(&format!("{}/domain/example.com", server.uri()))
1524 .await
1525 .unwrap_err();
1526 assert!(matches!(err, SeerError::JsonError(_)), "got: {err:?}");
1527 }
1528
1529 #[tokio::test]
1530 async fn mock_rdap_candidate_fallback_uses_second_url() {
1531 let bad = MockServer::start().await;
1532 Mock::given(method("GET"))
1533 .respond_with(ResponseTemplate::new(500))
1534 .mount(&bad)
1535 .await;
1536 let good = MockServer::start().await;
1537 Mock::given(method("GET"))
1538 .respond_with(ResponseTemplate::new(200).set_body_raw(
1539 r#"{"objectClassName":"domain","handle":"MOCK-2"}"#,
1540 "application/rdap+json",
1541 ))
1542 .mount(&good)
1543 .await;
1544
1545 let client = RdapClient::new()
1546 .without_retries()
1547 .allowing_reserved_for_tests();
1548 let urls = vec![
1549 url::Url::parse(&format!("{}/domain/example.com", bad.uri())).unwrap(),
1550 url::Url::parse(&format!("{}/domain/example.com", good.uri())).unwrap(),
1551 ];
1552 let resp = client.query_rdap_urls(&urls).await.unwrap();
1553 assert_eq!(resp.handle.as_deref(), Some("MOCK-2"));
1554 }
1555
1556 #[tokio::test]
1562 async fn mock_rdap_404_on_first_candidate_survives_later_non_404_failure() {
1563 let not_found = MockServer::start().await;
1564 Mock::given(method("GET"))
1565 .respond_with(ResponseTemplate::new(404))
1566 .mount(¬_found)
1567 .await;
1568 let broken = MockServer::start().await;
1569 Mock::given(method("GET"))
1570 .respond_with(ResponseTemplate::new(500))
1571 .mount(&broken)
1572 .await;
1573
1574 let client = RdapClient::new()
1575 .without_retries()
1576 .allowing_reserved_for_tests();
1577 let urls = vec![
1578 url::Url::parse(&format!("{}/domain/example.com", not_found.uri())).unwrap(),
1579 url::Url::parse(&format!("{}/domain/example.com", broken.uri())).unwrap(),
1580 ];
1581 let err = client.query_rdap_urls(&urls).await.unwrap_err();
1582 assert!(
1583 crate::rdap::rdap_error_is_404(&err),
1584 "404 from candidate 1 must survive candidate 2's non-404 failure, got: {err:?}"
1585 );
1586 }
1587}