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, instrument, warn};
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 debug!(
263 dns_entries = data.dns.len(),
264 ipv4_entries = data.ipv4.len(),
265 ipv6_entries = data.ipv6.len(),
266 asn_entries = data.asn.len(),
267 "RDAP bootstrap loaded/refreshed"
268 );
269 let mut cache = BOOTSTRAP_CACHE.write().await;
270 let should_store = cache.as_ref().map(|c| c.is_expired()).unwrap_or(true);
273 if should_store {
274 *cache = Some(CachedBootstrap::new(data));
275 }
276 Ok(())
277 }
278 Err(e) => {
279 let cache = BOOTSTRAP_CACHE.read().await;
281 if let Some(cached) = cache.as_ref() {
282 warn!(
283 error = %e,
284 age_hours = cached.age().as_secs() / 3600,
285 "Bootstrap refresh failed, using stale data"
286 );
287 Ok(())
288 } else {
289 Err(e)
291 }
292 }
293 };
294
295 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
297 outcome
298 }
299
300 fn get_rdap_urls_for_domain(cache: &BootstrapData, domain: &str) -> Option<Arc<Vec<url::Url>>> {
302 let tld = domain.rsplit('.').next()?;
303 cache.dns.get(&tld.to_lowercase()).cloned()
304 }
305
306 fn get_rdap_urls_for_ip(cache: &BootstrapData, ip: &IpAddr) -> Option<Arc<Vec<url::Url>>> {
308 match ip {
309 IpAddr::V4(addr) => {
310 for (range, urls) in &cache.ipv4 {
311 if ipv4_matches_prefix(&range.prefix, addr) {
312 return Some(Arc::clone(urls));
313 }
314 }
315 }
316 IpAddr::V6(addr) => {
317 for (range, urls) in &cache.ipv6 {
318 if ipv6_matches_prefix(&range.prefix, addr) {
319 return Some(Arc::clone(urls));
320 }
321 }
322 }
323 }
324
325 None
326 }
327
328 fn get_rdap_urls_for_asn(cache: &BootstrapData, asn: u32) -> Option<Arc<Vec<url::Url>>> {
330 for (range, urls) in &cache.asn {
331 if asn >= range.start && asn <= range.end {
332 return Some(Arc::clone(urls));
333 }
334 }
335
336 None
337 }
338
339 #[instrument(skip(self), fields(domain = %domain))]
343 pub async fn lookup_domain(&self, domain: &str) -> Result<RdapResponse> {
344 self.ensure_bootstrap().await?;
345
346 let domain = normalize_domain(domain)?;
347
348 let urls = {
350 let cache_guard = BOOTSTRAP_CACHE.read().await;
351 let cache = cache_guard.as_ref().ok_or_else(|| {
352 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
353 })?;
354
355 let bases = Self::get_rdap_urls_for_domain(&cache.data, &domain).ok_or_else(|| {
356 SeerError::RdapBootstrapError(format!("no RDAP server for {}", domain))
357 })?;
358
359 build_rdap_urls(&bases, &format!("domain/{}", domain))
360 }; self.query_rdap_urls(&urls).await
363 }
364
365 #[instrument(skip(self), fields(ip = %ip))]
369 pub async fn lookup_ip(&self, ip: &str) -> Result<RdapResponse> {
370 self.ensure_bootstrap().await?;
371
372 let ip_addr: IpAddr = ip
373 .parse()
374 .map_err(|_| SeerError::InvalidIpAddress(ip.to_string()))?;
375
376 let urls = {
377 let cache_guard = BOOTSTRAP_CACHE.read().await;
378 let cache = cache_guard.as_ref().ok_or_else(|| {
379 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
380 })?;
381
382 let bases = Self::get_rdap_urls_for_ip(&cache.data, &ip_addr).ok_or_else(|| {
383 SeerError::RdapBootstrapError(format!("no RDAP server for {}", ip))
384 })?;
385
386 build_rdap_urls(&bases, &format!("ip/{}", ip))
387 };
388
389 self.query_rdap_urls(&urls).await
390 }
391
392 #[instrument(skip(self), fields(asn = %asn))]
396 pub async fn lookup_asn(&self, asn: u32) -> Result<RdapResponse> {
397 self.ensure_bootstrap().await?;
398
399 let urls = {
400 let cache_guard = BOOTSTRAP_CACHE.read().await;
401 let cache = cache_guard.as_ref().ok_or_else(|| {
402 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
403 })?;
404
405 let bases = Self::get_rdap_urls_for_asn(&cache.data, asn).ok_or_else(|| {
406 SeerError::RdapBootstrapError(format!("no RDAP server for AS{}", asn))
407 })?;
408
409 build_rdap_urls(&bases, &format!("autnum/{}", asn))
410 };
411
412 self.query_rdap_urls(&urls).await
413 }
414
415 #[instrument(skip(self), fields(tld = %tld))]
421 pub async fn get_rdap_base_url_for_tld(&self, tld: &str) -> Option<String> {
422 if self.ensure_bootstrap().await.is_err() {
423 return None;
424 }
425
426 let cache_guard = BOOTSTRAP_CACHE.read().await;
427 let cache = cache_guard.as_ref()?;
428 cache
429 .data
430 .dns
431 .get(&tld.to_lowercase())
432 .and_then(|urls| urls.first())
433 .map(|u| u.to_string())
434 }
435
436 async fn query_rdap_urls(&self, urls: &[url::Url]) -> Result<RdapResponse> {
440 if urls.is_empty() {
441 return Err(SeerError::RdapError(
442 "no candidate RDAP URLs available".to_string(),
443 ));
444 }
445
446 let mut last_error: Option<SeerError> = None;
447 for (idx, url) in urls.iter().enumerate() {
448 let url_str = url.as_str().to_string();
449 debug!(url = %url_str, candidate = idx + 1, total = urls.len(), "Querying RDAP");
450 match self.query_rdap_with_retry(&url_str).await {
451 Ok(resp) => return Ok(resp),
452 Err(e) => {
453 if urls.len() > 1 {
454 warn!(
455 url = %url_str,
456 error = %e,
457 candidate = idx + 1,
458 total = urls.len(),
459 "RDAP candidate failed, trying next",
460 );
461 }
462 last_error = Some(e);
463 }
464 }
465 }
466
467 Err(wrap_all_candidates_failed(last_error, urls.len()))
469 }
470
471 async fn query_rdap_with_retry(&self, url: &str) -> Result<RdapResponse> {
473 let executor = RetryExecutor::new(self.retry_policy.clone());
474 let url = url.to_string();
475
476 executor
477 .execute(|| {
478 let url = url.clone();
479 async move { query_rdap_internal(&url).await }
480 })
481 .await
482 }
483}
484
485const MAX_RDAP_RESPONSE_SIZE: usize = 10 * 1024 * 1024;
487
488async fn validate_url_not_reserved(url: &str) -> Result<Vec<SocketAddr>> {
495 let parsed = url::Url::parse(url)
496 .map_err(|e| SeerError::RdapError(format!("invalid URL '{}': {}", url, e)))?;
497 let host = parsed
498 .host_str()
499 .ok_or_else(|| SeerError::RdapError(format!("URL '{}' has no host", url)))?;
500 let port = parsed.port_or_known_default().unwrap_or(443);
501
502 if let Ok(ip) = host.parse::<IpAddr>() {
504 if let Some(reason) = describe_reserved_ip(&ip) {
505 return Err(SeerError::RdapError(format!(
506 "RDAP URL resolves to reserved IP {}: {} — request blocked (SSRF protection)",
507 ip, reason
508 )));
509 }
510 return Ok(vec![SocketAddr::new(ip, port)]);
511 }
512
513 let addr = format!("{}:{}", host, port);
514
515 let socket_addrs: Vec<SocketAddr> = tokio::net::lookup_host(&addr)
516 .await
517 .map_err(|e| SeerError::RdapError(format!("failed to resolve host '{}': {}", host, e)))?
518 .collect();
519
520 if socket_addrs.is_empty() {
521 return Err(SeerError::RdapError(format!(
522 "host '{}' resolved to no addresses",
523 host
524 )));
525 }
526
527 for socket_addr in &socket_addrs {
528 if let Some(reason) = describe_reserved_ip(&socket_addr.ip()) {
529 return Err(SeerError::RdapError(format!(
530 "RDAP URL resolves to reserved IP {}: {} — request blocked (SSRF protection)",
531 socket_addr.ip(),
532 reason
533 )));
534 }
535 }
536
537 Ok(socket_addrs)
538}
539
540fn validate_bootstrap_url(s: &str) -> Result<url::Url> {
546 let parsed = url::Url::parse(s)
547 .map_err(|e| SeerError::RdapError(format!("bad bootstrap URL {}: {}", s, e)))?;
548 if parsed.scheme() != "https" {
549 return Err(SeerError::RdapError(format!(
550 "bootstrap URL must be https, got {}",
551 parsed.scheme()
552 )));
553 }
554 let host = parsed
555 .host()
556 .ok_or_else(|| SeerError::RdapError(format!("bootstrap URL has no host: {}", s)))?;
557 match host {
558 url::Host::Ipv4(_) | url::Host::Ipv6(_) => {
559 return Err(SeerError::RdapError(format!(
560 "bootstrap URL must not be an IP literal: {}",
561 s
562 )));
563 }
564 url::Host::Domain(d) => {
565 if d.is_empty() || d.chars().any(|c| c.is_whitespace() || c.is_control()) {
566 return Err(SeerError::RdapError(format!(
567 "bootstrap URL has invalid host: {}",
568 s
569 )));
570 }
571 }
572 }
573 Ok(parsed)
574}
575
576async fn query_rdap_internal(url: &str) -> Result<RdapResponse> {
581 let resolved = validate_url_not_reserved(url).await?;
584
585 let parsed = url::Url::parse(url)
586 .map_err(|e| SeerError::RdapError(format!("invalid URL '{}': {}", url, e)))?;
587 let host = parsed
588 .host_str()
589 .ok_or_else(|| SeerError::RdapError(format!("URL '{}' has no host", url)))?;
590
591 let client = Client::builder()
595 .timeout(DEFAULT_TIMEOUT)
596 .connect_timeout(CONNECT_TIMEOUT)
597 .user_agent("Seer/1.0 (RDAP Client)")
598 .resolve_to_addrs(host, &resolved)
599 .build()
600 .map_err(|e| SeerError::RdapError(format!("failed to build HTTP client: {}", e)))?;
601
602 let response = client
603 .get(url)
604 .header("Accept", "application/rdap+json")
605 .send()
606 .await?;
607
608 if !response.status().is_success() {
609 return Err(SeerError::RdapError(format!(
610 "query failed with status {}",
611 response.status()
612 )));
613 }
614
615 let mut body = Vec::new();
620 let mut stream = response.bytes_stream();
621 let streamed = tokio::time::timeout(DEFAULT_TIMEOUT, async {
622 while let Some(chunk) = stream.next().await {
623 let chunk = chunk
624 .map_err(|e| SeerError::RdapError(format!("failed to read response: {}", e)))?;
625 body.extend_from_slice(&chunk);
626 if body.len() > MAX_RDAP_RESPONSE_SIZE {
627 return Err(SeerError::RdapError(format!(
628 "RDAP response exceeds {} byte limit",
629 MAX_RDAP_RESPONSE_SIZE
630 )));
631 }
632 }
633 Ok::<(), SeerError>(())
634 })
635 .await;
636
637 match streamed {
638 Ok(Ok(())) => {}
639 Ok(Err(e)) => return Err(e),
640 Err(_) => {
641 return Err(SeerError::Timeout(format!(
642 "timed out reading RDAP response body from {} after {:?}",
643 host, DEFAULT_TIMEOUT
644 )));
645 }
646 }
647
648 let rdap: RdapResponse = serde_json::from_slice(&body)?;
649 rdap.validate_size()?;
654 Ok(rdap)
655}
656
657async fn load_bootstrap_data_with_retry(policy: &RetryPolicy) -> Result<BootstrapData> {
659 let executor = RetryExecutor::new(policy.clone());
660 executor.execute(load_bootstrap_data).await
661}
662
663async fn load_bootstrap_data() -> Result<BootstrapData> {
665 debug!("Loading RDAP bootstrap data from IANA");
666
667 let http = &*RDAP_HTTP_CLIENT;
671
672 let dns_future = http.get(IANA_BOOTSTRAP_DNS).send();
673 let ipv4_future = http.get(IANA_BOOTSTRAP_IPV4).send();
674 let ipv6_future = http.get(IANA_BOOTSTRAP_IPV6).send();
675 let asn_future = http.get(IANA_BOOTSTRAP_ASN).send();
676
677 let (dns_resp, ipv4_resp, ipv6_resp, asn_resp) =
680 tokio::join!(dns_future, ipv4_future, ipv6_future, asn_future);
681
682 const MAX_BOOTSTRAP_SIZE: usize = 10 * 1024 * 1024; async fn read_bootstrap(resp: reqwest::Response) -> Result<BootstrapResponse> {
686 let mut body = Vec::new();
687 let mut stream = resp.bytes_stream();
688 while let Some(chunk) = stream.next().await {
689 let chunk = chunk.map_err(|e| {
690 SeerError::RdapBootstrapError(format!("failed to read body: {}", e))
691 })?;
692 body.extend_from_slice(&chunk);
693 if body.len() > MAX_BOOTSTRAP_SIZE {
694 return Err(SeerError::RdapBootstrapError(format!(
695 "bootstrap response too large (exceeds {} bytes)",
696 MAX_BOOTSTRAP_SIZE
697 )));
698 }
699 }
700 serde_json::from_slice(&body).map_err(Into::into)
701 }
702
703 let dns_data = match dns_resp {
705 Ok(resp) => match read_bootstrap(resp).await {
706 Ok(data) => Some(data),
707 Err(e) => {
708 warn!(error = %e, "Failed to parse DNS bootstrap response");
709 None
710 }
711 },
712 Err(e) => {
713 warn!(error = %e, "Failed to fetch DNS bootstrap from IANA");
714 None
715 }
716 };
717 let ipv4_data = match ipv4_resp {
718 Ok(resp) => match read_bootstrap(resp).await {
719 Ok(data) => Some(data),
720 Err(e) => {
721 warn!(error = %e, "Failed to parse IPv4 bootstrap response");
722 None
723 }
724 },
725 Err(e) => {
726 warn!(error = %e, "Failed to fetch IPv4 bootstrap from IANA");
727 None
728 }
729 };
730 let ipv6_data = match ipv6_resp {
731 Ok(resp) => match read_bootstrap(resp).await {
732 Ok(data) => Some(data),
733 Err(e) => {
734 warn!(error = %e, "Failed to parse IPv6 bootstrap response");
735 None
736 }
737 },
738 Err(e) => {
739 warn!(error = %e, "Failed to fetch IPv6 bootstrap from IANA");
740 None
741 }
742 };
743 let asn_data = match asn_resp {
744 Ok(resp) => match read_bootstrap(resp).await {
745 Ok(data) => Some(data),
746 Err(e) => {
747 warn!(error = %e, "Failed to parse ASN bootstrap response");
748 None
749 }
750 },
751 Err(e) => {
752 warn!(error = %e, "Failed to fetch ASN bootstrap from IANA");
753 None
754 }
755 };
756
757 if dns_data.is_none() && ipv4_data.is_none() && ipv6_data.is_none() && asn_data.is_none() {
759 return Err(SeerError::RdapBootstrapError(
760 "all IANA bootstrap registries failed".to_string(),
761 ));
762 }
763
764 let mut dns = HashMap::new();
765 let mut ipv4 = Vec::new();
766 let mut ipv6 = Vec::new();
767 let mut asn = Vec::new();
768
769 fn collect_valid_urls(urls: &[serde_json::Value]) -> Option<Arc<Vec<url::Url>>> {
773 let mut out = Vec::new();
774 for u in urls {
775 if let Some(s) = u.as_str() {
776 match validate_bootstrap_url(s) {
777 Ok(parsed) => out.push(parsed),
778 Err(e) => {
779 warn!(url = s, error = %e, "Skipping invalid bootstrap URL");
780 }
781 }
782 }
783 }
784 if out.is_empty() {
785 None
786 } else {
787 Some(Arc::new(out))
788 }
789 }
790
791 if let Some(dns_data) = dns_data {
793 for service in dns_data.services {
794 if service.len() >= 2 {
795 if let (Some(tlds), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
796 if let Some(urls_arc) = collect_valid_urls(urls) {
797 for tld in tlds {
798 if let Some(tld_str) = tld.as_str() {
799 dns.insert(tld_str.to_lowercase(), Arc::clone(&urls_arc));
800 }
801 }
802 }
803 }
804 }
805 }
806 }
807
808 if let Some(ipv4_data) = ipv4_data {
810 for service in ipv4_data.services {
811 if service.len() >= 2 {
812 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array())
813 {
814 if let Some(urls_arc) = collect_valid_urls(urls) {
815 for prefix in prefixes {
816 if let Some(prefix_str) = prefix.as_str() {
817 ipv4.push((
818 IpRange {
819 prefix: prefix_str.to_string(),
820 },
821 Arc::clone(&urls_arc),
822 ));
823 }
824 }
825 }
826 }
827 }
828 }
829 }
830
831 if let Some(ipv6_data) = ipv6_data {
833 for service in ipv6_data.services {
834 if service.len() >= 2 {
835 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array())
836 {
837 if let Some(urls_arc) = collect_valid_urls(urls) {
838 for prefix in prefixes {
839 if let Some(prefix_str) = prefix.as_str() {
840 ipv6.push((
841 IpRange {
842 prefix: prefix_str.to_string(),
843 },
844 Arc::clone(&urls_arc),
845 ));
846 }
847 }
848 }
849 }
850 }
851 }
852 }
853
854 if let Some(asn_data) = asn_data {
856 for service in asn_data.services {
857 if service.len() >= 2 {
858 if let (Some(ranges), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
859 if let Some(urls_arc) = collect_valid_urls(urls) {
860 for range in ranges {
861 if let Some(range_str) = range.as_str() {
862 if let Some((start, end)) = parse_asn_range(range_str) {
863 asn.push((AsnRange { start, end }, Arc::clone(&urls_arc)));
864 }
865 }
866 }
867 }
868 }
869 }
870 }
871 }
872
873 Ok(BootstrapData {
874 dns,
875 ipv4,
876 ipv6,
877 asn,
878 })
879}
880
881fn wrap_all_candidates_failed(last_error: Option<SeerError>, candidate_count: usize) -> SeerError {
890 let last = last_error.unwrap_or_else(|| SeerError::RdapError("no candidates".to_string()));
891
892 if candidate_count <= 1 {
893 return last;
894 }
895
896 match last {
897 SeerError::Timeout(msg) => SeerError::Timeout(format!(
898 "all {} RDAP candidate URLs timed out; last error: {}",
899 candidate_count, msg
900 )),
901 other => SeerError::RdapError(format!(
902 "all {} RDAP candidate URLs failed; last error: {}",
903 candidate_count, other
904 )),
905 }
906}
907
908fn build_rdap_urls(bases: &[url::Url], path: &str) -> Vec<url::Url> {
910 bases
911 .iter()
912 .filter_map(|base| {
913 let base_str = base.as_str();
916 let normalized = if base_str.ends_with('/') {
917 base_str.to_string()
918 } else {
919 format!("{}/", base_str)
920 };
921 url::Url::parse(&normalized).and_then(|u| u.join(path)).ok()
922 })
923 .collect()
924}
925
926fn parse_asn_range(range: &str) -> Option<(u32, u32)> {
927 if let Some(pos) = range.find('-') {
928 let start = range[..pos].parse().ok()?;
929 let end = range[pos + 1..].parse().ok()?;
930 Some((start, end))
931 } else {
932 let num = range.parse().ok()?;
933 Some((num, num))
934 }
935}
936
937fn ipv4_matches_prefix(prefix: &str, ip: &Ipv4Addr) -> bool {
938 let (addr_part, mask_part) = match prefix.split_once('/') {
939 Some((a, m)) => (a, Some(m)),
940 None => (prefix, None),
941 };
942
943 let prefix_ip: Ipv4Addr = match addr_part.parse() {
944 Ok(ip) => ip,
945 Err(_) => return false,
946 };
947
948 let mask_bits: u32 = match mask_part.and_then(|s| s.parse().ok()) {
949 Some(bits) if bits <= 32 => bits,
950 Some(_) => return false,
951 None => 32,
952 };
953
954 let mask = if mask_bits == 0 {
955 0
956 } else {
957 u32::MAX << (32 - mask_bits)
958 };
959
960 let ip_value = u32::from(*ip);
961 let prefix_value = u32::from(prefix_ip);
962
963 (ip_value & mask) == (prefix_value & mask)
964}
965
966fn ipv6_matches_prefix(prefix: &str, ip: &Ipv6Addr) -> bool {
967 let (addr_part, mask_part) = match prefix.split_once('/') {
968 Some((a, m)) => (a, Some(m)),
969 None => (prefix, None),
970 };
971
972 let prefix_ip: Ipv6Addr = match addr_part.parse() {
973 Ok(ip) => ip,
974 Err(_) => return false,
975 };
976
977 let mask_bits: u32 = match mask_part.and_then(|s| s.parse().ok()) {
978 Some(bits) if bits <= 128 => bits,
979 Some(_) => return false,
980 None => 128,
981 };
982
983 let mask = if mask_bits == 0 {
984 0u128
985 } else {
986 u128::MAX << (128 - mask_bits)
987 };
988
989 let ip_value = ipv6_to_u128(ip);
990 let prefix_value = ipv6_to_u128(&prefix_ip);
991
992 (ip_value & mask) == (prefix_value & mask)
993}
994
995fn ipv6_to_u128(ip: &Ipv6Addr) -> u128 {
996 let segments = ip.segments();
997 let mut value = 0u128;
998 for segment in segments {
999 value = (value << 16) | segment as u128;
1000 }
1001 value
1002}
1003
1004#[cfg(test)]
1005mod tests {
1006 use super::*;
1007
1008 #[test]
1009 fn test_default_client_has_retry_policy() {
1010 let client = RdapClient::new();
1011 assert_eq!(client.retry_policy.max_attempts, 2);
1012 }
1013
1014 #[test]
1015 fn test_client_without_retries() {
1016 let client = RdapClient::new().without_retries();
1017 assert_eq!(client.retry_policy.max_attempts, 1);
1018 }
1019
1020 #[test]
1021 fn test_client_custom_retry_policy() {
1022 let policy = RetryPolicy::new().with_max_attempts(5);
1023 let client = RdapClient::new().with_retry_policy(policy);
1024 assert_eq!(client.retry_policy.max_attempts, 5);
1025 }
1026
1027 #[test]
1028 fn test_cached_bootstrap_expiration() {
1029 let data = BootstrapData {
1030 dns: HashMap::new(),
1031 ipv4: Vec::new(),
1032 ipv6: Vec::new(),
1033 asn: Vec::new(),
1034 };
1035 let cached = CachedBootstrap::new(data);
1036 assert!(!cached.is_expired());
1038 }
1039
1040 #[test]
1041 fn test_ipv4_prefix_matching_partial_mask() {
1042 let ip_in = Ipv4Addr::new(203, 0, 114, 1);
1043 let ip_out = Ipv4Addr::new(203, 0, 120, 1);
1044 assert!(ipv4_matches_prefix("203.0.112.0/21", &ip_in));
1045 assert!(!ipv4_matches_prefix("203.0.112.0/21", &ip_out));
1046 }
1047
1048 #[test]
1049 fn test_ipv6_prefix_matching_partial_mask() {
1050 let ip_in: Ipv6Addr = "2001:db8::1".parse().unwrap();
1051 let ip_out: Ipv6Addr = "2001:db9::1".parse().unwrap();
1052 assert!(ipv6_matches_prefix("2001:db8::/33", &ip_in));
1053 assert!(!ipv6_matches_prefix("2001:db8::/33", &ip_out));
1054 }
1055
1056 #[test]
1057 fn test_rdap_http_client_is_configured() {
1058 let _client = &*RDAP_HTTP_CLIENT;
1060 }
1061
1062 #[test]
1063 fn test_parse_bootstrap_empty_services() {
1064 let data = BootstrapData {
1066 dns: HashMap::new(),
1067 ipv4: Vec::new(),
1068 ipv6: Vec::new(),
1069 asn: Vec::new(),
1070 };
1071 assert!(RdapClient::get_rdap_urls_for_domain(&data, "example.com").is_none());
1073 assert!(RdapClient::get_rdap_urls_for_asn(&data, 12345).is_none());
1074 }
1075
1076 #[test]
1079 fn test_validate_bootstrap_url_accepts_https() {
1080 let url = validate_bootstrap_url("https://rdap.example.com/").unwrap();
1081 assert_eq!(url.scheme(), "https");
1082 assert_eq!(url.host_str(), Some("rdap.example.com"));
1083 }
1084
1085 #[test]
1086 fn test_validate_bootstrap_url_rejects_http() {
1087 let err = validate_bootstrap_url("http://rdap.example.com/").unwrap_err();
1088 assert!(
1089 matches!(err, SeerError::RdapError(ref s) if s.contains("https")),
1090 "expected https-scheme error, got: {:?}",
1091 err
1092 );
1093 }
1094
1095 #[test]
1096 fn test_validate_bootstrap_url_rejects_ftp() {
1097 let err = validate_bootstrap_url("ftp://rdap.example.com/").unwrap_err();
1098 assert!(matches!(err, SeerError::RdapError(_)));
1099 }
1100
1101 #[test]
1102 fn test_validate_bootstrap_url_rejects_ip_literal_v4() {
1103 let err = validate_bootstrap_url("https://192.0.2.1/").unwrap_err();
1104 assert!(
1105 matches!(err, SeerError::RdapError(ref s) if s.contains("IP literal")),
1106 "expected IP-literal error, got: {:?}",
1107 err
1108 );
1109 }
1110
1111 #[test]
1112 fn test_validate_bootstrap_url_rejects_ip_literal_v6() {
1113 let err = validate_bootstrap_url("https://[2001:db8::1]/").unwrap_err();
1114 assert!(
1115 matches!(err, SeerError::RdapError(ref s) if s.contains("IP literal")),
1116 "expected IP-literal error, got: {:?}",
1117 err
1118 );
1119 }
1120
1121 #[test]
1122 fn test_validate_bootstrap_url_rejects_garbage() {
1123 let err = validate_bootstrap_url("not a url").unwrap_err();
1124 assert!(matches!(err, SeerError::RdapError(_)));
1125 }
1126
1127 #[tokio::test]
1130 async fn test_validate_url_not_reserved_rejects_loopback_literal() {
1131 let err = validate_url_not_reserved("https://127.0.0.1/domain/example.com")
1132 .await
1133 .unwrap_err();
1134 assert!(
1135 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1136 "expected reserved-IP error, got: {:?}",
1137 err
1138 );
1139 }
1140
1141 #[tokio::test]
1142 async fn test_validate_url_not_reserved_rejects_private_ipv4_literal() {
1143 let err = validate_url_not_reserved("https://10.0.0.1/")
1144 .await
1145 .unwrap_err();
1146 assert!(
1147 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1148 "expected reserved-IP error, got: {:?}",
1149 err
1150 );
1151 }
1152
1153 #[tokio::test]
1154 async fn test_validate_url_not_reserved_rejects_ipv6_loopback_literal() {
1155 let err = validate_url_not_reserved("https://[::1]/")
1156 .await
1157 .unwrap_err();
1158 assert!(
1159 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1160 "expected reserved-IP error, got: {:?}",
1161 err
1162 );
1163 }
1164
1165 #[tokio::test]
1166 async fn test_validate_url_not_reserved_returns_resolved_addrs_for_public_literal() {
1167 let addrs = validate_url_not_reserved("https://8.8.8.8/").await.unwrap();
1170 assert_eq!(addrs.len(), 1);
1171 assert!(addrs[0].ip().is_ipv4());
1172 assert_eq!(addrs[0].port(), 443);
1173 }
1174
1175 #[test]
1178 fn test_build_rdap_urls_preserves_order_and_appends_path() {
1179 let bases = vec![
1180 url::Url::parse("https://rdap.a.example/").unwrap(),
1181 url::Url::parse("https://rdap.b.example").unwrap(), ];
1183 let built = build_rdap_urls(&bases, "domain/example.com");
1184 assert_eq!(built.len(), 2);
1185 assert_eq!(
1186 built[0].as_str(),
1187 "https://rdap.a.example/domain/example.com"
1188 );
1189 assert_eq!(
1190 built[1].as_str(),
1191 "https://rdap.b.example/domain/example.com"
1192 );
1193 }
1194
1195 #[test]
1196 fn test_build_rdap_urls_empty_input_returns_empty() {
1197 let built = build_rdap_urls(&[], "domain/example.com");
1198 assert!(built.is_empty());
1199 }
1200
1201 #[test]
1204 fn test_wrap_all_candidates_failed_preserves_timeout_variant() {
1205 let last = SeerError::Timeout("body read timed out".to_string());
1208 let wrapped = wrap_all_candidates_failed(Some(last), 3);
1209 match wrapped {
1210 SeerError::Timeout(msg) => {
1211 assert!(
1212 msg.contains("all 3 RDAP candidate URLs timed out"),
1213 "expected wrapped timeout message, got: {}",
1214 msg
1215 );
1216 assert!(
1217 msg.contains("body read timed out"),
1218 "expected original message preserved, got: {}",
1219 msg
1220 );
1221 }
1222 other => panic!(
1223 "expected SeerError::Timeout after wrapping a Timeout, got: {:?}",
1224 other
1225 ),
1226 }
1227 }
1228
1229 #[test]
1230 fn test_wrap_all_candidates_failed_wraps_non_timeout_as_rdap_error() {
1231 let last = SeerError::RdapError("500 internal error".to_string());
1232 let wrapped = wrap_all_candidates_failed(Some(last), 2);
1233 assert!(
1234 matches!(wrapped, SeerError::RdapError(ref s) if s.contains("all 2 RDAP candidate URLs failed")),
1235 "expected wrapped RdapError, got: {:?}",
1236 wrapped
1237 );
1238 }
1239
1240 #[test]
1241 fn test_wrap_all_candidates_failed_single_candidate_returns_unchanged() {
1242 let last = SeerError::Timeout("single timeout".to_string());
1245 let wrapped = wrap_all_candidates_failed(Some(last), 1);
1246 assert!(
1247 matches!(wrapped, SeerError::Timeout(ref s) if s == "single timeout"),
1248 "expected unchanged Timeout, got: {:?}",
1249 wrapped
1250 );
1251 }
1252
1253 #[test]
1254 fn test_wrap_all_candidates_failed_no_last_error_returns_placeholder() {
1255 let wrapped = wrap_all_candidates_failed(None, 0);
1256 assert!(matches!(wrapped, SeerError::RdapError(_)));
1257 }
1258
1259 static BOOTSTRAP_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
1278
1279 #[tokio::test]
1280 async fn test_bootstrap_load_notify_wakes_waiter_when_cache_populated() {
1281 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1282
1283 {
1285 let mut cache = BOOTSTRAP_CACHE.write().await;
1286 *cache = None;
1287 }
1288
1289 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1292 tokio::pin!(notified);
1293
1294 {
1296 let mut cache = BOOTSTRAP_CACHE.write().await;
1297 *cache = Some(CachedBootstrap::new(BootstrapData {
1298 dns: HashMap::new(),
1299 ipv4: Vec::new(),
1300 ipv6: Vec::new(),
1301 asn: Vec::new(),
1302 }));
1303 }
1304 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1305
1306 let result = wait_for_in_flight_load(notified).await;
1307 assert!(
1308 result.is_ok(),
1309 "expected waiter to see populated cache, got: {:?}",
1310 result
1311 );
1312
1313 {
1315 let mut cache = BOOTSTRAP_CACHE.write().await;
1316 *cache = None;
1317 }
1318 }
1319
1320 #[tokio::test]
1321 async fn test_bootstrap_load_notify_empty_cache_after_wake_returns_error() {
1322 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1323
1324 {
1326 let mut cache = BOOTSTRAP_CACHE.write().await;
1327 *cache = None;
1328 }
1329
1330 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1331 tokio::pin!(notified);
1332
1333 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1335
1336 let result = wait_for_in_flight_load(notified).await;
1337 assert!(
1338 matches!(
1339 result,
1340 Err(SeerError::RdapBootstrapError(ref s))
1341 if s.contains("throttled and no cache available")
1342 ),
1343 "expected throttled error when cache still empty after notify, got: {:?}",
1344 result
1345 );
1346 }
1347}