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 reqwest::Client;
8use serde::Deserialize;
9use std::sync::LazyLock;
10use tokio::sync::{Notify, RwLock};
11use tracing::{debug, info, instrument, warn};
12
13use super::bootstrap::{ip_matches_prefix, parse_asn_range, validate_bootstrap_url};
14use super::types::RdapResponse;
15use crate::error::{Result, SeerError};
16use crate::http::{read_body_capped, BodyReadError, Overflow};
17use crate::retry::{RetryExecutor, RetryPolicy};
18use crate::validation::normalize_domain;
19
20pub(crate) const 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
26pub(crate) const MAX_BOOTSTRAP_SIZE: usize = 10 * 1024 * 1024;
29
30const DEFAULT_TIMEOUT: Duration = Duration::from_secs(15);
35
36const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
39
40const BOOTSTRAP_TTL: Duration = Duration::from_secs(24 * 60 * 60);
42
43const BOOTSTRAP_REFRESH_MIN_INTERVAL: Duration = Duration::from_secs(60);
47
48const BOOTSTRAP_PARTIAL_TTL: Duration = Duration::from_secs(5 * 60);
55
56const RDAP_ACCEPT: &str = "application/rdap+json, application/json;q=0.9";
60
61const MAX_RDAP_REDIRECTS: usize = 3;
66
67const RDAP_USER_AGENT: &str = "Seer/1.0 (RDAP Client)";
69
70static RDAP_HTTP_CLIENT: LazyLock<Option<Client>> = LazyLock::new(|| {
80 rdap_client_builder(DEFAULT_TIMEOUT)
84 .pool_max_idle_per_host(10)
85 .build()
86 .ok()
87});
88
89fn rdap_http_client() -> Result<&'static Client> {
93 RDAP_HTTP_CLIENT
94 .as_ref()
95 .ok_or_else(|| SeerError::HttpError("failed to initialize HTTP client".into()))
96}
97
98static BOOTSTRAP_CACHE: LazyLock<RwLock<Option<CachedBootstrap>>> =
100 LazyLock::new(|| RwLock::new(None));
101
102static BOOTSTRAP_LAST_ATTEMPT: LazyLock<RwLock<Option<Instant>>> =
106 LazyLock::new(|| RwLock::new(None));
107
108static BOOTSTRAP_LOAD_NOTIFY: LazyLock<Notify> = LazyLock::new(Notify::new);
116
117static BOOTSTRAP_LOAD_IN_FLIGHT: AtomicBool = AtomicBool::new(false);
123
124struct BootstrapLoadGuard;
128
129impl BootstrapLoadGuard {
130 fn start() -> Self {
131 BOOTSTRAP_LOAD_IN_FLIGHT.store(true, Ordering::SeqCst);
132 Self
133 }
134}
135
136impl Drop for BootstrapLoadGuard {
137 fn drop(&mut self) {
138 BOOTSTRAP_LOAD_IN_FLIGHT.store(false, Ordering::SeqCst);
139 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
140 }
141}
142
143struct CachedBootstrap {
145 data: BootstrapData,
146 loaded_at: Instant,
147 ttl: Duration,
150}
151
152impl CachedBootstrap {
153 #[cfg(test)]
154 fn new(data: BootstrapData) -> Self {
155 Self::with_ttl(data, BOOTSTRAP_TTL)
156 }
157
158 fn with_ttl(data: BootstrapData, ttl: Duration) -> Self {
159 Self {
160 data,
161 loaded_at: Instant::now(),
162 ttl,
163 }
164 }
165
166 fn is_expired(&self) -> bool {
167 self.loaded_at.elapsed() > self.ttl
168 }
169
170 fn age(&self) -> Duration {
171 self.loaded_at.elapsed()
172 }
173}
174
175struct BootstrapData {
180 dns: HashMap<String, Arc<Vec<url::Url>>>,
181 ipv4: Vec<(IpRange, Arc<Vec<url::Url>>)>,
182 ipv6: Vec<(IpRange, Arc<Vec<url::Url>>)>,
183 asn: Vec<(AsnRange, Arc<Vec<url::Url>>)>,
184}
185
186#[derive(Clone)]
187struct IpRange {
188 prefix: String,
189}
190
191#[derive(Clone)]
192struct AsnRange {
193 start: u32,
194 end: u32,
195}
196
197#[derive(Deserialize)]
198struct BootstrapResponse {
199 services: Vec<Vec<serde_json::Value>>,
200}
201
202struct BootstrapLoad {
206 dns: Option<HashMap<String, Arc<Vec<url::Url>>>>,
207 ipv4: Option<Vec<(IpRange, Arc<Vec<url::Url>>)>>,
208 ipv6: Option<Vec<(IpRange, Arc<Vec<url::Url>>)>>,
209 asn: Option<Vec<(AsnRange, Arc<Vec<url::Url>>)>>,
210}
211
212impl BootstrapLoad {
213 fn is_complete(&self) -> bool {
215 self.dns.is_some() && self.ipv4.is_some() && self.ipv6.is_some() && self.asn.is_some()
216 }
217
218 fn ttl(&self) -> Duration {
221 if self.is_complete() {
222 BOOTSTRAP_TTL
223 } else {
224 BOOTSTRAP_PARTIAL_TTL
225 }
226 }
227
228 fn merge_over(self, previous: Option<BootstrapData>) -> BootstrapData {
232 let (dns, ipv4, ipv6, asn) = match previous {
233 Some(p) => (Some(p.dns), Some(p.ipv4), Some(p.ipv6), Some(p.asn)),
234 None => (None, None, None, None),
235 };
236 BootstrapData {
237 dns: self.dns.or(dns).unwrap_or_default(),
238 ipv4: self.ipv4.or(ipv4).unwrap_or_default(),
239 ipv6: self.ipv6.or(ipv6).unwrap_or_default(),
240 asn: self.asn.or(asn).unwrap_or_default(),
241 }
242 }
243}
244
245fn store_bootstrap_load(cache: &mut Option<CachedBootstrap>, load: BootstrapLoad) {
249 if cache.as_ref().is_some_and(|c| !c.is_expired()) {
250 return;
251 }
252 let ttl = load.ttl();
253 if !load.is_complete() {
254 warn!(
255 dns = load.dns.is_some(),
256 ipv4 = load.ipv4.is_some(),
257 ipv6 = load.ipv6.is_some(),
258 asn = load.asn.is_some(),
259 retry_in_secs = ttl.as_secs(),
260 "RDAP bootstrap partially loaded; keeping previous data for failed registries"
261 );
262 }
263 let previous = cache.take().map(|c| c.data);
264 *cache = Some(CachedBootstrap::with_ttl(load.merge_over(previous), ttl));
265}
266
267async fn wait_for_in_flight_load(
277 notified: std::pin::Pin<&mut tokio::sync::futures::Notified<'_>>,
278) -> Result<()> {
279 if BOOTSTRAP_LOAD_IN_FLIGHT.load(Ordering::SeqCst) {
286 let _ = tokio::time::timeout(DEFAULT_TIMEOUT, notified).await;
287 }
288 let cache = BOOTSTRAP_CACHE.read().await;
289 if cache.is_some() {
290 Ok(())
291 } else {
292 Err(SeerError::RdapBootstrapError(
293 "bootstrap refresh throttled and no cache available".to_string(),
294 ))
295 }
296}
297
298enum BootstrapClaim {
300 Won(BootstrapLoadGuard),
302 Busy,
305}
306
307async fn claim_bootstrap_load() -> BootstrapClaim {
312 let mut last = BOOTSTRAP_LAST_ATTEMPT.write().await;
313 let throttled = last.is_some_and(|ts| ts.elapsed() < BOOTSTRAP_REFRESH_MIN_INTERVAL);
314 if throttled || BOOTSTRAP_LOAD_IN_FLIGHT.load(Ordering::SeqCst) {
315 return BootstrapClaim::Busy;
316 }
317 *last = Some(Instant::now());
318 BootstrapClaim::Won(BootstrapLoadGuard::start())
319}
320
321async fn finish_bootstrap_load(result: Result<BootstrapLoad>) -> Result<()> {
325 match result {
326 Ok(load) => {
327 store_bootstrap_load(&mut *BOOTSTRAP_CACHE.write().await, load);
328 Ok(())
329 }
330 Err(e) => match BOOTSTRAP_CACHE.read().await.as_ref() {
331 Some(cached) => {
332 debug!(
333 error = %e,
334 age_hours = cached.age().as_secs() / 3600,
335 "Bootstrap refresh failed, using stale data"
336 );
337 Ok(())
338 }
339 None => Err(e),
340 },
341 }
342}
343
344async fn ensure_bootstrap_with<F>(load: F) -> Result<()>
356where
357 F: std::future::Future<Output = Result<BootstrapLoad>> + Send + 'static,
358{
359 let stale = match BOOTSTRAP_CACHE.read().await.as_ref() {
360 Some(cached) if !cached.is_expired() => return Ok(()),
361 Some(_) => true,
362 None => false,
363 };
364
365 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
368 tokio::pin!(notified);
369
370 let guard = match claim_bootstrap_load().await {
371 BootstrapClaim::Won(guard) => guard,
372 BootstrapClaim::Busy if stale => return Ok(()),
373 BootstrapClaim::Busy => return wait_for_in_flight_load(notified).await,
374 };
375
376 if stale {
377 debug!("Serving stale RDAP bootstrap data; refreshing in the background");
378 tokio::spawn(async move {
379 let _ = finish_bootstrap_load(load.await).await;
382 drop(guard);
383 });
384 return Ok(());
385 }
386
387 debug!("Loading RDAP bootstrap data");
388 let outcome = finish_bootstrap_load(load.await).await;
389 drop(guard);
392 outcome
393}
394
395#[derive(Debug, Clone)]
396pub struct RdapClient {
397 retry_policy: RetryPolicy,
398 timeout: Duration,
400 allow_reserved: bool,
406}
407
408impl Default for RdapClient {
409 fn default() -> Self {
410 Self::new()
411 }
412}
413
414impl RdapClient {
415 pub fn new() -> Self {
417 Self {
418 retry_policy: RetryPolicy::new()
425 .with_max_attempts(3)
426 .with_initial_delay(Duration::from_millis(500))
427 .with_max_delay(Duration::from_secs(5)),
428 timeout: DEFAULT_TIMEOUT,
429 allow_reserved: false,
430 }
431 }
432
433 pub fn from_config(config: &crate::config::SeerConfig) -> Self {
440 Self::new().with_timeout(config.rdap_timeout())
441 }
442
443 pub fn with_timeout(mut self, timeout: Duration) -> Self {
445 self.timeout = timeout;
446 self
447 }
448
449 #[cfg(test)]
451 pub(crate) fn allowing_reserved_for_tests(mut self) -> Self {
452 self.allow_reserved = true;
453 self
454 }
455
456 pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
460 self.retry_policy = policy;
461 self
462 }
463
464 pub fn without_retries(mut self) -> Self {
466 self.retry_policy = RetryPolicy::no_retry();
467 self
468 }
469
470 async fn ensure_bootstrap(&self) -> Result<()> {
477 ensure_bootstrap_with(load_bootstrap_data_with_retry(self.retry_policy.clone())).await
478 }
479
480 fn get_rdap_urls_for_domain(cache: &BootstrapData, domain: &str) -> Option<Arc<Vec<url::Url>>> {
482 let tld = domain.rsplit('.').next()?;
483 cache.dns.get(&tld.to_lowercase()).cloned()
484 }
485
486 fn get_rdap_urls_for_ip(cache: &BootstrapData, ip: IpAddr) -> Option<Arc<Vec<url::Url>>> {
488 let ranges = match ip {
489 IpAddr::V4(_) => &cache.ipv4,
490 IpAddr::V6(_) => &cache.ipv6,
491 };
492 ranges
493 .iter()
494 .find(|(range, _)| ip_matches_prefix(&range.prefix, ip))
495 .map(|(_, urls)| Arc::clone(urls))
496 }
497
498 fn get_rdap_urls_for_asn(cache: &BootstrapData, asn: u32) -> Option<Arc<Vec<url::Url>>> {
500 cache
501 .asn
502 .iter()
503 .find(|(range, _)| (range.start..=range.end).contains(&asn))
504 .map(|(_, urls)| Arc::clone(urls))
505 }
506
507 async fn candidate_urls(
512 pick: impl FnOnce(&BootstrapData) -> Option<Arc<Vec<url::Url>>>,
513 what: &str,
514 path: &str,
515 ) -> Result<Vec<url::Url>> {
516 let cache_guard = BOOTSTRAP_CACHE.read().await;
517 let cache = cache_guard.as_ref().ok_or_else(|| {
518 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
519 })?;
520 let bases = pick(&cache.data)
521 .ok_or_else(|| SeerError::RdapBootstrapError(format!("no RDAP server for {}", what)))?;
522 Ok(build_rdap_urls(&bases, path))
523 }
524
525 #[instrument(skip(self), fields(domain = %domain))]
529 pub async fn lookup_domain(&self, domain: &str) -> Result<RdapResponse> {
530 self.ensure_bootstrap().await?;
531
532 let domain = normalize_domain(domain)?;
533 let urls = Self::candidate_urls(
534 |data| Self::get_rdap_urls_for_domain(data, &domain),
535 &domain,
536 &format!("domain/{}", domain),
537 )
538 .await?;
539 self.query_rdap_urls(&urls).await
540 }
541
542 #[instrument(skip(self), fields(ip = %ip))]
546 pub async fn lookup_ip(&self, ip: &str) -> Result<RdapResponse> {
547 self.ensure_bootstrap().await?;
548
549 let ip_addr: IpAddr = ip
550 .parse()
551 .map_err(|_| SeerError::InvalidIpAddress(ip.to_string()))?;
552 let urls = Self::candidate_urls(
553 |data| Self::get_rdap_urls_for_ip(data, ip_addr),
554 ip,
555 &format!("ip/{}", ip),
556 )
557 .await?;
558 self.query_rdap_urls(&urls).await
559 }
560
561 #[instrument(skip(self), fields(asn = %asn))]
565 pub async fn lookup_asn(&self, asn: u32) -> Result<RdapResponse> {
566 self.ensure_bootstrap().await?;
567 let urls = Self::candidate_urls(
568 |data| Self::get_rdap_urls_for_asn(data, asn),
569 &format!("AS{}", asn),
570 &format!("autnum/{}", asn),
571 )
572 .await?;
573 self.query_rdap_urls(&urls).await
574 }
575
576 #[instrument(skip(self), fields(tld = %tld))]
582 pub async fn get_rdap_base_url_for_tld(&self, tld: &str) -> Option<String> {
583 if self.ensure_bootstrap().await.is_err() {
584 return None;
585 }
586
587 let cache_guard = BOOTSTRAP_CACHE.read().await;
588 let cache = cache_guard.as_ref()?;
589 let lower = tld.to_lowercase();
593 let key = crate::validation::domain_to_ascii(&lower).unwrap_or(lower);
594 cache
595 .data
596 .dns
597 .get(&key)
598 .and_then(|urls| urls.first())
599 .map(|u| u.to_string())
600 }
601
602 async fn query_rdap_urls(&self, urls: &[url::Url]) -> Result<RdapResponse> {
606 if urls.is_empty() {
607 return Err(SeerError::RdapError(
608 "no candidate RDAP URLs available".to_string(),
609 ));
610 }
611
612 let mut last_error: Option<SeerError> = None;
613 let mut not_found_error: Option<SeerError> = None;
619 for (idx, url) in urls.iter().enumerate() {
620 let url_str = url.as_str().to_string();
621 debug!(url = %url_str, candidate = idx + 1, total = urls.len(), "Querying RDAP");
622 match self.query_rdap_with_retry(&url_str).await {
623 Ok(resp) => return Ok(resp),
624 Err(e) => {
625 if urls.len() > 1 {
626 debug!(
627 url = %url_str,
628 error = %e,
629 candidate = idx + 1,
630 total = urls.len(),
631 "RDAP candidate failed, trying next",
632 );
633 }
634 if not_found_error.is_none() && crate::rdap::rdap_error_is_404(&e) {
635 not_found_error = Some(e);
636 } else {
637 last_error = Some(e);
638 }
639 }
640 }
641 }
642
643 Err(wrap_all_candidates_failed(
646 not_found_error.or(last_error),
647 urls.len(),
648 ))
649 }
650
651 async fn query_rdap_with_retry(&self, url: &str) -> Result<RdapResponse> {
658 RetryExecutor::new(self.retry_policy.clone())
659 .execute_with_delay_hint(|| query_rdap_attempt(url, self.timeout, self.allow_reserved))
660 .await
661 }
662}
663
664const MAX_RDAP_RESPONSE_SIZE: usize = 10 * 1024 * 1024;
666
667fn parse_rdap_url(url: &str) -> Result<(String, u16)> {
677 let parsed = url::Url::parse(url)
678 .map_err(|e| SeerError::RdapError(format!("invalid URL '{}': {}", url, e)))?;
679 if parsed.scheme() != "https" {
680 return Err(SeerError::RdapError(format!(
681 "RDAP URL '{}' is not https — request blocked (downgrade/SSRF protection)",
682 url
683 )));
684 }
685 let host = crate::net::url_host(&parsed)
686 .ok_or_else(|| SeerError::RdapError(format!("URL '{}' has no host", url)))?;
687 let port = parsed.port_or_known_default().unwrap_or(443);
688 Ok((host, port))
689}
690
691async fn resolve_rdap_host(host: &str, port: u16) -> Result<Vec<SocketAddr>> {
703 crate::net::resolve_public_host(host, port)
704 .await
705 .map_err(|e| SeerError::RdapError(format!("{} — request blocked (SSRF protection)", e)))
706}
707
708#[cfg(test)]
713async fn validate_url_not_reserved(url: &str) -> Result<Vec<SocketAddr>> {
714 let (host, port) = parse_rdap_url(url)?;
715 resolve_rdap_host(&host, port).await
716}
717
718fn parse_retry_after(value: &str) -> Option<Duration> {
723 value.trim().parse::<u64>().ok().map(Duration::from_secs)
724}
725
726const PINNED_CLIENT_TTL: Duration = Duration::from_secs(60);
733
734type PinnedClientKey = (String, u16, Duration);
739
740static PINNED_CLIENT_CACHE: LazyLock<crate::cache::TtlCache<PinnedClientKey, Client>> =
749 LazyLock::new(|| crate::cache::TtlCache::with_max_capacity(PINNED_CLIENT_TTL, 64));
750
751fn rdap_client_builder(timeout: Duration) -> reqwest::ClientBuilder {
759 crate::net::client_builder(timeout)
760 .connect_timeout(CONNECT_TIMEOUT.min(timeout))
761 .user_agent(RDAP_USER_AGENT)
762}
763
764async fn send_rdap_request(
780 url: &str,
781 timeout: Duration,
782 allow_reserved: bool,
783) -> Result<reqwest::Response> {
784 if allow_reserved && is_loopback_url(url) {
785 let client = rdap_client_builder(timeout)
789 .build()
790 .map_err(|e| SeerError::RdapError(format!("failed to build HTTP client: {}", e)))?;
791 return client
792 .get(url)
793 .header(reqwest::header::ACCEPT, RDAP_ACCEPT)
794 .send()
795 .await
796 .map_err(Into::into);
797 }
798
799 let (host, port) = parse_rdap_url(url)?;
801
802 let key = (host.clone(), port, timeout);
803 let client = match PINNED_CLIENT_CACHE.get(&key) {
804 Some(client) => client,
805 None => {
806 let resolved = resolve_rdap_host(&host, port).await?;
811 let client = rdap_client_builder(timeout)
812 .resolve_to_addrs(&host, &resolved)
813 .build()
814 .map_err(|e| SeerError::RdapError(format!("failed to build HTTP client: {}", e)))?;
815 PINNED_CLIENT_CACHE.insert(key.clone(), client.clone());
816 client
817 }
818 };
819
820 match client
821 .get(url)
822 .header(reqwest::header::ACCEPT, RDAP_ACCEPT)
823 .send()
824 .await
825 {
826 Ok(resp) => Ok(resp),
827 Err(e) => {
828 if e.is_connect() {
833 PINNED_CLIENT_CACHE.remove(&key);
834 }
835 Err(e.into())
836 }
837 }
838}
839
840async fn read_and_parse_rdap_body(
845 response: reqwest::Response,
846 url: &str,
847 timeout: Duration,
848) -> Result<RdapResponse> {
849 let body = read_body_capped(response, MAX_RDAP_RESPONSE_SIZE, timeout, Overflow::Reject)
854 .await
855 .map_err(|e| match e {
856 BodyReadError::Chunk(e) => {
857 SeerError::RdapError(format!("failed to read response: {}", e))
858 }
859 BodyReadError::TooLarge => SeerError::RdapError(format!(
860 "RDAP response exceeds {} byte limit",
861 MAX_RDAP_RESPONSE_SIZE
862 )),
863 BodyReadError::TimedOut => SeerError::Timeout(format!(
864 "timed out reading RDAP response body from {} after {:?}",
865 url, timeout
866 )),
867 })?;
868
869 let rdap: RdapResponse = serde_json::from_slice(&body)?;
870 rdap.validate()?;
876 Ok(rdap)
877}
878
879fn is_loopback_url(url: &str) -> bool {
882 match url::Url::parse(url).ok().as_ref().and_then(url::Url::host) {
883 Some(url::Host::Ipv4(ip)) => ip.is_loopback(),
884 Some(url::Host::Ipv6(ip)) => ip.is_loopback(),
885 _ => false,
886 }
887}
888
889fn redirect_location(response: &reqwest::Response) -> Option<&str> {
894 match response.status().as_u16() {
895 301 | 302 | 303 | 307 | 308 => response
896 .headers()
897 .get(reqwest::header::LOCATION)
898 .and_then(|v| v.to_str().ok()),
899 _ => None,
900 }
901}
902
903fn resolve_redirect_target(current: &str, location: &str) -> Result<String> {
908 url::Url::parse(current)
909 .and_then(|base| base.join(location))
910 .map(String::from)
911 .map_err(|e| SeerError::RdapError(format!("invalid RDAP redirect target: {}", e)))
912}
913
914async fn query_rdap_attempt(
926 url: &str,
927 timeout: Duration,
928 allow_reserved: bool,
929) -> std::result::Result<RdapResponse, (SeerError, Option<Duration>)> {
930 let mut current = url.to_string();
931 let mut visited: HashSet<String> = HashSet::new();
932 let response = loop {
933 visited.insert(current.clone());
934 let response = send_rdap_request(¤t, timeout, allow_reserved)
935 .await
936 .map_err(|e| (e, None))?;
937 let Some(location) = redirect_location(&response) else {
938 break response;
939 };
940 if visited.len() > MAX_RDAP_REDIRECTS {
941 return Err((
942 SeerError::RdapError(format!(
943 "RDAP query exceeded {} redirects",
944 MAX_RDAP_REDIRECTS
945 )),
946 None,
947 ));
948 }
949 let next = resolve_redirect_target(¤t, location).map_err(|e| (e, None))?;
950 if visited.contains(&next) {
951 return Err((
952 SeerError::RdapError("RDAP redirect loop detected".to_string()),
953 None,
954 ));
955 }
956 debug!(from = %current, to = %next, "Following RDAP redirect");
957 current = next;
958 };
959
960 if !response.status().is_success() {
961 let status = response.status();
962 let retry_after = if status.as_u16() == 429 {
965 response
966 .headers()
967 .get(reqwest::header::RETRY_AFTER)
968 .and_then(|v| v.to_str().ok())
969 .and_then(parse_retry_after)
970 } else {
971 None
972 };
973 let error = if status.as_u16() == 429 {
977 SeerError::RateLimited(format!("RDAP query failed with status {}", status))
978 } else {
979 SeerError::RdapError(format!("query failed with status {}", status))
980 };
981 return Err((error, retry_after));
982 }
983
984 read_and_parse_rdap_body(response, ¤t, timeout)
985 .await
986 .map_err(|e| (e, None))
987}
988
989async fn load_bootstrap_data_with_retry(policy: RetryPolicy) -> Result<BootstrapLoad> {
993 RetryExecutor::new(policy)
994 .execute(load_bootstrap_data)
995 .await
996}
997
998async fn load_bootstrap_data() -> Result<BootstrapLoad> {
1001 debug!("Loading RDAP bootstrap data from IANA");
1002
1003 let http = rdap_http_client()?;
1008
1009 let dns_future = http.get(IANA_BOOTSTRAP_DNS).send();
1010 let ipv4_future = http.get(IANA_BOOTSTRAP_IPV4).send();
1011 let ipv6_future = http.get(IANA_BOOTSTRAP_IPV6).send();
1012 let asn_future = http.get(IANA_BOOTSTRAP_ASN).send();
1013
1014 let (dns_resp, ipv4_resp, ipv6_resp, asn_resp) =
1017 tokio::join!(dns_future, ipv4_future, ipv6_future, asn_future);
1018
1019 async fn read_bootstrap(resp: reqwest::Response) -> Result<BootstrapResponse> {
1020 let body = read_body_capped(resp, MAX_BOOTSTRAP_SIZE, DEFAULT_TIMEOUT, Overflow::Reject)
1025 .await
1026 .map_err(|e| match e {
1027 BodyReadError::Chunk(e) => {
1028 SeerError::RdapBootstrapError(format!("failed to read body: {}", e))
1029 }
1030 BodyReadError::TooLarge => SeerError::RdapBootstrapError(format!(
1031 "bootstrap response too large (exceeds {} bytes)",
1032 MAX_BOOTSTRAP_SIZE
1033 )),
1034 BodyReadError::TimedOut => SeerError::Timeout(format!(
1035 "RDAP bootstrap body read timed out after {:?}",
1036 DEFAULT_TIMEOUT
1037 )),
1038 })?;
1039
1040 serde_json::from_slice(&body).map_err(Into::into)
1041 }
1042
1043 let dns_data = match dns_resp {
1045 Ok(resp) => match read_bootstrap(resp).await {
1046 Ok(data) => Some(data),
1047 Err(e) => {
1048 warn!(error = %e, "Failed to parse DNS bootstrap response");
1049 None
1050 }
1051 },
1052 Err(e) => {
1053 warn!(error = %e, "Failed to fetch DNS bootstrap from IANA");
1054 None
1055 }
1056 };
1057 let ipv4_data = match ipv4_resp {
1058 Ok(resp) => match read_bootstrap(resp).await {
1059 Ok(data) => Some(data),
1060 Err(e) => {
1061 warn!(error = %e, "Failed to parse IPv4 bootstrap response");
1062 None
1063 }
1064 },
1065 Err(e) => {
1066 warn!(error = %e, "Failed to fetch IPv4 bootstrap from IANA");
1067 None
1068 }
1069 };
1070 let ipv6_data = match ipv6_resp {
1071 Ok(resp) => match read_bootstrap(resp).await {
1072 Ok(data) => Some(data),
1073 Err(e) => {
1074 warn!(error = %e, "Failed to parse IPv6 bootstrap response");
1075 None
1076 }
1077 },
1078 Err(e) => {
1079 warn!(error = %e, "Failed to fetch IPv6 bootstrap from IANA");
1080 None
1081 }
1082 };
1083 let asn_data = match asn_resp {
1084 Ok(resp) => match read_bootstrap(resp).await {
1085 Ok(data) => Some(data),
1086 Err(e) => {
1087 warn!(error = %e, "Failed to parse ASN bootstrap response");
1088 None
1089 }
1090 },
1091 Err(e) => {
1092 warn!(error = %e, "Failed to fetch ASN bootstrap from IANA");
1093 None
1094 }
1095 };
1096
1097 fn collect_valid_urls(urls: &[serde_json::Value]) -> Option<Arc<Vec<url::Url>>> {
1101 let mut out = Vec::new();
1102 for u in urls {
1103 if let Some(s) = u.as_str() {
1104 match validate_bootstrap_url(s) {
1105 Ok(parsed) => out.push(parsed),
1106 Err(e) => {
1107 debug!(url = s, error = %e, "Skipping invalid bootstrap URL");
1108 }
1109 }
1110 }
1111 }
1112 if out.is_empty() {
1113 None
1114 } else {
1115 Some(Arc::new(out))
1116 }
1117 }
1118
1119 fn parse_prefix_services(data: BootstrapResponse) -> Vec<(IpRange, Arc<Vec<url::Url>>)> {
1121 let mut out = Vec::new();
1122 for service in data.services {
1123 if service.len() >= 2 {
1124 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array())
1125 {
1126 if let Some(urls_arc) = collect_valid_urls(urls) {
1127 for prefix in prefixes {
1128 if let Some(prefix_str) = prefix.as_str() {
1129 out.push((
1130 IpRange {
1131 prefix: prefix_str.to_string(),
1132 },
1133 Arc::clone(&urls_arc),
1134 ));
1135 }
1136 }
1137 }
1138 }
1139 }
1140 }
1141 out
1142 }
1143
1144 let dns = dns_data
1150 .map(|data| {
1151 let mut dns = HashMap::new();
1152 for service in data.services {
1153 if service.len() >= 2 {
1154 if let (Some(tlds), Some(urls)) = (service[0].as_array(), service[1].as_array())
1155 {
1156 if let Some(urls_arc) = collect_valid_urls(urls) {
1157 for tld in tlds {
1158 if let Some(tld_str) = tld.as_str() {
1159 dns.insert(tld_str.to_lowercase(), Arc::clone(&urls_arc));
1160 }
1161 }
1162 }
1163 }
1164 }
1165 }
1166 dns
1167 })
1168 .filter(|dns| !dns.is_empty());
1169
1170 let ipv4 = ipv4_data
1172 .map(parse_prefix_services)
1173 .filter(|v| !v.is_empty());
1174 let ipv6 = ipv6_data
1175 .map(parse_prefix_services)
1176 .filter(|v| !v.is_empty());
1177
1178 let asn = asn_data
1180 .map(|data| {
1181 let mut asn = Vec::new();
1182 for service in data.services {
1183 if service.len() >= 2 {
1184 if let (Some(ranges), Some(urls)) =
1185 (service[0].as_array(), service[1].as_array())
1186 {
1187 if let Some(urls_arc) = collect_valid_urls(urls) {
1188 for range in ranges {
1189 if let Some(range_str) = range.as_str() {
1190 if let Some((start, end)) = parse_asn_range(range_str) {
1191 asn.push((AsnRange { start, end }, Arc::clone(&urls_arc)));
1192 }
1193 }
1194 }
1195 }
1196 }
1197 }
1198 }
1199 asn
1200 })
1201 .filter(|v| !v.is_empty());
1202
1203 if dns.is_none() && ipv4.is_none() && ipv6.is_none() && asn.is_none() {
1207 return Err(SeerError::RdapBootstrapError(
1208 "all IANA bootstrap registries failed".to_string(),
1209 ));
1210 }
1211
1212 info!(
1213 dns_entries = dns.as_ref().map_or(0, HashMap::len),
1214 ipv4_ranges = ipv4.as_ref().map_or(0, Vec::len),
1215 ipv6_ranges = ipv6.as_ref().map_or(0, Vec::len),
1216 asn_ranges = asn.as_ref().map_or(0, Vec::len),
1217 "RDAP bootstrap loaded"
1218 );
1219
1220 Ok(BootstrapLoad {
1221 dns,
1222 ipv4,
1223 ipv6,
1224 asn,
1225 })
1226}
1227
1228fn wrap_all_candidates_failed(last_error: Option<SeerError>, candidate_count: usize) -> SeerError {
1238 let last = last_error.unwrap_or_else(|| SeerError::RdapError("no candidates".to_string()));
1239
1240 if candidate_count <= 1 {
1241 return last;
1242 }
1243
1244 match last {
1245 SeerError::Timeout(msg) => SeerError::Timeout(format!(
1246 "all {} RDAP candidate URLs timed out; last error: {}",
1247 candidate_count, msg
1248 )),
1249 SeerError::RetryExhausted { ref last_error, .. }
1250 if matches!(**last_error, SeerError::Timeout(_)) =>
1251 {
1252 SeerError::Timeout(format!(
1253 "all {} RDAP candidate URLs timed out; last error: {}",
1254 candidate_count, last
1255 ))
1256 }
1257 SeerError::RateLimited(_) => SeerError::RateLimited(format!(
1258 "all {} RDAP candidate URLs failed; last error: {}",
1259 candidate_count, last
1260 )),
1261 SeerError::RetryExhausted { ref last_error, .. }
1262 if matches!(**last_error, SeerError::RateLimited(_)) =>
1263 {
1264 SeerError::RateLimited(format!(
1265 "all {} RDAP candidate URLs failed; last error: {}",
1266 candidate_count, last
1267 ))
1268 }
1269 other => SeerError::RdapError(format!(
1270 "all {} RDAP candidate URLs failed; last error: {}",
1271 candidate_count, other
1272 )),
1273 }
1274}
1275
1276fn build_rdap_urls(bases: &[url::Url], path: &str) -> Vec<url::Url> {
1278 bases
1279 .iter()
1280 .filter_map(|base| {
1281 let base_str = base.as_str();
1284 let normalized = if base_str.ends_with('/') {
1285 base_str.to_string()
1286 } else {
1287 format!("{}/", base_str)
1288 };
1289 url::Url::parse(&normalized).and_then(|u| u.join(path)).ok()
1290 })
1291 .collect()
1292}
1293
1294#[cfg(test)]
1295mod tests {
1296 use super::*;
1297
1298 #[test]
1299 fn from_config_applies_rdap_timeout() {
1300 let mut config = crate::config::SeerConfig::default();
1301 config.timeouts.rdap_secs = 33;
1302 let client = RdapClient::from_config(&config);
1303 assert_eq!(client.timeout, Duration::from_secs(33));
1304 }
1305
1306 #[test]
1307 fn test_default_client_has_retry_policy() {
1308 let client = RdapClient::new();
1309 assert_eq!(client.retry_policy.max_attempts, 3);
1313 }
1314
1315 #[test]
1318 fn parse_retry_after_parses_delta_seconds() {
1319 assert_eq!(parse_retry_after("5"), Some(Duration::from_secs(5)));
1320 assert_eq!(parse_retry_after(" 10 "), Some(Duration::from_secs(10)));
1321 assert_eq!(parse_retry_after("0"), Some(Duration::from_secs(0)));
1322 }
1323
1324 #[test]
1325 fn parse_retry_after_rejects_http_date_and_junk() {
1326 assert_eq!(parse_retry_after("Wed, 21 Oct 2015 07:28:00 GMT"), None);
1329 assert_eq!(parse_retry_after("soon"), None);
1330 assert_eq!(parse_retry_after(""), None);
1331 }
1332
1333 #[test]
1334 fn default_policy_caps_retry_after_at_five_seconds() {
1335 assert_eq!(
1338 RdapClient::new().retry_policy.max_delay,
1339 Duration::from_secs(5)
1340 );
1341 }
1342
1343 #[test]
1344 fn test_client_without_retries() {
1345 let client = RdapClient::new().without_retries();
1346 assert_eq!(client.retry_policy.max_attempts, 1);
1347 }
1348
1349 #[test]
1350 fn test_client_custom_retry_policy() {
1351 let policy = RetryPolicy::new().with_max_attempts(5);
1352 let client = RdapClient::new().with_retry_policy(policy);
1353 assert_eq!(client.retry_policy.max_attempts, 5);
1354 }
1355
1356 #[test]
1357 fn test_cached_bootstrap_expiration() {
1358 let data = BootstrapData {
1359 dns: HashMap::new(),
1360 ipv4: Vec::new(),
1361 ipv6: Vec::new(),
1362 asn: Vec::new(),
1363 };
1364 let cached = CachedBootstrap::new(data);
1365 assert!(!cached.is_expired());
1367 }
1368
1369 #[test]
1370 fn test_rdap_http_client_is_configured() {
1371 let client = rdap_http_client();
1374 assert!(client.is_ok(), "RDAP HTTP client builder must succeed");
1375 }
1376
1377 #[test]
1378 fn test_parse_bootstrap_empty_services() {
1379 let data = BootstrapData {
1381 dns: HashMap::new(),
1382 ipv4: Vec::new(),
1383 ipv6: Vec::new(),
1384 asn: Vec::new(),
1385 };
1386 assert!(RdapClient::get_rdap_urls_for_domain(&data, "example.com").is_none());
1388 assert!(RdapClient::get_rdap_urls_for_asn(&data, 12345).is_none());
1389 }
1390
1391 #[tokio::test]
1394 async fn test_validate_url_not_reserved_rejects_loopback_literal() {
1395 let err = validate_url_not_reserved("https://127.0.0.1/domain/example.com")
1396 .await
1397 .unwrap_err();
1398 assert!(
1399 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved")),
1400 "expected reserved-IP error, got: {:?}",
1401 err
1402 );
1403 }
1404
1405 #[tokio::test]
1406 async fn test_validate_url_not_reserved_rejects_private_ipv4_literal() {
1407 let err = validate_url_not_reserved("https://10.0.0.1/")
1408 .await
1409 .unwrap_err();
1410 assert!(
1411 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved")),
1412 "expected reserved-IP error, got: {:?}",
1413 err
1414 );
1415 }
1416
1417 #[tokio::test]
1418 async fn test_validate_url_not_reserved_rejects_non_https_scheme() {
1419 let err = validate_url_not_reserved("http://93.184.216.34/domain/example.com")
1423 .await
1424 .unwrap_err();
1425 assert!(
1426 matches!(err, SeerError::RdapError(ref s) if s.contains("not https")),
1427 "expected non-https rejection, got: {:?}",
1428 err
1429 );
1430 }
1431
1432 #[tokio::test]
1433 async fn test_validate_url_not_reserved_rejects_ipv6_loopback_literal() {
1434 let err = validate_url_not_reserved("https://[::1]/")
1437 .await
1438 .unwrap_err();
1439 assert!(
1440 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved")),
1441 "expected reserved-IP error, got: {:?}",
1442 err
1443 );
1444 }
1445
1446 #[tokio::test]
1447 async fn test_validate_url_not_reserved_returns_resolved_addrs_for_public_literal() {
1448 let addrs = validate_url_not_reserved("https://8.8.8.8/").await.unwrap();
1451 assert_eq!(addrs.len(), 1);
1452 assert!(addrs[0].ip().is_ipv4());
1453 assert_eq!(addrs[0].port(), 443);
1454 }
1455
1456 #[test]
1459 fn test_build_rdap_urls_preserves_order_and_appends_path() {
1460 let bases = vec![
1461 url::Url::parse("https://rdap.a.example/").unwrap(),
1462 url::Url::parse("https://rdap.b.example").unwrap(), ];
1464 let built = build_rdap_urls(&bases, "domain/example.com");
1465 assert_eq!(built.len(), 2);
1466 assert_eq!(
1467 built[0].as_str(),
1468 "https://rdap.a.example/domain/example.com"
1469 );
1470 assert_eq!(
1471 built[1].as_str(),
1472 "https://rdap.b.example/domain/example.com"
1473 );
1474 }
1475
1476 #[test]
1477 fn test_build_rdap_urls_empty_input_returns_empty() {
1478 let built = build_rdap_urls(&[], "domain/example.com");
1479 assert!(built.is_empty());
1480 }
1481
1482 #[test]
1485 fn test_wrap_all_candidates_failed_preserves_timeout_variant() {
1486 let last = SeerError::Timeout("body read timed out".to_string());
1489 let wrapped = wrap_all_candidates_failed(Some(last), 3);
1490 match wrapped {
1491 SeerError::Timeout(msg) => {
1492 assert!(
1493 msg.contains("all 3 RDAP candidate URLs timed out"),
1494 "expected wrapped timeout message, got: {}",
1495 msg
1496 );
1497 assert!(
1498 msg.contains("body read timed out"),
1499 "expected original message preserved, got: {}",
1500 msg
1501 );
1502 }
1503 other => panic!(
1504 "expected SeerError::Timeout after wrapping a Timeout, got: {:?}",
1505 other
1506 ),
1507 }
1508 }
1509
1510 #[test]
1511 fn test_wrap_all_candidates_failed_preserves_retried_timeout() {
1512 let last = SeerError::RetryExhausted {
1515 attempts: 3,
1516 last_error: Box::new(SeerError::Timeout("request timed out".to_string())),
1517 };
1518 let wrapped = wrap_all_candidates_failed(Some(last), 2);
1519 assert!(
1520 matches!(wrapped, SeerError::Timeout(ref s)
1521 if s.contains("all 2 RDAP candidate URLs timed out")
1522 && s.contains("request timed out")),
1523 "expected wrapped Timeout, got: {wrapped:?}"
1524 );
1525 let last = SeerError::RetryExhausted {
1527 attempts: 3,
1528 last_error: Box::new(SeerError::RdapError("query failed with status 503".into())),
1529 };
1530 assert!(matches!(
1531 wrap_all_candidates_failed(Some(last), 2),
1532 SeerError::RdapError(_)
1533 ));
1534 }
1535
1536 #[test]
1537 fn test_wrap_all_candidates_failed_wraps_non_timeout_as_rdap_error() {
1538 let last = SeerError::RdapError("500 internal error".to_string());
1539 let wrapped = wrap_all_candidates_failed(Some(last), 2);
1540 assert!(
1541 matches!(wrapped, SeerError::RdapError(ref s) if s.contains("all 2 RDAP candidate URLs failed")),
1542 "expected wrapped RdapError, got: {:?}",
1543 wrapped
1544 );
1545 }
1546
1547 #[test]
1548 fn test_wrap_all_candidates_failed_single_candidate_returns_unchanged() {
1549 let last = SeerError::Timeout("single timeout".to_string());
1552 let wrapped = wrap_all_candidates_failed(Some(last), 1);
1553 assert!(
1554 matches!(wrapped, SeerError::Timeout(ref s) if s == "single timeout"),
1555 "expected unchanged Timeout, got: {:?}",
1556 wrapped
1557 );
1558 }
1559
1560 #[test]
1561 fn test_wrap_all_candidates_failed_no_last_error_returns_placeholder() {
1562 let wrapped = wrap_all_candidates_failed(None, 0);
1563 assert!(matches!(wrapped, SeerError::RdapError(_)));
1564 }
1565
1566 static BOOTSTRAP_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
1585
1586 #[tokio::test]
1587 async fn test_bootstrap_load_notify_wakes_waiter_when_cache_populated() {
1588 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1589
1590 {
1592 let mut cache = BOOTSTRAP_CACHE.write().await;
1593 *cache = None;
1594 }
1595
1596 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1599 tokio::pin!(notified);
1600
1601 {
1603 let mut cache = BOOTSTRAP_CACHE.write().await;
1604 *cache = Some(CachedBootstrap::new(BootstrapData {
1605 dns: HashMap::new(),
1606 ipv4: Vec::new(),
1607 ipv6: Vec::new(),
1608 asn: Vec::new(),
1609 }));
1610 }
1611 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1612
1613 let result = wait_for_in_flight_load(notified).await;
1614 assert!(
1615 result.is_ok(),
1616 "expected waiter to see populated cache, got: {:?}",
1617 result
1618 );
1619
1620 {
1622 let mut cache = BOOTSTRAP_CACHE.write().await;
1623 *cache = None;
1624 }
1625 }
1626
1627 #[tokio::test]
1628 async fn test_bootstrap_load_notify_empty_cache_after_wake_returns_error() {
1629 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1630
1631 {
1633 let mut cache = BOOTSTRAP_CACHE.write().await;
1634 *cache = None;
1635 }
1636
1637 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1638 tokio::pin!(notified);
1639
1640 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1642
1643 let result = wait_for_in_flight_load(notified).await;
1644 assert!(
1645 matches!(
1646 result,
1647 Err(SeerError::RdapBootstrapError(ref s))
1648 if s.contains("throttled and no cache available")
1649 ),
1650 "expected throttled error when cache still empty after notify, got: {:?}",
1651 result
1652 );
1653 }
1654
1655 use wiremock::matchers::method;
1665 use wiremock::{Mock, MockServer, ResponseTemplate};
1666
1667 #[tokio::test]
1668 async fn mock_rdap_404_is_nonretryable_typed_error() {
1669 let server = MockServer::start().await;
1670 Mock::given(method("GET"))
1671 .respond_with(ResponseTemplate::new(404))
1672 .mount(&server)
1673 .await;
1674
1675 let client = RdapClient::new()
1676 .without_retries()
1677 .allowing_reserved_for_tests();
1678 let err = client
1679 .query_rdap_with_retry(&format!("{}/domain/example.com", server.uri()))
1680 .await
1681 .unwrap_err();
1682 assert!(
1683 matches!(err, SeerError::RdapError(ref m) if m.contains("404")),
1684 "got: {err:?}"
1685 );
1686 }
1687
1688 #[tokio::test]
1689 async fn mock_rdap_429_honors_retry_after_and_succeeds() {
1690 let server = MockServer::start().await;
1691 Mock::given(method("GET"))
1694 .respond_with(ResponseTemplate::new(429).insert_header("Retry-After", "0"))
1695 .up_to_n_times(1)
1696 .mount(&server)
1697 .await;
1698 Mock::given(method("GET"))
1699 .respond_with(ResponseTemplate::new(200).set_body_raw(
1700 r#"{"objectClassName":"domain","handle":"MOCK-1"}"#,
1701 "application/rdap+json",
1702 ))
1703 .mount(&server)
1704 .await;
1705
1706 let client = RdapClient::new().allowing_reserved_for_tests();
1707 let resp = client
1708 .query_rdap_with_retry(&format!("{}/domain/example.com", server.uri()))
1709 .await
1710 .unwrap();
1711 assert_eq!(resp.handle.as_deref(), Some("MOCK-1"));
1712 }
1713
1714 #[tokio::test]
1717 async fn mock_rdap_persistent_429_is_rate_limited() {
1718 let server = MockServer::start().await;
1719 Mock::given(method("GET"))
1720 .respond_with(ResponseTemplate::new(429).insert_header("Retry-After", "0"))
1721 .mount(&server)
1722 .await;
1723
1724 let client = RdapClient::new().allowing_reserved_for_tests();
1725 let err = client
1726 .query_rdap_with_retry(&format!("{}/domain/example.com", server.uri()))
1727 .await
1728 .unwrap_err();
1729 let inner = match &err {
1730 SeerError::RetryExhausted { last_error, .. } => last_error.as_ref(),
1731 other => other,
1732 };
1733 assert!(matches!(inner, SeerError::RateLimited(_)), "got: {err:?}");
1734
1735 let wrapped = wrap_all_candidates_failed(Some(err), 2);
1737 assert!(
1738 matches!(wrapped, SeerError::RateLimited(_)),
1739 "got: {wrapped:?}"
1740 );
1741 }
1742
1743 #[tokio::test]
1744 async fn mock_rdap_malformed_body_is_parse_error_not_panic() {
1745 let server = MockServer::start().await;
1746 Mock::given(method("GET"))
1747 .respond_with(ResponseTemplate::new(200).set_body_raw("not json", "text/plain"))
1748 .mount(&server)
1749 .await;
1750
1751 let client = RdapClient::new()
1752 .without_retries()
1753 .allowing_reserved_for_tests();
1754 let err = client
1755 .query_rdap_with_retry(&format!("{}/domain/example.com", server.uri()))
1756 .await
1757 .unwrap_err();
1758 assert!(matches!(err, SeerError::JsonError(_)), "got: {err:?}");
1759 }
1760
1761 #[tokio::test]
1762 async fn mock_rdap_candidate_fallback_uses_second_url() {
1763 let bad = MockServer::start().await;
1764 Mock::given(method("GET"))
1765 .respond_with(ResponseTemplate::new(500))
1766 .mount(&bad)
1767 .await;
1768 let good = MockServer::start().await;
1769 Mock::given(method("GET"))
1770 .respond_with(ResponseTemplate::new(200).set_body_raw(
1771 r#"{"objectClassName":"domain","handle":"MOCK-2"}"#,
1772 "application/rdap+json",
1773 ))
1774 .mount(&good)
1775 .await;
1776
1777 let client = RdapClient::new()
1778 .without_retries()
1779 .allowing_reserved_for_tests();
1780 let urls = vec![
1781 url::Url::parse(&format!("{}/domain/example.com", bad.uri())).unwrap(),
1782 url::Url::parse(&format!("{}/domain/example.com", good.uri())).unwrap(),
1783 ];
1784 let resp = client.query_rdap_urls(&urls).await.unwrap();
1785 assert_eq!(resp.handle.as_deref(), Some("MOCK-2"));
1786 }
1787
1788 #[tokio::test]
1794 async fn mock_rdap_404_on_first_candidate_survives_later_non_404_failure() {
1795 let not_found = MockServer::start().await;
1796 Mock::given(method("GET"))
1797 .respond_with(ResponseTemplate::new(404))
1798 .mount(¬_found)
1799 .await;
1800 let broken = MockServer::start().await;
1801 Mock::given(method("GET"))
1802 .respond_with(ResponseTemplate::new(500))
1803 .mount(&broken)
1804 .await;
1805
1806 let client = RdapClient::new()
1807 .without_retries()
1808 .allowing_reserved_for_tests();
1809 let urls = vec![
1810 url::Url::parse(&format!("{}/domain/example.com", not_found.uri())).unwrap(),
1811 url::Url::parse(&format!("{}/domain/example.com", broken.uri())).unwrap(),
1812 ];
1813 let err = client.query_rdap_urls(&urls).await.unwrap_err();
1814 assert!(
1815 crate::rdap::rdap_error_is_404(&err),
1816 "404 from candidate 1 must survive candidate 2's non-404 failure, got: {err:?}"
1817 );
1818 }
1819
1820 #[tokio::test]
1831 async fn pinned_client_cache_hit_skips_dns_and_revalidation() {
1832 let host = "pinned-cache-hit.seer-test.invalid";
1833 let timeout = Duration::from_secs(2);
1834 let key = (host.to_string(), 443u16, timeout);
1835
1836 let pinned: Vec<SocketAddr> = vec!["127.0.0.1:443".parse().expect("valid addr")];
1841 let client = Client::builder()
1842 .timeout(timeout)
1843 .connect_timeout(timeout)
1844 .resolve_to_addrs(host, &pinned)
1845 .redirect(reqwest::redirect::Policy::none())
1846 .build()
1847 .expect("client builds");
1848 PINNED_CLIENT_CACHE.insert(key.clone(), client);
1849
1850 let err = send_rdap_request(
1851 &format!("https://{}/domain/example.com", host),
1852 timeout,
1853 false,
1854 )
1855 .await
1856 .unwrap_err();
1857 assert!(
1858 matches!(err, SeerError::ReqwestError { .. }),
1859 "cache hit must reach the connect stage (ReqwestError), not fail \
1860 SSRF validation (RdapError) — got: {err:?}"
1861 );
1862
1863 PINNED_CLIENT_CACHE.remove(&key);
1866 }
1867
1868 #[tokio::test]
1873 async fn test_mode_requests_never_populate_pinned_client_cache() {
1874 let server = MockServer::start().await;
1875 Mock::given(method("GET"))
1876 .respond_with(ResponseTemplate::new(200).set_body_raw(
1877 r#"{"objectClassName":"domain","handle":"MOCK-CACHE"}"#,
1878 "application/rdap+json",
1879 ))
1880 .mount(&server)
1881 .await;
1882
1883 let client = RdapClient::new()
1884 .without_retries()
1885 .allowing_reserved_for_tests();
1886 let uri = format!("{}/domain/example.com", server.uri());
1887 let resp = client.query_rdap_with_retry(&uri).await.unwrap();
1888 assert_eq!(resp.handle.as_deref(), Some("MOCK-CACHE"));
1889
1890 let parsed = url::Url::parse(&uri).unwrap();
1893 let host = parsed.host_str().unwrap().to_string();
1894 let port = parsed.port_or_known_default().unwrap();
1895 assert!(
1896 PINNED_CLIENT_CACHE
1897 .get(&(host, port, DEFAULT_TIMEOUT))
1898 .is_none(),
1899 "test-mode request must not populate the shared pinned-client cache"
1900 );
1901 }
1902
1903 use wiremock::matchers::path;
1906
1907 const MOCK_OK_BODY: &str = r#"{"objectClassName":"domain","handle":"MOCK-REDIRECT"}"#;
1908
1909 #[test]
1910 fn is_loopback_url_only_matches_loopback_literals() {
1911 assert!(is_loopback_url("http://127.0.0.1:8080/domain/x"));
1912 assert!(is_loopback_url("http://[::1]:8080/domain/x"));
1913 assert!(!is_loopback_url("https://10.0.0.1/domain/x"));
1914 assert!(!is_loopback_url("https://rdap.example.com/domain/x"));
1915 assert!(!is_loopback_url("not a url"));
1916 }
1917
1918 #[tokio::test]
1919 async fn mock_rdap_303_redirect_is_followed() {
1920 let target = MockServer::start().await;
1923 Mock::given(method("GET"))
1924 .and(path("/ip/192.36.148.17"))
1925 .respond_with(
1926 ResponseTemplate::new(200).set_body_raw(MOCK_OK_BODY, "application/rdap+json"),
1927 )
1928 .expect(1)
1929 .mount(&target)
1930 .await;
1931 let origin = MockServer::start().await;
1932 Mock::given(method("GET"))
1933 .respond_with(
1934 ResponseTemplate::new(303)
1935 .insert_header("Location", format!("{}/ip/192.36.148.17", target.uri())),
1936 )
1937 .mount(&origin)
1938 .await;
1939
1940 let client = RdapClient::new()
1941 .without_retries()
1942 .allowing_reserved_for_tests();
1943 let resp = client
1944 .query_rdap_with_retry(&format!("{}/ip/192.36.148.17", origin.uri()))
1945 .await
1946 .unwrap();
1947 assert_eq!(resp.handle.as_deref(), Some("MOCK-REDIRECT"));
1948 }
1949
1950 #[tokio::test]
1951 async fn mock_rdap_relative_redirect_is_resolved_against_current_url() {
1952 let server = MockServer::start().await;
1953 Mock::given(method("GET"))
1954 .and(path("/old/domain/example.com"))
1955 .respond_with(
1956 ResponseTemplate::new(301).insert_header("Location", "/new/domain/example.com"),
1957 )
1958 .mount(&server)
1959 .await;
1960 Mock::given(method("GET"))
1961 .and(path("/new/domain/example.com"))
1962 .respond_with(
1963 ResponseTemplate::new(200).set_body_raw(MOCK_OK_BODY, "application/rdap+json"),
1964 )
1965 .mount(&server)
1966 .await;
1967
1968 let client = RdapClient::new()
1969 .without_retries()
1970 .allowing_reserved_for_tests();
1971 let resp = client
1972 .query_rdap_with_retry(&format!("{}/old/domain/example.com", server.uri()))
1973 .await
1974 .unwrap();
1975 assert_eq!(resp.handle.as_deref(), Some("MOCK-REDIRECT"));
1976 }
1977
1978 async fn redirect_error(location: &str) -> SeerError {
1982 let server = MockServer::start().await;
1983 Mock::given(method("GET"))
1984 .respond_with(ResponseTemplate::new(302).insert_header("Location", location))
1985 .mount(&server)
1986 .await;
1987 let client = RdapClient::new()
1988 .without_retries()
1989 .allowing_reserved_for_tests();
1990 client
1991 .query_rdap_with_retry(&format!("{}/domain/example.com", server.uri()))
1992 .await
1993 .unwrap_err()
1994 }
1995
1996 #[tokio::test]
1997 async fn mock_rdap_redirect_to_plain_http_is_refused() {
1998 let err = redirect_error("http://93.184.216.34/domain/example.com").await;
2001 assert!(
2002 matches!(err, SeerError::RdapError(ref m) if m.contains("not https")),
2003 "got: {err:?}"
2004 );
2005 }
2006
2007 #[tokio::test]
2008 async fn mock_rdap_redirect_to_reserved_address_is_refused() {
2009 for location in [
2010 "https://10.0.0.1/domain/example.com",
2011 "https://169.254.169.254/latest/meta-data/",
2012 ] {
2013 let err = redirect_error(location).await;
2014 assert!(
2015 matches!(err, SeerError::RdapError(ref m) if m.contains("reserved")),
2016 "{location}: got {err:?}"
2017 );
2018 }
2019 }
2020
2021 #[tokio::test]
2022 async fn mock_rdap_redirect_loop_is_detected() {
2023 let server = MockServer::start().await;
2024 Mock::given(method("GET"))
2025 .and(path("/a"))
2026 .respond_with(ResponseTemplate::new(307).insert_header("Location", "/b"))
2027 .mount(&server)
2028 .await;
2029 Mock::given(method("GET"))
2030 .and(path("/b"))
2031 .respond_with(ResponseTemplate::new(307).insert_header("Location", "/a"))
2032 .mount(&server)
2033 .await;
2034 let client = RdapClient::new()
2035 .without_retries()
2036 .allowing_reserved_for_tests();
2037 let err = client
2038 .query_rdap_with_retry(&format!("{}/a", server.uri()))
2039 .await
2040 .unwrap_err();
2041 assert!(
2042 matches!(err, SeerError::RdapError(ref m) if m.contains("loop")),
2043 "got: {err:?}"
2044 );
2045 }
2046
2047 #[tokio::test]
2048 async fn mock_rdap_redirect_chain_is_capped() {
2049 let server = MockServer::start().await;
2050 for hop in 0..=MAX_RDAP_REDIRECTS {
2051 Mock::given(method("GET"))
2052 .and(path(format!("/hop{hop}")))
2053 .respond_with(
2054 ResponseTemplate::new(308)
2055 .insert_header("Location", format!("/hop{}", hop + 1)),
2056 )
2057 .mount(&server)
2058 .await;
2059 }
2060 let client = RdapClient::new()
2061 .without_retries()
2062 .allowing_reserved_for_tests();
2063 let err = client
2064 .query_rdap_with_retry(&format!("{}/hop0", server.uri()))
2065 .await
2066 .unwrap_err();
2067 assert!(
2068 matches!(err, SeerError::RdapError(ref m) if m.contains("exceeded")),
2069 "got: {err:?}"
2070 );
2071 }
2072
2073 #[tokio::test]
2074 async fn mock_rdap_sends_accept_header_with_json_fallback() {
2075 let server = MockServer::start().await;
2077 Mock::given(method("GET"))
2078 .and(|req: &wiremock::Request| {
2081 req.headers.get("accept").and_then(|v| v.to_str().ok()) == Some(RDAP_ACCEPT)
2082 })
2083 .respond_with(ResponseTemplate::new(200).set_body_raw(MOCK_OK_BODY, "application/json"))
2084 .mount(&server)
2085 .await;
2086 Mock::given(method("GET"))
2087 .respond_with(ResponseTemplate::new(406))
2088 .mount(&server)
2089 .await;
2090 let client = RdapClient::new()
2091 .without_retries()
2092 .allowing_reserved_for_tests();
2093 let resp = client
2094 .query_rdap_with_retry(&format!("{}/domain/example.sn", server.uri()))
2095 .await
2096 .unwrap();
2097 assert_eq!(resp.handle.as_deref(), Some("MOCK-REDIRECT"));
2098 assert!(RDAP_ACCEPT.contains("application/json"));
2099 }
2100
2101 fn url_arc(u: &str) -> Arc<Vec<url::Url>> {
2104 Arc::new(vec![url::Url::parse(u).expect("valid url")])
2105 }
2106
2107 fn full_load() -> BootstrapLoad {
2108 let mut dns = HashMap::new();
2109 dns.insert("com".to_string(), url_arc("https://rdap.verisign.example/"));
2110 BootstrapLoad {
2111 dns: Some(dns),
2112 ipv4: Some(vec![(
2113 IpRange {
2114 prefix: "8.0.0.0/8".to_string(),
2115 },
2116 url_arc("https://rdap.arin.example/"),
2117 )]),
2118 ipv6: Some(Vec::new()),
2119 asn: Some(vec![(
2120 AsnRange { start: 1, end: 10 },
2121 url_arc("https://rdap.arin.example/"),
2122 )]),
2123 }
2124 }
2125
2126 #[test]
2127 fn complete_bootstrap_load_gets_full_ttl() {
2128 let load = full_load();
2129 assert!(load.is_complete());
2130 assert_eq!(load.ttl(), BOOTSTRAP_TTL);
2131 }
2132
2133 #[test]
2134 fn partial_bootstrap_load_keeps_previous_section_and_short_ttl() {
2135 let mut previous = Some(CachedBootstrap::with_ttl(
2137 full_load().merge_over(None),
2138 Duration::ZERO,
2139 ));
2140 std::thread::sleep(Duration::from_millis(2));
2141 assert!(previous.as_ref().is_some_and(CachedBootstrap::is_expired));
2142
2143 let mut partial = full_load();
2145 partial.dns = None;
2146 assert!(!partial.is_complete());
2147 store_bootstrap_load(&mut previous, partial);
2148
2149 let cached = previous.expect("cache populated");
2150 assert_eq!(cached.ttl, BOOTSTRAP_PARTIAL_TTL);
2151 assert!(
2152 RdapClient::get_rdap_urls_for_domain(&cached.data, "example.com").is_some(),
2153 "the failed dns section must be carried over, not wiped"
2154 );
2155 assert!(RdapClient::get_rdap_urls_for_asn(&cached.data, 5).is_some());
2156 }
2157
2158 #[test]
2159 fn partial_cold_bootstrap_load_leaves_failed_section_empty() {
2160 let mut cache = None;
2161 let mut partial = full_load();
2162 partial.dns = None;
2163 store_bootstrap_load(&mut cache, partial);
2164 let cached = cache.expect("cache populated");
2165 assert_eq!(cached.ttl, BOOTSTRAP_PARTIAL_TTL);
2166 assert!(RdapClient::get_rdap_urls_for_domain(&cached.data, "example.com").is_none());
2167 }
2168
2169 #[test]
2170 fn bootstrap_store_does_not_overwrite_fresh_data() {
2171 let mut fresh = full_load();
2172 fresh.dns = Some(HashMap::from([(
2173 "org".to_string(),
2174 url_arc("https://rdap.pir.example/"),
2175 )]));
2176 let mut cache = Some(CachedBootstrap::new(fresh.merge_over(None)));
2177 store_bootstrap_load(&mut cache, full_load());
2178 let cached = cache.expect("cache populated");
2179 assert!(RdapClient::get_rdap_urls_for_domain(&cached.data, "example.org").is_some());
2180 assert!(RdapClient::get_rdap_urls_for_domain(&cached.data, "example.com").is_none());
2181 }
2182
2183 #[tokio::test]
2186 async fn wait_returns_immediately_when_no_load_is_in_flight() {
2187 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
2188 {
2189 let mut cache = BOOTSTRAP_CACHE.write().await;
2190 *cache = None;
2191 }
2192 assert!(!BOOTSTRAP_LOAD_IN_FLIGHT.load(Ordering::SeqCst));
2193
2194 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
2198 tokio::pin!(notified);
2199 let result =
2200 tokio::time::timeout(Duration::from_secs(2), wait_for_in_flight_load(notified))
2201 .await
2202 .expect("must not block when no load is in flight");
2203 assert!(
2204 matches!(result, Err(SeerError::RdapBootstrapError(ref s)) if s.contains("throttled")),
2205 "got: {result:?}"
2206 );
2207 }
2208
2209 #[tokio::test]
2210 async fn wait_blocks_until_in_flight_load_finishes() {
2211 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
2212 {
2213 let mut cache = BOOTSTRAP_CACHE.write().await;
2214 *cache = None;
2215 }
2216
2217 let load = BootstrapLoadGuard::start();
2218 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
2219 tokio::pin!(notified);
2220
2221 let loader = tokio::spawn(async move {
2222 tokio::time::sleep(Duration::from_millis(50)).await;
2223 {
2224 let mut cache = BOOTSTRAP_CACHE.write().await;
2225 *cache = Some(CachedBootstrap::new(full_load().merge_over(None)));
2226 }
2227 drop(load);
2229 });
2230
2231 let result =
2232 tokio::time::timeout(Duration::from_secs(5), wait_for_in_flight_load(notified))
2233 .await
2234 .expect("waiter must wake via the loader's notify");
2235 assert!(result.is_ok(), "got: {result:?}");
2236 loader.await.expect("loader joined");
2237 assert!(!BOOTSTRAP_LOAD_IN_FLIGHT.load(Ordering::SeqCst));
2238
2239 let mut cache = BOOTSTRAP_CACHE.write().await;
2240 *cache = None;
2241 }
2242
2243 async fn seed_stale_bootstrap() {
2245 *BOOTSTRAP_CACHE.write().await = Some(CachedBootstrap::with_ttl(
2246 full_load().merge_over(None),
2247 Duration::ZERO,
2248 ));
2249 *BOOTSTRAP_LAST_ATTEMPT.write().await = None;
2250 tokio::time::sleep(Duration::from_millis(2)).await;
2251 assert!(BOOTSTRAP_CACHE.read().await.as_ref().unwrap().is_expired());
2252 }
2253
2254 #[tokio::test]
2258 async fn stale_bootstrap_is_served_while_refreshing_in_background() {
2259 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
2260 seed_stale_bootstrap().await;
2261
2262 let (release, gate) = tokio::sync::oneshot::channel::<()>();
2263 let loads = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2264 let counted = loads.clone();
2265 let slow_load = async move {
2266 counted.fetch_add(1, Ordering::SeqCst);
2267 let _ = gate.await;
2268 Ok(full_load())
2269 };
2270
2271 tokio::time::timeout(Duration::from_secs(1), ensure_bootstrap_with(slow_load))
2273 .await
2274 .expect("a stale read must not wait for the refresh")
2275 .expect("stale data is served");
2276 assert!(BOOTSTRAP_LOAD_IN_FLIGHT.load(Ordering::SeqCst));
2277
2278 *BOOTSTRAP_LAST_ATTEMPT.write().await = None; let second = loads.clone();
2281 tokio::time::timeout(
2282 Duration::from_secs(1),
2283 ensure_bootstrap_with(async move {
2284 second.fetch_add(1, Ordering::SeqCst);
2285 Ok(full_load())
2286 }),
2287 )
2288 .await
2289 .expect("second stale read must not wait")
2290 .expect("stale data is served");
2291 tokio::task::yield_now().await;
2292 assert_eq!(loads.load(Ordering::SeqCst), 1, "one refresh at a time");
2293
2294 release.send(()).expect("refresh task is waiting");
2296 let refreshed = async {
2297 while BOOTSTRAP_LOAD_IN_FLIGHT.load(Ordering::SeqCst) {
2298 tokio::time::sleep(Duration::from_millis(5)).await;
2299 }
2300 };
2301 tokio::time::timeout(Duration::from_secs(5), refreshed)
2302 .await
2303 .expect("background refresh completes");
2304 {
2305 let cache = BOOTSTRAP_CACHE.read().await;
2306 let cached = cache.as_ref().expect("cache populated");
2307 assert!(!cached.is_expired());
2308 assert_eq!(cached.ttl, BOOTSTRAP_TTL);
2309 }
2310
2311 *BOOTSTRAP_CACHE.write().await = None;
2312 *BOOTSTRAP_LAST_ATTEMPT.write().await = None;
2313 }
2314
2315 #[tokio::test]
2317 async fn failed_background_refresh_keeps_stale_bootstrap() {
2318 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
2319 seed_stale_bootstrap().await;
2320
2321 ensure_bootstrap_with(async {
2322 Err(SeerError::RdapBootstrapError(
2323 "all IANA bootstrap registries failed".to_string(),
2324 ))
2325 })
2326 .await
2327 .expect("stale data is served");
2328 let settled = async {
2329 while BOOTSTRAP_LOAD_IN_FLIGHT.load(Ordering::SeqCst) {
2330 tokio::time::sleep(Duration::from_millis(5)).await;
2331 }
2332 };
2333 tokio::time::timeout(Duration::from_secs(5), settled)
2334 .await
2335 .expect("background refresh settles");
2336 assert!(
2337 BOOTSTRAP_CACHE.read().await.is_some(),
2338 "stale data survives a failed refresh"
2339 );
2340
2341 *BOOTSTRAP_CACHE.write().await = None;
2342 *BOOTSTRAP_LAST_ATTEMPT.write().await = None;
2343 }
2344}