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<Option<Client>> = Lazy::new(|| {
50 Client::builder()
51 .timeout(DEFAULT_TIMEOUT)
52 .connect_timeout(CONNECT_TIMEOUT)
53 .user_agent("Seer/1.0 (RDAP Client)")
54 .pool_max_idle_per_host(10)
55 .build()
56 .ok()
57});
58
59fn rdap_http_client() -> Result<&'static Client> {
63 RDAP_HTTP_CLIENT
64 .as_ref()
65 .ok_or_else(|| SeerError::HttpError("failed to initialize HTTP client".into()))
66}
67
68static BOOTSTRAP_CACHE: Lazy<RwLock<Option<CachedBootstrap>>> = Lazy::new(|| RwLock::new(None));
70
71static BOOTSTRAP_LAST_ATTEMPT: Lazy<RwLock<Option<Instant>>> = Lazy::new(|| RwLock::new(None));
75
76static BOOTSTRAP_LOAD_NOTIFY: Lazy<Notify> = Lazy::new(Notify::new);
84
85struct CachedBootstrap {
87 data: BootstrapData,
88 loaded_at: Instant,
89}
90
91impl CachedBootstrap {
92 fn new(data: BootstrapData) -> Self {
93 Self {
94 data,
95 loaded_at: Instant::now(),
96 }
97 }
98
99 fn is_expired(&self) -> bool {
100 self.loaded_at.elapsed() > BOOTSTRAP_TTL
101 }
102
103 fn age(&self) -> Duration {
104 self.loaded_at.elapsed()
105 }
106}
107
108struct BootstrapData {
113 dns: HashMap<String, Arc<Vec<url::Url>>>,
114 ipv4: Vec<(IpRange, Arc<Vec<url::Url>>)>,
115 ipv6: Vec<(IpRange, Arc<Vec<url::Url>>)>,
116 asn: Vec<(AsnRange, Arc<Vec<url::Url>>)>,
117}
118
119#[derive(Clone)]
120struct IpRange {
121 prefix: String,
122}
123
124#[derive(Clone)]
125struct AsnRange {
126 start: u32,
127 end: u32,
128}
129
130#[derive(Deserialize)]
131struct BootstrapResponse {
132 services: Vec<Vec<serde_json::Value>>,
133}
134
135async fn wait_for_in_flight_load(
145 notified: std::pin::Pin<&mut tokio::sync::futures::Notified<'_>>,
146) -> Result<()> {
147 let _ = tokio::time::timeout(DEFAULT_TIMEOUT, notified).await;
150 let cache = BOOTSTRAP_CACHE.read().await;
151 if cache.is_some() {
152 Ok(())
153 } else {
154 Err(SeerError::RdapBootstrapError(
155 "bootstrap refresh throttled and no cache available".to_string(),
156 ))
157 }
158}
159
160#[derive(Debug, Clone)]
161pub struct RdapClient {
162 retry_policy: RetryPolicy,
163}
164
165impl Default for RdapClient {
166 fn default() -> Self {
167 Self::new()
168 }
169}
170
171impl RdapClient {
172 pub fn new() -> Self {
174 Self {
175 retry_policy: RetryPolicy::default().with_max_attempts(2),
176 }
177 }
178
179 pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
183 self.retry_policy = policy;
184 self
185 }
186
187 pub fn without_retries(mut self) -> Self {
189 self.retry_policy = RetryPolicy::no_retry();
190 self
191 }
192
193 async fn ensure_bootstrap(&self) -> Result<()> {
208 {
210 let cache = BOOTSTRAP_CACHE.read().await;
211 if let Some(cached) = cache.as_ref() {
212 if !cached.is_expired() {
213 return Ok(());
214 }
215 }
216 }
217
218 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
224 tokio::pin!(notified);
225
226 {
230 let last = BOOTSTRAP_LAST_ATTEMPT.read().await;
231 if let Some(ts) = *last {
232 if ts.elapsed() < BOOTSTRAP_REFRESH_MIN_INTERVAL {
233 let cache = BOOTSTRAP_CACHE.read().await;
235 if cache.is_some() {
236 return Ok(());
238 }
239 drop(cache);
242 drop(last);
243 return wait_for_in_flight_load(notified).await;
244 }
245 }
246 }
247
248 {
251 let mut last = BOOTSTRAP_LAST_ATTEMPT.write().await;
252 if let Some(ts) = *last {
254 if ts.elapsed() < BOOTSTRAP_REFRESH_MIN_INTERVAL {
255 drop(last);
256 let cache = BOOTSTRAP_CACHE.read().await;
257 if cache.is_some() {
258 return Ok(());
259 }
260 drop(cache);
261 return wait_for_in_flight_load(notified).await;
262 }
263 }
264 *last = Some(Instant::now());
265 }
266
267 debug!("Loading/refreshing RDAP bootstrap data");
271 let load_result = load_bootstrap_data_with_retry(&self.retry_policy).await;
272
273 let outcome = match load_result {
274 Ok(data) => {
275 let mut cache = BOOTSTRAP_CACHE.write().await;
276 let should_store = cache.as_ref().map(|c| c.is_expired()).unwrap_or(true);
279 if should_store {
280 *cache = Some(CachedBootstrap::new(data));
281 }
282 Ok(())
283 }
284 Err(e) => {
285 let cache = BOOTSTRAP_CACHE.read().await;
287 if let Some(cached) = cache.as_ref() {
288 debug!(
289 error = %e,
290 age_hours = cached.age().as_secs() / 3600,
291 "Bootstrap refresh failed, using stale data"
292 );
293 Ok(())
294 } else {
295 Err(e)
297 }
298 }
299 };
300
301 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
303 outcome
304 }
305
306 fn get_rdap_urls_for_domain(cache: &BootstrapData, domain: &str) -> Option<Arc<Vec<url::Url>>> {
308 let tld = domain.rsplit('.').next()?;
309 cache.dns.get(&tld.to_lowercase()).cloned()
310 }
311
312 fn get_rdap_urls_for_ip(cache: &BootstrapData, ip: &IpAddr) -> Option<Arc<Vec<url::Url>>> {
314 match ip {
315 IpAddr::V4(addr) => {
316 for (range, urls) in &cache.ipv4 {
317 if ipv4_matches_prefix(&range.prefix, addr) {
318 return Some(Arc::clone(urls));
319 }
320 }
321 }
322 IpAddr::V6(addr) => {
323 for (range, urls) in &cache.ipv6 {
324 if ipv6_matches_prefix(&range.prefix, addr) {
325 return Some(Arc::clone(urls));
326 }
327 }
328 }
329 }
330
331 None
332 }
333
334 fn get_rdap_urls_for_asn(cache: &BootstrapData, asn: u32) -> Option<Arc<Vec<url::Url>>> {
336 for (range, urls) in &cache.asn {
337 if asn >= range.start && asn <= range.end {
338 return Some(Arc::clone(urls));
339 }
340 }
341
342 None
343 }
344
345 #[instrument(skip(self), fields(domain = %domain))]
349 pub async fn lookup_domain(&self, domain: &str) -> Result<RdapResponse> {
350 self.ensure_bootstrap().await?;
351
352 let domain = normalize_domain(domain)?;
353
354 let urls = {
356 let cache_guard = BOOTSTRAP_CACHE.read().await;
357 let cache = cache_guard.as_ref().ok_or_else(|| {
358 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
359 })?;
360
361 let bases = Self::get_rdap_urls_for_domain(&cache.data, &domain).ok_or_else(|| {
362 SeerError::RdapBootstrapError(format!("no RDAP server for {}", domain))
363 })?;
364
365 build_rdap_urls(&bases, &format!("domain/{}", domain))
366 }; self.query_rdap_urls(&urls).await
369 }
370
371 #[instrument(skip(self), fields(ip = %ip))]
375 pub async fn lookup_ip(&self, ip: &str) -> Result<RdapResponse> {
376 self.ensure_bootstrap().await?;
377
378 let ip_addr: IpAddr = ip
379 .parse()
380 .map_err(|_| SeerError::InvalidIpAddress(ip.to_string()))?;
381
382 let urls = {
383 let cache_guard = BOOTSTRAP_CACHE.read().await;
384 let cache = cache_guard.as_ref().ok_or_else(|| {
385 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
386 })?;
387
388 let bases = Self::get_rdap_urls_for_ip(&cache.data, &ip_addr).ok_or_else(|| {
389 SeerError::RdapBootstrapError(format!("no RDAP server for {}", ip))
390 })?;
391
392 build_rdap_urls(&bases, &format!("ip/{}", ip))
393 };
394
395 self.query_rdap_urls(&urls).await
396 }
397
398 #[instrument(skip(self), fields(asn = %asn))]
402 pub async fn lookup_asn(&self, asn: u32) -> Result<RdapResponse> {
403 self.ensure_bootstrap().await?;
404
405 let urls = {
406 let cache_guard = BOOTSTRAP_CACHE.read().await;
407 let cache = cache_guard.as_ref().ok_or_else(|| {
408 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
409 })?;
410
411 let bases = Self::get_rdap_urls_for_asn(&cache.data, asn).ok_or_else(|| {
412 SeerError::RdapBootstrapError(format!("no RDAP server for AS{}", asn))
413 })?;
414
415 build_rdap_urls(&bases, &format!("autnum/{}", asn))
416 };
417
418 self.query_rdap_urls(&urls).await
419 }
420
421 #[instrument(skip(self), fields(tld = %tld))]
427 pub async fn get_rdap_base_url_for_tld(&self, tld: &str) -> Option<String> {
428 if self.ensure_bootstrap().await.is_err() {
429 return None;
430 }
431
432 let cache_guard = BOOTSTRAP_CACHE.read().await;
433 let cache = cache_guard.as_ref()?;
434 cache
435 .data
436 .dns
437 .get(&tld.to_lowercase())
438 .and_then(|urls| urls.first())
439 .map(|u| u.to_string())
440 }
441
442 async fn query_rdap_urls(&self, urls: &[url::Url]) -> Result<RdapResponse> {
446 if urls.is_empty() {
447 return Err(SeerError::RdapError(
448 "no candidate RDAP URLs available".to_string(),
449 ));
450 }
451
452 let mut last_error: Option<SeerError> = None;
453 for (idx, url) in urls.iter().enumerate() {
454 let url_str = url.as_str().to_string();
455 debug!(url = %url_str, candidate = idx + 1, total = urls.len(), "Querying RDAP");
456 match self.query_rdap_with_retry(&url_str).await {
457 Ok(resp) => return Ok(resp),
458 Err(e) => {
459 if urls.len() > 1 {
460 debug!(
461 url = %url_str,
462 error = %e,
463 candidate = idx + 1,
464 total = urls.len(),
465 "RDAP candidate failed, trying next",
466 );
467 }
468 last_error = Some(e);
469 }
470 }
471 }
472
473 Err(wrap_all_candidates_failed(last_error, urls.len()))
475 }
476
477 async fn query_rdap_with_retry(&self, url: &str) -> Result<RdapResponse> {
479 let executor = RetryExecutor::new(self.retry_policy.clone());
480 let url = url.to_string();
481
482 executor
483 .execute(|| {
484 let url = url.clone();
485 async move { query_rdap_internal(&url).await }
486 })
487 .await
488 }
489}
490
491const MAX_RDAP_RESPONSE_SIZE: usize = 10 * 1024 * 1024;
493
494async fn validate_url_not_reserved(url: &str) -> Result<Vec<SocketAddr>> {
501 let parsed = url::Url::parse(url)
502 .map_err(|e| SeerError::RdapError(format!("invalid URL '{}': {}", url, e)))?;
503 let host = parsed
504 .host_str()
505 .ok_or_else(|| SeerError::RdapError(format!("URL '{}' has no host", url)))?;
506 let port = parsed.port_or_known_default().unwrap_or(443);
507
508 if let Ok(ip) = host.parse::<IpAddr>() {
510 if let Some(reason) = describe_reserved_ip(&ip) {
511 return Err(SeerError::RdapError(format!(
512 "RDAP URL resolves to reserved IP {}: {} — request blocked (SSRF protection)",
513 ip, reason
514 )));
515 }
516 return Ok(vec![SocketAddr::new(ip, port)]);
517 }
518
519 let addr = format!("{}:{}", host, port);
520
521 let socket_addrs: Vec<SocketAddr> = tokio::net::lookup_host(&addr)
522 .await
523 .map_err(|e| SeerError::RdapError(format!("failed to resolve host '{}': {}", host, e)))?
524 .collect();
525
526 if socket_addrs.is_empty() {
527 return Err(SeerError::RdapError(format!(
528 "host '{}' resolved to no addresses",
529 host
530 )));
531 }
532
533 for socket_addr in &socket_addrs {
534 if let Some(reason) = describe_reserved_ip(&socket_addr.ip()) {
535 return Err(SeerError::RdapError(format!(
536 "RDAP URL resolves to reserved IP {}: {} — request blocked (SSRF protection)",
537 socket_addr.ip(),
538 reason
539 )));
540 }
541 }
542
543 Ok(socket_addrs)
544}
545
546fn validate_bootstrap_url(s: &str) -> Result<url::Url> {
552 let parsed = url::Url::parse(s)
553 .map_err(|e| SeerError::RdapError(format!("bad bootstrap URL {}: {}", s, e)))?;
554 if parsed.scheme() != "https" {
555 return Err(SeerError::RdapError(format!(
556 "bootstrap URL must be https, got {}",
557 parsed.scheme()
558 )));
559 }
560 let host = parsed
561 .host()
562 .ok_or_else(|| SeerError::RdapError(format!("bootstrap URL has no host: {}", s)))?;
563 match host {
564 url::Host::Ipv4(_) | url::Host::Ipv6(_) => {
565 return Err(SeerError::RdapError(format!(
566 "bootstrap URL must not be an IP literal: {}",
567 s
568 )));
569 }
570 url::Host::Domain(d) => {
571 if d.is_empty() || d.chars().any(|c| c.is_whitespace() || c.is_control()) {
572 return Err(SeerError::RdapError(format!(
573 "bootstrap URL has invalid host: {}",
574 s
575 )));
576 }
577 }
578 }
579 Ok(parsed)
580}
581
582async fn query_rdap_internal(url: &str) -> Result<RdapResponse> {
587 let resolved = validate_url_not_reserved(url).await?;
590
591 let parsed = url::Url::parse(url)
592 .map_err(|e| SeerError::RdapError(format!("invalid URL '{}': {}", url, e)))?;
593 let host = parsed
594 .host_str()
595 .ok_or_else(|| SeerError::RdapError(format!("URL '{}' has no host", url)))?;
596
597 let client = Client::builder()
601 .timeout(DEFAULT_TIMEOUT)
602 .connect_timeout(CONNECT_TIMEOUT)
603 .user_agent("Seer/1.0 (RDAP Client)")
604 .resolve_to_addrs(host, &resolved)
605 .build()
606 .map_err(|e| SeerError::RdapError(format!("failed to build HTTP client: {}", e)))?;
607
608 let response = client
609 .get(url)
610 .header("Accept", "application/rdap+json")
611 .send()
612 .await?;
613
614 if !response.status().is_success() {
615 return Err(SeerError::RdapError(format!(
616 "query failed with status {}",
617 response.status()
618 )));
619 }
620
621 let mut body = Vec::new();
626 let mut stream = response.bytes_stream();
627 let streamed = tokio::time::timeout(DEFAULT_TIMEOUT, async {
628 while let Some(chunk) = stream.next().await {
629 let chunk = chunk
630 .map_err(|e| SeerError::RdapError(format!("failed to read response: {}", e)))?;
631 body.extend_from_slice(&chunk);
632 if body.len() > MAX_RDAP_RESPONSE_SIZE {
633 return Err(SeerError::RdapError(format!(
634 "RDAP response exceeds {} byte limit",
635 MAX_RDAP_RESPONSE_SIZE
636 )));
637 }
638 }
639 Ok::<(), SeerError>(())
640 })
641 .await;
642
643 match streamed {
644 Ok(Ok(())) => {}
645 Ok(Err(e)) => return Err(e),
646 Err(_) => {
647 return Err(SeerError::Timeout(format!(
648 "timed out reading RDAP response body from {} after {:?}",
649 host, DEFAULT_TIMEOUT
650 )));
651 }
652 }
653
654 let rdap: RdapResponse = serde_json::from_slice(&body)?;
655 rdap.validate()?;
661 Ok(rdap)
662}
663
664async fn load_bootstrap_data_with_retry(policy: &RetryPolicy) -> Result<BootstrapData> {
666 let executor = RetryExecutor::new(policy.clone());
667 executor.execute(load_bootstrap_data).await
668}
669
670async fn load_bootstrap_data() -> Result<BootstrapData> {
672 debug!("Loading RDAP bootstrap data from IANA");
673
674 let http = rdap_http_client()?;
678
679 let dns_future = http.get(IANA_BOOTSTRAP_DNS).send();
680 let ipv4_future = http.get(IANA_BOOTSTRAP_IPV4).send();
681 let ipv6_future = http.get(IANA_BOOTSTRAP_IPV6).send();
682 let asn_future = http.get(IANA_BOOTSTRAP_ASN).send();
683
684 let (dns_resp, ipv4_resp, ipv6_resp, asn_resp) =
687 tokio::join!(dns_future, ipv4_future, ipv6_future, asn_future);
688
689 const MAX_BOOTSTRAP_SIZE: usize = 10 * 1024 * 1024; async fn read_bootstrap(resp: reqwest::Response) -> Result<BootstrapResponse> {
693 let mut body = Vec::new();
699 let mut stream = resp.bytes_stream();
700 let streamed = tokio::time::timeout(DEFAULT_TIMEOUT, async {
701 while let Some(chunk) = stream.next().await {
702 let chunk = chunk.map_err(|e| {
703 SeerError::RdapBootstrapError(format!("failed to read body: {}", e))
704 })?;
705 body.extend_from_slice(&chunk);
706 if body.len() > MAX_BOOTSTRAP_SIZE {
707 return Err(SeerError::RdapBootstrapError(format!(
708 "bootstrap response too large (exceeds {} bytes)",
709 MAX_BOOTSTRAP_SIZE
710 )));
711 }
712 }
713 Ok::<(), SeerError>(())
714 })
715 .await;
716
717 match streamed {
718 Ok(Ok(())) => {}
719 Ok(Err(e)) => return Err(e),
720 Err(_) => {
721 return Err(SeerError::Timeout(format!(
722 "RDAP bootstrap body read timed out after {:?}",
723 DEFAULT_TIMEOUT
724 )));
725 }
726 }
727
728 serde_json::from_slice(&body).map_err(Into::into)
729 }
730
731 let dns_data = match dns_resp {
733 Ok(resp) => match read_bootstrap(resp).await {
734 Ok(data) => Some(data),
735 Err(e) => {
736 debug!(error = %e, "Failed to parse DNS bootstrap response");
737 None
738 }
739 },
740 Err(e) => {
741 debug!(error = %e, "Failed to fetch DNS bootstrap from IANA");
742 None
743 }
744 };
745 let ipv4_data = match ipv4_resp {
746 Ok(resp) => match read_bootstrap(resp).await {
747 Ok(data) => Some(data),
748 Err(e) => {
749 debug!(error = %e, "Failed to parse IPv4 bootstrap response");
750 None
751 }
752 },
753 Err(e) => {
754 debug!(error = %e, "Failed to fetch IPv4 bootstrap from IANA");
755 None
756 }
757 };
758 let ipv6_data = match ipv6_resp {
759 Ok(resp) => match read_bootstrap(resp).await {
760 Ok(data) => Some(data),
761 Err(e) => {
762 debug!(error = %e, "Failed to parse IPv6 bootstrap response");
763 None
764 }
765 },
766 Err(e) => {
767 debug!(error = %e, "Failed to fetch IPv6 bootstrap from IANA");
768 None
769 }
770 };
771 let asn_data = match asn_resp {
772 Ok(resp) => match read_bootstrap(resp).await {
773 Ok(data) => Some(data),
774 Err(e) => {
775 debug!(error = %e, "Failed to parse ASN bootstrap response");
776 None
777 }
778 },
779 Err(e) => {
780 debug!(error = %e, "Failed to fetch ASN bootstrap from IANA");
781 None
782 }
783 };
784
785 if dns_data.is_none() && ipv4_data.is_none() && ipv6_data.is_none() && asn_data.is_none() {
787 return Err(SeerError::RdapBootstrapError(
788 "all IANA bootstrap registries failed".to_string(),
789 ));
790 }
791
792 let mut dns = HashMap::new();
793 let mut ipv4 = Vec::new();
794 let mut ipv6 = Vec::new();
795 let mut asn = Vec::new();
796
797 fn collect_valid_urls(urls: &[serde_json::Value]) -> Option<Arc<Vec<url::Url>>> {
801 let mut out = Vec::new();
802 for u in urls {
803 if let Some(s) = u.as_str() {
804 match validate_bootstrap_url(s) {
805 Ok(parsed) => out.push(parsed),
806 Err(e) => {
807 debug!(url = s, error = %e, "Skipping invalid bootstrap URL");
808 }
809 }
810 }
811 }
812 if out.is_empty() {
813 None
814 } else {
815 Some(Arc::new(out))
816 }
817 }
818
819 if let Some(dns_data) = dns_data {
821 for service in dns_data.services {
822 if service.len() >= 2 {
823 if let (Some(tlds), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
824 if let Some(urls_arc) = collect_valid_urls(urls) {
825 for tld in tlds {
826 if let Some(tld_str) = tld.as_str() {
827 dns.insert(tld_str.to_lowercase(), Arc::clone(&urls_arc));
828 }
829 }
830 }
831 }
832 }
833 }
834 }
835
836 if let Some(ipv4_data) = ipv4_data {
838 for service in ipv4_data.services {
839 if service.len() >= 2 {
840 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array())
841 {
842 if let Some(urls_arc) = collect_valid_urls(urls) {
843 for prefix in prefixes {
844 if let Some(prefix_str) = prefix.as_str() {
845 ipv4.push((
846 IpRange {
847 prefix: prefix_str.to_string(),
848 },
849 Arc::clone(&urls_arc),
850 ));
851 }
852 }
853 }
854 }
855 }
856 }
857 }
858
859 if let Some(ipv6_data) = ipv6_data {
861 for service in ipv6_data.services {
862 if service.len() >= 2 {
863 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array())
864 {
865 if let Some(urls_arc) = collect_valid_urls(urls) {
866 for prefix in prefixes {
867 if let Some(prefix_str) = prefix.as_str() {
868 ipv6.push((
869 IpRange {
870 prefix: prefix_str.to_string(),
871 },
872 Arc::clone(&urls_arc),
873 ));
874 }
875 }
876 }
877 }
878 }
879 }
880 }
881
882 if let Some(asn_data) = asn_data {
884 for service in asn_data.services {
885 if service.len() >= 2 {
886 if let (Some(ranges), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
887 if let Some(urls_arc) = collect_valid_urls(urls) {
888 for range in ranges {
889 if let Some(range_str) = range.as_str() {
890 if let Some((start, end)) = parse_asn_range(range_str) {
891 asn.push((AsnRange { start, end }, Arc::clone(&urls_arc)));
892 }
893 }
894 }
895 }
896 }
897 }
898 }
899 }
900
901 info!(
902 dns_entries = dns.len(),
903 ipv4_ranges = ipv4.len(),
904 ipv6_ranges = ipv6.len(),
905 asn_ranges = asn.len(),
906 "RDAP bootstrap loaded"
907 );
908
909 Ok(BootstrapData {
910 dns,
911 ipv4,
912 ipv6,
913 asn,
914 })
915}
916
917fn wrap_all_candidates_failed(last_error: Option<SeerError>, candidate_count: usize) -> SeerError {
926 let last = last_error.unwrap_or_else(|| SeerError::RdapError("no candidates".to_string()));
927
928 if candidate_count <= 1 {
929 return last;
930 }
931
932 match last {
933 SeerError::Timeout(msg) => SeerError::Timeout(format!(
934 "all {} RDAP candidate URLs timed out; last error: {}",
935 candidate_count, msg
936 )),
937 other => SeerError::RdapError(format!(
938 "all {} RDAP candidate URLs failed; last error: {}",
939 candidate_count, other
940 )),
941 }
942}
943
944fn build_rdap_urls(bases: &[url::Url], path: &str) -> Vec<url::Url> {
946 bases
947 .iter()
948 .filter_map(|base| {
949 let base_str = base.as_str();
952 let normalized = if base_str.ends_with('/') {
953 base_str.to_string()
954 } else {
955 format!("{}/", base_str)
956 };
957 url::Url::parse(&normalized).and_then(|u| u.join(path)).ok()
958 })
959 .collect()
960}
961
962fn parse_asn_range(range: &str) -> Option<(u32, u32)> {
963 if let Some(pos) = range.find('-') {
964 let start = range[..pos].parse().ok()?;
965 let end = range[pos + 1..].parse().ok()?;
966 Some((start, end))
967 } else {
968 let num = range.parse().ok()?;
969 Some((num, num))
970 }
971}
972
973fn ipv4_matches_prefix(prefix: &str, ip: &Ipv4Addr) -> bool {
974 let (addr_part, mask_part) = match prefix.split_once('/') {
975 Some((a, m)) => (a, Some(m)),
976 None => (prefix, None),
977 };
978
979 let prefix_ip: Ipv4Addr = match addr_part.parse() {
980 Ok(ip) => ip,
981 Err(_) => return false,
982 };
983
984 let mask_bits: u32 = match mask_part.and_then(|s| s.parse().ok()) {
985 Some(bits) if bits <= 32 => bits,
986 Some(_) => return false,
987 None => 32,
988 };
989
990 let mask = if mask_bits == 0 {
991 0
992 } else {
993 u32::MAX << (32 - mask_bits)
994 };
995
996 let ip_value = u32::from(*ip);
997 let prefix_value = u32::from(prefix_ip);
998
999 (ip_value & mask) == (prefix_value & mask)
1000}
1001
1002fn ipv6_matches_prefix(prefix: &str, ip: &Ipv6Addr) -> bool {
1003 let (addr_part, mask_part) = match prefix.split_once('/') {
1004 Some((a, m)) => (a, Some(m)),
1005 None => (prefix, None),
1006 };
1007
1008 let prefix_ip: Ipv6Addr = match addr_part.parse() {
1009 Ok(ip) => ip,
1010 Err(_) => return false,
1011 };
1012
1013 let mask_bits: u32 = match mask_part.and_then(|s| s.parse().ok()) {
1014 Some(bits) if bits <= 128 => bits,
1015 Some(_) => return false,
1016 None => 128,
1017 };
1018
1019 let mask = if mask_bits == 0 {
1020 0u128
1021 } else {
1022 u128::MAX << (128 - mask_bits)
1023 };
1024
1025 let ip_value = ipv6_to_u128(ip);
1026 let prefix_value = ipv6_to_u128(&prefix_ip);
1027
1028 (ip_value & mask) == (prefix_value & mask)
1029}
1030
1031fn ipv6_to_u128(ip: &Ipv6Addr) -> u128 {
1032 let segments = ip.segments();
1033 let mut value = 0u128;
1034 for segment in segments {
1035 value = (value << 16) | segment as u128;
1036 }
1037 value
1038}
1039
1040#[cfg(test)]
1041mod tests {
1042 use super::*;
1043
1044 #[test]
1045 fn test_default_client_has_retry_policy() {
1046 let client = RdapClient::new();
1047 assert_eq!(client.retry_policy.max_attempts, 2);
1048 }
1049
1050 #[test]
1051 fn test_client_without_retries() {
1052 let client = RdapClient::new().without_retries();
1053 assert_eq!(client.retry_policy.max_attempts, 1);
1054 }
1055
1056 #[test]
1057 fn test_client_custom_retry_policy() {
1058 let policy = RetryPolicy::new().with_max_attempts(5);
1059 let client = RdapClient::new().with_retry_policy(policy);
1060 assert_eq!(client.retry_policy.max_attempts, 5);
1061 }
1062
1063 #[test]
1064 fn test_cached_bootstrap_expiration() {
1065 let data = BootstrapData {
1066 dns: HashMap::new(),
1067 ipv4: Vec::new(),
1068 ipv6: Vec::new(),
1069 asn: Vec::new(),
1070 };
1071 let cached = CachedBootstrap::new(data);
1072 assert!(!cached.is_expired());
1074 }
1075
1076 #[test]
1077 fn test_ipv4_prefix_matching_partial_mask() {
1078 let ip_in = Ipv4Addr::new(203, 0, 114, 1);
1079 let ip_out = Ipv4Addr::new(203, 0, 120, 1);
1080 assert!(ipv4_matches_prefix("203.0.112.0/21", &ip_in));
1081 assert!(!ipv4_matches_prefix("203.0.112.0/21", &ip_out));
1082 }
1083
1084 #[test]
1085 fn test_ipv6_prefix_matching_partial_mask() {
1086 let ip_in: Ipv6Addr = "2001:db8::1".parse().unwrap();
1087 let ip_out: Ipv6Addr = "2001:db9::1".parse().unwrap();
1088 assert!(ipv6_matches_prefix("2001:db8::/33", &ip_in));
1089 assert!(!ipv6_matches_prefix("2001:db8::/33", &ip_out));
1090 }
1091
1092 #[test]
1093 fn test_rdap_http_client_is_configured() {
1094 let client = rdap_http_client();
1097 assert!(client.is_ok(), "RDAP HTTP client builder must succeed");
1098 }
1099
1100 #[test]
1101 fn test_parse_bootstrap_empty_services() {
1102 let data = BootstrapData {
1104 dns: HashMap::new(),
1105 ipv4: Vec::new(),
1106 ipv6: Vec::new(),
1107 asn: Vec::new(),
1108 };
1109 assert!(RdapClient::get_rdap_urls_for_domain(&data, "example.com").is_none());
1111 assert!(RdapClient::get_rdap_urls_for_asn(&data, 12345).is_none());
1112 }
1113
1114 #[test]
1117 fn test_validate_bootstrap_url_accepts_https() {
1118 let url = validate_bootstrap_url("https://rdap.example.com/").unwrap();
1119 assert_eq!(url.scheme(), "https");
1120 assert_eq!(url.host_str(), Some("rdap.example.com"));
1121 }
1122
1123 #[test]
1124 fn test_validate_bootstrap_url_rejects_http() {
1125 let err = validate_bootstrap_url("http://rdap.example.com/").unwrap_err();
1126 assert!(
1127 matches!(err, SeerError::RdapError(ref s) if s.contains("https")),
1128 "expected https-scheme error, got: {:?}",
1129 err
1130 );
1131 }
1132
1133 #[test]
1134 fn test_validate_bootstrap_url_rejects_ftp() {
1135 let err = validate_bootstrap_url("ftp://rdap.example.com/").unwrap_err();
1136 assert!(matches!(err, SeerError::RdapError(_)));
1137 }
1138
1139 #[test]
1140 fn test_validate_bootstrap_url_rejects_ip_literal_v4() {
1141 let err = validate_bootstrap_url("https://192.0.2.1/").unwrap_err();
1142 assert!(
1143 matches!(err, SeerError::RdapError(ref s) if s.contains("IP literal")),
1144 "expected IP-literal error, got: {:?}",
1145 err
1146 );
1147 }
1148
1149 #[test]
1150 fn test_validate_bootstrap_url_rejects_ip_literal_v6() {
1151 let err = validate_bootstrap_url("https://[2001:db8::1]/").unwrap_err();
1152 assert!(
1153 matches!(err, SeerError::RdapError(ref s) if s.contains("IP literal")),
1154 "expected IP-literal error, got: {:?}",
1155 err
1156 );
1157 }
1158
1159 #[test]
1160 fn test_validate_bootstrap_url_rejects_garbage() {
1161 let err = validate_bootstrap_url("not a url").unwrap_err();
1162 assert!(matches!(err, SeerError::RdapError(_)));
1163 }
1164
1165 #[tokio::test]
1168 async fn test_validate_url_not_reserved_rejects_loopback_literal() {
1169 let err = validate_url_not_reserved("https://127.0.0.1/domain/example.com")
1170 .await
1171 .unwrap_err();
1172 assert!(
1173 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1174 "expected reserved-IP error, got: {:?}",
1175 err
1176 );
1177 }
1178
1179 #[tokio::test]
1180 async fn test_validate_url_not_reserved_rejects_private_ipv4_literal() {
1181 let err = validate_url_not_reserved("https://10.0.0.1/")
1182 .await
1183 .unwrap_err();
1184 assert!(
1185 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1186 "expected reserved-IP error, got: {:?}",
1187 err
1188 );
1189 }
1190
1191 #[tokio::test]
1192 async fn test_validate_url_not_reserved_rejects_ipv6_loopback_literal() {
1193 let err = validate_url_not_reserved("https://[::1]/")
1194 .await
1195 .unwrap_err();
1196 assert!(
1197 matches!(err, SeerError::RdapError(ref s) if s.contains("reserved IP")),
1198 "expected reserved-IP error, got: {:?}",
1199 err
1200 );
1201 }
1202
1203 #[tokio::test]
1204 async fn test_validate_url_not_reserved_returns_resolved_addrs_for_public_literal() {
1205 let addrs = validate_url_not_reserved("https://8.8.8.8/").await.unwrap();
1208 assert_eq!(addrs.len(), 1);
1209 assert!(addrs[0].ip().is_ipv4());
1210 assert_eq!(addrs[0].port(), 443);
1211 }
1212
1213 #[test]
1216 fn test_build_rdap_urls_preserves_order_and_appends_path() {
1217 let bases = vec![
1218 url::Url::parse("https://rdap.a.example/").unwrap(),
1219 url::Url::parse("https://rdap.b.example").unwrap(), ];
1221 let built = build_rdap_urls(&bases, "domain/example.com");
1222 assert_eq!(built.len(), 2);
1223 assert_eq!(
1224 built[0].as_str(),
1225 "https://rdap.a.example/domain/example.com"
1226 );
1227 assert_eq!(
1228 built[1].as_str(),
1229 "https://rdap.b.example/domain/example.com"
1230 );
1231 }
1232
1233 #[test]
1234 fn test_build_rdap_urls_empty_input_returns_empty() {
1235 let built = build_rdap_urls(&[], "domain/example.com");
1236 assert!(built.is_empty());
1237 }
1238
1239 #[test]
1242 fn test_wrap_all_candidates_failed_preserves_timeout_variant() {
1243 let last = SeerError::Timeout("body read timed out".to_string());
1246 let wrapped = wrap_all_candidates_failed(Some(last), 3);
1247 match wrapped {
1248 SeerError::Timeout(msg) => {
1249 assert!(
1250 msg.contains("all 3 RDAP candidate URLs timed out"),
1251 "expected wrapped timeout message, got: {}",
1252 msg
1253 );
1254 assert!(
1255 msg.contains("body read timed out"),
1256 "expected original message preserved, got: {}",
1257 msg
1258 );
1259 }
1260 other => panic!(
1261 "expected SeerError::Timeout after wrapping a Timeout, got: {:?}",
1262 other
1263 ),
1264 }
1265 }
1266
1267 #[test]
1268 fn test_wrap_all_candidates_failed_wraps_non_timeout_as_rdap_error() {
1269 let last = SeerError::RdapError("500 internal error".to_string());
1270 let wrapped = wrap_all_candidates_failed(Some(last), 2);
1271 assert!(
1272 matches!(wrapped, SeerError::RdapError(ref s) if s.contains("all 2 RDAP candidate URLs failed")),
1273 "expected wrapped RdapError, got: {:?}",
1274 wrapped
1275 );
1276 }
1277
1278 #[test]
1279 fn test_wrap_all_candidates_failed_single_candidate_returns_unchanged() {
1280 let last = SeerError::Timeout("single timeout".to_string());
1283 let wrapped = wrap_all_candidates_failed(Some(last), 1);
1284 assert!(
1285 matches!(wrapped, SeerError::Timeout(ref s) if s == "single timeout"),
1286 "expected unchanged Timeout, got: {:?}",
1287 wrapped
1288 );
1289 }
1290
1291 #[test]
1292 fn test_wrap_all_candidates_failed_no_last_error_returns_placeholder() {
1293 let wrapped = wrap_all_candidates_failed(None, 0);
1294 assert!(matches!(wrapped, SeerError::RdapError(_)));
1295 }
1296
1297 static BOOTSTRAP_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
1316
1317 #[tokio::test]
1318 async fn test_bootstrap_load_notify_wakes_waiter_when_cache_populated() {
1319 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1320
1321 {
1323 let mut cache = BOOTSTRAP_CACHE.write().await;
1324 *cache = None;
1325 }
1326
1327 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1330 tokio::pin!(notified);
1331
1332 {
1334 let mut cache = BOOTSTRAP_CACHE.write().await;
1335 *cache = Some(CachedBootstrap::new(BootstrapData {
1336 dns: HashMap::new(),
1337 ipv4: Vec::new(),
1338 ipv6: Vec::new(),
1339 asn: Vec::new(),
1340 }));
1341 }
1342 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1343
1344 let result = wait_for_in_flight_load(notified).await;
1345 assert!(
1346 result.is_ok(),
1347 "expected waiter to see populated cache, got: {:?}",
1348 result
1349 );
1350
1351 {
1353 let mut cache = BOOTSTRAP_CACHE.write().await;
1354 *cache = None;
1355 }
1356 }
1357
1358 #[tokio::test]
1359 async fn test_bootstrap_load_notify_empty_cache_after_wake_returns_error() {
1360 let _guard = BOOTSTRAP_TEST_LOCK.lock().await;
1361
1362 {
1364 let mut cache = BOOTSTRAP_CACHE.write().await;
1365 *cache = None;
1366 }
1367
1368 let notified = BOOTSTRAP_LOAD_NOTIFY.notified();
1369 tokio::pin!(notified);
1370
1371 BOOTSTRAP_LOAD_NOTIFY.notify_waiters();
1373
1374 let result = wait_for_in_flight_load(notified).await;
1375 assert!(
1376 matches!(
1377 result,
1378 Err(SeerError::RdapBootstrapError(ref s))
1379 if s.contains("throttled and no cache available")
1380 ),
1381 "expected throttled error when cache still empty after notify, got: {:?}",
1382 result
1383 );
1384 }
1385}