1use std::collections::HashMap;
2use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
3use std::sync::Arc;
4use std::time::{Duration, Instant};
5
6use once_cell::sync::Lazy;
7use reqwest::Client;
8use serde::Deserialize;
9use tokio::sync::RwLock;
10use tracing::{debug, instrument, warn};
11
12use super::types::RdapResponse;
13use crate::error::{Result, SeerError};
14use crate::retry::{RetryExecutor, RetryPolicy};
15use crate::validation::normalize_domain;
16
17const IANA_BOOTSTRAP_DNS: &str = "https://data.iana.org/rdap/dns.json";
18const IANA_BOOTSTRAP_IPV4: &str = "https://data.iana.org/rdap/ipv4.json";
19const IANA_BOOTSTRAP_IPV6: &str = "https://data.iana.org/rdap/ipv6.json";
20const IANA_BOOTSTRAP_ASN: &str = "https://data.iana.org/rdap/asn.json";
21
22const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
26
27const BOOTSTRAP_TTL: Duration = Duration::from_secs(24 * 60 * 60);
29
30static RDAP_HTTP_CLIENT: Lazy<Client> = Lazy::new(|| {
33 Client::builder()
34 .timeout(DEFAULT_TIMEOUT)
35 .user_agent("Seer/1.0 (RDAP Client)")
36 .pool_max_idle_per_host(10)
37 .build()
38 .expect("Failed to build RDAP HTTP client - invalid configuration")
39});
40
41static BOOTSTRAP_CACHE: Lazy<RwLock<Option<CachedBootstrap>>> = Lazy::new(|| RwLock::new(None));
43
44struct CachedBootstrap {
46 data: BootstrapData,
47 loaded_at: Instant,
48}
49
50impl CachedBootstrap {
51 fn new(data: BootstrapData) -> Self {
52 Self {
53 data,
54 loaded_at: Instant::now(),
55 }
56 }
57
58 fn is_expired(&self) -> bool {
59 self.loaded_at.elapsed() > BOOTSTRAP_TTL
60 }
61
62 fn age(&self) -> Duration {
63 self.loaded_at.elapsed()
64 }
65}
66
67struct BootstrapData {
71 dns: HashMap<String, Arc<str>>,
72 ipv4: Vec<(IpRange, Arc<str>)>,
73 ipv6: Vec<(IpRange, Arc<str>)>,
74 asn: Vec<(AsnRange, Arc<str>)>,
75}
76
77#[derive(Clone)]
78struct IpRange {
79 prefix: String,
80}
81
82#[derive(Clone)]
83struct AsnRange {
84 start: u32,
85 end: u32,
86}
87
88#[derive(Deserialize)]
89struct BootstrapResponse {
90 services: Vec<Vec<serde_json::Value>>,
91}
92
93#[derive(Debug, Clone)]
94pub struct RdapClient {
95 retry_policy: RetryPolicy,
96}
97
98impl Default for RdapClient {
99 fn default() -> Self {
100 Self::new()
101 }
102}
103
104impl RdapClient {
105 pub fn new() -> Self {
107 Self {
108 retry_policy: RetryPolicy::default(),
109 }
110 }
111
112 pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
116 self.retry_policy = policy;
117 self
118 }
119
120 pub fn without_retries(mut self) -> Self {
122 self.retry_policy = RetryPolicy::no_retry();
123 self
124 }
125
126 async fn ensure_bootstrap(&self) -> Result<()> {
129 {
131 let cache = BOOTSTRAP_CACHE.read().await;
132 if let Some(cached) = cache.as_ref() {
133 if !cached.is_expired() {
134 return Ok(());
135 }
136 }
137 }
138
139 let mut cache = BOOTSTRAP_CACHE.write().await;
141
142 if let Some(cached) = cache.as_ref() {
144 if !cached.is_expired() {
145 return Ok(());
146 }
147 }
148
149 debug!("Loading/refreshing RDAP bootstrap data");
151 match load_bootstrap_data_with_retry(&self.retry_policy).await {
152 Ok(data) => {
153 debug!(
154 dns_entries = data.dns.len(),
155 ipv4_entries = data.ipv4.len(),
156 ipv6_entries = data.ipv6.len(),
157 asn_entries = data.asn.len(),
158 "RDAP bootstrap loaded/refreshed"
159 );
160 *cache = Some(CachedBootstrap::new(data));
161 Ok(())
162 }
163 Err(e) => {
164 if let Some(cached) = cache.as_ref() {
166 warn!(
167 error = %e,
168 age_hours = cached.age().as_secs() / 3600,
169 "Bootstrap refresh failed, using stale data"
170 );
171 Ok(())
172 } else {
173 Err(e)
175 }
176 }
177 }
178 }
179
180 fn get_rdap_url_for_domain(cache: &BootstrapData, domain: &str) -> Option<Arc<str>> {
182 let tld = domain.rsplit('.').next()?;
183 cache.dns.get(&tld.to_lowercase()).cloned()
184 }
185
186 fn get_rdap_url_for_ip(cache: &BootstrapData, ip: &IpAddr) -> Option<Arc<str>> {
188 match ip {
189 IpAddr::V4(addr) => {
190 for (range, url) in &cache.ipv4 {
191 if ipv4_matches_prefix(&range.prefix, addr) {
192 return Some(Arc::clone(url));
193 }
194 }
195 }
196 IpAddr::V6(addr) => {
197 for (range, url) in &cache.ipv6 {
198 if ipv6_matches_prefix(&range.prefix, addr) {
199 return Some(Arc::clone(url));
200 }
201 }
202 }
203 }
204
205 None
206 }
207
208 fn get_rdap_url_for_asn(cache: &BootstrapData, asn: u32) -> Option<Arc<str>> {
210 for (range, url) in &cache.asn {
211 if asn >= range.start && asn <= range.end {
212 return Some(Arc::clone(url));
213 }
214 }
215
216 None
217 }
218
219 #[instrument(skip(self), fields(domain = %domain))]
223 pub async fn lookup_domain(&self, domain: &str) -> Result<RdapResponse> {
224 self.ensure_bootstrap().await?;
225
226 let domain = normalize_domain(domain)?;
227
228 let url = {
230 let cache_guard = BOOTSTRAP_CACHE.read().await;
231 let cache = cache_guard.as_ref().ok_or_else(|| {
232 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
233 })?;
234
235 let base_url =
236 Self::get_rdap_url_for_domain(&cache.data, &domain).ok_or_else(|| {
237 SeerError::RdapBootstrapError(format!("no RDAP server for {}", domain))
238 })?;
239
240 build_rdap_url(&base_url, &format!("domain/{}", domain))
241 }; debug!(url = %url, "Querying RDAP");
244 self.query_rdap_with_retry(&url).await
245 }
246
247 #[instrument(skip(self), fields(ip = %ip))]
251 pub async fn lookup_ip(&self, ip: &str) -> Result<RdapResponse> {
252 self.ensure_bootstrap().await?;
253
254 let ip_addr: IpAddr = ip
255 .parse()
256 .map_err(|_| SeerError::InvalidIpAddress(ip.to_string()))?;
257
258 let url = {
260 let cache_guard = BOOTSTRAP_CACHE.read().await;
261 let cache = cache_guard.as_ref().ok_or_else(|| {
262 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
263 })?;
264
265 let base_url = Self::get_rdap_url_for_ip(&cache.data, &ip_addr).ok_or_else(|| {
266 SeerError::RdapBootstrapError(format!("no RDAP server for {}", ip))
267 })?;
268
269 build_rdap_url(&base_url, &format!("ip/{}", ip))
270 }; debug!(url = %url, "Querying RDAP");
273 self.query_rdap_with_retry(&url).await
274 }
275
276 #[instrument(skip(self), fields(asn = %asn))]
280 pub async fn lookup_asn(&self, asn: u32) -> Result<RdapResponse> {
281 self.ensure_bootstrap().await?;
282
283 let url = {
285 let cache_guard = BOOTSTRAP_CACHE.read().await;
286 let cache = cache_guard.as_ref().ok_or_else(|| {
287 SeerError::RdapBootstrapError("bootstrap data not loaded".to_string())
288 })?;
289
290 let base_url = Self::get_rdap_url_for_asn(&cache.data, asn).ok_or_else(|| {
291 SeerError::RdapBootstrapError(format!("no RDAP server for AS{}", asn))
292 })?;
293
294 build_rdap_url(&base_url, &format!("autnum/{}", asn))
295 }; debug!(url = %url, "Querying RDAP");
298 self.query_rdap_with_retry(&url).await
299 }
300
301 async fn query_rdap_with_retry(&self, url: &str) -> Result<RdapResponse> {
303 let executor = RetryExecutor::new(self.retry_policy.clone());
304 let url = url.to_string();
305
306 executor
307 .execute(|| {
308 let http = RDAP_HTTP_CLIENT.clone();
309 let url = url.clone();
310 async move { query_rdap_internal(&http, &url).await }
311 })
312 .await
313 }
314}
315
316async fn query_rdap_internal(http: &Client, url: &str) -> Result<RdapResponse> {
318 let response = http
319 .get(url)
320 .header("Accept", "application/rdap+json")
321 .send()
322 .await?;
323
324 if !response.status().is_success() {
325 return Err(SeerError::RdapError(format!(
326 "query failed with status {}",
327 response.status()
328 )));
329 }
330
331 let rdap: RdapResponse = response.json().await?;
332 Ok(rdap)
333}
334
335async fn load_bootstrap_data_with_retry(policy: &RetryPolicy) -> Result<BootstrapData> {
337 let executor = RetryExecutor::new(policy.clone());
338 executor.execute(load_bootstrap_data).await
339}
340
341async fn load_bootstrap_data() -> Result<BootstrapData> {
343 debug!("Loading RDAP bootstrap data from IANA");
344
345 let http = &*RDAP_HTTP_CLIENT;
346
347 let dns_future = http.get(IANA_BOOTSTRAP_DNS).send();
348 let ipv4_future = http.get(IANA_BOOTSTRAP_IPV4).send();
349 let ipv6_future = http.get(IANA_BOOTSTRAP_IPV6).send();
350 let asn_future = http.get(IANA_BOOTSTRAP_ASN).send();
351
352 let (dns_resp, ipv4_resp, ipv6_resp, asn_resp) =
353 tokio::try_join!(dns_future, ipv4_future, ipv6_future, asn_future)?;
354
355 let dns_data: BootstrapResponse = dns_resp.json().await?;
356 let ipv4_data: BootstrapResponse = ipv4_resp.json().await?;
357 let ipv6_data: BootstrapResponse = ipv6_resp.json().await?;
358 let asn_data: BootstrapResponse = asn_resp.json().await?;
359
360 let mut dns = HashMap::new();
361 let mut ipv4 = Vec::new();
362 let mut ipv6 = Vec::new();
363 let mut asn = Vec::new();
364
365 for service in dns_data.services {
367 if service.len() >= 2 {
368 if let (Some(tlds), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
369 if let Some(url) = urls.first().and_then(|u| u.as_str()) {
370 let url_arc: Arc<str> = Arc::from(url);
371 for tld in tlds {
372 if let Some(tld_str) = tld.as_str() {
373 dns.insert(tld_str.to_lowercase(), Arc::clone(&url_arc));
374 }
375 }
376 }
377 }
378 }
379 }
380
381 for service in ipv4_data.services {
383 if service.len() >= 2 {
384 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
385 if let Some(url) = urls.first().and_then(|u| u.as_str()) {
386 let url_arc: Arc<str> = Arc::from(url);
387 for prefix in prefixes {
388 if let Some(prefix_str) = prefix.as_str() {
389 ipv4.push((
390 IpRange {
391 prefix: prefix_str.to_string(),
392 },
393 Arc::clone(&url_arc),
394 ));
395 }
396 }
397 }
398 }
399 }
400 }
401
402 for service in ipv6_data.services {
404 if service.len() >= 2 {
405 if let (Some(prefixes), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
406 if let Some(url) = urls.first().and_then(|u| u.as_str()) {
407 let url_arc: Arc<str> = Arc::from(url);
408 for prefix in prefixes {
409 if let Some(prefix_str) = prefix.as_str() {
410 ipv6.push((
411 IpRange {
412 prefix: prefix_str.to_string(),
413 },
414 Arc::clone(&url_arc),
415 ));
416 }
417 }
418 }
419 }
420 }
421 }
422
423 for service in asn_data.services {
425 if service.len() >= 2 {
426 if let (Some(ranges), Some(urls)) = (service[0].as_array(), service[1].as_array()) {
427 if let Some(url) = urls.first().and_then(|u| u.as_str()) {
428 let url_arc: Arc<str> = Arc::from(url);
429 for range in ranges {
430 if let Some(range_str) = range.as_str() {
431 if let Some((start, end)) = parse_asn_range(range_str) {
432 asn.push((AsnRange { start, end }, Arc::clone(&url_arc)));
433 }
434 }
435 }
436 }
437 }
438 }
439 }
440
441 Ok(BootstrapData {
442 dns,
443 ipv4,
444 ipv6,
445 asn,
446 })
447}
448
449fn build_rdap_url(base_url: &str, path: &str) -> String {
451 if base_url.ends_with('/') {
452 format!("{}{}", base_url, path)
453 } else {
454 format!("{}/{}", base_url, path)
455 }
456}
457
458fn parse_asn_range(range: &str) -> Option<(u32, u32)> {
459 if let Some(pos) = range.find('-') {
460 let start = range[..pos].parse().ok()?;
461 let end = range[pos + 1..].parse().ok()?;
462 Some((start, end))
463 } else {
464 let num = range.parse().ok()?;
465 Some((num, num))
466 }
467}
468
469fn ipv4_matches_prefix(prefix: &str, ip: &Ipv4Addr) -> bool {
470 let (addr_part, mask_part) = match prefix.split_once('/') {
471 Some((a, m)) => (a, Some(m)),
472 None => (prefix, None),
473 };
474
475 let prefix_ip: Ipv4Addr = match addr_part.parse() {
476 Ok(ip) => ip,
477 Err(_) => return false,
478 };
479
480 let mask_bits: u32 = match mask_part.and_then(|s| s.parse().ok()) {
481 Some(bits) if bits <= 32 => bits,
482 Some(_) => return false,
483 None => 32,
484 };
485
486 let mask = if mask_bits == 0 {
487 0
488 } else {
489 u32::MAX << (32 - mask_bits)
490 };
491
492 let ip_value = u32::from(*ip);
493 let prefix_value = u32::from(prefix_ip);
494
495 (ip_value & mask) == (prefix_value & mask)
496}
497
498fn ipv6_matches_prefix(prefix: &str, ip: &Ipv6Addr) -> bool {
499 let (addr_part, mask_part) = match prefix.split_once('/') {
500 Some((a, m)) => (a, Some(m)),
501 None => (prefix, None),
502 };
503
504 let prefix_ip: Ipv6Addr = match addr_part.parse() {
505 Ok(ip) => ip,
506 Err(_) => return false,
507 };
508
509 let mask_bits: u32 = match mask_part.and_then(|s| s.parse().ok()) {
510 Some(bits) if bits <= 128 => bits,
511 Some(_) => return false,
512 None => 128,
513 };
514
515 let mask = if mask_bits == 0 {
516 0u128
517 } else {
518 u128::MAX << (128 - mask_bits)
519 };
520
521 let ip_value = ipv6_to_u128(ip);
522 let prefix_value = ipv6_to_u128(&prefix_ip);
523
524 (ip_value & mask) == (prefix_value & mask)
525}
526
527fn ipv6_to_u128(ip: &Ipv6Addr) -> u128 {
528 let segments = ip.segments();
529 let mut value = 0u128;
530 for segment in segments {
531 value = (value << 16) | segment as u128;
532 }
533 value
534}
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539
540 #[test]
541 fn test_default_client_has_retry_policy() {
542 let client = RdapClient::new();
543 assert_eq!(client.retry_policy.max_attempts, 3);
544 }
545
546 #[test]
547 fn test_client_without_retries() {
548 let client = RdapClient::new().without_retries();
549 assert_eq!(client.retry_policy.max_attempts, 1);
550 }
551
552 #[test]
553 fn test_client_custom_retry_policy() {
554 let policy = RetryPolicy::new().with_max_attempts(5);
555 let client = RdapClient::new().with_retry_policy(policy);
556 assert_eq!(client.retry_policy.max_attempts, 5);
557 }
558
559 #[test]
560 fn test_cached_bootstrap_expiration() {
561 let data = BootstrapData {
562 dns: HashMap::new(),
563 ipv4: Vec::new(),
564 ipv6: Vec::new(),
565 asn: Vec::new(),
566 };
567 let cached = CachedBootstrap::new(data);
568 assert!(!cached.is_expired());
570 }
571
572 #[test]
573 fn test_ipv4_prefix_matching_partial_mask() {
574 let ip_in = Ipv4Addr::new(203, 0, 114, 1);
575 let ip_out = Ipv4Addr::new(203, 0, 120, 1);
576 assert!(ipv4_matches_prefix("203.0.112.0/21", &ip_in));
577 assert!(!ipv4_matches_prefix("203.0.112.0/21", &ip_out));
578 }
579
580 #[test]
581 fn test_ipv6_prefix_matching_partial_mask() {
582 let ip_in: Ipv6Addr = "2001:db8::1".parse().unwrap();
583 let ip_out: Ipv6Addr = "2001:db9::1".parse().unwrap();
584 assert!(ipv6_matches_prefix("2001:db8::/33", &ip_in));
585 assert!(!ipv6_matches_prefix("2001:db8::/33", &ip_out));
586 }
587}