1use std::collections::HashMap;
2use std::net::{IpAddr, SocketAddr};
3use std::sync::Arc;
4use std::time::{Duration, Instant};
5
6use futures::StreamExt;
7use once_cell::sync::Lazy;
8use reqwest::Client;
9use serde::Deserialize;
10use tokio::sync::{Notify, RwLock};
11use tracing::{debug, info, instrument, warn};
12
13use super::bootstrap::{
14 ipv4_matches_prefix, ipv6_matches_prefix, parse_asn_range, validate_bootstrap_url,
15};
16use super::types::RdapResponse;
17use crate::error::{Result, SeerError};
18use crate::retry::{NetworkRetryClassifier, RetryClassifier, RetryExecutor, RetryPolicy};
19use crate::validation::normalize_domain;
20
21const IANA_BOOTSTRAP_DNS: &str = "https://data.iana.org/rdap/dns.json";
22const IANA_BOOTSTRAP_IPV4: &str = "https://data.iana.org/rdap/ipv4.json";
23const IANA_BOOTSTRAP_IPV6: &str = "https://data.iana.org/rdap/ipv6.json";
24const IANA_BOOTSTRAP_ASN: &str = "https://data.iana.org/rdap/asn.json";
25
26const DEFAULT_TIMEOUT: Duration = Duration::from_secs(15);
31
32const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
35
36const BOOTSTRAP_TTL: Duration = Duration::from_secs(24 * 60 * 60);
38
39const BOOTSTRAP_REFRESH_MIN_INTERVAL: Duration = Duration::from_secs(60);
43
44static RDAP_HTTP_CLIENT: Lazy<Option<Client>> = Lazy::new(|| {
54 Client::builder()
55 .timeout(DEFAULT_TIMEOUT)
56 .connect_timeout(CONNECT_TIMEOUT)
57 .user_agent("Seer/1.0 (RDAP Client)")
58 .pool_max_idle_per_host(10)
59 .redirect(reqwest::redirect::Policy::none())
63 .build()
64 .ok()
65});
66
67fn rdap_http_client() -> Result<&'static Client> {
71 RDAP_HTTP_CLIENT
72 .as_ref()
73 .ok_or_else(|| SeerError::HttpError("failed to initialize HTTP client".into()))
74}
75
76static BOOTSTRAP_CACHE: Lazy<RwLock<Option<CachedBootstrap>>> = Lazy::new(|| RwLock::new(None));
78
79static BOOTSTRAP_LAST_ATTEMPT: Lazy<RwLock<Option<Instant>>> = Lazy::new(|| RwLock::new(None));
83
84static BOOTSTRAP_LOAD_NOTIFY: Lazy<Notify> = Lazy::new(Notify::new);
92
93struct CachedBootstrap {
95 data: BootstrapData,
96 loaded_at: Instant,
97}
98
99impl CachedBootstrap {
100 fn new(data: BootstrapData) -> Self {
101 Self {
102 data,
103 loaded_at: Instant::now(),
104 }
105 }
106
107 fn is_expired(&self) -> bool {
108 self.loaded_at.elapsed() > BOOTSTRAP_TTL
109 }
110
111 fn age(&self) -> Duration {
112 self.loaded_at.elapsed()
113 }
114}
115
116struct BootstrapData {
121 dns: HashMap<String, Arc<Vec<url::Url>>>,
122 ipv4: Vec<(IpRange, Arc<Vec<url::Url>>)>,
123 ipv6: Vec<(IpRange, Arc<Vec<url::Url>>)>,
124 asn: Vec<(AsnRange, Arc<Vec<url::Url>>)>,
125}
126
127#[derive(Clone)]
128struct IpRange {
129 prefix: String,
130}
131
132#[derive(Clone)]
133struct AsnRange {
134 start: u32,
135 end: u32,
136}
137
138#[derive(Deserialize)]
139struct BootstrapResponse {
140 services: Vec<Vec<serde_json::Value>>,
141}
142
143async fn wait_for_in_flight_load(
153 notified: std::pin::Pin<&mut tokio::sync::futures::Notified<'_>>,
154) -> Result<()> {
155 let _ = tokio::time::timeout(DEFAULT_TIMEOUT, notified).await;
158 let cache = BOOTSTRAP_CACHE.read().await;
159 if cache.is_some() {
160 Ok(())
161 } else {
162 Err(SeerError::RdapBootstrapError(
163 "bootstrap refresh throttled and no cache available".to_string(),
164 ))
165 }
166}
167
168#[derive(Debug, Clone)]
169pub struct RdapClient {
170 retry_policy: RetryPolicy,
171 timeout: Duration,
173 allow_reserved: bool,
177}
178
179impl Default for RdapClient {
180 fn default() -> Self {
181 Self::new()
182 }
183}
184
185impl RdapClient {
186 pub fn new() -> Self {
188 Self {
189 retry_policy: RetryPolicy::new()
196 .with_max_attempts(3)
197 .with_initial_delay(Duration::from_millis(500))
198 .with_max_delay(Duration::from_secs(5)),
199 timeout: DEFAULT_TIMEOUT,
200 allow_reserved: false,
201 }
202 }
203
204 pub fn from_config(config: &crate::config::SeerConfig) -> Self {
211 Self::new().with_timeout(config.rdap_timeout())
212 }
213
214 pub fn with_timeout(mut self, timeout: Duration) -> Self {
216 self.timeout = timeout;
217 self
218 }
219
220 #[cfg(test)]
222 pub(crate) fn allowing_reserved_for_tests(mut self) -> Self {
223 self.allow_reserved = true;
224 self
225 }
226
227 pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
231 self.retry_policy = policy;
232 self
233 }
234
235 pub fn without_retries(mut self) -> Self {
237 self.retry_policy = RetryPolicy::no_retry();
238 self
239 }
240
241 async fn ensure_bootstrap(&self) -> Result<()> {
256 {
258 let cache = BOOTSTRAP_CACHE.read().await;
259 if let Some(cached) = cache.as_ref() {
260 if !cached.is_expired() {
261 return Ok(());
262 }
263 }
264 }
265
266 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
272 tokio::pin!(notified);
273
274 {
278 let last = BOOTSTRAP_LAST_ATTEMPT.read().await;
279 if let Some(ts) = *last {
280 if ts.elapsed() < BOOTSTRAP_REFRESH_MIN_INTERVAL {
281 let cache = BOOTSTRAP_CACHE.read().await;
283 if cache.is_some() {
284 return Ok(());
286 }
287 drop(cache);
290 drop(last);
291 return wait_for_in_flight_load(notified).await;
292 }
293 }
294 }
295
296 {
299 let mut last = BOOTSTRAP_LAST_ATTEMPT.write().await;
300 if let Some(ts) = *last {
302 if ts.elapsed() < BOOTSTRAP_REFRESH_MIN_INTERVAL {
303 drop(last);
304 let cache = BOOTSTRAP_CACHE.read().await;
305 if cache.is_some() {
306 return Ok(());
307 }
308 drop(cache);
309 return wait_for_in_flight_load(notified).await;
310 }
311 }
312 *last = Some(Instant::now());
313 }
314
315 debug!("Loading/refreshing RDAP bootstrap data");
319 let load_result = load_bootstrap_data_with_retry(&self.retry_policy).await;
320
321 let outcome = match load_result {
322 Ok(data) => {
323 let mut cache = BOOTSTRAP_CACHE.write().await;
324 let should_store = cache.as_ref().map(|c| c.is_expired()).unwrap_or(true);
327 if should_store {
328 *cache = Some(CachedBootstrap::new(data));
329 }
330 Ok(())
331 }
332 Err(e) => {
333 let cache = BOOTSTRAP_CACHE.read().await;
335 if let Some(cached) = cache.as_ref() {
336 debug!(
337 error = %e,
338 age_hours = cached.age().as_secs() / 3600,
339 "Bootstrap refresh failed, using stale data"
340 );
341 Ok(())
342 } else {
343 Err(e)
345 }
346 }
347 };
348
349 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
351 outcome
352 }
353
354 fn get_rdap_urls_for_domain(cache: &BootstrapData, domain: &str) -> Option<Arc<Vec<url::Url>>> {
356 let tld = domain.rsplit('.').next()?;
357 cache.dns.get(&tld.to_lowercase()).cloned()
358 }
359
360 fn get_rdap_urls_for_ip(cache: &BootstrapData, ip: &IpAddr) -> Option<Arc<Vec<url::Url>>> {
362 match ip {
363 IpAddr::V4(addr) => {
364 for (range, urls) in &cache.ipv4 {
365 if ipv4_matches_prefix(&range.prefix, addr) {
366 return Some(Arc::clone(urls));
367 }
368 }
369 }
370 IpAddr::V6(addr) => {
371 for (range, urls) in &cache.ipv6 {
372 if ipv6_matches_prefix(&range.prefix, addr) {
373 return Some(Arc::clone(urls));
374 }
375 }
376 }
377 }
378
379 None
380 }
381
382 fn get_rdap_urls_for_asn(cache: &BootstrapData, asn: u32) -> Option<Arc<Vec<url::Url>>> {
384 for (range, urls) in &cache.asn {
385 if asn >= range.start && asn <= range.end {
386 return Some(Arc::clone(urls));
387 }
388 }
389
390 None
391 }
392
393 #[instrument(skip(self), fields(domain = %domain))]
397 pub async fn lookup_domain(&self, domain: &str) -> Result<RdapResponse> {
398 self.ensure_bootstrap().await?;
399
400 let domain = normalize_domain(domain)?;
401
402 let urls = {
404 let cache_guard = BOOTSTRAP_CACHE.read().await;
405 let cache = cache_guard.as_ref().ok_or_else(|| {
406 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
407 })?;
408
409 let bases = Self::get_rdap_urls_for_domain(&cache.data, &domain).ok_or_else(|| {
410 SeerError::RdapBootstrapError(format!("no RDAP server for {}", domain))
411 })?;
412
413 build_rdap_urls(&bases, &format!("domain/{}", domain))
414 }; self.query_rdap_urls(&urls).await
417 }
418
419 #[instrument(skip(self), fields(ip = %ip))]
423 pub async fn lookup_ip(&self, ip: &str) -> Result<RdapResponse> {
424 self.ensure_bootstrap().await?;
425
426 let ip_addr: IpAddr = ip
427 .parse()
428 .map_err(|_| SeerError::InvalidIpAddress(ip.to_string()))?;
429
430 let urls = {
431 let cache_guard = BOOTSTRAP_CACHE.read().await;
432 let cache = cache_guard.as_ref().ok_or_else(|| {
433 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
434 })?;
435
436 let bases = Self::get_rdap_urls_for_ip(&cache.data, &ip_addr).ok_or_else(|| {
437 SeerError::RdapBootstrapError(format!("no RDAP server for {}", ip))
438 })?;
439
440 build_rdap_urls(&bases, &format!("ip/{}", ip))
441 };
442
443 self.query_rdap_urls(&urls).await
444 }
445
446 #[instrument(skip(self), fields(asn = %asn))]
450 pub async fn lookup_asn(&self, asn: u32) -> Result<RdapResponse> {
451 self.ensure_bootstrap().await?;
452
453 let urls = {
454 let cache_guard = BOOTSTRAP_CACHE.read().await;
455 let cache = cache_guard.as_ref().ok_or_else(|| {
456 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
457 })?;
458
459 let bases = Self::get_rdap_urls_for_asn(&cache.data, asn).ok_or_else(|| {
460 SeerError::RdapBootstrapError(format!("no RDAP server for AS{}", asn))
461 })?;
462
463 build_rdap_urls(&bases, &format!("autnum/{}", asn))
464 };
465
466 self.query_rdap_urls(&urls).await
467 }
468
469 #[instrument(skip(self), fields(tld = %tld))]
475 pub async fn get_rdap_base_url_for_tld(&self, tld: &str) -> Option<String> {
476 if self.ensure_bootstrap().await.is_err() {
477 return None;
478 }
479
480 let cache_guard = BOOTSTRAP_CACHE.read().await;
481 let cache = cache_guard.as_ref()?;
482 let lower = tld.to_lowercase();
486 let key = crate::validation::domain_to_ascii(&lower).unwrap_or(lower);
487 cache
488 .data
489 .dns
490 .get(&key)
491 .and_then(|urls| urls.first())
492 .map(|u| u.to_string())
493 }
494
495 async fn query_rdap_urls(&self, urls: &[url::Url]) -> Result<RdapResponse> {
499 if urls.is_empty() {
500 return Err(SeerError::RdapError(
501 "no candidate RDAP URLs available".to_string(),
502 ));
503 }
504
505 let mut last_error: Option<SeerError> = None;
506 let mut not_found_error: Option<SeerError> = None;
512 for (idx, url) in urls.iter().enumerate() {
513 let url_str = url.as_str().to_string();
514 debug!(url = %url_str, candidate = idx + 1, total = urls.len(), "Querying RDAP");
515 match self.query_rdap_with_retry(&url_str).await {
516 Ok(resp) => return Ok(resp),
517 Err(e) => {
518 if urls.len() > 1 {
519 debug!(
520 url = %url_str,
521 error = %e,
522 candidate = idx + 1,
523 total = urls.len(),
524 "RDAP candidate failed, trying next",
525 );
526 }
527 if not_found_error.is_none() && crate::rdap::rdap_error_is_404(&e) {
528 not_found_error = Some(e);
529 } else {
530 last_error = Some(e);
531 }
532 }
533 }
534 }
535
536 Err(wrap_all_candidates_failed(
539 not_found_error.or(last_error),
540 urls.len(),
541 ))
542 }
543
544 async fn query_rdap_with_retry(&self, url: &str) -> Result<RdapResponse> {
549 let classifier = NetworkRetryClassifier::new();
550 let mut attempt = 0;
551 loop {
552 match query_rdap_attempt(url, self.timeout, self.allow_reserved).await {
553 Ok(resp) => return Ok(resp),
554 Err((err, retry_after)) => {
555 let attempts_remaining =
556 self.retry_policy.max_attempts.saturating_sub(attempt + 1);
557 if !classifier.is_retryable(&err) || attempts_remaining == 0 {
558 return Err(if attempt > 0 {
559 SeerError::RetryExhausted {
560 attempts: attempt + 1,
561 last_error: Box::new(err),
562 }
563 } else {
564 err
565 });
566 }
567 let backoff = self.retry_policy.delay_for_attempt(attempt);
568 let delay = effective_retry_delay(backoff, retry_after);
569 debug!(
570 url = %url,
571 attempt = attempt + 1,
572 max_attempts = self.retry_policy.max_attempts,
573 delay_ms = delay.as_millis(),
574 error = %err,
575 "Retrying RDAP after transient error"
576 );
577 tokio::time::sleep(delay).await;
578 attempt += 1;
579 }
580 }
581 }
582 }
583}
584
585const MAX_RDAP_RESPONSE_SIZE: usize = 10 * 1024 * 1024;
587
588const MAX_RETRY_AFTER: Duration = Duration::from_secs(5);
593
594fn parse_rdap_url(url: &str) -> Result<(String, u16)> {
604 let parsed = url::Url::parse(url)
605 .map_err(|e| SeerError::RdapError(format!("invalid URL '{}': {}", url, e)))?;
606 if parsed.scheme() != "https" {
607 return Err(SeerError::RdapError(format!(
608 "RDAP URL '{}' is not https — request blocked (downgrade/SSRF protection)",
609 url
610 )));
611 }
612 let host = match parsed.host() {
615 Some(url::Host::Domain(d)) => d.to_string(),
616 Some(url::Host::Ipv4(ip)) => ip.to_string(),
617 Some(url::Host::Ipv6(ip)) => ip.to_string(),
618 None => return Err(SeerError::RdapError(format!("URL '{}' has no host", url))),
619 };
620 let port = parsed.port_or_known_default().unwrap_or(443);
621 Ok((host, port))
622}
623
624async fn resolve_rdap_host(host: &str, port: u16) -> Result<Vec<SocketAddr>> {
636 crate::net::resolve_public_host(host, port)
637 .await
638 .map_err(|e| SeerError::RdapError(format!("{} — request blocked (SSRF protection)", e)))
639}
640
641#[cfg(test)]
646async fn validate_url_not_reserved(url: &str) -> Result<Vec<SocketAddr>> {
647 let (host, port) = parse_rdap_url(url)?;
648 resolve_rdap_host(&host, port).await
649}
650
651fn parse_retry_after(value: &str) -> Option<Duration> {
656 value.trim().parse::<u64>().ok().map(Duration::from_secs)
657}
658
659fn effective_retry_delay(backoff: Duration, retry_after: Option<Duration>) -> Duration {
663 match retry_after {
664 Some(hint) => hint.min(MAX_RETRY_AFTER),
665 None => backoff,
666 }
667}
668
669const PINNED_CLIENT_TTL: Duration = Duration::from_secs(60);
676
677type PinnedClientKey = (String, u16, Duration);
682
683static PINNED_CLIENT_CACHE: Lazy<crate::cache::TtlCache<PinnedClientKey, Client>> =
690 Lazy::new(|| crate::cache::TtlCache::with_max_capacity(PINNED_CLIENT_TTL, 64));
691
692async fn send_rdap_request(
706 url: &str,
707 timeout: Duration,
708 allow_reserved: bool,
709) -> Result<reqwest::Response> {
710 let connect_timeout = CONNECT_TIMEOUT.min(timeout);
713 if allow_reserved {
714 let client = Client::builder()
718 .timeout(timeout)
719 .connect_timeout(connect_timeout)
720 .user_agent("Seer/1.0 (RDAP Client)")
721 .redirect(reqwest::redirect::Policy::none())
722 .build()
723 .map_err(|e| SeerError::RdapError(format!("failed to build HTTP client: {}", e)))?;
724 return client
725 .get(url)
726 .header("Accept", "application/rdap+json")
727 .send()
728 .await
729 .map_err(Into::into);
730 }
731
732 let (host, port) = parse_rdap_url(url)?;
734
735 let key = (host.clone(), port, timeout);
736 let client = match PINNED_CLIENT_CACHE.get(&key) {
737 Some(client) => client,
738 None => {
739 let resolved = resolve_rdap_host(&host, port).await?;
744 let client = Client::builder()
745 .timeout(timeout)
746 .connect_timeout(connect_timeout)
747 .user_agent("Seer/1.0 (RDAP Client)")
748 .resolve_to_addrs(&host, &resolved)
749 .redirect(reqwest::redirect::Policy::none())
758 .build()
759 .map_err(|e| SeerError::RdapError(format!("failed to build HTTP client: {}", e)))?;
760 PINNED_CLIENT_CACHE.insert(key.clone(), client.clone());
761 client
762 }
763 };
764
765 match client
766 .get(url)
767 .header("Accept", "application/rdap+json")
768 .send()
769 .await
770 {
771 Ok(resp) => Ok(resp),
772 Err(e) => {
773 if e.is_connect() {
778 PINNED_CLIENT_CACHE.remove(&key);
779 }
780 Err(e.into())
781 }
782 }
783}
784
785async fn read_and_parse_rdap_body(
790 response: reqwest::Response,
791 url: &str,
792 timeout: Duration,
793) -> Result<RdapResponse> {
794 let mut body = Vec::new();
799 let mut stream = response.bytes_stream();
800 let streamed = tokio::time::timeout(timeout, async {
801 while let Some(chunk) = stream.next().await {
802 let chunk = chunk
803 .map_err(|e| SeerError::RdapError(format!("failed to read response: {}", e)))?;
804 body.extend_from_slice(&chunk);
805 if body.len() > MAX_RDAP_RESPONSE_SIZE {
806 return Err(SeerError::RdapError(format!(
807 "RDAP response exceeds {} byte limit",
808 MAX_RDAP_RESPONSE_SIZE
809 )));
810 }
811 }
812 Ok::<(), SeerError>(())
813 })
814 .await;
815
816 match streamed {
817 Ok(Ok(())) => {}
818 Ok(Err(e)) => return Err(e),
819 Err(_) => {
820 return Err(SeerError::Timeout(format!(
821 "timed out reading RDAP response body from {} after {:?}",
822 url, timeout
823 )));
824 }
825 }
826
827 let rdap: RdapResponse = serde_json::from_slice(&body)?;
828 rdap.validate()?;
834 Ok(rdap)
835}
836
837async fn query_rdap_attempt(
843 url: &str,
844 timeout: Duration,
845 allow_reserved: bool,
846) -> std::result::Result<RdapResponse, (SeerError, Option<Duration>)> {
847 let response = send_rdap_request(url, timeout, allow_reserved)
848 .await
849 .map_err(|e| (e, None))?;
850
851 if !response.status().is_success() {
852 let status = response.status();
853 let retry_after = if status.as_u16() == 429 {
856 response
857 .headers()
858 .get(reqwest::header::RETRY_AFTER)
859 .and_then(|v| v.to_str().ok())
860 .and_then(parse_retry_after)
861 } else {
862 None
863 };
864 return Err((
865 SeerError::RdapError(format!("query failed with status {}", status)),
866 retry_after,
867 ));
868 }
869
870 read_and_parse_rdap_body(response, url, timeout)
871 .await
872 .map_err(|e| (e, None))
873}
874
875async fn load_bootstrap_data_with_retry(policy: &RetryPolicy) -> Result<BootstrapData> {
877 let executor = RetryExecutor::new(policy.clone());
878 executor.execute(load_bootstrap_data).await
879}
880
881async fn load_bootstrap_data() -> Result<BootstrapData> {
883 debug!("Loading RDAP bootstrap data from IANA");
884
885 let http = rdap_http_client()?;
890
891 let dns_future = http.get(IANA_BOOTSTRAP_DNS).send();
892 let ipv4_future = http.get(IANA_BOOTSTRAP_IPV4).send();
893 let ipv6_future = http.get(IANA_BOOTSTRAP_IPV6).send();
894 let asn_future = http.get(IANA_BOOTSTRAP_ASN).send();
895
896 let (dns_resp, ipv4_resp, ipv6_resp, asn_resp) =
899 tokio::join!(dns_future, ipv4_future, ipv6_future, asn_future);
900
901 const MAX_BOOTSTRAP_SIZE: usize = 10 * 1024 * 1024; async fn read_bootstrap(resp: reqwest::Response) -> Result<BootstrapResponse> {
905 let mut body = Vec::new();
911 let mut stream = resp.bytes_stream();
912 let streamed = tokio::time::timeout(DEFAULT_TIMEOUT, async {
913 while let Some(chunk) = stream.next().await {
914 let chunk = chunk.map_err(|e| {
915 SeerError::RdapBootstrapError(format!("failed to read body: {}", e))
916 })?;
917 body.extend_from_slice(&chunk);
918 if body.len() > MAX_BOOTSTRAP_SIZE {
919 return Err(SeerError::RdapBootstrapError(format!(
920 "bootstrap response too large (exceeds {} bytes)",
921 MAX_BOOTSTRAP_SIZE
922 )));
923 }
924 }
925 Ok::<(), SeerError>(())
926 })
927 .await;
928
929 match streamed {
930 Ok(Ok(())) => {}
931 Ok(Err(e)) => return Err(e),
932 Err(_) => {
933 return Err(SeerError::Timeout(format!(
934 "RDAP bootstrap body read timed out after {:?}",
935 DEFAULT_TIMEOUT
936 )));
937 }
938 }
939
940 serde_json::from_slice(&body).map_err(Into::into)
941 }
942
943 let dns_data = match dns_resp {
945 Ok(resp) => match read_bootstrap(resp).await {
946 Ok(data) => Some(data),
947 Err(e) => {
948 warn!(error = %e, "Failed to parse DNS bootstrap response");
949 None
950 }
951 },
952 Err(e) => {
953 warn!(error = %e, "Failed to fetch DNS bootstrap from IANA");
954 None
955 }
956 };
957 let ipv4_data = match ipv4_resp {
958 Ok(resp) => match read_bootstrap(resp).await {
959 Ok(data) => Some(data),
960 Err(e) => {
961 warn!(error = %e, "Failed to parse IPv4 bootstrap response");
962 None
963 }
964 },
965 Err(e) => {
966 warn!(error = %e, "Failed to fetch IPv4 bootstrap from IANA");
967 None
968 }
969 };
970 let ipv6_data = match ipv6_resp {
971 Ok(resp) => match read_bootstrap(resp).await {
972 Ok(data) => Some(data),
973 Err(e) => {
974 warn!(error = %e, "Failed to parse IPv6 bootstrap response");
975 None
976 }
977 },
978 Err(e) => {
979 warn!(error = %e, "Failed to fetch IPv6 bootstrap from IANA");
980 None
981 }
982 };
983 let asn_data = match asn_resp {
984 Ok(resp) => match read_bootstrap(resp).await {
985 Ok(data) => Some(data),
986 Err(e) => {
987 warn!(error = %e, "Failed to parse ASN bootstrap response");
988 None
989 }
990 },
991 Err(e) => {
992 warn!(error = %e, "Failed to fetch ASN bootstrap from IANA");
993 None
994 }
995 };
996
997 if dns_data.is_none() && ipv4_data.is_none() && ipv6_data.is_none() && asn_data.is_none() {
999 return Err(SeerError::RdapBootstrapError(
1000 "all IANA bootstrap registries failed".to_string(),
1001 ));
1002 }
1003
1004 let mut dns = HashMap::new();
1005 let mut ipv4 = Vec::new();
1006 let mut ipv6 = Vec::new();
1007 let mut asn = Vec::new();
1008
1009 fn collect_valid_urls(urls: &[serde_json::Value]) -> Option<Arc<Vec<url::Url>>> {
1013 let mut out = Vec::new();
1014 for u in urls {
1015 if let Some(s) = u.as_str() {
1016 match validate_bootstrap_url(s) {
1017 Ok(parsed) => out.push(parsed),
1018 Err(e) => {
1019 debug!(url = s, error = %e, "Skipping invalid bootstrap URL");
1020 }
1021 }
1022 }
1023 }
1024 if out.is_empty() {
1025 None
1026 } else {
1027 Some(Arc::new(out))
1028 }
1029 }
1030
1031 if let Some(dns_data) = dns_data {
1033 for service in dns_data.services {
1034 if service.len() >= 2 {
1035 if let (Some(tlds), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
1036 if let Some(urls_arc) = collect_valid_urls(urls) {
1037 for tld in tlds {
1038 if let Some(tld_str) = tld.as_str() {
1039 dns.insert(tld_str.to_lowercase(), Arc::clone(&urls_arc));
1040 }
1041 }
1042 }
1043 }
1044 }
1045 }
1046 }
1047
1048 if let Some(ipv4_data) = ipv4_data {
1050 for service in ipv4_data.services {
1051 if service.len() >= 2 {
1052 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array())
1053 {
1054 if let Some(urls_arc) = collect_valid_urls(urls) {
1055 for prefix in prefixes {
1056 if let Some(prefix_str) = prefix.as_str() {
1057 ipv4.push((
1058 IpRange {
1059 prefix: prefix_str.to_string(),
1060 },
1061 Arc::clone(&urls_arc),
1062 ));
1063 }
1064 }
1065 }
1066 }
1067 }
1068 }
1069 }
1070
1071 if let Some(ipv6_data) = ipv6_data {
1073 for service in ipv6_data.services {
1074 if service.len() >= 2 {
1075 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array())
1076 {
1077 if let Some(urls_arc) = collect_valid_urls(urls) {
1078 for prefix in prefixes {
1079 if let Some(prefix_str) = prefix.as_str() {
1080 ipv6.push((
1081 IpRange {
1082 prefix: prefix_str.to_string(),
1083 },
1084 Arc::clone(&urls_arc),
1085 ));
1086 }
1087 }
1088 }
1089 }
1090 }
1091 }
1092 }
1093
1094 if let Some(asn_data) = asn_data {
1096 for service in asn_data.services {
1097 if service.len() >= 2 {
1098 if let (Some(ranges), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
1099 if let Some(urls_arc) = collect_valid_urls(urls) {
1100 for range in ranges {
1101 if let Some(range_str) = range.as_str() {
1102 if let Some((start, end)) = parse_asn_range(range_str) {
1103 asn.push((AsnRange { start, end }, Arc::clone(&urls_arc)));
1104 }
1105 }
1106 }
1107 }
1108 }
1109 }
1110 }
1111 }
1112
1113 info!(
1114 dns_entries = dns.len(),
1115 ipv4_ranges = ipv4.len(),
1116 ipv6_ranges = ipv6.len(),
1117 asn_ranges = asn.len(),
1118 "RDAP bootstrap loaded"
1119 );
1120
1121 Ok(BootstrapData {
1122 dns,
1123 ipv4,
1124 ipv6,
1125 asn,
1126 })
1127}
1128
1129fn wrap_all_candidates_failed(last_error: Option<SeerError>, candidate_count: usize) -> SeerError {
1138 let last = last_error.unwrap_or_else(|| SeerError::RdapError("no candidates".to_string()));
1139
1140 if candidate_count <= 1 {
1141 return last;
1142 }
1143
1144 match last {
1145 SeerError::Timeout(msg) => SeerError::Timeout(format!(
1146 "all {} RDAP candidate URLs timed out; last error: {}",
1147 candidate_count, msg
1148 )),
1149 other => SeerError::RdapError(format!(
1150 "all {} RDAP candidate URLs failed; last error: {}",
1151 candidate_count, other
1152 )),
1153 }
1154}
1155
1156fn build_rdap_urls(bases: &[url::Url], path: &str) -> Vec<url::Url> {
1158 bases
1159 .iter()
1160 .filter_map(|base| {
1161 let base_str = base.as_str();
1164 let normalized = if base_str.ends_with('/') {
1165 base_str.to_string()
1166 } else {
1167 format!("{}/", base_str)
1168 };
1169 url::Url::parse(&normalized).and_then(|u| u.join(path)).ok()
1170 })
1171 .collect()
1172}
1173
1174#[cfg(test)]
1175mod tests {
1176 use super::*;
1177
1178 #[test]
1179 fn from_config_applies_rdap_timeout() {
1180 let mut config = crate::config::SeerConfig::default();
1181 config.timeouts.rdap_secs = 33;
1182 let client = RdapClient::from_config(&config);
1183 assert_eq!(client.timeout, Duration::from_secs(33));
1184 }
1185
1186 #[test]
1187 fn test_default_client_has_retry_policy() {
1188 let client = RdapClient::new();
1189 assert_eq!(client.retry_policy.max_attempts, 3);
1193 }
1194
1195 #[test]
1198 fn parse_retry_after_parses_delta_seconds() {
1199 assert_eq!(parse_retry_after("5"), Some(Duration::from_secs(5)));
1200 assert_eq!(parse_retry_after(" 10 "), Some(Duration::from_secs(10)));
1201 assert_eq!(parse_retry_after("0"), Some(Duration::from_secs(0)));
1202 }
1203
1204 #[test]
1205 fn parse_retry_after_rejects_http_date_and_junk() {
1206 assert_eq!(parse_retry_after("Wed, 21 Oct 2015 07:28:00 GMT"), None);
1209 assert_eq!(parse_retry_after("soon"), None);
1210 assert_eq!(parse_retry_after(""), None);
1211 }
1212
1213 #[test]
1214 fn effective_retry_delay_prefers_capped_retry_after() {
1215 assert_eq!(
1217 effective_retry_delay(Duration::from_millis(100), Some(Duration::from_secs(5))),
1218 Duration::from_secs(5)
1219 );
1220 assert_eq!(
1222 effective_retry_delay(Duration::from_millis(100), Some(Duration::from_secs(600))),
1223 MAX_RETRY_AFTER
1224 );
1225 }
1226
1227 #[test]
1228 fn effective_retry_delay_falls_back_to_backoff() {
1229 assert_eq!(
1230 effective_retry_delay(Duration::from_millis(250), None),
1231 Duration::from_millis(250)
1232 );
1233 }
1234
1235 #[test]
1236 fn test_client_without_retries() {
1237 let client = RdapClient::new().without_retries();
1238 assert_eq!(client.retry_policy.max_attempts, 1);
1239 }
1240
1241 #[test]
1242 fn test_client_custom_retry_policy() {
1243 let policy = RetryPolicy::new().with_max_attempts(5);
1244 let client = RdapClient::new().with_retry_policy(policy);
1245 assert_eq!(client.retry_policy.max_attempts, 5);
1246 }
1247
1248 #[test]
1249 fn test_cached_bootstrap_expiration() {
1250 let data = BootstrapData {
1251 dns: HashMap::new(),
1252 ipv4: Vec::new(),
1253 ipv6: Vec::new(),
1254 asn: Vec::new(),
1255 };
1256 let cached = CachedBootstrap::new(data);
1257 assert!(!cached.is_expired());
1259 }
1260
1261 #[test]
1262 fn test_rdap_http_client_is_configured() {
1263 let client = rdap_http_client();
1266 assert!(client.is_ok(), "RDAP HTTP client builder must succeed");
1267 }
1268
1269 #[test]
1270 fn test_parse_bootstrap_empty_services() {
1271 let data = BootstrapData {
1273 dns: HashMap::new(),
1274 ipv4: Vec::new(),
1275 ipv6: Vec::new(),
1276 asn: Vec::new(),
1277 };
1278 assert!(RdapClient::get_rdap_urls_for_domain(&data, "example.com").is_none());
1280 assert!(RdapClient::get_rdap_urls_for_asn(&data, 12345).is_none());
1281 }
1282
1283 #[tokio::test]
1286 async fn test_validate_url_not_reserved_rejects_loopback_literal() {
1287 let err = validate_url_not_reserved("https://127.0.0.1/domain/example.com")
1288 .await
1289 .unwrap_err();
1290 assert!(
1291 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved")),
1292 "expected reserved-IP error, got: {:?}",
1293 err
1294 );
1295 }
1296
1297 #[tokio::test]
1298 async fn test_validate_url_not_reserved_rejects_private_ipv4_literal() {
1299 let err = validate_url_not_reserved("https://10.0.0.1/")
1300 .await
1301 .unwrap_err();
1302 assert!(
1303 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved")),
1304 "expected reserved-IP error, got: {:?}",
1305 err
1306 );
1307 }
1308
1309 #[tokio::test]
1310 async fn test_validate_url_not_reserved_rejects_non_https_scheme() {
1311 let err = validate_url_not_reserved("http://93.184.216.34/domain/example.com")
1315 .await
1316 .unwrap_err();
1317 assert!(
1318 matches!(err, SeerError::RdapError(ref s) if s.contains("not https")),
1319 "expected non-https rejection, got: {:?}",
1320 err
1321 );
1322 }
1323
1324 #[tokio::test]
1325 async fn test_validate_url_not_reserved_rejects_ipv6_loopback_literal() {
1326 let err = validate_url_not_reserved("https://[::1]/")
1329 .await
1330 .unwrap_err();
1331 assert!(
1332 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved")),
1333 "expected reserved-IP error, got: {:?}",
1334 err
1335 );
1336 }
1337
1338 #[tokio::test]
1339 async fn test_validate_url_not_reserved_returns_resolved_addrs_for_public_literal() {
1340 let addrs = validate_url_not_reserved("https://8.8.8.8/").await.unwrap();
1343 assert_eq!(addrs.len(), 1);
1344 assert!(addrs[0].ip().is_ipv4());
1345 assert_eq!(addrs[0].port(), 443);
1346 }
1347
1348 #[test]
1351 fn test_build_rdap_urls_preserves_order_and_appends_path() {
1352 let bases = vec![
1353 url::Url::parse("https://rdap.a.example/").unwrap(),
1354 url::Url::parse("https://rdap.b.example").unwrap(), ];
1356 let built = build_rdap_urls(&bases, "domain/example.com");
1357 assert_eq!(built.len(), 2);
1358 assert_eq!(
1359 built[0].as_str(),
1360 "https://rdap.a.example/domain/example.com"
1361 );
1362 assert_eq!(
1363 built[1].as_str(),
1364 "https://rdap.b.example/domain/example.com"
1365 );
1366 }
1367
1368 #[test]
1369 fn test_build_rdap_urls_empty_input_returns_empty() {
1370 let built = build_rdap_urls(&[], "domain/example.com");
1371 assert!(built.is_empty());
1372 }
1373
1374 #[test]
1377 fn test_wrap_all_candidates_failed_preserves_timeout_variant() {
1378 let last = SeerError::Timeout("body read timed out".to_string());
1381 let wrapped = wrap_all_candidates_failed(Some(last), 3);
1382 match wrapped {
1383 SeerError::Timeout(msg) => {
1384 assert!(
1385 msg.contains("all 3 RDAP candidate URLs timed out"),
1386 "expected wrapped timeout message, got: {}",
1387 msg
1388 );
1389 assert!(
1390 msg.contains("body read timed out"),
1391 "expected original message preserved, got: {}",
1392 msg
1393 );
1394 }
1395 other => panic!(
1396 "expected SeerError::Timeout after wrapping a Timeout, got: {:?}",
1397 other
1398 ),
1399 }
1400 }
1401
1402 #[test]
1403 fn test_wrap_all_candidates_failed_wraps_non_timeout_as_rdap_error() {
1404 let last = SeerError::RdapError("500 internal error".to_string());
1405 let wrapped = wrap_all_candidates_failed(Some(last), 2);
1406 assert!(
1407 matches!(wrapped, SeerError::RdapError(ref s) if s.contains("all 2 RDAP candidate URLs failed")),
1408 "expected wrapped RdapError, got: {:?}",
1409 wrapped
1410 );
1411 }
1412
1413 #[test]
1414 fn test_wrap_all_candidates_failed_single_candidate_returns_unchanged() {
1415 let last = SeerError::Timeout("single timeout".to_string());
1418 let wrapped = wrap_all_candidates_failed(Some(last), 1);
1419 assert!(
1420 matches!(wrapped, SeerError::Timeout(ref s) if s == "single timeout"),
1421 "expected unchanged Timeout, got: {:?}",
1422 wrapped
1423 );
1424 }
1425
1426 #[test]
1427 fn test_wrap_all_candidates_failed_no_last_error_returns_placeholder() {
1428 let wrapped = wrap_all_candidates_failed(None, 0);
1429 assert!(matches!(wrapped, SeerError::RdapError(_)));
1430 }
1431
1432 static BOOTSTRAP_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
1451
1452 #[tokio::test]
1453 async fn test_bootstrap_load_notify_wakes_waiter_when_cache_populated() {
1454 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1455
1456 {
1458 let mut cache = BOOTSTRAP_CACHE.write().await;
1459 *cache = None;
1460 }
1461
1462 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1465 tokio::pin!(notified);
1466
1467 {
1469 let mut cache = BOOTSTRAP_CACHE.write().await;
1470 *cache = Some(CachedBootstrap::new(BootstrapData {
1471 dns: HashMap::new(),
1472 ipv4: Vec::new(),
1473 ipv6: Vec::new(),
1474 asn: Vec::new(),
1475 }));
1476 }
1477 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1478
1479 let result = wait_for_in_flight_load(notified).await;
1480 assert!(
1481 result.is_ok(),
1482 "expected waiter to see populated cache, got: {:?}",
1483 result
1484 );
1485
1486 {
1488 let mut cache = BOOTSTRAP_CACHE.write().await;
1489 *cache = None;
1490 }
1491 }
1492
1493 #[tokio::test]
1494 async fn test_bootstrap_load_notify_empty_cache_after_wake_returns_error() {
1495 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1496
1497 {
1499 let mut cache = BOOTSTRAP_CACHE.write().await;
1500 *cache = None;
1501 }
1502
1503 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1504 tokio::pin!(notified);
1505
1506 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1508
1509 let result = wait_for_in_flight_load(notified).await;
1510 assert!(
1511 matches!(
1512 result,
1513 Err(SeerError::RdapBootstrapError(ref s))
1514 if s.contains("throttled and no cache available")
1515 ),
1516 "expected throttled error when cache still empty after notify, got: {:?}",
1517 result
1518 );
1519 }
1520
1521 use wiremock::matchers::method;
1531 use wiremock::{Mock, MockServer, ResponseTemplate};
1532
1533 #[tokio::test]
1534 async fn mock_rdap_404_is_nonretryable_typed_error() {
1535 let server = MockServer::start().await;
1536 Mock::given(method("GET"))
1537 .respond_with(ResponseTemplate::new(404))
1538 .mount(&server)
1539 .await;
1540
1541 let client = RdapClient::new()
1542 .without_retries()
1543 .allowing_reserved_for_tests();
1544 let err = client
1545 .query_rdap_with_retry(&format!("{}/domain/example.com", server.uri()))
1546 .await
1547 .unwrap_err();
1548 assert!(
1549 matches!(err, SeerError::RdapError(ref m) if m.contains("404")),
1550 "got: {err:?}"
1551 );
1552 }
1553
1554 #[tokio::test]
1555 async fn mock_rdap_429_honors_retry_after_and_succeeds() {
1556 let server = MockServer::start().await;
1557 Mock::given(method("GET"))
1560 .respond_with(ResponseTemplate::new(429).insert_header("Retry-After", "0"))
1561 .up_to_n_times(1)
1562 .mount(&server)
1563 .await;
1564 Mock::given(method("GET"))
1565 .respond_with(ResponseTemplate::new(200).set_body_raw(
1566 r#"{"objectClassName":"domain","handle":"MOCK-1"}"#,
1567 "application/rdap+json",
1568 ))
1569 .mount(&server)
1570 .await;
1571
1572 let client = RdapClient::new().allowing_reserved_for_tests();
1573 let resp = client
1574 .query_rdap_with_retry(&format!("{}/domain/example.com", server.uri()))
1575 .await
1576 .unwrap();
1577 assert_eq!(resp.handle.as_deref(), Some("MOCK-1"));
1578 }
1579
1580 #[tokio::test]
1581 async fn mock_rdap_malformed_body_is_parse_error_not_panic() {
1582 let server = MockServer::start().await;
1583 Mock::given(method("GET"))
1584 .respond_with(ResponseTemplate::new(200).set_body_raw("not json", "text/plain"))
1585 .mount(&server)
1586 .await;
1587
1588 let client = RdapClient::new()
1589 .without_retries()
1590 .allowing_reserved_for_tests();
1591 let err = client
1592 .query_rdap_with_retry(&format!("{}/domain/example.com", server.uri()))
1593 .await
1594 .unwrap_err();
1595 assert!(matches!(err, SeerError::JsonError(_)), "got: {err:?}");
1596 }
1597
1598 #[tokio::test]
1599 async fn mock_rdap_candidate_fallback_uses_second_url() {
1600 let bad = MockServer::start().await;
1601 Mock::given(method("GET"))
1602 .respond_with(ResponseTemplate::new(500))
1603 .mount(&bad)
1604 .await;
1605 let good = MockServer::start().await;
1606 Mock::given(method("GET"))
1607 .respond_with(ResponseTemplate::new(200).set_body_raw(
1608 r#"{"objectClassName":"domain","handle":"MOCK-2"}"#,
1609 "application/rdap+json",
1610 ))
1611 .mount(&good)
1612 .await;
1613
1614 let client = RdapClient::new()
1615 .without_retries()
1616 .allowing_reserved_for_tests();
1617 let urls = vec![
1618 url::Url::parse(&format!("{}/domain/example.com", bad.uri())).unwrap(),
1619 url::Url::parse(&format!("{}/domain/example.com", good.uri())).unwrap(),
1620 ];
1621 let resp = client.query_rdap_urls(&urls).await.unwrap();
1622 assert_eq!(resp.handle.as_deref(), Some("MOCK-2"));
1623 }
1624
1625 #[tokio::test]
1631 async fn mock_rdap_404_on_first_candidate_survives_later_non_404_failure() {
1632 let not_found = MockServer::start().await;
1633 Mock::given(method("GET"))
1634 .respond_with(ResponseTemplate::new(404))
1635 .mount(¬_found)
1636 .await;
1637 let broken = MockServer::start().await;
1638 Mock::given(method("GET"))
1639 .respond_with(ResponseTemplate::new(500))
1640 .mount(&broken)
1641 .await;
1642
1643 let client = RdapClient::new()
1644 .without_retries()
1645 .allowing_reserved_for_tests();
1646 let urls = vec![
1647 url::Url::parse(&format!("{}/domain/example.com", not_found.uri())).unwrap(),
1648 url::Url::parse(&format!("{}/domain/example.com", broken.uri())).unwrap(),
1649 ];
1650 let err = client.query_rdap_urls(&urls).await.unwrap_err();
1651 assert!(
1652 crate::rdap::rdap_error_is_404(&err),
1653 "404 from candidate 1 must survive candidate 2's non-404 failure, got: {err:?}"
1654 );
1655 }
1656
1657 #[tokio::test]
1668 async fn pinned_client_cache_hit_skips_dns_and_revalidation() {
1669 let host = "pinned-cache-hit.seer-test.invalid";
1670 let timeout = Duration::from_secs(2);
1671 let key = (host.to_string(), 443u16, timeout);
1672
1673 let pinned: Vec<SocketAddr> = vec!["127.0.0.1:443".parse().expect("valid addr")];
1678 let client = Client::builder()
1679 .timeout(timeout)
1680 .connect_timeout(timeout)
1681 .resolve_to_addrs(host, &pinned)
1682 .redirect(reqwest::redirect::Policy::none())
1683 .build()
1684 .expect("client builds");
1685 PINNED_CLIENT_CACHE.insert(key.clone(), client);
1686
1687 let err = send_rdap_request(
1688 &format!("https://{}/domain/example.com", host),
1689 timeout,
1690 false,
1691 )
1692 .await
1693 .unwrap_err();
1694 assert!(
1695 matches!(err, SeerError::ReqwestError { .. }),
1696 "cache hit must reach the connect stage (ReqwestError), not fail \
1697 SSRF validation (RdapError) — got: {err:?}"
1698 );
1699
1700 PINNED_CLIENT_CACHE.remove(&key);
1703 }
1704
1705 #[tokio::test]
1710 async fn test_mode_requests_never_populate_pinned_client_cache() {
1711 let server = MockServer::start().await;
1712 Mock::given(method("GET"))
1713 .respond_with(ResponseTemplate::new(200).set_body_raw(
1714 r#"{"objectClassName":"domain","handle":"MOCK-CACHE"}"#,
1715 "application/rdap+json",
1716 ))
1717 .mount(&server)
1718 .await;
1719
1720 let client = RdapClient::new()
1721 .without_retries()
1722 .allowing_reserved_for_tests();
1723 let uri = format!("{}/domain/example.com", server.uri());
1724 let resp = client.query_rdap_with_retry(&uri).await.unwrap();
1725 assert_eq!(resp.handle.as_deref(), Some("MOCK-CACHE"));
1726
1727 let parsed = url::Url::parse(&uri).unwrap();
1730 let host = parsed.host_str().unwrap().to_string();
1731 let port = parsed.port_or_known_default().unwrap();
1732 assert!(
1733 PINNED_CLIENT_CACHE
1734 .get(&(host, port, DEFAULT_TIMEOUT))
1735 .is_none(),
1736 "test-mode request must not populate the shared pinned-client cache"
1737 );
1738 }
1739}