1use std::collections::HashMap;
2use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, 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};
12
13use super::types::RdapResponse;
14use crate::error::{Result, SeerError};
15use crate::retry::{RetryExecutor, RetryPolicy};
16use crate::validation::{describe_reserved_ip, normalize_domain};
17
18const IANA_BOOTSTRAP_DNS: &str = "https://data.iana.org/rdap/dns.json";
19const IANA_BOOTSTRAP_IPV4: &str = "https://data.iana.org/rdap/ipv4.json";
20const IANA_BOOTSTRAP_IPV6: &str = "https://data.iana.org/rdap/ipv6.json";
21const IANA_BOOTSTRAP_ASN: &str = "https://data.iana.org/rdap/asn.json";
22
23const DEFAULT_TIMEOUT: Duration = Duration::from_secs(15);
28
29const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
32
33const BOOTSTRAP_TTL: Duration = Duration::from_secs(24 * 60 * 60);
35
36const BOOTSTRAP_REFRESH_MIN_INTERVAL: Duration = Duration::from_secs(60);
40
41static RDAP_HTTP_CLIENT: Lazy<Client> = Lazy::new(|| {
46 Client::builder()
47 .timeout(DEFAULT_TIMEOUT)
48 .connect_timeout(CONNECT_TIMEOUT)
49 .user_agent("Seer/1.0 (RDAP Client)")
50 .pool_max_idle_per_host(10)
51 .build()
52 .expect("Failed to build RDAP HTTP client - invalid configuration")
53});
54
55static BOOTSTRAP_CACHE: Lazy<RwLock<Option<CachedBootstrap>>> = Lazy::new(|| RwLock::new(None));
57
58static BOOTSTRAP_LAST_ATTEMPT: Lazy<RwLock<Option<Instant>>> = Lazy::new(|| RwLock::new(None));
62
63static BOOTSTRAP_LOAD_NOTIFY: Lazy<Notify> = Lazy::new(Notify::new);
71
72struct CachedBootstrap {
74 data: BootstrapData,
75 loaded_at: Instant,
76}
77
78impl CachedBootstrap {
79 fn new(data: BootstrapData) -> Self {
80 Self {
81 data,
82 loaded_at: Instant::now(),
83 }
84 }
85
86 fn is_expired(&self) -> bool {
87 self.loaded_at.elapsed() > BOOTSTRAP_TTL
88 }
89
90 fn age(&self) -> Duration {
91 self.loaded_at.elapsed()
92 }
93}
94
95struct BootstrapData {
100 dns: HashMap<String, Arc<Vec<url::Url>>>,
101 ipv4: Vec<(IpRange, Arc<Vec<url::Url>>)>,
102 ipv6: Vec<(IpRange, Arc<Vec<url::Url>>)>,
103 asn: Vec<(AsnRange, Arc<Vec<url::Url>>)>,
104}
105
106#[derive(Clone)]
107struct IpRange {
108 prefix: String,
109}
110
111#[derive(Clone)]
112struct AsnRange {
113 start: u32,
114 end: u32,
115}
116
117#[derive(Deserialize)]
118struct BootstrapResponse {
119 services: Vec<Vec<serde_json::Value>>,
120}
121
122async fn wait_for_in_flight_load(
132 notified: std::pin::Pin<&mut tokio::sync::futures::Notified<'_>>,
133) -> Result<()> {
134 let _ = tokio::time::timeout(DEFAULT_TIMEOUT, notified).await;
137 let cache = BOOTSTRAP_CACHE.read().await;
138 if cache.is_some() {
139 Ok(())
140 } else {
141 Err(SeerError::RdapBootstrapError(
142 "bootstrap refresh throttled and no cache available".to_string(),
143 ))
144 }
145}
146
147#[derive(Debug, Clone)]
148pub struct RdapClient {
149 retry_policy: RetryPolicy,
150}
151
152impl Default for RdapClient {
153 fn default() -> Self {
154 Self::new()
155 }
156}
157
158impl RdapClient {
159 pub fn new() -> Self {
161 Self {
162 retry_policy: RetryPolicy::default().with_max_attempts(2),
163 }
164 }
165
166 pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
170 self.retry_policy = policy;
171 self
172 }
173
174 pub fn without_retries(mut self) -> Self {
176 self.retry_policy = RetryPolicy::no_retry();
177 self
178 }
179
180 async fn ensure_bootstrap(&self) -> Result<()> {
195 {
197 let cache = BOOTSTRAP_CACHE.read().await;
198 if let Some(cached) = cache.as_ref() {
199 if !cached.is_expired() {
200 return Ok(());
201 }
202 }
203 }
204
205 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
211 tokio::pin!(notified);
212
213 {
217 let last = BOOTSTRAP_LAST_ATTEMPT.read().await;
218 if let Some(ts) = *last {
219 if ts.elapsed() < BOOTSTRAP_REFRESH_MIN_INTERVAL {
220 let cache = BOOTSTRAP_CACHE.read().await;
222 if cache.is_some() {
223 return Ok(());
225 }
226 drop(cache);
229 drop(last);
230 return wait_for_in_flight_load(notified).await;
231 }
232 }
233 }
234
235 {
238 let mut last = BOOTSTRAP_LAST_ATTEMPT.write().await;
239 if let Some(ts) = *last {
241 if ts.elapsed() < BOOTSTRAP_REFRESH_MIN_INTERVAL {
242 drop(last);
243 let cache = BOOTSTRAP_CACHE.read().await;
244 if cache.is_some() {
245 return Ok(());
246 }
247 drop(cache);
248 return wait_for_in_flight_load(notified).await;
249 }
250 }
251 *last = Some(Instant::now());
252 }
253
254 debug!("Loading/refreshing RDAP bootstrap data");
258 let load_result = load_bootstrap_data_with_retry(&self.retry_policy).await;
259
260 let outcome = match load_result {
261 Ok(data) => {
262 let mut cache = BOOTSTRAP_CACHE.write().await;
263 let should_store = cache.as_ref().map(|c| c.is_expired()).unwrap_or(true);
266 if should_store {
267 *cache = Some(CachedBootstrap::new(data));
268 }
269 Ok(())
270 }
271 Err(e) => {
272 let cache = BOOTSTRAP_CACHE.read().await;
274 if let Some(cached) = cache.as_ref() {
275 debug!(
276 error = %e,
277 age_hours = cached.age().as_secs() / 3600,
278 "Bootstrap refresh failed, using stale data"
279 );
280 Ok(())
281 } else {
282 Err(e)
284 }
285 }
286 };
287
288 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
290 outcome
291 }
292
293 fn get_rdap_urls_for_domain(cache: &BootstrapData, domain: &str) -> Option<Arc<Vec<url::Url>>> {
295 let tld = domain.rsplit('.').next()?;
296 cache.dns.get(&tld.to_lowercase()).cloned()
297 }
298
299 fn get_rdap_urls_for_ip(cache: &BootstrapData, ip: &IpAddr) -> Option<Arc<Vec<url::Url>>> {
301 match ip {
302 IpAddr::V4(addr) => {
303 for (range, urls) in &cache.ipv4 {
304 if ipv4_matches_prefix(&range.prefix, addr) {
305 return Some(Arc::clone(urls));
306 }
307 }
308 }
309 IpAddr::V6(addr) => {
310 for (range, urls) in &cache.ipv6 {
311 if ipv6_matches_prefix(&range.prefix, addr) {
312 return Some(Arc::clone(urls));
313 }
314 }
315 }
316 }
317
318 None
319 }
320
321 fn get_rdap_urls_for_asn(cache: &BootstrapData, asn: u32) -> Option<Arc<Vec<url::Url>>> {
323 for (range, urls) in &cache.asn {
324 if asn >= range.start && asn <= range.end {
325 return Some(Arc::clone(urls));
326 }
327 }
328
329 None
330 }
331
332 #[instrument(skip(self), fields(domain = %domain))]
336 pub async fn lookup_domain(&self, domain: &str) -> Result<RdapResponse> {
337 self.ensure_bootstrap().await?;
338
339 let domain = normalize_domain(domain)?;
340
341 let urls = {
343 let cache_guard = BOOTSTRAP_CACHE.read().await;
344 let cache = cache_guard.as_ref().ok_or_else(|| {
345 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
346 })?;
347
348 let bases = Self::get_rdap_urls_for_domain(&cache.data, &domain).ok_or_else(|| {
349 SeerError::RdapBootstrapError(format!("no RDAP server for {}", domain))
350 })?;
351
352 build_rdap_urls(&bases, &format!("domain/{}", domain))
353 }; self.query_rdap_urls(&urls).await
356 }
357
358 #[instrument(skip(self), fields(ip = %ip))]
362 pub async fn lookup_ip(&self, ip: &str) -> Result<RdapResponse> {
363 self.ensure_bootstrap().await?;
364
365 let ip_addr: IpAddr = ip
366 .parse()
367 .map_err(|_| SeerError::InvalidIpAddress(ip.to_string()))?;
368
369 let urls = {
370 let cache_guard = BOOTSTRAP_CACHE.read().await;
371 let cache = cache_guard.as_ref().ok_or_else(|| {
372 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
373 })?;
374
375 let bases = Self::get_rdap_urls_for_ip(&cache.data, &ip_addr).ok_or_else(|| {
376 SeerError::RdapBootstrapError(format!("no RDAP server for {}", ip))
377 })?;
378
379 build_rdap_urls(&bases, &format!("ip/{}", ip))
380 };
381
382 self.query_rdap_urls(&urls).await
383 }
384
385 #[instrument(skip(self), fields(asn = %asn))]
389 pub async fn lookup_asn(&self, asn: u32) -> Result<RdapResponse> {
390 self.ensure_bootstrap().await?;
391
392 let urls = {
393 let cache_guard = BOOTSTRAP_CACHE.read().await;
394 let cache = cache_guard.as_ref().ok_or_else(|| {
395 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
396 })?;
397
398 let bases = Self::get_rdap_urls_for_asn(&cache.data, asn).ok_or_else(|| {
399 SeerError::RdapBootstrapError(format!("no RDAP server for AS{}", asn))
400 })?;
401
402 build_rdap_urls(&bases, &format!("autnum/{}", asn))
403 };
404
405 self.query_rdap_urls(&urls).await
406 }
407
408 #[instrument(skip(self), fields(tld = %tld))]
414 pub async fn get_rdap_base_url_for_tld(&self, tld: &str) -> Option<String> {
415 if self.ensure_bootstrap().await.is_err() {
416 return None;
417 }
418
419 let cache_guard = BOOTSTRAP_CACHE.read().await;
420 let cache = cache_guard.as_ref()?;
421 cache
422 .data
423 .dns
424 .get(&tld.to_lowercase())
425 .and_then(|urls| urls.first())
426 .map(|u| u.to_string())
427 }
428
429 async fn query_rdap_urls(&self, urls: &[url::Url]) -> Result<RdapResponse> {
433 if urls.is_empty() {
434 return Err(SeerError::RdapError(
435 "no candidate RDAP URLs available".to_string(),
436 ));
437 }
438
439 let mut last_error: Option<SeerError> = None;
440 for (idx, url) in urls.iter().enumerate() {
441 let url_str = url.as_str().to_string();
442 debug!(url = %url_str, candidate = idx + 1, total = urls.len(), "Querying RDAP");
443 match self.query_rdap_with_retry(&url_str).await {
444 Ok(resp) => return Ok(resp),
445 Err(e) => {
446 if urls.len() > 1 {
447 debug!(
448 url = %url_str,
449 error = %e,
450 candidate = idx + 1,
451 total = urls.len(),
452 "RDAP candidate failed, trying next",
453 );
454 }
455 last_error = Some(e);
456 }
457 }
458 }
459
460 Err(wrap_all_candidates_failed(last_error, urls.len()))
462 }
463
464 async fn query_rdap_with_retry(&self, url: &str) -> Result<RdapResponse> {
466 let executor = RetryExecutor::new(self.retry_policy.clone());
467 let url = url.to_string();
468
469 executor
470 .execute(|| {
471 let url = url.clone();
472 async move { query_rdap_internal(&url).await }
473 })
474 .await
475 }
476}
477
478const MAX_RDAP_RESPONSE_SIZE: usize = 10 * 1024 * 1024;
480
481async fn validate_url_not_reserved(url: &str) -> Result<Vec<SocketAddr>> {
488 let parsed = url::Url::parse(url)
489 .map_err(|e| SeerError::RdapError(format!("invalid URL '{}': {}", url, e)))?;
490 let host = parsed
491 .host_str()
492 .ok_or_else(|| SeerError::RdapError(format!("URL '{}' has no host", url)))?;
493 let port = parsed.port_or_known_default().unwrap_or(443);
494
495 if let Ok(ip) = host.parse::<IpAddr>() {
497 if let Some(reason) = describe_reserved_ip(&ip) {
498 return Err(SeerError::RdapError(format!(
499 "RDAP URL resolves to reserved IP {}: {} — request blocked (SSRF protection)",
500 ip, reason
501 )));
502 }
503 return Ok(vec![SocketAddr::new(ip, port)]);
504 }
505
506 let addr = format!("{}:{}", host, port);
507
508 let socket_addrs: Vec<SocketAddr> = tokio::net::lookup_host(&addr)
509 .await
510 .map_err(|e| SeerError::RdapError(format!("failed to resolve host '{}': {}", host, e)))?
511 .collect();
512
513 if socket_addrs.is_empty() {
514 return Err(SeerError::RdapError(format!(
515 "host '{}' resolved to no addresses",
516 host
517 )));
518 }
519
520 for socket_addr in &socket_addrs {
521 if let Some(reason) = describe_reserved_ip(&socket_addr.ip()) {
522 return Err(SeerError::RdapError(format!(
523 "RDAP URL resolves to reserved IP {}: {} — request blocked (SSRF protection)",
524 socket_addr.ip(),
525 reason
526 )));
527 }
528 }
529
530 Ok(socket_addrs)
531}
532
533fn validate_bootstrap_url(s: &str) -> Result<url::Url> {
539 let parsed = url::Url::parse(s)
540 .map_err(|e| SeerError::RdapError(format!("bad bootstrap URL {}: {}", s, e)))?;
541 if parsed.scheme() != "https" {
542 return Err(SeerError::RdapError(format!(
543 "bootstrap URL must be https, got {}",
544 parsed.scheme()
545 )));
546 }
547 let host = parsed
548 .host()
549 .ok_or_else(|| SeerError::RdapError(format!("bootstrap URL has no host: {}", s)))?;
550 match host {
551 url::Host::Ipv4(_) | url::Host::Ipv6(_) => {
552 return Err(SeerError::RdapError(format!(
553 "bootstrap URL must not be an IP literal: {}",
554 s
555 )));
556 }
557 url::Host::Domain(d) => {
558 if d.is_empty() || d.chars().any(|c| c.is_whitespace() || c.is_control()) {
559 return Err(SeerError::RdapError(format!(
560 "bootstrap URL has invalid host: {}",
561 s
562 )));
563 }
564 }
565 }
566 Ok(parsed)
567}
568
569async fn query_rdap_internal(url: &str) -> Result<RdapResponse> {
574 let resolved = validate_url_not_reserved(url).await?;
577
578 let parsed = url::Url::parse(url)
579 .map_err(|e| SeerError::RdapError(format!("invalid URL '{}': {}", url, e)))?;
580 let host = parsed
581 .host_str()
582 .ok_or_else(|| SeerError::RdapError(format!("URL '{}' has no host", url)))?;
583
584 let client = Client::builder()
588 .timeout(DEFAULT_TIMEOUT)
589 .connect_timeout(CONNECT_TIMEOUT)
590 .user_agent("Seer/1.0 (RDAP Client)")
591 .resolve_to_addrs(host, &resolved)
592 .build()
593 .map_err(|e| SeerError::RdapError(format!("failed to build HTTP client: {}", e)))?;
594
595 let response = client
596 .get(url)
597 .header("Accept", "application/rdap+json")
598 .send()
599 .await?;
600
601 if !response.status().is_success() {
602 return Err(SeerError::RdapError(format!(
603 "query failed with status {}",
604 response.status()
605 )));
606 }
607
608 let mut body = Vec::new();
613 let mut stream = response.bytes_stream();
614 let streamed = tokio::time::timeout(DEFAULT_TIMEOUT, async {
615 while let Some(chunk) = stream.next().await {
616 let chunk = chunk
617 .map_err(|e| SeerError::RdapError(format!("failed to read response: {}", e)))?;
618 body.extend_from_slice(&chunk);
619 if body.len() > MAX_RDAP_RESPONSE_SIZE {
620 return Err(SeerError::RdapError(format!(
621 "RDAP response exceeds {} byte limit",
622 MAX_RDAP_RESPONSE_SIZE
623 )));
624 }
625 }
626 Ok::<(), SeerError>(())
627 })
628 .await;
629
630 match streamed {
631 Ok(Ok(())) => {}
632 Ok(Err(e)) => return Err(e),
633 Err(_) => {
634 return Err(SeerError::Timeout(format!(
635 "timed out reading RDAP response body from {} after {:?}",
636 host, DEFAULT_TIMEOUT
637 )));
638 }
639 }
640
641 let rdap: RdapResponse = serde_json::from_slice(&body)?;
642 rdap.validate_size()?;
647 Ok(rdap)
648}
649
650async fn load_bootstrap_data_with_retry(policy: &RetryPolicy) -> Result<BootstrapData> {
652 let executor = RetryExecutor::new(policy.clone());
653 executor.execute(load_bootstrap_data).await
654}
655
656async fn load_bootstrap_data() -> Result<BootstrapData> {
658 debug!("Loading RDAP bootstrap data from IANA");
659
660 let http = &*RDAP_HTTP_CLIENT;
664
665 let dns_future = http.get(IANA_BOOTSTRAP_DNS).send();
666 let ipv4_future = http.get(IANA_BOOTSTRAP_IPV4).send();
667 let ipv6_future = http.get(IANA_BOOTSTRAP_IPV6).send();
668 let asn_future = http.get(IANA_BOOTSTRAP_ASN).send();
669
670 let (dns_resp, ipv4_resp, ipv6_resp, asn_resp) =
673 tokio::join!(dns_future, ipv4_future, ipv6_future, asn_future);
674
675 const MAX_BOOTSTRAP_SIZE: usize = 10 * 1024 * 1024; async fn read_bootstrap(resp: reqwest::Response) -> Result<BootstrapResponse> {
679 let mut body = Vec::new();
680 let mut stream = resp.bytes_stream();
681 while let Some(chunk) = stream.next().await {
682 let chunk = chunk.map_err(|e| {
683 SeerError::RdapBootstrapError(format!("failed to read body: {}", e))
684 })?;
685 body.extend_from_slice(&chunk);
686 if body.len() > MAX_BOOTSTRAP_SIZE {
687 return Err(SeerError::RdapBootstrapError(format!(
688 "bootstrap response too large (exceeds {} bytes)",
689 MAX_BOOTSTRAP_SIZE
690 )));
691 }
692 }
693 serde_json::from_slice(&body).map_err(Into::into)
694 }
695
696 let dns_data = match dns_resp {
698 Ok(resp) => match read_bootstrap(resp).await {
699 Ok(data) => Some(data),
700 Err(e) => {
701 debug!(error = %e, "Failed to parse DNS bootstrap response");
702 None
703 }
704 },
705 Err(e) => {
706 debug!(error = %e, "Failed to fetch DNS bootstrap from IANA");
707 None
708 }
709 };
710 let ipv4_data = match ipv4_resp {
711 Ok(resp) => match read_bootstrap(resp).await {
712 Ok(data) => Some(data),
713 Err(e) => {
714 debug!(error = %e, "Failed to parse IPv4 bootstrap response");
715 None
716 }
717 },
718 Err(e) => {
719 debug!(error = %e, "Failed to fetch IPv4 bootstrap from IANA");
720 None
721 }
722 };
723 let ipv6_data = match ipv6_resp {
724 Ok(resp) => match read_bootstrap(resp).await {
725 Ok(data) => Some(data),
726 Err(e) => {
727 debug!(error = %e, "Failed to parse IPv6 bootstrap response");
728 None
729 }
730 },
731 Err(e) => {
732 debug!(error = %e, "Failed to fetch IPv6 bootstrap from IANA");
733 None
734 }
735 };
736 let asn_data = match asn_resp {
737 Ok(resp) => match read_bootstrap(resp).await {
738 Ok(data) => Some(data),
739 Err(e) => {
740 debug!(error = %e, "Failed to parse ASN bootstrap response");
741 None
742 }
743 },
744 Err(e) => {
745 debug!(error = %e, "Failed to fetch ASN bootstrap from IANA");
746 None
747 }
748 };
749
750 if dns_data.is_none() && ipv4_data.is_none() && ipv6_data.is_none() && asn_data.is_none() {
752 return Err(SeerError::RdapBootstrapError(
753 "all IANA bootstrap registries failed".to_string(),
754 ));
755 }
756
757 let mut dns = HashMap::new();
758 let mut ipv4 = Vec::new();
759 let mut ipv6 = Vec::new();
760 let mut asn = Vec::new();
761
762 fn collect_valid_urls(urls: &[serde_json::Value]) -> Option<Arc<Vec<url::Url>>> {
766 let mut out = Vec::new();
767 for u in urls {
768 if let Some(s) = u.as_str() {
769 match validate_bootstrap_url(s) {
770 Ok(parsed) => out.push(parsed),
771 Err(e) => {
772 debug!(url = s, error = %e, "Skipping invalid bootstrap URL");
773 }
774 }
775 }
776 }
777 if out.is_empty() {
778 None
779 } else {
780 Some(Arc::new(out))
781 }
782 }
783
784 if let Some(dns_data) = dns_data {
786 for service in dns_data.services {
787 if service.len() >= 2 {
788 if let (Some(tlds), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
789 if let Some(urls_arc) = collect_valid_urls(urls) {
790 for tld in tlds {
791 if let Some(tld_str) = tld.as_str() {
792 dns.insert(tld_str.to_lowercase(), Arc::clone(&urls_arc));
793 }
794 }
795 }
796 }
797 }
798 }
799 }
800
801 if let Some(ipv4_data) = ipv4_data {
803 for service in ipv4_data.services {
804 if service.len() >= 2 {
805 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array())
806 {
807 if let Some(urls_arc) = collect_valid_urls(urls) {
808 for prefix in prefixes {
809 if let Some(prefix_str) = prefix.as_str() {
810 ipv4.push((
811 IpRange {
812 prefix: prefix_str.to_string(),
813 },
814 Arc::clone(&urls_arc),
815 ));
816 }
817 }
818 }
819 }
820 }
821 }
822 }
823
824 if let Some(ipv6_data) = ipv6_data {
826 for service in ipv6_data.services {
827 if service.len() >= 2 {
828 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array())
829 {
830 if let Some(urls_arc) = collect_valid_urls(urls) {
831 for prefix in prefixes {
832 if let Some(prefix_str) = prefix.as_str() {
833 ipv6.push((
834 IpRange {
835 prefix: prefix_str.to_string(),
836 },
837 Arc::clone(&urls_arc),
838 ));
839 }
840 }
841 }
842 }
843 }
844 }
845 }
846
847 if let Some(asn_data) = asn_data {
849 for service in asn_data.services {
850 if service.len() >= 2 {
851 if let (Some(ranges), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
852 if let Some(urls_arc) = collect_valid_urls(urls) {
853 for range in ranges {
854 if let Some(range_str) = range.as_str() {
855 if let Some((start, end)) = parse_asn_range(range_str) {
856 asn.push((AsnRange { start, end }, Arc::clone(&urls_arc)));
857 }
858 }
859 }
860 }
861 }
862 }
863 }
864 }
865
866 info!(
867 dns_entries = dns.len(),
868 ipv4_ranges = ipv4.len(),
869 ipv6_ranges = ipv6.len(),
870 asn_ranges = asn.len(),
871 "RDAP bootstrap loaded"
872 );
873
874 Ok(BootstrapData {
875 dns,
876 ipv4,
877 ipv6,
878 asn,
879 })
880}
881
882fn wrap_all_candidates_failed(last_error: Option<SeerError>, candidate_count: usize) -> SeerError {
891 let last = last_error.unwrap_or_else(|| SeerError::RdapError("no candidates".to_string()));
892
893 if candidate_count <= 1 {
894 return last;
895 }
896
897 match last {
898 SeerError::Timeout(msg) => SeerError::Timeout(format!(
899 "all {} RDAP candidate URLs timed out; last error: {}",
900 candidate_count, msg
901 )),
902 other => SeerError::RdapError(format!(
903 "all {} RDAP candidate URLs failed; last error: {}",
904 candidate_count, other
905 )),
906 }
907}
908
909fn build_rdap_urls(bases: &[url::Url], path: &str) -> Vec<url::Url> {
911 bases
912 .iter()
913 .filter_map(|base| {
914 let base_str = base.as_str();
917 let normalized = if base_str.ends_with('/') {
918 base_str.to_string()
919 } else {
920 format!("{}/", base_str)
921 };
922 url::Url::parse(&normalized).and_then(|u| u.join(path)).ok()
923 })
924 .collect()
925}
926
927fn parse_asn_range(range: &str) -> Option<(u32, u32)> {
928 if let Some(pos) = range.find('-') {
929 let start = range[..pos].parse().ok()?;
930 let end = range[pos + 1..].parse().ok()?;
931 Some((start, end))
932 } else {
933 let num = range.parse().ok()?;
934 Some((num, num))
935 }
936}
937
938fn ipv4_matches_prefix(prefix: &str, ip: &Ipv4Addr) -> bool {
939 let (addr_part, mask_part) = match prefix.split_once('/') {
940 Some((a, m)) => (a, Some(m)),
941 None => (prefix, None),
942 };
943
944 let prefix_ip: Ipv4Addr = match addr_part.parse() {
945 Ok(ip) => ip,
946 Err(_) => return false,
947 };
948
949 let mask_bits: u32 = match mask_part.and_then(|s| s.parse().ok()) {
950 Some(bits) if bits <= 32 => bits,
951 Some(_) => return false,
952 None => 32,
953 };
954
955 let mask = if mask_bits == 0 {
956 0
957 } else {
958 u32::MAX << (32 - mask_bits)
959 };
960
961 let ip_value = u32::from(*ip);
962 let prefix_value = u32::from(prefix_ip);
963
964 (ip_value & mask) == (prefix_value & mask)
965}
966
967fn ipv6_matches_prefix(prefix: &str, ip: &Ipv6Addr) -> bool {
968 let (addr_part, mask_part) = match prefix.split_once('/') {
969 Some((a, m)) => (a, Some(m)),
970 None => (prefix, None),
971 };
972
973 let prefix_ip: Ipv6Addr = match addr_part.parse() {
974 Ok(ip) => ip,
975 Err(_) => return false,
976 };
977
978 let mask_bits: u32 = match mask_part.and_then(|s| s.parse().ok()) {
979 Some(bits) if bits <= 128 => bits,
980 Some(_) => return false,
981 None => 128,
982 };
983
984 let mask = if mask_bits == 0 {
985 0u128
986 } else {
987 u128::MAX << (128 - mask_bits)
988 };
989
990 let ip_value = ipv6_to_u128(ip);
991 let prefix_value = ipv6_to_u128(&prefix_ip);
992
993 (ip_value & mask) == (prefix_value & mask)
994}
995
996fn ipv6_to_u128(ip: &Ipv6Addr) -> u128 {
997 let segments = ip.segments();
998 let mut value = 0u128;
999 for segment in segments {
1000 value = (value << 16) | segment as u128;
1001 }
1002 value
1003}
1004
1005#[cfg(test)]
1006mod tests {
1007 use super::*;
1008
1009 #[test]
1010 fn test_default_client_has_retry_policy() {
1011 let client = RdapClient::new();
1012 assert_eq!(client.retry_policy.max_attempts, 2);
1013 }
1014
1015 #[test]
1016 fn test_client_without_retries() {
1017 let client = RdapClient::new().without_retries();
1018 assert_eq!(client.retry_policy.max_attempts, 1);
1019 }
1020
1021 #[test]
1022 fn test_client_custom_retry_policy() {
1023 let policy = RetryPolicy::new().with_max_attempts(5);
1024 let client = RdapClient::new().with_retry_policy(policy);
1025 assert_eq!(client.retry_policy.max_attempts, 5);
1026 }
1027
1028 #[test]
1029 fn test_cached_bootstrap_expiration() {
1030 let data = BootstrapData {
1031 dns: HashMap::new(),
1032 ipv4: Vec::new(),
1033 ipv6: Vec::new(),
1034 asn: Vec::new(),
1035 };
1036 let cached = CachedBootstrap::new(data);
1037 assert!(!cached.is_expired());
1039 }
1040
1041 #[test]
1042 fn test_ipv4_prefix_matching_partial_mask() {
1043 let ip_in = Ipv4Addr::new(203, 0, 114, 1);
1044 let ip_out = Ipv4Addr::new(203, 0, 120, 1);
1045 assert!(ipv4_matches_prefix("203.0.112.0/21", &ip_in));
1046 assert!(!ipv4_matches_prefix("203.0.112.0/21", &ip_out));
1047 }
1048
1049 #[test]
1050 fn test_ipv6_prefix_matching_partial_mask() {
1051 let ip_in: Ipv6Addr = "2001:db8::1".parse().unwrap();
1052 let ip_out: Ipv6Addr = "2001:db9::1".parse().unwrap();
1053 assert!(ipv6_matches_prefix("2001:db8::/33", &ip_in));
1054 assert!(!ipv6_matches_prefix("2001:db8::/33", &ip_out));
1055 }
1056
1057 #[test]
1058 fn test_rdap_http_client_is_configured() {
1059 let _client = &*RDAP_HTTP_CLIENT;
1061 }
1062
1063 #[test]
1064 fn test_parse_bootstrap_empty_services() {
1065 let data = BootstrapData {
1067 dns: HashMap::new(),
1068 ipv4: Vec::new(),
1069 ipv6: Vec::new(),
1070 asn: Vec::new(),
1071 };
1072 assert!(RdapClient::get_rdap_urls_for_domain(&data, "example.com").is_none());
1074 assert!(RdapClient::get_rdap_urls_for_asn(&data, 12345).is_none());
1075 }
1076
1077 #[test]
1080 fn test_validate_bootstrap_url_accepts_https() {
1081 let url = validate_bootstrap_url("https://rdap.example.com/").unwrap();
1082 assert_eq!(url.scheme(), "https");
1083 assert_eq!(url.host_str(), Some("rdap.example.com"));
1084 }
1085
1086 #[test]
1087 fn test_validate_bootstrap_url_rejects_http() {
1088 let err = validate_bootstrap_url("http://rdap.example.com/").unwrap_err();
1089 assert!(
1090 matches!(err, SeerError::RdapError(ref s) if s.contains("https")),
1091 "expected https-scheme error, got: {:?}",
1092 err
1093 );
1094 }
1095
1096 #[test]
1097 fn test_validate_bootstrap_url_rejects_ftp() {
1098 let err = validate_bootstrap_url("ftp://rdap.example.com/").unwrap_err();
1099 assert!(matches!(err, SeerError::RdapError(_)));
1100 }
1101
1102 #[test]
1103 fn test_validate_bootstrap_url_rejects_ip_literal_v4() {
1104 let err = validate_bootstrap_url("https://192.0.2.1/").unwrap_err();
1105 assert!(
1106 matches!(err, SeerError::RdapError(ref s) if s.contains("IP literal")),
1107 "expected IP-literal error, got: {:?}",
1108 err
1109 );
1110 }
1111
1112 #[test]
1113 fn test_validate_bootstrap_url_rejects_ip_literal_v6() {
1114 let err = validate_bootstrap_url("https://[2001:db8::1]/").unwrap_err();
1115 assert!(
1116 matches!(err, SeerError::RdapError(ref s) if s.contains("IP literal")),
1117 "expected IP-literal error, got: {:?}",
1118 err
1119 );
1120 }
1121
1122 #[test]
1123 fn test_validate_bootstrap_url_rejects_garbage() {
1124 let err = validate_bootstrap_url("not a url").unwrap_err();
1125 assert!(matches!(err, SeerError::RdapError(_)));
1126 }
1127
1128 #[tokio::test]
1131 async fn test_validate_url_not_reserved_rejects_loopback_literal() {
1132 let err = validate_url_not_reserved("https://127.0.0.1/domain/example.com")
1133 .await
1134 .unwrap_err();
1135 assert!(
1136 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1137 "expected reserved-IP error, got: {:?}",
1138 err
1139 );
1140 }
1141
1142 #[tokio::test]
1143 async fn test_validate_url_not_reserved_rejects_private_ipv4_literal() {
1144 let err = validate_url_not_reserved("https://10.0.0.1/")
1145 .await
1146 .unwrap_err();
1147 assert!(
1148 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1149 "expected reserved-IP error, got: {:?}",
1150 err
1151 );
1152 }
1153
1154 #[tokio::test]
1155 async fn test_validate_url_not_reserved_rejects_ipv6_loopback_literal() {
1156 let err = validate_url_not_reserved("https://[::1]/")
1157 .await
1158 .unwrap_err();
1159 assert!(
1160 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1161 "expected reserved-IP error, got: {:?}",
1162 err
1163 );
1164 }
1165
1166 #[tokio::test]
1167 async fn test_validate_url_not_reserved_returns_resolved_addrs_for_public_literal() {
1168 let addrs = validate_url_not_reserved("https://8.8.8.8/").await.unwrap();
1171 assert_eq!(addrs.len(), 1);
1172 assert!(addrs[0].ip().is_ipv4());
1173 assert_eq!(addrs[0].port(), 443);
1174 }
1175
1176 #[test]
1179 fn test_build_rdap_urls_preserves_order_and_appends_path() {
1180 let bases = vec![
1181 url::Url::parse("https://rdap.a.example/").unwrap(),
1182 url::Url::parse("https://rdap.b.example").unwrap(), ];
1184 let built = build_rdap_urls(&bases, "domain/example.com");
1185 assert_eq!(built.len(), 2);
1186 assert_eq!(
1187 built[0].as_str(),
1188 "https://rdap.a.example/domain/example.com"
1189 );
1190 assert_eq!(
1191 built[1].as_str(),
1192 "https://rdap.b.example/domain/example.com"
1193 );
1194 }
1195
1196 #[test]
1197 fn test_build_rdap_urls_empty_input_returns_empty() {
1198 let built = build_rdap_urls(&[], "domain/example.com");
1199 assert!(built.is_empty());
1200 }
1201
1202 #[test]
1205 fn test_wrap_all_candidates_failed_preserves_timeout_variant() {
1206 let last = SeerError::Timeout("body read timed out".to_string());
1209 let wrapped = wrap_all_candidates_failed(Some(last), 3);
1210 match wrapped {
1211 SeerError::Timeout(msg) => {
1212 assert!(
1213 msg.contains("all 3 RDAP candidate URLs timed out"),
1214 "expected wrapped timeout message, got: {}",
1215 msg
1216 );
1217 assert!(
1218 msg.contains("body read timed out"),
1219 "expected original message preserved, got: {}",
1220 msg
1221 );
1222 }
1223 other => panic!(
1224 "expected SeerError::Timeout after wrapping a Timeout, got: {:?}",
1225 other
1226 ),
1227 }
1228 }
1229
1230 #[test]
1231 fn test_wrap_all_candidates_failed_wraps_non_timeout_as_rdap_error() {
1232 let last = SeerError::RdapError("500 internal error".to_string());
1233 let wrapped = wrap_all_candidates_failed(Some(last), 2);
1234 assert!(
1235 matches!(wrapped, SeerError::RdapError(ref s) if s.contains("all 2 RDAP candidate URLs failed")),
1236 "expected wrapped RdapError, got: {:?}",
1237 wrapped
1238 );
1239 }
1240
1241 #[test]
1242 fn test_wrap_all_candidates_failed_single_candidate_returns_unchanged() {
1243 let last = SeerError::Timeout("single timeout".to_string());
1246 let wrapped = wrap_all_candidates_failed(Some(last), 1);
1247 assert!(
1248 matches!(wrapped, SeerError::Timeout(ref s) if s == "single timeout"),
1249 "expected unchanged Timeout, got: {:?}",
1250 wrapped
1251 );
1252 }
1253
1254 #[test]
1255 fn test_wrap_all_candidates_failed_no_last_error_returns_placeholder() {
1256 let wrapped = wrap_all_candidates_failed(None, 0);
1257 assert!(matches!(wrapped, SeerError::RdapError(_)));
1258 }
1259
1260 static BOOTSTRAP_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
1279
1280 #[tokio::test]
1281 async fn test_bootstrap_load_notify_wakes_waiter_when_cache_populated() {
1282 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1283
1284 {
1286 let mut cache = BOOTSTRAP_CACHE.write().await;
1287 *cache = None;
1288 }
1289
1290 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1293 tokio::pin!(notified);
1294
1295 {
1297 let mut cache = BOOTSTRAP_CACHE.write().await;
1298 *cache = Some(CachedBootstrap::new(BootstrapData {
1299 dns: HashMap::new(),
1300 ipv4: Vec::new(),
1301 ipv6: Vec::new(),
1302 asn: Vec::new(),
1303 }));
1304 }
1305 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1306
1307 let result = wait_for_in_flight_load(notified).await;
1308 assert!(
1309 result.is_ok(),
1310 "expected waiter to see populated cache, got: {:?}",
1311 result
1312 );
1313
1314 {
1316 let mut cache = BOOTSTRAP_CACHE.write().await;
1317 *cache = None;
1318 }
1319 }
1320
1321 #[tokio::test]
1322 async fn test_bootstrap_load_notify_empty_cache_after_wake_returns_error() {
1323 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1324
1325 {
1327 let mut cache = BOOTSTRAP_CACHE.write().await;
1328 *cache = None;
1329 }
1330
1331 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1332 tokio::pin!(notified);
1333
1334 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1336
1337 let result = wait_for_in_flight_load(notified).await;
1338 assert!(
1339 matches!(
1340 result,
1341 Err(SeerError::RdapBootstrapError(ref s))
1342 if s.contains("throttled and no cache available")
1343 ),
1344 "expected throttled error when cache still empty after notify, got: {:?}",
1345 result
1346 );
1347 }
1348}