1use std::collections::{HashMap, HashSet};
2use std::net::{IpAddr, SocketAddr};
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6
7use futures::StreamExt;
8use once_cell::sync::Lazy;
9use reqwest::Client;
10use serde::Deserialize;
11use tokio::sync::{Notify, RwLock};
12use tracing::{debug, info, instrument, warn};
13
14use super::bootstrap::{
15 ipv4_matches_prefix, ipv6_matches_prefix, parse_asn_range, validate_bootstrap_url,
16};
17use super::types::RdapResponse;
18use crate::error::{Result, SeerError};
19use crate::retry::{NetworkRetryClassifier, RetryClassifier, RetryExecutor, RetryPolicy};
20use crate::validation::normalize_domain;
21
22const IANA_BOOTSTRAP_DNS: &str = "https://data.iana.org/rdap/dns.json";
23const IANA_BOOTSTRAP_IPV4: &str = "https://data.iana.org/rdap/ipv4.json";
24const IANA_BOOTSTRAP_IPV6: &str = "https://data.iana.org/rdap/ipv6.json";
25const IANA_BOOTSTRAP_ASN: &str = "https://data.iana.org/rdap/asn.json";
26
27const DEFAULT_TIMEOUT: Duration = Duration::from_secs(15);
32
33const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
36
37const BOOTSTRAP_TTL: Duration = Duration::from_secs(24 * 60 * 60);
39
40const BOOTSTRAP_REFRESH_MIN_INTERVAL: Duration = Duration::from_secs(60);
44
45const BOOTSTRAP_PARTIAL_TTL: Duration = Duration::from_secs(5 * 60);
52
53const RDAP_ACCEPT: &str = "application/rdap+json, application/json;q=0.9";
57
58const MAX_RDAP_REDIRECTS: usize = 3;
63
64static RDAP_HTTP_CLIENT: Lazy<Option<Client>> = Lazy::new(|| {
74 Client::builder()
75 .timeout(DEFAULT_TIMEOUT)
76 .connect_timeout(CONNECT_TIMEOUT)
77 .user_agent("Seer/1.0 (RDAP Client)")
78 .pool_max_idle_per_host(10)
79 .redirect(reqwest::redirect::Policy::none())
83 .build()
84 .ok()
85});
86
87fn rdap_http_client() -> Result<&'static Client> {
91 RDAP_HTTP_CLIENT
92 .as_ref()
93 .ok_or_else(|| SeerError::HttpError("failed to initialize HTTP client".into()))
94}
95
96static BOOTSTRAP_CACHE: Lazy<RwLock<Option<CachedBootstrap>>> = Lazy::new(|| RwLock::new(None));
98
99static BOOTSTRAP_LAST_ATTEMPT: Lazy<RwLock<Option<Instant>>> = Lazy::new(|| RwLock::new(None));
103
104static BOOTSTRAP_LOAD_NOTIFY: Lazy<Notify> = Lazy::new(Notify::new);
112
113static BOOTSTRAP_LOAD_IN_FLIGHT: AtomicBool = AtomicBool::new(false);
119
120struct BootstrapLoadGuard;
124
125impl BootstrapLoadGuard {
126 fn start() -> Self {
127 BOOTSTRAP_LOAD_IN_FLIGHT.store(true, Ordering::SeqCst);
128 Self
129 }
130}
131
132impl Drop for BootstrapLoadGuard {
133 fn drop(&mut self) {
134 BOOTSTRAP_LOAD_IN_FLIGHT.store(false, Ordering::SeqCst);
135 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
136 }
137}
138
139struct CachedBootstrap {
141 data: BootstrapData,
142 loaded_at: Instant,
143 ttl: Duration,
146}
147
148impl CachedBootstrap {
149 #[cfg(test)]
150 fn new(data: BootstrapData) -> Self {
151 Self::with_ttl(data, BOOTSTRAP_TTL)
152 }
153
154 fn with_ttl(data: BootstrapData, ttl: Duration) -> Self {
155 Self {
156 data,
157 loaded_at: Instant::now(),
158 ttl,
159 }
160 }
161
162 fn is_expired(&self) -> bool {
163 self.loaded_at.elapsed() > self.ttl
164 }
165
166 fn age(&self) -> Duration {
167 self.loaded_at.elapsed()
168 }
169}
170
171struct BootstrapData {
176 dns: HashMap<String, Arc<Vec<url::Url>>>,
177 ipv4: Vec<(IpRange, Arc<Vec<url::Url>>)>,
178 ipv6: Vec<(IpRange, Arc<Vec<url::Url>>)>,
179 asn: Vec<(AsnRange, Arc<Vec<url::Url>>)>,
180}
181
182#[derive(Clone)]
183struct IpRange {
184 prefix: String,
185}
186
187#[derive(Clone)]
188struct AsnRange {
189 start: u32,
190 end: u32,
191}
192
193#[derive(Deserialize)]
194struct BootstrapResponse {
195 services: Vec<Vec<serde_json::Value>>,
196}
197
198struct BootstrapLoad {
202 dns: Option<HashMap<String, Arc<Vec<url::Url>>>>,
203 ipv4: Option<Vec<(IpRange, Arc<Vec<url::Url>>)>>,
204 ipv6: Option<Vec<(IpRange, Arc<Vec<url::Url>>)>>,
205 asn: Option<Vec<(AsnRange, Arc<Vec<url::Url>>)>>,
206}
207
208impl BootstrapLoad {
209 fn is_complete(&self) -> bool {
211 self.dns.is_some() && self.ipv4.is_some() && self.ipv6.is_some() && self.asn.is_some()
212 }
213
214 fn ttl(&self) -> Duration {
217 if self.is_complete() {
218 BOOTSTRAP_TTL
219 } else {
220 BOOTSTRAP_PARTIAL_TTL
221 }
222 }
223
224 fn merge_over(self, previous: Option<BootstrapData>) -> BootstrapData {
228 let (dns, ipv4, ipv6, asn) = match previous {
229 Some(p) => (Some(p.dns), Some(p.ipv4), Some(p.ipv6), Some(p.asn)),
230 None => (None, None, None, None),
231 };
232 BootstrapData {
233 dns: self.dns.or(dns).unwrap_or_default(),
234 ipv4: self.ipv4.or(ipv4).unwrap_or_default(),
235 ipv6: self.ipv6.or(ipv6).unwrap_or_default(),
236 asn: self.asn.or(asn).unwrap_or_default(),
237 }
238 }
239}
240
241fn store_bootstrap_load(cache: &mut Option<CachedBootstrap>, load: BootstrapLoad) {
245 if cache.as_ref().is_some_and(|c| !c.is_expired()) {
246 return;
247 }
248 let ttl = load.ttl();
249 if !load.is_complete() {
250 warn!(
251 dns = load.dns.is_some(),
252 ipv4 = load.ipv4.is_some(),
253 ipv6 = load.ipv6.is_some(),
254 asn = load.asn.is_some(),
255 retry_in_secs = ttl.as_secs(),
256 "RDAP bootstrap partially loaded; keeping previous data for failed registries"
257 );
258 }
259 let previous = cache.take().map(|c| c.data);
260 *cache = Some(CachedBootstrap::with_ttl(load.merge_over(previous), ttl));
261}
262
263async fn wait_for_in_flight_load(
273 notified: std::pin::Pin<&mut tokio::sync::futures::Notified<'_>>,
274) -> Result<()> {
275 if BOOTSTRAP_LOAD_IN_FLIGHT.load(Ordering::SeqCst) {
282 let _ = tokio::time::timeout(DEFAULT_TIMEOUT, notified).await;
283 }
284 let cache = BOOTSTRAP_CACHE.read().await;
285 if cache.is_some() {
286 Ok(())
287 } else {
288 Err(SeerError::RdapBootstrapError(
289 "bootstrap refresh throttled and no cache available".to_string(),
290 ))
291 }
292}
293
294#[derive(Debug, Clone)]
295pub struct RdapClient {
296 retry_policy: RetryPolicy,
297 timeout: Duration,
299 allow_reserved: bool,
305}
306
307impl Default for RdapClient {
308 fn default() -> Self {
309 Self::new()
310 }
311}
312
313impl RdapClient {
314 pub fn new() -> Self {
316 Self {
317 retry_policy: RetryPolicy::new()
324 .with_max_attempts(3)
325 .with_initial_delay(Duration::from_millis(500))
326 .with_max_delay(Duration::from_secs(5)),
327 timeout: DEFAULT_TIMEOUT,
328 allow_reserved: false,
329 }
330 }
331
332 pub fn from_config(config: &crate::config::SeerConfig) -> Self {
339 Self::new().with_timeout(config.rdap_timeout())
340 }
341
342 pub fn with_timeout(mut self, timeout: Duration) -> Self {
344 self.timeout = timeout;
345 self
346 }
347
348 #[cfg(test)]
350 pub(crate) fn allowing_reserved_for_tests(mut self) -> Self {
351 self.allow_reserved = true;
352 self
353 }
354
355 pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
359 self.retry_policy = policy;
360 self
361 }
362
363 pub fn without_retries(mut self) -> Self {
365 self.retry_policy = RetryPolicy::no_retry();
366 self
367 }
368
369 async fn ensure_bootstrap(&self) -> Result<()> {
384 {
386 let cache = BOOTSTRAP_CACHE.read().await;
387 if let Some(cached) = cache.as_ref() {
388 if !cached.is_expired() {
389 return Ok(());
390 }
391 }
392 }
393
394 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
400 tokio::pin!(notified);
401
402 {
406 let last = BOOTSTRAP_LAST_ATTEMPT.read().await;
407 if let Some(ts) = *last {
408 if ts.elapsed() < BOOTSTRAP_REFRESH_MIN_INTERVAL {
409 let cache = BOOTSTRAP_CACHE.read().await;
411 if cache.is_some() {
412 return Ok(());
414 }
415 drop(cache);
420 drop(last);
421 return wait_for_in_flight_load(notified).await;
422 }
423 }
424 }
425
426 let load_guard = {
431 let mut last = BOOTSTRAP_LAST_ATTEMPT.write().await;
432 if let Some(ts) = *last {
434 if ts.elapsed() < BOOTSTRAP_REFRESH_MIN_INTERVAL {
435 drop(last);
436 let cache = BOOTSTRAP_CACHE.read().await;
437 if cache.is_some() {
438 return Ok(());
439 }
440 drop(cache);
441 return wait_for_in_flight_load(notified).await;
442 }
443 }
444 *last = Some(Instant::now());
445 BootstrapLoadGuard::start()
446 };
447
448 debug!("Loading/refreshing RDAP bootstrap data");
453 let load_result = load_bootstrap_data_with_retry(&self.retry_policy).await;
454
455 let outcome = match load_result {
456 Ok(load) => {
457 let mut cache = BOOTSTRAP_CACHE.write().await;
461 store_bootstrap_load(&mut cache, load);
462 Ok(())
463 }
464 Err(e) => {
465 let cache = BOOTSTRAP_CACHE.read().await;
467 if let Some(cached) = cache.as_ref() {
468 debug!(
469 error = %e,
470 age_hours = cached.age().as_secs() / 3600,
471 "Bootstrap refresh failed, using stale data"
472 );
473 Ok(())
474 } else {
475 Err(e)
477 }
478 }
479 };
480
481 drop(load_guard);
484 outcome
485 }
486
487 fn get_rdap_urls_for_domain(cache: &BootstrapData, domain: &str) -> Option<Arc<Vec<url::Url>>> {
489 let tld = domain.rsplit('.').next()?;
490 cache.dns.get(&tld.to_lowercase()).cloned()
491 }
492
493 fn get_rdap_urls_for_ip(cache: &BootstrapData, ip: &IpAddr) -> Option<Arc<Vec<url::Url>>> {
495 match ip {
496 IpAddr::V4(addr) => {
497 for (range, urls) in &cache.ipv4 {
498 if ipv4_matches_prefix(&range.prefix, addr) {
499 return Some(Arc::clone(urls));
500 }
501 }
502 }
503 IpAddr::V6(addr) => {
504 for (range, urls) in &cache.ipv6 {
505 if ipv6_matches_prefix(&range.prefix, addr) {
506 return Some(Arc::clone(urls));
507 }
508 }
509 }
510 }
511
512 None
513 }
514
515 fn get_rdap_urls_for_asn(cache: &BootstrapData, asn: u32) -> Option<Arc<Vec<url::Url>>> {
517 for (range, urls) in &cache.asn {
518 if asn >= range.start && asn <= range.end {
519 return Some(Arc::clone(urls));
520 }
521 }
522
523 None
524 }
525
526 #[instrument(skip(self), fields(domain = %domain))]
530 pub async fn lookup_domain(&self, domain: &str) -> Result<RdapResponse> {
531 self.ensure_bootstrap().await?;
532
533 let domain = normalize_domain(domain)?;
534
535 let urls = {
537 let cache_guard = BOOTSTRAP_CACHE.read().await;
538 let cache = cache_guard.as_ref().ok_or_else(|| {
539 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
540 })?;
541
542 let bases = Self::get_rdap_urls_for_domain(&cache.data, &domain).ok_or_else(|| {
543 SeerError::RdapBootstrapError(format!("no RDAP server for {}", domain))
544 })?;
545
546 build_rdap_urls(&bases, &format!("domain/{}", domain))
547 }; self.query_rdap_urls(&urls).await
550 }
551
552 #[instrument(skip(self), fields(ip = %ip))]
556 pub async fn lookup_ip(&self, ip: &str) -> Result<RdapResponse> {
557 self.ensure_bootstrap().await?;
558
559 let ip_addr: IpAddr = ip
560 .parse()
561 .map_err(|_| SeerError::InvalidIpAddress(ip.to_string()))?;
562
563 let urls = {
564 let cache_guard = BOOTSTRAP_CACHE.read().await;
565 let cache = cache_guard.as_ref().ok_or_else(|| {
566 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
567 })?;
568
569 let bases = Self::get_rdap_urls_for_ip(&cache.data, &ip_addr).ok_or_else(|| {
570 SeerError::RdapBootstrapError(format!("no RDAP server for {}", ip))
571 })?;
572
573 build_rdap_urls(&bases, &format!("ip/{}", ip))
574 };
575
576 self.query_rdap_urls(&urls).await
577 }
578
579 #[instrument(skip(self), fields(asn = %asn))]
583 pub async fn lookup_asn(&self, asn: u32) -> Result<RdapResponse> {
584 self.ensure_bootstrap().await?;
585
586 let urls = {
587 let cache_guard = BOOTSTRAP_CACHE.read().await;
588 let cache = cache_guard.as_ref().ok_or_else(|| {
589 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
590 })?;
591
592 let bases = Self::get_rdap_urls_for_asn(&cache.data, asn).ok_or_else(|| {
593 SeerError::RdapBootstrapError(format!("no RDAP server for AS{}", asn))
594 })?;
595
596 build_rdap_urls(&bases, &format!("autnum/{}", asn))
597 };
598
599 self.query_rdap_urls(&urls).await
600 }
601
602 #[instrument(skip(self), fields(tld = %tld))]
608 pub async fn get_rdap_base_url_for_tld(&self, tld: &str) -> Option<String> {
609 if self.ensure_bootstrap().await.is_err() {
610 return None;
611 }
612
613 let cache_guard = BOOTSTRAP_CACHE.read().await;
614 let cache = cache_guard.as_ref()?;
615 let lower = tld.to_lowercase();
619 let key = crate::validation::domain_to_ascii(&lower).unwrap_or(lower);
620 cache
621 .data
622 .dns
623 .get(&key)
624 .and_then(|urls| urls.first())
625 .map(|u| u.to_string())
626 }
627
628 async fn query_rdap_urls(&self, urls: &[url::Url]) -> Result<RdapResponse> {
632 if urls.is_empty() {
633 return Err(SeerError::RdapError(
634 "no candidate RDAP URLs available".to_string(),
635 ));
636 }
637
638 let mut last_error: Option<SeerError> = None;
639 let mut not_found_error: Option<SeerError> = None;
645 for (idx, url) in urls.iter().enumerate() {
646 let url_str = url.as_str().to_string();
647 debug!(url = %url_str, candidate = idx + 1, total = urls.len(), "Querying RDAP");
648 match self.query_rdap_with_retry(&url_str).await {
649 Ok(resp) => return Ok(resp),
650 Err(e) => {
651 if urls.len() > 1 {
652 debug!(
653 url = %url_str,
654 error = %e,
655 candidate = idx + 1,
656 total = urls.len(),
657 "RDAP candidate failed, trying next",
658 );
659 }
660 if not_found_error.is_none() && crate::rdap::rdap_error_is_404(&e) {
661 not_found_error = Some(e);
662 } else {
663 last_error = Some(e);
664 }
665 }
666 }
667 }
668
669 Err(wrap_all_candidates_failed(
672 not_found_error.or(last_error),
673 urls.len(),
674 ))
675 }
676
677 async fn query_rdap_with_retry(&self, url: &str) -> Result<RdapResponse> {
682 let classifier = NetworkRetryClassifier::new();
683 let mut attempt = 0;
684 loop {
685 match query_rdap_attempt(url, self.timeout, self.allow_reserved).await {
686 Ok(resp) => return Ok(resp),
687 Err((err, retry_after)) => {
688 let attempts_remaining =
689 self.retry_policy.max_attempts.saturating_sub(attempt + 1);
690 if !classifier.is_retryable(&err) || attempts_remaining == 0 {
691 return Err(if attempt > 0 {
692 SeerError::RetryExhausted {
693 attempts: attempt + 1,
694 last_error: Box::new(err),
695 }
696 } else {
697 err
698 });
699 }
700 let backoff = self.retry_policy.delay_for_attempt(attempt);
701 let delay = effective_retry_delay(backoff, retry_after);
702 debug!(
703 url = %url,
704 attempt = attempt + 1,
705 max_attempts = self.retry_policy.max_attempts,
706 delay_ms = delay.as_millis(),
707 error = %err,
708 "Retrying RDAP after transient error"
709 );
710 tokio::time::sleep(delay).await;
711 attempt += 1;
712 }
713 }
714 }
715 }
716}
717
718const MAX_RDAP_RESPONSE_SIZE: usize = 10 * 1024 * 1024;
720
721const MAX_RETRY_AFTER: Duration = Duration::from_secs(5);
726
727fn parse_rdap_url(url: &str) -> Result<(String, u16)> {
737 let parsed = url::Url::parse(url)
738 .map_err(|e| SeerError::RdapError(format!("invalid URL '{}': {}", url, e)))?;
739 if parsed.scheme() != "https" {
740 return Err(SeerError::RdapError(format!(
741 "RDAP URL '{}' is not https — request blocked (downgrade/SSRF protection)",
742 url
743 )));
744 }
745 let host = match parsed.host() {
748 Some(url::Host::Domain(d)) => d.to_string(),
749 Some(url::Host::Ipv4(ip)) => ip.to_string(),
750 Some(url::Host::Ipv6(ip)) => ip.to_string(),
751 None => return Err(SeerError::RdapError(format!("URL '{}' has no host", url))),
752 };
753 let port = parsed.port_or_known_default().unwrap_or(443);
754 Ok((host, port))
755}
756
757async fn resolve_rdap_host(host: &str, port: u16) -> Result<Vec<SocketAddr>> {
769 crate::net::resolve_public_host(host, port)
770 .await
771 .map_err(|e| SeerError::RdapError(format!("{} — request blocked (SSRF protection)", e)))
772}
773
774#[cfg(test)]
779async fn validate_url_not_reserved(url: &str) -> Result<Vec<SocketAddr>> {
780 let (host, port) = parse_rdap_url(url)?;
781 resolve_rdap_host(&host, port).await
782}
783
784fn parse_retry_after(value: &str) -> Option<Duration> {
789 value.trim().parse::<u64>().ok().map(Duration::from_secs)
790}
791
792fn effective_retry_delay(backoff: Duration, retry_after: Option<Duration>) -> Duration {
796 match retry_after {
797 Some(hint) => hint.min(MAX_RETRY_AFTER),
798 None => backoff,
799 }
800}
801
802const PINNED_CLIENT_TTL: Duration = Duration::from_secs(60);
809
810type PinnedClientKey = (String, u16, Duration);
815
816static PINNED_CLIENT_CACHE: Lazy<crate::cache::TtlCache<PinnedClientKey, Client>> =
824 Lazy::new(|| crate::cache::TtlCache::with_max_capacity(PINNED_CLIENT_TTL, 64));
825
826async fn send_rdap_request(
842 url: &str,
843 timeout: Duration,
844 allow_reserved: bool,
845) -> Result<reqwest::Response> {
846 let connect_timeout = CONNECT_TIMEOUT.min(timeout);
849 if allow_reserved && is_loopback_url(url) {
850 let client = Client::builder()
854 .timeout(timeout)
855 .connect_timeout(connect_timeout)
856 .user_agent("Seer/1.0 (RDAP Client)")
857 .redirect(reqwest::redirect::Policy::none())
858 .build()
859 .map_err(|e| SeerError::RdapError(format!("failed to build HTTP client: {}", e)))?;
860 return client
861 .get(url)
862 .header(reqwest::header::ACCEPT, RDAP_ACCEPT)
863 .send()
864 .await
865 .map_err(Into::into);
866 }
867
868 let (host, port) = parse_rdap_url(url)?;
870
871 let key = (host.clone(), port, timeout);
872 let client = match PINNED_CLIENT_CACHE.get(&key) {
873 Some(client) => client,
874 None => {
875 let resolved = resolve_rdap_host(&host, port).await?;
880 let client = Client::builder()
881 .timeout(timeout)
882 .connect_timeout(connect_timeout)
883 .user_agent("Seer/1.0 (RDAP Client)")
884 .resolve_to_addrs(&host, &resolved)
885 .redirect(reqwest::redirect::Policy::none())
893 .build()
894 .map_err(|e| SeerError::RdapError(format!("failed to build HTTP client: {}", e)))?;
895 PINNED_CLIENT_CACHE.insert(key.clone(), client.clone());
896 client
897 }
898 };
899
900 match client
901 .get(url)
902 .header(reqwest::header::ACCEPT, RDAP_ACCEPT)
903 .send()
904 .await
905 {
906 Ok(resp) => Ok(resp),
907 Err(e) => {
908 if e.is_connect() {
913 PINNED_CLIENT_CACHE.remove(&key);
914 }
915 Err(e.into())
916 }
917 }
918}
919
920async fn read_and_parse_rdap_body(
925 response: reqwest::Response,
926 url: &str,
927 timeout: Duration,
928) -> Result<RdapResponse> {
929 let mut body = Vec::new();
934 let mut stream = response.bytes_stream();
935 let streamed = tokio::time::timeout(timeout, async {
936 while let Some(chunk) = stream.next().await {
937 let chunk = chunk
938 .map_err(|e| SeerError::RdapError(format!("failed to read response: {}", e)))?;
939 body.extend_from_slice(&chunk);
940 if body.len() > MAX_RDAP_RESPONSE_SIZE {
941 return Err(SeerError::RdapError(format!(
942 "RDAP response exceeds {} byte limit",
943 MAX_RDAP_RESPONSE_SIZE
944 )));
945 }
946 }
947 Ok::<(), SeerError>(())
948 })
949 .await;
950
951 match streamed {
952 Ok(Ok(())) => {}
953 Ok(Err(e)) => return Err(e),
954 Err(_) => {
955 return Err(SeerError::Timeout(format!(
956 "timed out reading RDAP response body from {} after {:?}",
957 url, timeout
958 )));
959 }
960 }
961
962 let rdap: RdapResponse = serde_json::from_slice(&body)?;
963 rdap.validate()?;
969 Ok(rdap)
970}
971
972fn is_loopback_url(url: &str) -> bool {
975 match url::Url::parse(url).ok().as_ref().and_then(url::Url::host) {
976 Some(url::Host::Ipv4(ip)) => ip.is_loopback(),
977 Some(url::Host::Ipv6(ip)) => ip.is_loopback(),
978 _ => false,
979 }
980}
981
982fn redirect_location(response: &reqwest::Response) -> Option<&str> {
987 match response.status().as_u16() {
988 301 | 302 | 303 | 307 | 308 => response
989 .headers()
990 .get(reqwest::header::LOCATION)
991 .and_then(|v| v.to_str().ok()),
992 _ => None,
993 }
994}
995
996fn resolve_redirect_target(current: &str, location: &str) -> Result<String> {
1001 url::Url::parse(current)
1002 .and_then(|base| base.join(location))
1003 .map(String::from)
1004 .map_err(|e| SeerError::RdapError(format!("invalid RDAP redirect target: {}", e)))
1005}
1006
1007async fn query_rdap_attempt(
1019 url: &str,
1020 timeout: Duration,
1021 allow_reserved: bool,
1022) -> std::result::Result<RdapResponse, (SeerError, Option<Duration>)> {
1023 let mut current = url.to_string();
1024 let mut visited: HashSet<String> = HashSet::new();
1025 let response = loop {
1026 visited.insert(current.clone());
1027 let response = send_rdap_request(¤t, timeout, allow_reserved)
1028 .await
1029 .map_err(|e| (e, None))?;
1030 let Some(location) = redirect_location(&response) else {
1031 break response;
1032 };
1033 if visited.len() > MAX_RDAP_REDIRECTS {
1034 return Err((
1035 SeerError::RdapError(format!(
1036 "RDAP query exceeded {} redirects",
1037 MAX_RDAP_REDIRECTS
1038 )),
1039 None,
1040 ));
1041 }
1042 let next = resolve_redirect_target(¤t, location).map_err(|e| (e, None))?;
1043 if visited.contains(&next) {
1044 return Err((
1045 SeerError::RdapError("RDAP redirect loop detected".to_string()),
1046 None,
1047 ));
1048 }
1049 debug!(from = %current, to = %next, "Following RDAP redirect");
1050 current = next;
1051 };
1052
1053 if !response.status().is_success() {
1054 let status = response.status();
1055 let retry_after = if status.as_u16() == 429 {
1058 response
1059 .headers()
1060 .get(reqwest::header::RETRY_AFTER)
1061 .and_then(|v| v.to_str().ok())
1062 .and_then(parse_retry_after)
1063 } else {
1064 None
1065 };
1066 return Err((
1067 SeerError::RdapError(format!("query failed with status {}", status)),
1068 retry_after,
1069 ));
1070 }
1071
1072 read_and_parse_rdap_body(response, ¤t, timeout)
1073 .await
1074 .map_err(|e| (e, None))
1075}
1076
1077async fn load_bootstrap_data_with_retry(policy: &RetryPolicy) -> Result<BootstrapLoad> {
1081 let executor = RetryExecutor::new(policy.clone());
1082 executor.execute(load_bootstrap_data).await
1083}
1084
1085async fn load_bootstrap_data() -> Result<BootstrapLoad> {
1088 debug!("Loading RDAP bootstrap data from IANA");
1089
1090 let http = rdap_http_client()?;
1095
1096 let dns_future = http.get(IANA_BOOTSTRAP_DNS).send();
1097 let ipv4_future = http.get(IANA_BOOTSTRAP_IPV4).send();
1098 let ipv6_future = http.get(IANA_BOOTSTRAP_IPV6).send();
1099 let asn_future = http.get(IANA_BOOTSTRAP_ASN).send();
1100
1101 let (dns_resp, ipv4_resp, ipv6_resp, asn_resp) =
1104 tokio::join!(dns_future, ipv4_future, ipv6_future, asn_future);
1105
1106 const MAX_BOOTSTRAP_SIZE: usize = 10 * 1024 * 1024; async fn read_bootstrap(resp: reqwest::Response) -> Result<BootstrapResponse> {
1110 let mut body = Vec::new();
1116 let mut stream = resp.bytes_stream();
1117 let streamed = tokio::time::timeout(DEFAULT_TIMEOUT, async {
1118 while let Some(chunk) = stream.next().await {
1119 let chunk = chunk.map_err(|e| {
1120 SeerError::RdapBootstrapError(format!("failed to read body: {}", e))
1121 })?;
1122 body.extend_from_slice(&chunk);
1123 if body.len() > MAX_BOOTSTRAP_SIZE {
1124 return Err(SeerError::RdapBootstrapError(format!(
1125 "bootstrap response too large (exceeds {} bytes)",
1126 MAX_BOOTSTRAP_SIZE
1127 )));
1128 }
1129 }
1130 Ok::<(), SeerError>(())
1131 })
1132 .await;
1133
1134 match streamed {
1135 Ok(Ok(())) => {}
1136 Ok(Err(e)) => return Err(e),
1137 Err(_) => {
1138 return Err(SeerError::Timeout(format!(
1139 "RDAP bootstrap body read timed out after {:?}",
1140 DEFAULT_TIMEOUT
1141 )));
1142 }
1143 }
1144
1145 serde_json::from_slice(&body).map_err(Into::into)
1146 }
1147
1148 let dns_data = match dns_resp {
1150 Ok(resp) => match read_bootstrap(resp).await {
1151 Ok(data) => Some(data),
1152 Err(e) => {
1153 warn!(error = %e, "Failed to parse DNS bootstrap response");
1154 None
1155 }
1156 },
1157 Err(e) => {
1158 warn!(error = %e, "Failed to fetch DNS bootstrap from IANA");
1159 None
1160 }
1161 };
1162 let ipv4_data = match ipv4_resp {
1163 Ok(resp) => match read_bootstrap(resp).await {
1164 Ok(data) => Some(data),
1165 Err(e) => {
1166 warn!(error = %e, "Failed to parse IPv4 bootstrap response");
1167 None
1168 }
1169 },
1170 Err(e) => {
1171 warn!(error = %e, "Failed to fetch IPv4 bootstrap from IANA");
1172 None
1173 }
1174 };
1175 let ipv6_data = match ipv6_resp {
1176 Ok(resp) => match read_bootstrap(resp).await {
1177 Ok(data) => Some(data),
1178 Err(e) => {
1179 warn!(error = %e, "Failed to parse IPv6 bootstrap response");
1180 None
1181 }
1182 },
1183 Err(e) => {
1184 warn!(error = %e, "Failed to fetch IPv6 bootstrap from IANA");
1185 None
1186 }
1187 };
1188 let asn_data = match asn_resp {
1189 Ok(resp) => match read_bootstrap(resp).await {
1190 Ok(data) => Some(data),
1191 Err(e) => {
1192 warn!(error = %e, "Failed to parse ASN bootstrap response");
1193 None
1194 }
1195 },
1196 Err(e) => {
1197 warn!(error = %e, "Failed to fetch ASN bootstrap from IANA");
1198 None
1199 }
1200 };
1201
1202 fn collect_valid_urls(urls: &[serde_json::Value]) -> Option<Arc<Vec<url::Url>>> {
1206 let mut out = Vec::new();
1207 for u in urls {
1208 if let Some(s) = u.as_str() {
1209 match validate_bootstrap_url(s) {
1210 Ok(parsed) => out.push(parsed),
1211 Err(e) => {
1212 debug!(url = s, error = %e, "Skipping invalid bootstrap URL");
1213 }
1214 }
1215 }
1216 }
1217 if out.is_empty() {
1218 None
1219 } else {
1220 Some(Arc::new(out))
1221 }
1222 }
1223
1224 fn parse_prefix_services(data: BootstrapResponse) -> Vec<(IpRange, Arc<Vec<url::Url>>)> {
1226 let mut out = Vec::new();
1227 for service in data.services {
1228 if service.len() >= 2 {
1229 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array())
1230 {
1231 if let Some(urls_arc) = collect_valid_urls(urls) {
1232 for prefix in prefixes {
1233 if let Some(prefix_str) = prefix.as_str() {
1234 out.push((
1235 IpRange {
1236 prefix: prefix_str.to_string(),
1237 },
1238 Arc::clone(&urls_arc),
1239 ));
1240 }
1241 }
1242 }
1243 }
1244 }
1245 }
1246 out
1247 }
1248
1249 let dns = dns_data
1255 .map(|data| {
1256 let mut dns = HashMap::new();
1257 for service in data.services {
1258 if service.len() >= 2 {
1259 if let (Some(tlds), Some(urls)) = (service[0].as_array(), service[1].as_array())
1260 {
1261 if let Some(urls_arc) = collect_valid_urls(urls) {
1262 for tld in tlds {
1263 if let Some(tld_str) = tld.as_str() {
1264 dns.insert(tld_str.to_lowercase(), Arc::clone(&urls_arc));
1265 }
1266 }
1267 }
1268 }
1269 }
1270 }
1271 dns
1272 })
1273 .filter(|dns| !dns.is_empty());
1274
1275 let ipv4 = ipv4_data
1277 .map(parse_prefix_services)
1278 .filter(|v| !v.is_empty());
1279 let ipv6 = ipv6_data
1280 .map(parse_prefix_services)
1281 .filter(|v| !v.is_empty());
1282
1283 let asn = asn_data
1285 .map(|data| {
1286 let mut asn = Vec::new();
1287 for service in data.services {
1288 if service.len() >= 2 {
1289 if let (Some(ranges), Some(urls)) =
1290 (service[0].as_array(), service[1].as_array())
1291 {
1292 if let Some(urls_arc) = collect_valid_urls(urls) {
1293 for range in ranges {
1294 if let Some(range_str) = range.as_str() {
1295 if let Some((start, end)) = parse_asn_range(range_str) {
1296 asn.push((AsnRange { start, end }, Arc::clone(&urls_arc)));
1297 }
1298 }
1299 }
1300 }
1301 }
1302 }
1303 }
1304 asn
1305 })
1306 .filter(|v| !v.is_empty());
1307
1308 if dns.is_none() && ipv4.is_none() && ipv6.is_none() && asn.is_none() {
1312 return Err(SeerError::RdapBootstrapError(
1313 "all IANA bootstrap registries failed".to_string(),
1314 ));
1315 }
1316
1317 info!(
1318 dns_entries = dns.as_ref().map_or(0, HashMap::len),
1319 ipv4_ranges = ipv4.as_ref().map_or(0, Vec::len),
1320 ipv6_ranges = ipv6.as_ref().map_or(0, Vec::len),
1321 asn_ranges = asn.as_ref().map_or(0, Vec::len),
1322 "RDAP bootstrap loaded"
1323 );
1324
1325 Ok(BootstrapLoad {
1326 dns,
1327 ipv4,
1328 ipv6,
1329 asn,
1330 })
1331}
1332
1333fn wrap_all_candidates_failed(last_error: Option<SeerError>, candidate_count: usize) -> SeerError {
1342 let last = last_error.unwrap_or_else(|| SeerError::RdapError("no candidates".to_string()));
1343
1344 if candidate_count <= 1 {
1345 return last;
1346 }
1347
1348 match last {
1349 SeerError::Timeout(msg) => SeerError::Timeout(format!(
1350 "all {} RDAP candidate URLs timed out; last error: {}",
1351 candidate_count, msg
1352 )),
1353 other => SeerError::RdapError(format!(
1354 "all {} RDAP candidate URLs failed; last error: {}",
1355 candidate_count, other
1356 )),
1357 }
1358}
1359
1360fn build_rdap_urls(bases: &[url::Url], path: &str) -> Vec<url::Url> {
1362 bases
1363 .iter()
1364 .filter_map(|base| {
1365 let base_str = base.as_str();
1368 let normalized = if base_str.ends_with('/') {
1369 base_str.to_string()
1370 } else {
1371 format!("{}/", base_str)
1372 };
1373 url::Url::parse(&normalized).and_then(|u| u.join(path)).ok()
1374 })
1375 .collect()
1376}
1377
1378#[cfg(test)]
1379mod tests {
1380 use super::*;
1381
1382 #[test]
1383 fn from_config_applies_rdap_timeout() {
1384 let mut config = crate::config::SeerConfig::default();
1385 config.timeouts.rdap_secs = 33;
1386 let client = RdapClient::from_config(&config);
1387 assert_eq!(client.timeout, Duration::from_secs(33));
1388 }
1389
1390 #[test]
1391 fn test_default_client_has_retry_policy() {
1392 let client = RdapClient::new();
1393 assert_eq!(client.retry_policy.max_attempts, 3);
1397 }
1398
1399 #[test]
1402 fn parse_retry_after_parses_delta_seconds() {
1403 assert_eq!(parse_retry_after("5"), Some(Duration::from_secs(5)));
1404 assert_eq!(parse_retry_after(" 10 "), Some(Duration::from_secs(10)));
1405 assert_eq!(parse_retry_after("0"), Some(Duration::from_secs(0)));
1406 }
1407
1408 #[test]
1409 fn parse_retry_after_rejects_http_date_and_junk() {
1410 assert_eq!(parse_retry_after("Wed, 21 Oct 2015 07:28:00 GMT"), None);
1413 assert_eq!(parse_retry_after("soon"), None);
1414 assert_eq!(parse_retry_after(""), None);
1415 }
1416
1417 #[test]
1418 fn effective_retry_delay_prefers_capped_retry_after() {
1419 assert_eq!(
1421 effective_retry_delay(Duration::from_millis(100), Some(Duration::from_secs(5))),
1422 Duration::from_secs(5)
1423 );
1424 assert_eq!(
1426 effective_retry_delay(Duration::from_millis(100), Some(Duration::from_secs(600))),
1427 MAX_RETRY_AFTER
1428 );
1429 }
1430
1431 #[test]
1432 fn effective_retry_delay_falls_back_to_backoff() {
1433 assert_eq!(
1434 effective_retry_delay(Duration::from_millis(250), None),
1435 Duration::from_millis(250)
1436 );
1437 }
1438
1439 #[test]
1440 fn test_client_without_retries() {
1441 let client = RdapClient::new().without_retries();
1442 assert_eq!(client.retry_policy.max_attempts, 1);
1443 }
1444
1445 #[test]
1446 fn test_client_custom_retry_policy() {
1447 let policy = RetryPolicy::new().with_max_attempts(5);
1448 let client = RdapClient::new().with_retry_policy(policy);
1449 assert_eq!(client.retry_policy.max_attempts, 5);
1450 }
1451
1452 #[test]
1453 fn test_cached_bootstrap_expiration() {
1454 let data = BootstrapData {
1455 dns: HashMap::new(),
1456 ipv4: Vec::new(),
1457 ipv6: Vec::new(),
1458 asn: Vec::new(),
1459 };
1460 let cached = CachedBootstrap::new(data);
1461 assert!(!cached.is_expired());
1463 }
1464
1465 #[test]
1466 fn test_rdap_http_client_is_configured() {
1467 let client = rdap_http_client();
1470 assert!(client.is_ok(), "RDAP HTTP client builder must succeed");
1471 }
1472
1473 #[test]
1474 fn test_parse_bootstrap_empty_services() {
1475 let data = BootstrapData {
1477 dns: HashMap::new(),
1478 ipv4: Vec::new(),
1479 ipv6: Vec::new(),
1480 asn: Vec::new(),
1481 };
1482 assert!(RdapClient::get_rdap_urls_for_domain(&data, "example.com").is_none());
1484 assert!(RdapClient::get_rdap_urls_for_asn(&data, 12345).is_none());
1485 }
1486
1487 #[tokio::test]
1490 async fn test_validate_url_not_reserved_rejects_loopback_literal() {
1491 let err = validate_url_not_reserved("https://127.0.0.1/domain/example.com")
1492 .await
1493 .unwrap_err();
1494 assert!(
1495 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved")),
1496 "expected reserved-IP error, got: {:?}",
1497 err
1498 );
1499 }
1500
1501 #[tokio::test]
1502 async fn test_validate_url_not_reserved_rejects_private_ipv4_literal() {
1503 let err = validate_url_not_reserved("https://10.0.0.1/")
1504 .await
1505 .unwrap_err();
1506 assert!(
1507 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved")),
1508 "expected reserved-IP error, got: {:?}",
1509 err
1510 );
1511 }
1512
1513 #[tokio::test]
1514 async fn test_validate_url_not_reserved_rejects_non_https_scheme() {
1515 let err = validate_url_not_reserved("http://93.184.216.34/domain/example.com")
1519 .await
1520 .unwrap_err();
1521 assert!(
1522 matches!(err, SeerError::RdapError(ref s) if s.contains("not https")),
1523 "expected non-https rejection, got: {:?}",
1524 err
1525 );
1526 }
1527
1528 #[tokio::test]
1529 async fn test_validate_url_not_reserved_rejects_ipv6_loopback_literal() {
1530 let err = validate_url_not_reserved("https://[::1]/")
1533 .await
1534 .unwrap_err();
1535 assert!(
1536 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved")),
1537 "expected reserved-IP error, got: {:?}",
1538 err
1539 );
1540 }
1541
1542 #[tokio::test]
1543 async fn test_validate_url_not_reserved_returns_resolved_addrs_for_public_literal() {
1544 let addrs = validate_url_not_reserved("https://8.8.8.8/").await.unwrap();
1547 assert_eq!(addrs.len(), 1);
1548 assert!(addrs[0].ip().is_ipv4());
1549 assert_eq!(addrs[0].port(), 443);
1550 }
1551
1552 #[test]
1555 fn test_build_rdap_urls_preserves_order_and_appends_path() {
1556 let bases = vec![
1557 url::Url::parse("https://rdap.a.example/").unwrap(),
1558 url::Url::parse("https://rdap.b.example").unwrap(), ];
1560 let built = build_rdap_urls(&bases, "domain/example.com");
1561 assert_eq!(built.len(), 2);
1562 assert_eq!(
1563 built[0].as_str(),
1564 "https://rdap.a.example/domain/example.com"
1565 );
1566 assert_eq!(
1567 built[1].as_str(),
1568 "https://rdap.b.example/domain/example.com"
1569 );
1570 }
1571
1572 #[test]
1573 fn test_build_rdap_urls_empty_input_returns_empty() {
1574 let built = build_rdap_urls(&[], "domain/example.com");
1575 assert!(built.is_empty());
1576 }
1577
1578 #[test]
1581 fn test_wrap_all_candidates_failed_preserves_timeout_variant() {
1582 let last = SeerError::Timeout("body read timed out".to_string());
1585 let wrapped = wrap_all_candidates_failed(Some(last), 3);
1586 match wrapped {
1587 SeerError::Timeout(msg) => {
1588 assert!(
1589 msg.contains("all 3 RDAP candidate URLs timed out"),
1590 "expected wrapped timeout message, got: {}",
1591 msg
1592 );
1593 assert!(
1594 msg.contains("body read timed out"),
1595 "expected original message preserved, got: {}",
1596 msg
1597 );
1598 }
1599 other => panic!(
1600 "expected SeerError::Timeout after wrapping a Timeout, got: {:?}",
1601 other
1602 ),
1603 }
1604 }
1605
1606 #[test]
1607 fn test_wrap_all_candidates_failed_wraps_non_timeout_as_rdap_error() {
1608 let last = SeerError::RdapError("500 internal error".to_string());
1609 let wrapped = wrap_all_candidates_failed(Some(last), 2);
1610 assert!(
1611 matches!(wrapped, SeerError::RdapError(ref s) if s.contains("all 2 RDAP candidate URLs failed")),
1612 "expected wrapped RdapError, got: {:?}",
1613 wrapped
1614 );
1615 }
1616
1617 #[test]
1618 fn test_wrap_all_candidates_failed_single_candidate_returns_unchanged() {
1619 let last = SeerError::Timeout("single timeout".to_string());
1622 let wrapped = wrap_all_candidates_failed(Some(last), 1);
1623 assert!(
1624 matches!(wrapped, SeerError::Timeout(ref s) if s == "single timeout"),
1625 "expected unchanged Timeout, got: {:?}",
1626 wrapped
1627 );
1628 }
1629
1630 #[test]
1631 fn test_wrap_all_candidates_failed_no_last_error_returns_placeholder() {
1632 let wrapped = wrap_all_candidates_failed(None, 0);
1633 assert!(matches!(wrapped, SeerError::RdapError(_)));
1634 }
1635
1636 static BOOTSTRAP_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
1655
1656 #[tokio::test]
1657 async fn test_bootstrap_load_notify_wakes_waiter_when_cache_populated() {
1658 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1659
1660 {
1662 let mut cache = BOOTSTRAP_CACHE.write().await;
1663 *cache = None;
1664 }
1665
1666 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1669 tokio::pin!(notified);
1670
1671 {
1673 let mut cache = BOOTSTRAP_CACHE.write().await;
1674 *cache = Some(CachedBootstrap::new(BootstrapData {
1675 dns: HashMap::new(),
1676 ipv4: Vec::new(),
1677 ipv6: Vec::new(),
1678 asn: Vec::new(),
1679 }));
1680 }
1681 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1682
1683 let result = wait_for_in_flight_load(notified).await;
1684 assert!(
1685 result.is_ok(),
1686 "expected waiter to see populated cache, got: {:?}",
1687 result
1688 );
1689
1690 {
1692 let mut cache = BOOTSTRAP_CACHE.write().await;
1693 *cache = None;
1694 }
1695 }
1696
1697 #[tokio::test]
1698 async fn test_bootstrap_load_notify_empty_cache_after_wake_returns_error() {
1699 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1700
1701 {
1703 let mut cache = BOOTSTRAP_CACHE.write().await;
1704 *cache = None;
1705 }
1706
1707 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1708 tokio::pin!(notified);
1709
1710 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1712
1713 let result = wait_for_in_flight_load(notified).await;
1714 assert!(
1715 matches!(
1716 result,
1717 Err(SeerError::RdapBootstrapError(ref s))
1718 if s.contains("throttled and no cache available")
1719 ),
1720 "expected throttled error when cache still empty after notify, got: {:?}",
1721 result
1722 );
1723 }
1724
1725 use wiremock::matchers::method;
1735 use wiremock::{Mock, MockServer, ResponseTemplate};
1736
1737 #[tokio::test]
1738 async fn mock_rdap_404_is_nonretryable_typed_error() {
1739 let server = MockServer::start().await;
1740 Mock::given(method("GET"))
1741 .respond_with(ResponseTemplate::new(404))
1742 .mount(&server)
1743 .await;
1744
1745 let client = RdapClient::new()
1746 .without_retries()
1747 .allowing_reserved_for_tests();
1748 let err = client
1749 .query_rdap_with_retry(&format!("{}/domain/example.com", server.uri()))
1750 .await
1751 .unwrap_err();
1752 assert!(
1753 matches!(err, SeerError::RdapError(ref m) if m.contains("404")),
1754 "got: {err:?}"
1755 );
1756 }
1757
1758 #[tokio::test]
1759 async fn mock_rdap_429_honors_retry_after_and_succeeds() {
1760 let server = MockServer::start().await;
1761 Mock::given(method("GET"))
1764 .respond_with(ResponseTemplate::new(429).insert_header("Retry-After", "0"))
1765 .up_to_n_times(1)
1766 .mount(&server)
1767 .await;
1768 Mock::given(method("GET"))
1769 .respond_with(ResponseTemplate::new(200).set_body_raw(
1770 r#"{"objectClassName":"domain","handle":"MOCK-1"}"#,
1771 "application/rdap+json",
1772 ))
1773 .mount(&server)
1774 .await;
1775
1776 let client = RdapClient::new().allowing_reserved_for_tests();
1777 let resp = client
1778 .query_rdap_with_retry(&format!("{}/domain/example.com", server.uri()))
1779 .await
1780 .unwrap();
1781 assert_eq!(resp.handle.as_deref(), Some("MOCK-1"));
1782 }
1783
1784 #[tokio::test]
1785 async fn mock_rdap_malformed_body_is_parse_error_not_panic() {
1786 let server = MockServer::start().await;
1787 Mock::given(method("GET"))
1788 .respond_with(ResponseTemplate::new(200).set_body_raw("not json", "text/plain"))
1789 .mount(&server)
1790 .await;
1791
1792 let client = RdapClient::new()
1793 .without_retries()
1794 .allowing_reserved_for_tests();
1795 let err = client
1796 .query_rdap_with_retry(&format!("{}/domain/example.com", server.uri()))
1797 .await
1798 .unwrap_err();
1799 assert!(matches!(err, SeerError::JsonError(_)), "got: {err:?}");
1800 }
1801
1802 #[tokio::test]
1803 async fn mock_rdap_candidate_fallback_uses_second_url() {
1804 let bad = MockServer::start().await;
1805 Mock::given(method("GET"))
1806 .respond_with(ResponseTemplate::new(500))
1807 .mount(&bad)
1808 .await;
1809 let good = MockServer::start().await;
1810 Mock::given(method("GET"))
1811 .respond_with(ResponseTemplate::new(200).set_body_raw(
1812 r#"{"objectClassName":"domain","handle":"MOCK-2"}"#,
1813 "application/rdap+json",
1814 ))
1815 .mount(&good)
1816 .await;
1817
1818 let client = RdapClient::new()
1819 .without_retries()
1820 .allowing_reserved_for_tests();
1821 let urls = vec![
1822 url::Url::parse(&format!("{}/domain/example.com", bad.uri())).unwrap(),
1823 url::Url::parse(&format!("{}/domain/example.com", good.uri())).unwrap(),
1824 ];
1825 let resp = client.query_rdap_urls(&urls).await.unwrap();
1826 assert_eq!(resp.handle.as_deref(), Some("MOCK-2"));
1827 }
1828
1829 #[tokio::test]
1835 async fn mock_rdap_404_on_first_candidate_survives_later_non_404_failure() {
1836 let not_found = MockServer::start().await;
1837 Mock::given(method("GET"))
1838 .respond_with(ResponseTemplate::new(404))
1839 .mount(¬_found)
1840 .await;
1841 let broken = MockServer::start().await;
1842 Mock::given(method("GET"))
1843 .respond_with(ResponseTemplate::new(500))
1844 .mount(&broken)
1845 .await;
1846
1847 let client = RdapClient::new()
1848 .without_retries()
1849 .allowing_reserved_for_tests();
1850 let urls = vec![
1851 url::Url::parse(&format!("{}/domain/example.com", not_found.uri())).unwrap(),
1852 url::Url::parse(&format!("{}/domain/example.com", broken.uri())).unwrap(),
1853 ];
1854 let err = client.query_rdap_urls(&urls).await.unwrap_err();
1855 assert!(
1856 crate::rdap::rdap_error_is_404(&err),
1857 "404 from candidate 1 must survive candidate 2's non-404 failure, got: {err:?}"
1858 );
1859 }
1860
1861 #[tokio::test]
1872 async fn pinned_client_cache_hit_skips_dns_and_revalidation() {
1873 let host = "pinned-cache-hit.seer-test.invalid";
1874 let timeout = Duration::from_secs(2);
1875 let key = (host.to_string(), 443u16, timeout);
1876
1877 let pinned: Vec<SocketAddr> = vec!["127.0.0.1:443".parse().expect("valid addr")];
1882 let client = Client::builder()
1883 .timeout(timeout)
1884 .connect_timeout(timeout)
1885 .resolve_to_addrs(host, &pinned)
1886 .redirect(reqwest::redirect::Policy::none())
1887 .build()
1888 .expect("client builds");
1889 PINNED_CLIENT_CACHE.insert(key.clone(), client);
1890
1891 let err = send_rdap_request(
1892 &format!("https://{}/domain/example.com", host),
1893 timeout,
1894 false,
1895 )
1896 .await
1897 .unwrap_err();
1898 assert!(
1899 matches!(err, SeerError::ReqwestError { .. }),
1900 "cache hit must reach the connect stage (ReqwestError), not fail \
1901 SSRF validation (RdapError) — got: {err:?}"
1902 );
1903
1904 PINNED_CLIENT_CACHE.remove(&key);
1907 }
1908
1909 #[tokio::test]
1914 async fn test_mode_requests_never_populate_pinned_client_cache() {
1915 let server = MockServer::start().await;
1916 Mock::given(method("GET"))
1917 .respond_with(ResponseTemplate::new(200).set_body_raw(
1918 r#"{"objectClassName":"domain","handle":"MOCK-CACHE"}"#,
1919 "application/rdap+json",
1920 ))
1921 .mount(&server)
1922 .await;
1923
1924 let client = RdapClient::new()
1925 .without_retries()
1926 .allowing_reserved_for_tests();
1927 let uri = format!("{}/domain/example.com", server.uri());
1928 let resp = client.query_rdap_with_retry(&uri).await.unwrap();
1929 assert_eq!(resp.handle.as_deref(), Some("MOCK-CACHE"));
1930
1931 let parsed = url::Url::parse(&uri).unwrap();
1934 let host = parsed.host_str().unwrap().to_string();
1935 let port = parsed.port_or_known_default().unwrap();
1936 assert!(
1937 PINNED_CLIENT_CACHE
1938 .get(&(host, port, DEFAULT_TIMEOUT))
1939 .is_none(),
1940 "test-mode request must not populate the shared pinned-client cache"
1941 );
1942 }
1943
1944 use wiremock::matchers::path;
1947
1948 const MOCK_OK_BODY: &str = r#"{"objectClassName":"domain","handle":"MOCK-REDIRECT"}"#;
1949
1950 #[test]
1951 fn is_loopback_url_only_matches_loopback_literals() {
1952 assert!(is_loopback_url("http://127.0.0.1:8080/domain/x"));
1953 assert!(is_loopback_url("http://[::1]:8080/domain/x"));
1954 assert!(!is_loopback_url("https://10.0.0.1/domain/x"));
1955 assert!(!is_loopback_url("https://rdap.example.com/domain/x"));
1956 assert!(!is_loopback_url("not a url"));
1957 }
1958
1959 #[tokio::test]
1960 async fn mock_rdap_303_redirect_is_followed() {
1961 let target = MockServer::start().await;
1964 Mock::given(method("GET"))
1965 .and(path("/ip/192.36.148.17"))
1966 .respond_with(
1967 ResponseTemplate::new(200).set_body_raw(MOCK_OK_BODY, "application/rdap+json"),
1968 )
1969 .expect(1)
1970 .mount(&target)
1971 .await;
1972 let origin = MockServer::start().await;
1973 Mock::given(method("GET"))
1974 .respond_with(
1975 ResponseTemplate::new(303)
1976 .insert_header("Location", format!("{}/ip/192.36.148.17", target.uri())),
1977 )
1978 .mount(&origin)
1979 .await;
1980
1981 let client = RdapClient::new()
1982 .without_retries()
1983 .allowing_reserved_for_tests();
1984 let resp = client
1985 .query_rdap_with_retry(&format!("{}/ip/192.36.148.17", origin.uri()))
1986 .await
1987 .unwrap();
1988 assert_eq!(resp.handle.as_deref(), Some("MOCK-REDIRECT"));
1989 }
1990
1991 #[tokio::test]
1992 async fn mock_rdap_relative_redirect_is_resolved_against_current_url() {
1993 let server = MockServer::start().await;
1994 Mock::given(method("GET"))
1995 .and(path("/old/domain/example.com"))
1996 .respond_with(
1997 ResponseTemplate::new(301).insert_header("Location", "/new/domain/example.com"),
1998 )
1999 .mount(&server)
2000 .await;
2001 Mock::given(method("GET"))
2002 .and(path("/new/domain/example.com"))
2003 .respond_with(
2004 ResponseTemplate::new(200).set_body_raw(MOCK_OK_BODY, "application/rdap+json"),
2005 )
2006 .mount(&server)
2007 .await;
2008
2009 let client = RdapClient::new()
2010 .without_retries()
2011 .allowing_reserved_for_tests();
2012 let resp = client
2013 .query_rdap_with_retry(&format!("{}/old/domain/example.com", server.uri()))
2014 .await
2015 .unwrap();
2016 assert_eq!(resp.handle.as_deref(), Some("MOCK-REDIRECT"));
2017 }
2018
2019 async fn redirect_error(location: &str) -> SeerError {
2023 let server = MockServer::start().await;
2024 Mock::given(method("GET"))
2025 .respond_with(ResponseTemplate::new(302).insert_header("Location", location))
2026 .mount(&server)
2027 .await;
2028 let client = RdapClient::new()
2029 .without_retries()
2030 .allowing_reserved_for_tests();
2031 client
2032 .query_rdap_with_retry(&format!("{}/domain/example.com", server.uri()))
2033 .await
2034 .unwrap_err()
2035 }
2036
2037 #[tokio::test]
2038 async fn mock_rdap_redirect_to_plain_http_is_refused() {
2039 let err = redirect_error("http://93.184.216.34/domain/example.com").await;
2042 assert!(
2043 matches!(err, SeerError::RdapError(ref m) if m.contains("not https")),
2044 "got: {err:?}"
2045 );
2046 }
2047
2048 #[tokio::test]
2049 async fn mock_rdap_redirect_to_reserved_address_is_refused() {
2050 for location in [
2051 "https://10.0.0.1/domain/example.com",
2052 "https://169.254.169.254/latest/meta-data/",
2053 ] {
2054 let err = redirect_error(location).await;
2055 assert!(
2056 matches!(err, SeerError::RdapError(ref m) if m.contains("reserved")),
2057 "{location}: got {err:?}"
2058 );
2059 }
2060 }
2061
2062 #[tokio::test]
2063 async fn mock_rdap_redirect_loop_is_detected() {
2064 let server = MockServer::start().await;
2065 Mock::given(method("GET"))
2066 .and(path("/a"))
2067 .respond_with(ResponseTemplate::new(307).insert_header("Location", "/b"))
2068 .mount(&server)
2069 .await;
2070 Mock::given(method("GET"))
2071 .and(path("/b"))
2072 .respond_with(ResponseTemplate::new(307).insert_header("Location", "/a"))
2073 .mount(&server)
2074 .await;
2075 let client = RdapClient::new()
2076 .without_retries()
2077 .allowing_reserved_for_tests();
2078 let err = client
2079 .query_rdap_with_retry(&format!("{}/a", server.uri()))
2080 .await
2081 .unwrap_err();
2082 assert!(
2083 matches!(err, SeerError::RdapError(ref m) if m.contains("loop")),
2084 "got: {err:?}"
2085 );
2086 }
2087
2088 #[tokio::test]
2089 async fn mock_rdap_redirect_chain_is_capped() {
2090 let server = MockServer::start().await;
2091 for hop in 0..=MAX_RDAP_REDIRECTS {
2092 Mock::given(method("GET"))
2093 .and(path(format!("/hop{hop}")))
2094 .respond_with(
2095 ResponseTemplate::new(308)
2096 .insert_header("Location", format!("/hop{}", hop + 1)),
2097 )
2098 .mount(&server)
2099 .await;
2100 }
2101 let client = RdapClient::new()
2102 .without_retries()
2103 .allowing_reserved_for_tests();
2104 let err = client
2105 .query_rdap_with_retry(&format!("{}/hop0", server.uri()))
2106 .await
2107 .unwrap_err();
2108 assert!(
2109 matches!(err, SeerError::RdapError(ref m) if m.contains("exceeded")),
2110 "got: {err:?}"
2111 );
2112 }
2113
2114 #[tokio::test]
2115 async fn mock_rdap_sends_accept_header_with_json_fallback() {
2116 let server = MockServer::start().await;
2118 Mock::given(method("GET"))
2119 .and(|req: &wiremock::Request| {
2122 req.headers.get("accept").and_then(|v| v.to_str().ok()) == Some(RDAP_ACCEPT)
2123 })
2124 .respond_with(ResponseTemplate::new(200).set_body_raw(MOCK_OK_BODY, "application/json"))
2125 .mount(&server)
2126 .await;
2127 Mock::given(method("GET"))
2128 .respond_with(ResponseTemplate::new(406))
2129 .mount(&server)
2130 .await;
2131 let client = RdapClient::new()
2132 .without_retries()
2133 .allowing_reserved_for_tests();
2134 let resp = client
2135 .query_rdap_with_retry(&format!("{}/domain/example.sn", server.uri()))
2136 .await
2137 .unwrap();
2138 assert_eq!(resp.handle.as_deref(), Some("MOCK-REDIRECT"));
2139 assert!(RDAP_ACCEPT.contains("application/json"));
2140 }
2141
2142 fn url_arc(u: &str) -> Arc<Vec<url::Url>> {
2145 Arc::new(vec![url::Url::parse(u).expect("valid url")])
2146 }
2147
2148 fn full_load() -> BootstrapLoad {
2149 let mut dns = HashMap::new();
2150 dns.insert("com".to_string(), url_arc("https://rdap.verisign.example/"));
2151 BootstrapLoad {
2152 dns: Some(dns),
2153 ipv4: Some(vec![(
2154 IpRange {
2155 prefix: "8.0.0.0/8".to_string(),
2156 },
2157 url_arc("https://rdap.arin.example/"),
2158 )]),
2159 ipv6: Some(Vec::new()),
2160 asn: Some(vec![(
2161 AsnRange { start: 1, end: 10 },
2162 url_arc("https://rdap.arin.example/"),
2163 )]),
2164 }
2165 }
2166
2167 #[test]
2168 fn complete_bootstrap_load_gets_full_ttl() {
2169 let load = full_load();
2170 assert!(load.is_complete());
2171 assert_eq!(load.ttl(), BOOTSTRAP_TTL);
2172 }
2173
2174 #[test]
2175 fn partial_bootstrap_load_keeps_previous_section_and_short_ttl() {
2176 let mut previous = Some(CachedBootstrap::with_ttl(
2178 full_load().merge_over(None),
2179 Duration::ZERO,
2180 ));
2181 std::thread::sleep(Duration::from_millis(2));
2182 assert!(previous.as_ref().is_some_and(CachedBootstrap::is_expired));
2183
2184 let mut partial = full_load();
2186 partial.dns = None;
2187 assert!(!partial.is_complete());
2188 store_bootstrap_load(&mut previous, partial);
2189
2190 let cached = previous.expect("cache populated");
2191 assert_eq!(cached.ttl, BOOTSTRAP_PARTIAL_TTL);
2192 assert!(
2193 RdapClient::get_rdap_urls_for_domain(&cached.data, "example.com").is_some(),
2194 "the failed dns section must be carried over, not wiped"
2195 );
2196 assert!(RdapClient::get_rdap_urls_for_asn(&cached.data, 5).is_some());
2197 }
2198
2199 #[test]
2200 fn partial_cold_bootstrap_load_leaves_failed_section_empty() {
2201 let mut cache = None;
2202 let mut partial = full_load();
2203 partial.dns = None;
2204 store_bootstrap_load(&mut cache, partial);
2205 let cached = cache.expect("cache populated");
2206 assert_eq!(cached.ttl, BOOTSTRAP_PARTIAL_TTL);
2207 assert!(RdapClient::get_rdap_urls_for_domain(&cached.data, "example.com").is_none());
2208 }
2209
2210 #[test]
2211 fn bootstrap_store_does_not_overwrite_fresh_data() {
2212 let mut fresh = full_load();
2213 fresh.dns = Some(HashMap::from([(
2214 "org".to_string(),
2215 url_arc("https://rdap.pir.example/"),
2216 )]));
2217 let mut cache = Some(CachedBootstrap::new(fresh.merge_over(None)));
2218 store_bootstrap_load(&mut cache, full_load());
2219 let cached = cache.expect("cache populated");
2220 assert!(RdapClient::get_rdap_urls_for_domain(&cached.data, "example.org").is_some());
2221 assert!(RdapClient::get_rdap_urls_for_domain(&cached.data, "example.com").is_none());
2222 }
2223
2224 #[tokio::test]
2227 async fn wait_returns_immediately_when_no_load_is_in_flight() {
2228 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
2229 {
2230 let mut cache = BOOTSTRAP_CACHE.write().await;
2231 *cache = None;
2232 }
2233 assert!(!BOOTSTRAP_LOAD_IN_FLIGHT.load(Ordering::SeqCst));
2234
2235 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
2239 tokio::pin!(notified);
2240 let result =
2241 tokio::time::timeout(Duration::from_secs(2), wait_for_in_flight_load(notified))
2242 .await
2243 .expect("must not block when no load is in flight");
2244 assert!(
2245 matches!(result, Err(SeerError::RdapBootstrapError(ref s)) if s.contains("throttled")),
2246 "got: {result:?}"
2247 );
2248 }
2249
2250 #[tokio::test]
2251 async fn wait_blocks_until_in_flight_load_finishes() {
2252 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
2253 {
2254 let mut cache = BOOTSTRAP_CACHE.write().await;
2255 *cache = None;
2256 }
2257
2258 let load = BootstrapLoadGuard::start();
2259 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
2260 tokio::pin!(notified);
2261
2262 let loader = tokio::spawn(async move {
2263 tokio::time::sleep(Duration::from_millis(50)).await;
2264 {
2265 let mut cache = BOOTSTRAP_CACHE.write().await;
2266 *cache = Some(CachedBootstrap::new(full_load().merge_over(None)));
2267 }
2268 drop(load);
2270 });
2271
2272 let result =
2273 tokio::time::timeout(Duration::from_secs(5), wait_for_in_flight_load(notified))
2274 .await
2275 .expect("waiter must wake via the loader's notify");
2276 assert!(result.is_ok(), "got: {result:?}");
2277 loader.await.expect("loader joined");
2278 assert!(!BOOTSTRAP_LOAD_IN_FLIGHT.load(Ordering::SeqCst));
2279
2280 let mut cache = BOOTSTRAP_CACHE.write().await;
2281 *cache = None;
2282 }
2283}