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(
737 response: reqwest::Response,
738 url: &str,
739 timeout: Duration,
740) -> Result<RdapResponse> {
741 let mut body = Vec::new();
746 let mut stream = response.bytes_stream();
747 let streamed = tokio::time::timeout(timeout, async {
748 while let Some(chunk) = stream.next().await {
749 let chunk = chunk
750 .map_err(|e| SeerError::RdapError(format!("failed to read response: {}", e)))?;
751 body.extend_from_slice(&chunk);
752 if body.len() > MAX_RDAP_RESPONSE_SIZE {
753 return Err(SeerError::RdapError(format!(
754 "RDAP response exceeds {} byte limit",
755 MAX_RDAP_RESPONSE_SIZE
756 )));
757 }
758 }
759 Ok::<(), SeerError>(())
760 })
761 .await;
762
763 match streamed {
764 Ok(Ok(())) => {}
765 Ok(Err(e)) => return Err(e),
766 Err(_) => {
767 return Err(SeerError::Timeout(format!(
768 "timed out reading RDAP response body from {} after {:?}",
769 url, timeout
770 )));
771 }
772 }
773
774 let rdap: RdapResponse = serde_json::from_slice(&body)?;
775 rdap.validate()?;
781 Ok(rdap)
782}
783
784async fn query_rdap_attempt(
790 url: &str,
791 timeout: Duration,
792 allow_reserved: bool,
793) -> std::result::Result<RdapResponse, (SeerError, Option<Duration>)> {
794 let response = send_rdap_request(url, timeout, allow_reserved)
795 .await
796 .map_err(|e| (e, None))?;
797
798 if !response.status().is_success() {
799 let status = response.status();
800 let retry_after = if status.as_u16() == 429 {
803 response
804 .headers()
805 .get(reqwest::header::RETRY_AFTER)
806 .and_then(|v| v.to_str().ok())
807 .and_then(parse_retry_after)
808 } else {
809 None
810 };
811 return Err((
812 SeerError::RdapError(format!("query failed with status {}", status)),
813 retry_after,
814 ));
815 }
816
817 read_and_parse_rdap_body(response, url, timeout)
818 .await
819 .map_err(|e| (e, None))
820}
821
822async fn load_bootstrap_data_with_retry(policy: &RetryPolicy) -> Result<BootstrapData> {
824 let executor = RetryExecutor::new(policy.clone());
825 executor.execute(load_bootstrap_data).await
826}
827
828async fn load_bootstrap_data() -> Result<BootstrapData> {
830 debug!("Loading RDAP bootstrap data from IANA");
831
832 let http = rdap_http_client()?;
837
838 let dns_future = http.get(IANA_BOOTSTRAP_DNS).send();
839 let ipv4_future = http.get(IANA_BOOTSTRAP_IPV4).send();
840 let ipv6_future = http.get(IANA_BOOTSTRAP_IPV6).send();
841 let asn_future = http.get(IANA_BOOTSTRAP_ASN).send();
842
843 let (dns_resp, ipv4_resp, ipv6_resp, asn_resp) =
846 tokio::join!(dns_future, ipv4_future, ipv6_future, asn_future);
847
848 const MAX_BOOTSTRAP_SIZE: usize = 10 * 1024 * 1024; async fn read_bootstrap(resp: reqwest::Response) -> Result<BootstrapResponse> {
852 let mut body = Vec::new();
858 let mut stream = resp.bytes_stream();
859 let streamed = tokio::time::timeout(DEFAULT_TIMEOUT, async {
860 while let Some(chunk) = stream.next().await {
861 let chunk = chunk.map_err(|e| {
862 SeerError::RdapBootstrapError(format!("failed to read body: {}", e))
863 })?;
864 body.extend_from_slice(&chunk);
865 if body.len() > MAX_BOOTSTRAP_SIZE {
866 return Err(SeerError::RdapBootstrapError(format!(
867 "bootstrap response too large (exceeds {} bytes)",
868 MAX_BOOTSTRAP_SIZE
869 )));
870 }
871 }
872 Ok::<(), SeerError>(())
873 })
874 .await;
875
876 match streamed {
877 Ok(Ok(())) => {}
878 Ok(Err(e)) => return Err(e),
879 Err(_) => {
880 return Err(SeerError::Timeout(format!(
881 "RDAP bootstrap body read timed out after {:?}",
882 DEFAULT_TIMEOUT
883 )));
884 }
885 }
886
887 serde_json::from_slice(&body).map_err(Into::into)
888 }
889
890 let dns_data = match dns_resp {
892 Ok(resp) => match read_bootstrap(resp).await {
893 Ok(data) => Some(data),
894 Err(e) => {
895 warn!(error = %e, "Failed to parse DNS bootstrap response");
896 None
897 }
898 },
899 Err(e) => {
900 warn!(error = %e, "Failed to fetch DNS bootstrap from IANA");
901 None
902 }
903 };
904 let ipv4_data = match ipv4_resp {
905 Ok(resp) => match read_bootstrap(resp).await {
906 Ok(data) => Some(data),
907 Err(e) => {
908 warn!(error = %e, "Failed to parse IPv4 bootstrap response");
909 None
910 }
911 },
912 Err(e) => {
913 warn!(error = %e, "Failed to fetch IPv4 bootstrap from IANA");
914 None
915 }
916 };
917 let ipv6_data = match ipv6_resp {
918 Ok(resp) => match read_bootstrap(resp).await {
919 Ok(data) => Some(data),
920 Err(e) => {
921 warn!(error = %e, "Failed to parse IPv6 bootstrap response");
922 None
923 }
924 },
925 Err(e) => {
926 warn!(error = %e, "Failed to fetch IPv6 bootstrap from IANA");
927 None
928 }
929 };
930 let asn_data = match asn_resp {
931 Ok(resp) => match read_bootstrap(resp).await {
932 Ok(data) => Some(data),
933 Err(e) => {
934 warn!(error = %e, "Failed to parse ASN bootstrap response");
935 None
936 }
937 },
938 Err(e) => {
939 warn!(error = %e, "Failed to fetch ASN bootstrap from IANA");
940 None
941 }
942 };
943
944 if dns_data.is_none() && ipv4_data.is_none() && ipv6_data.is_none() && asn_data.is_none() {
946 return Err(SeerError::RdapBootstrapError(
947 "all IANA bootstrap registries failed".to_string(),
948 ));
949 }
950
951 let mut dns = HashMap::new();
952 let mut ipv4 = Vec::new();
953 let mut ipv6 = Vec::new();
954 let mut asn = Vec::new();
955
956 fn collect_valid_urls(urls: &[serde_json::Value]) -> Option<Arc<Vec<url::Url>>> {
960 let mut out = Vec::new();
961 for u in urls {
962 if let Some(s) = u.as_str() {
963 match validate_bootstrap_url(s) {
964 Ok(parsed) => out.push(parsed),
965 Err(e) => {
966 debug!(url = s, error = %e, "Skipping invalid bootstrap URL");
967 }
968 }
969 }
970 }
971 if out.is_empty() {
972 None
973 } else {
974 Some(Arc::new(out))
975 }
976 }
977
978 if let Some(dns_data) = dns_data {
980 for service in dns_data.services {
981 if service.len() >= 2 {
982 if let (Some(tlds), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
983 if let Some(urls_arc) = collect_valid_urls(urls) {
984 for tld in tlds {
985 if let Some(tld_str) = tld.as_str() {
986 dns.insert(tld_str.to_lowercase(), Arc::clone(&urls_arc));
987 }
988 }
989 }
990 }
991 }
992 }
993 }
994
995 if let Some(ipv4_data) = ipv4_data {
997 for service in ipv4_data.services {
998 if service.len() >= 2 {
999 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array())
1000 {
1001 if let Some(urls_arc) = collect_valid_urls(urls) {
1002 for prefix in prefixes {
1003 if let Some(prefix_str) = prefix.as_str() {
1004 ipv4.push((
1005 IpRange {
1006 prefix: prefix_str.to_string(),
1007 },
1008 Arc::clone(&urls_arc),
1009 ));
1010 }
1011 }
1012 }
1013 }
1014 }
1015 }
1016 }
1017
1018 if let Some(ipv6_data) = ipv6_data {
1020 for service in ipv6_data.services {
1021 if service.len() >= 2 {
1022 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array())
1023 {
1024 if let Some(urls_arc) = collect_valid_urls(urls) {
1025 for prefix in prefixes {
1026 if let Some(prefix_str) = prefix.as_str() {
1027 ipv6.push((
1028 IpRange {
1029 prefix: prefix_str.to_string(),
1030 },
1031 Arc::clone(&urls_arc),
1032 ));
1033 }
1034 }
1035 }
1036 }
1037 }
1038 }
1039 }
1040
1041 if let Some(asn_data) = asn_data {
1043 for service in asn_data.services {
1044 if service.len() >= 2 {
1045 if let (Some(ranges), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
1046 if let Some(urls_arc) = collect_valid_urls(urls) {
1047 for range in ranges {
1048 if let Some(range_str) = range.as_str() {
1049 if let Some((start, end)) = parse_asn_range(range_str) {
1050 asn.push((AsnRange { start, end }, Arc::clone(&urls_arc)));
1051 }
1052 }
1053 }
1054 }
1055 }
1056 }
1057 }
1058 }
1059
1060 info!(
1061 dns_entries = dns.len(),
1062 ipv4_ranges = ipv4.len(),
1063 ipv6_ranges = ipv6.len(),
1064 asn_ranges = asn.len(),
1065 "RDAP bootstrap loaded"
1066 );
1067
1068 Ok(BootstrapData {
1069 dns,
1070 ipv4,
1071 ipv6,
1072 asn,
1073 })
1074}
1075
1076fn wrap_all_candidates_failed(last_error: Option<SeerError>, candidate_count: usize) -> SeerError {
1085 let last = last_error.unwrap_or_else(|| SeerError::RdapError("no candidates".to_string()));
1086
1087 if candidate_count <= 1 {
1088 return last;
1089 }
1090
1091 match last {
1092 SeerError::Timeout(msg) => SeerError::Timeout(format!(
1093 "all {} RDAP candidate URLs timed out; last error: {}",
1094 candidate_count, msg
1095 )),
1096 other => SeerError::RdapError(format!(
1097 "all {} RDAP candidate URLs failed; last error: {}",
1098 candidate_count, other
1099 )),
1100 }
1101}
1102
1103fn build_rdap_urls(bases: &[url::Url], path: &str) -> Vec<url::Url> {
1105 bases
1106 .iter()
1107 .filter_map(|base| {
1108 let base_str = base.as_str();
1111 let normalized = if base_str.ends_with('/') {
1112 base_str.to_string()
1113 } else {
1114 format!("{}/", base_str)
1115 };
1116 url::Url::parse(&normalized).and_then(|u| u.join(path)).ok()
1117 })
1118 .collect()
1119}
1120
1121#[cfg(test)]
1122mod tests {
1123 use super::*;
1124
1125 #[test]
1126 fn test_default_client_has_retry_policy() {
1127 let client = RdapClient::new();
1128 assert_eq!(client.retry_policy.max_attempts, 3);
1132 }
1133
1134 #[test]
1137 fn parse_retry_after_parses_delta_seconds() {
1138 assert_eq!(parse_retry_after("5"), Some(Duration::from_secs(5)));
1139 assert_eq!(parse_retry_after(" 10 "), Some(Duration::from_secs(10)));
1140 assert_eq!(parse_retry_after("0"), Some(Duration::from_secs(0)));
1141 }
1142
1143 #[test]
1144 fn parse_retry_after_rejects_http_date_and_junk() {
1145 assert_eq!(parse_retry_after("Wed, 21 Oct 2015 07:28:00 GMT"), None);
1148 assert_eq!(parse_retry_after("soon"), None);
1149 assert_eq!(parse_retry_after(""), None);
1150 }
1151
1152 #[test]
1153 fn effective_retry_delay_prefers_capped_retry_after() {
1154 assert_eq!(
1156 effective_retry_delay(Duration::from_millis(100), Some(Duration::from_secs(5))),
1157 Duration::from_secs(5)
1158 );
1159 assert_eq!(
1161 effective_retry_delay(Duration::from_millis(100), Some(Duration::from_secs(600))),
1162 MAX_RETRY_AFTER
1163 );
1164 }
1165
1166 #[test]
1167 fn effective_retry_delay_falls_back_to_backoff() {
1168 assert_eq!(
1169 effective_retry_delay(Duration::from_millis(250), None),
1170 Duration::from_millis(250)
1171 );
1172 }
1173
1174 #[test]
1175 fn test_client_without_retries() {
1176 let client = RdapClient::new().without_retries();
1177 assert_eq!(client.retry_policy.max_attempts, 1);
1178 }
1179
1180 #[test]
1181 fn test_client_custom_retry_policy() {
1182 let policy = RetryPolicy::new().with_max_attempts(5);
1183 let client = RdapClient::new().with_retry_policy(policy);
1184 assert_eq!(client.retry_policy.max_attempts, 5);
1185 }
1186
1187 #[test]
1188 fn test_cached_bootstrap_expiration() {
1189 let data = BootstrapData {
1190 dns: HashMap::new(),
1191 ipv4: Vec::new(),
1192 ipv6: Vec::new(),
1193 asn: Vec::new(),
1194 };
1195 let cached = CachedBootstrap::new(data);
1196 assert!(!cached.is_expired());
1198 }
1199
1200 #[test]
1201 fn test_rdap_http_client_is_configured() {
1202 let client = rdap_http_client();
1205 assert!(client.is_ok(), "RDAP HTTP client builder must succeed");
1206 }
1207
1208 #[test]
1209 fn test_parse_bootstrap_empty_services() {
1210 let data = BootstrapData {
1212 dns: HashMap::new(),
1213 ipv4: Vec::new(),
1214 ipv6: Vec::new(),
1215 asn: Vec::new(),
1216 };
1217 assert!(RdapClient::get_rdap_urls_for_domain(&data, "example.com").is_none());
1219 assert!(RdapClient::get_rdap_urls_for_asn(&data, 12345).is_none());
1220 }
1221
1222 #[tokio::test]
1225 async fn test_validate_url_not_reserved_rejects_loopback_literal() {
1226 let err = validate_url_not_reserved("https://127.0.0.1/domain/example.com")
1227 .await
1228 .unwrap_err();
1229 assert!(
1230 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1231 "expected reserved-IP error, got: {:?}",
1232 err
1233 );
1234 }
1235
1236 #[tokio::test]
1237 async fn test_validate_url_not_reserved_rejects_private_ipv4_literal() {
1238 let err = validate_url_not_reserved("https://10.0.0.1/")
1239 .await
1240 .unwrap_err();
1241 assert!(
1242 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1243 "expected reserved-IP error, got: {:?}",
1244 err
1245 );
1246 }
1247
1248 #[tokio::test]
1249 async fn test_validate_url_not_reserved_rejects_non_https_scheme() {
1250 let err = validate_url_not_reserved("http://93.184.216.34/domain/example.com")
1254 .await
1255 .unwrap_err();
1256 assert!(
1257 matches!(err, SeerError::RdapError(ref s) if s.contains("not https")),
1258 "expected non-https rejection, got: {:?}",
1259 err
1260 );
1261 }
1262
1263 #[tokio::test]
1264 async fn test_validate_url_not_reserved_rejects_ipv6_loopback_literal() {
1265 let err = validate_url_not_reserved("https://[::1]/")
1266 .await
1267 .unwrap_err();
1268 assert!(
1269 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1270 "expected reserved-IP error, got: {:?}",
1271 err
1272 );
1273 }
1274
1275 #[tokio::test]
1276 async fn test_validate_url_not_reserved_returns_resolved_addrs_for_public_literal() {
1277 let addrs = validate_url_not_reserved("https://8.8.8.8/").await.unwrap();
1280 assert_eq!(addrs.len(), 1);
1281 assert!(addrs[0].ip().is_ipv4());
1282 assert_eq!(addrs[0].port(), 443);
1283 }
1284
1285 #[test]
1288 fn test_build_rdap_urls_preserves_order_and_appends_path() {
1289 let bases = vec![
1290 url::Url::parse("https://rdap.a.example/").unwrap(),
1291 url::Url::parse("https://rdap.b.example").unwrap(), ];
1293 let built = build_rdap_urls(&bases, "domain/example.com");
1294 assert_eq!(built.len(), 2);
1295 assert_eq!(
1296 built[0].as_str(),
1297 "https://rdap.a.example/domain/example.com"
1298 );
1299 assert_eq!(
1300 built[1].as_str(),
1301 "https://rdap.b.example/domain/example.com"
1302 );
1303 }
1304
1305 #[test]
1306 fn test_build_rdap_urls_empty_input_returns_empty() {
1307 let built = build_rdap_urls(&[], "domain/example.com");
1308 assert!(built.is_empty());
1309 }
1310
1311 #[test]
1314 fn test_wrap_all_candidates_failed_preserves_timeout_variant() {
1315 let last = SeerError::Timeout("body read timed out".to_string());
1318 let wrapped = wrap_all_candidates_failed(Some(last), 3);
1319 match wrapped {
1320 SeerError::Timeout(msg) => {
1321 assert!(
1322 msg.contains("all 3 RDAP candidate URLs timed out"),
1323 "expected wrapped timeout message, got: {}",
1324 msg
1325 );
1326 assert!(
1327 msg.contains("body read timed out"),
1328 "expected original message preserved, got: {}",
1329 msg
1330 );
1331 }
1332 other => panic!(
1333 "expected SeerError::Timeout after wrapping a Timeout, got: {:?}",
1334 other
1335 ),
1336 }
1337 }
1338
1339 #[test]
1340 fn test_wrap_all_candidates_failed_wraps_non_timeout_as_rdap_error() {
1341 let last = SeerError::RdapError("500 internal error".to_string());
1342 let wrapped = wrap_all_candidates_failed(Some(last), 2);
1343 assert!(
1344 matches!(wrapped, SeerError::RdapError(ref s) if s.contains("all 2 RDAP candidate URLs failed")),
1345 "expected wrapped RdapError, got: {:?}",
1346 wrapped
1347 );
1348 }
1349
1350 #[test]
1351 fn test_wrap_all_candidates_failed_single_candidate_returns_unchanged() {
1352 let last = SeerError::Timeout("single timeout".to_string());
1355 let wrapped = wrap_all_candidates_failed(Some(last), 1);
1356 assert!(
1357 matches!(wrapped, SeerError::Timeout(ref s) if s == "single timeout"),
1358 "expected unchanged Timeout, got: {:?}",
1359 wrapped
1360 );
1361 }
1362
1363 #[test]
1364 fn test_wrap_all_candidates_failed_no_last_error_returns_placeholder() {
1365 let wrapped = wrap_all_candidates_failed(None, 0);
1366 assert!(matches!(wrapped, SeerError::RdapError(_)));
1367 }
1368
1369 static BOOTSTRAP_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
1388
1389 #[tokio::test]
1390 async fn test_bootstrap_load_notify_wakes_waiter_when_cache_populated() {
1391 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1392
1393 {
1395 let mut cache = BOOTSTRAP_CACHE.write().await;
1396 *cache = None;
1397 }
1398
1399 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1402 tokio::pin!(notified);
1403
1404 {
1406 let mut cache = BOOTSTRAP_CACHE.write().await;
1407 *cache = Some(CachedBootstrap::new(BootstrapData {
1408 dns: HashMap::new(),
1409 ipv4: Vec::new(),
1410 ipv6: Vec::new(),
1411 asn: Vec::new(),
1412 }));
1413 }
1414 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1415
1416 let result = wait_for_in_flight_load(notified).await;
1417 assert!(
1418 result.is_ok(),
1419 "expected waiter to see populated cache, got: {:?}",
1420 result
1421 );
1422
1423 {
1425 let mut cache = BOOTSTRAP_CACHE.write().await;
1426 *cache = None;
1427 }
1428 }
1429
1430 #[tokio::test]
1431 async fn test_bootstrap_load_notify_empty_cache_after_wake_returns_error() {
1432 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1433
1434 {
1436 let mut cache = BOOTSTRAP_CACHE.write().await;
1437 *cache = None;
1438 }
1439
1440 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1441 tokio::pin!(notified);
1442
1443 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1445
1446 let result = wait_for_in_flight_load(notified).await;
1447 assert!(
1448 matches!(
1449 result,
1450 Err(SeerError::RdapBootstrapError(ref s))
1451 if s.contains("throttled and no cache available")
1452 ),
1453 "expected throttled error when cache still empty after notify, got: {:?}",
1454 result
1455 );
1456 }
1457
1458 use wiremock::matchers::method;
1468 use wiremock::{Mock, MockServer, ResponseTemplate};
1469
1470 #[tokio::test]
1471 async fn mock_rdap_404_is_nonretryable_typed_error() {
1472 let server = MockServer::start().await;
1473 Mock::given(method("GET"))
1474 .respond_with(ResponseTemplate::new(404))
1475 .mount(&server)
1476 .await;
1477
1478 let client = RdapClient::new()
1479 .without_retries()
1480 .allowing_reserved_for_tests();
1481 let err = client
1482 .query_rdap_with_retry(&format!("{}/domain/example.com", server.uri()))
1483 .await
1484 .unwrap_err();
1485 assert!(
1486 matches!(err, SeerError::RdapError(ref m) if m.contains("404")),
1487 "got: {err:?}"
1488 );
1489 }
1490
1491 #[tokio::test]
1492 async fn mock_rdap_429_honors_retry_after_and_succeeds() {
1493 let server = MockServer::start().await;
1494 Mock::given(method("GET"))
1497 .respond_with(ResponseTemplate::new(429).insert_header("Retry-After", "0"))
1498 .up_to_n_times(1)
1499 .mount(&server)
1500 .await;
1501 Mock::given(method("GET"))
1502 .respond_with(ResponseTemplate::new(200).set_body_raw(
1503 r#"{"objectClassName":"domain","handle":"MOCK-1"}"#,
1504 "application/rdap+json",
1505 ))
1506 .mount(&server)
1507 .await;
1508
1509 let client = RdapClient::new().allowing_reserved_for_tests();
1510 let resp = client
1511 .query_rdap_with_retry(&format!("{}/domain/example.com", server.uri()))
1512 .await
1513 .unwrap();
1514 assert_eq!(resp.handle.as_deref(), Some("MOCK-1"));
1515 }
1516
1517 #[tokio::test]
1518 async fn mock_rdap_malformed_body_is_parse_error_not_panic() {
1519 let server = MockServer::start().await;
1520 Mock::given(method("GET"))
1521 .respond_with(ResponseTemplate::new(200).set_body_raw("not json", "text/plain"))
1522 .mount(&server)
1523 .await;
1524
1525 let client = RdapClient::new()
1526 .without_retries()
1527 .allowing_reserved_for_tests();
1528 let err = client
1529 .query_rdap_with_retry(&format!("{}/domain/example.com", server.uri()))
1530 .await
1531 .unwrap_err();
1532 assert!(matches!(err, SeerError::JsonError(_)), "got: {err:?}");
1533 }
1534
1535 #[tokio::test]
1536 async fn mock_rdap_candidate_fallback_uses_second_url() {
1537 let bad = MockServer::start().await;
1538 Mock::given(method("GET"))
1539 .respond_with(ResponseTemplate::new(500))
1540 .mount(&bad)
1541 .await;
1542 let good = MockServer::start().await;
1543 Mock::given(method("GET"))
1544 .respond_with(ResponseTemplate::new(200).set_body_raw(
1545 r#"{"objectClassName":"domain","handle":"MOCK-2"}"#,
1546 "application/rdap+json",
1547 ))
1548 .mount(&good)
1549 .await;
1550
1551 let client = RdapClient::new()
1552 .without_retries()
1553 .allowing_reserved_for_tests();
1554 let urls = vec![
1555 url::Url::parse(&format!("{}/domain/example.com", bad.uri())).unwrap(),
1556 url::Url::parse(&format!("{}/domain/example.com", good.uri())).unwrap(),
1557 ];
1558 let resp = client.query_rdap_urls(&urls).await.unwrap();
1559 assert_eq!(resp.handle.as_deref(), Some("MOCK-2"));
1560 }
1561
1562 #[tokio::test]
1568 async fn mock_rdap_404_on_first_candidate_survives_later_non_404_failure() {
1569 let not_found = MockServer::start().await;
1570 Mock::given(method("GET"))
1571 .respond_with(ResponseTemplate::new(404))
1572 .mount(¬_found)
1573 .await;
1574 let broken = MockServer::start().await;
1575 Mock::given(method("GET"))
1576 .respond_with(ResponseTemplate::new(500))
1577 .mount(&broken)
1578 .await;
1579
1580 let client = RdapClient::new()
1581 .without_retries()
1582 .allowing_reserved_for_tests();
1583 let urls = vec![
1584 url::Url::parse(&format!("{}/domain/example.com", not_found.uri())).unwrap(),
1585 url::Url::parse(&format!("{}/domain/example.com", broken.uri())).unwrap(),
1586 ];
1587 let err = client.query_rdap_urls(&urls).await.unwrap_err();
1588 assert!(
1589 crate::rdap::rdap_error_is_404(&err),
1590 "404 from candidate 1 must survive candidate 2's non-404 failure, got: {err:?}"
1591 );
1592 }
1593}