Skip to main content

rdapify_client/
lib.rs

1//! High-level RDAP client with bootstrap, caching, and SSRF protection.
2
3#![forbid(unsafe_code)]
4
5use std::collections::HashMap;
6use std::net::IpAddr;
7
8use idna::domain_to_ascii;
9use tokio::sync::mpsc;
10use tokio_stream::wrappers::ReceiverStream;
11
12use rdap_bootstrap::Bootstrap;
13use rdap_cache::MemoryCache;
14use rdap_core::{Fetcher, FetcherConfig, Normalizer};
15use rdap_security::{SsrfConfig, SsrfGuard};
16use rdap_types::error::{RdapError, Result};
17use rdap_types::{
18    AsnResponse, AvailabilityResult, DomainResponse, EntityResponse, IpResponse, NameserverResponse,
19};
20
21pub use rdap_stream::{AsnEvent, DomainEvent, IpEvent, NameserverEvent, StreamConfig};
22
23// ── Client configuration ──────────────────────────────────────────────────────
24
25/// Configuration for [`RdapClient`].
26#[derive(Debug, Clone)]
27pub struct ClientConfig {
28    /// HTTP fetcher settings (timeout, retries, user-agent).
29    pub fetcher: FetcherConfig,
30    /// SSRF protection settings.
31    pub ssrf: SsrfConfig,
32    /// Whether to cache query responses in memory.
33    pub cache: bool,
34    /// Bootstrap base URL (defaults to the official IANA endpoint).
35    pub bootstrap_url: Option<String>,
36    /// Custom RDAP server overrides per TLD.
37    pub custom_bootstrap_servers: HashMap<String, String>,
38    /// Reuse TCP connections across requests.
39    pub reuse_connections: bool,
40    /// Maximum number of idle keep-alive connections per host.
41    pub max_connections_per_host: usize,
42}
43
44impl Default for ClientConfig {
45    fn default() -> Self {
46        Self {
47            fetcher: FetcherConfig::default(),
48            ssrf: SsrfConfig::default(),
49            cache: true,
50            bootstrap_url: None,
51            custom_bootstrap_servers: HashMap::new(),
52            reuse_connections: true,
53            max_connections_per_host: 10,
54        }
55    }
56}
57
58// ── Client ────────────────────────────────────────────────────────────────────
59
60/// The main RDAP client.
61///
62/// Cheap to clone — all inner state is behind `Arc`s.
63#[derive(Clone, Debug)]
64pub struct RdapClient {
65    fetcher: Fetcher,
66    bootstrap: Bootstrap,
67    normalizer: Normalizer,
68    cache: Option<MemoryCache>,
69}
70
71impl RdapClient {
72    /// Creates a client with the default configuration.
73    pub fn new() -> Result<Self> {
74        Self::with_config(ClientConfig::default())
75    }
76
77    /// Creates a client with custom configuration.
78    pub fn with_config(config: ClientConfig) -> Result<Self> {
79        let ssrf = SsrfGuard::with_config(config.ssrf);
80        let mut fetcher_config = config.fetcher;
81        fetcher_config.reuse_connections = config.reuse_connections;
82        fetcher_config.max_connections_per_host = config.max_connections_per_host;
83        let fetcher = Fetcher::with_config(ssrf, fetcher_config)?;
84        let reqwest_client = fetcher.reqwest_client();
85
86        let mut bootstrap = match config.bootstrap_url {
87            Some(url) => Bootstrap::with_base_url(url, reqwest_client),
88            None => Bootstrap::new(reqwest_client),
89        };
90
91        if !config.custom_bootstrap_servers.is_empty() {
92            bootstrap.set_custom_servers(config.custom_bootstrap_servers);
93        }
94
95        let cache = if config.cache {
96            Some(MemoryCache::new())
97        } else {
98            None
99        };
100
101        Ok(Self {
102            fetcher,
103            bootstrap,
104            normalizer: Normalizer::new(),
105            cache,
106        })
107    }
108
109    // ── Query methods ─────────────────────────────────────────────────────────
110
111    /// Queries RDAP information for a domain name.
112    pub async fn domain(&self, domain: &str) -> Result<DomainResponse> {
113        let domain = normalise_domain(domain)?;
114        let server = self.bootstrap.for_domain(&domain).await?;
115        let url = format!("{}/domain/{}", server.trim_end_matches('/'), domain);
116        let (raw, cached) = self.fetch_with_cache(&url).await?;
117        self.normalizer.domain(&domain, raw, &server, cached)
118    }
119
120    /// Queries RDAP information for an IP address (IPv4 or IPv6).
121    pub async fn ip(&self, ip: &str) -> Result<IpResponse> {
122        let addr: IpAddr = ip
123            .parse()
124            .map_err(|_| RdapError::InvalidInput(format!("Invalid IP address: {ip}")))?;
125
126        let server = match addr {
127            IpAddr::V4(_) => self.bootstrap.for_ipv4(ip).await?,
128            IpAddr::V6(_) => self.bootstrap.for_ipv6(ip).await?,
129        };
130
131        let url = format!("{}/ip/{}", server.trim_end_matches('/'), ip);
132        let (raw, cached) = self.fetch_with_cache(&url).await?;
133        self.normalizer.ip(ip, raw, &server, cached)
134    }
135
136    /// Queries RDAP information for an Autonomous System Number.
137    pub async fn asn(&self, asn: impl AsRef<str>) -> Result<AsnResponse> {
138        let asn_str = asn
139            .as_ref()
140            .trim_start_matches("AS")
141            .trim_start_matches("as");
142        let asn_num: u32 = asn_str
143            .parse()
144            .map_err(|_| RdapError::InvalidInput(format!("Invalid ASN: {}", asn.as_ref())))?;
145
146        let server = self.bootstrap.for_asn(asn_num).await?;
147        let url = format!("{}/autnum/{}", server.trim_end_matches('/'), asn_num);
148        let (raw, cached) = self.fetch_with_cache(&url).await?;
149        self.normalizer.asn(asn_num, raw, &server, cached)
150    }
151
152    /// Queries RDAP information for a nameserver.
153    pub async fn nameserver(&self, hostname: &str) -> Result<NameserverResponse> {
154        let hostname = normalise_domain(hostname)?;
155        let server = self.bootstrap.for_domain(&hostname).await?;
156        let url = format!("{}/nameserver/{}", server.trim_end_matches('/'), hostname);
157        let (raw, cached) = self.fetch_with_cache(&url).await?;
158        self.normalizer.nameserver(&hostname, raw, &server, cached)
159    }
160
161    /// Queries RDAP information for an entity (contact / registrar).
162    pub async fn entity(&self, handle: &str, server_url: &str) -> Result<EntityResponse> {
163        if handle.is_empty() {
164            return Err(RdapError::InvalidInput(
165                "Entity handle must not be empty".to_string(),
166            ));
167        }
168        if server_url.is_empty() {
169            return Err(RdapError::InvalidInput(
170                "Server URL must not be empty".to_string(),
171            ));
172        }
173
174        let url = format!("{}/entity/{}", server_url.trim_end_matches('/'), handle);
175        let (raw, cached) = self.fetch_with_cache(&url).await?;
176        self.normalizer.entity(handle, raw, server_url, cached)
177    }
178
179    /// Checks whether a domain is available for registration.
180    pub async fn domain_available(&self, name: &str) -> Result<AvailabilityResult> {
181        let domain_name = normalise_domain(name)?;
182        match self.domain(name).await {
183            Ok(response) => Ok(AvailabilityResult {
184                domain: domain_name,
185                available: false,
186                expires_at: response.expiration_date().map(|s| s.to_string()),
187            }),
188            Err(RdapError::HttpStatus { status: 404, .. }) => Ok(AvailabilityResult {
189                domain: domain_name,
190                available: true,
191                expires_at: None,
192            }),
193            Err(e) => Err(e),
194        }
195    }
196
197    /// Checks availability for multiple domains concurrently.
198    pub async fn domain_available_batch(
199        &self,
200        names: Vec<String>,
201        concurrency: Option<usize>,
202    ) -> Vec<Result<AvailabilityResult>> {
203        let limit = concurrency.unwrap_or(10).max(1);
204        let mut output: Vec<Option<Result<AvailabilityResult>>> =
205            (0..names.len()).map(|_| None).collect();
206
207        for (chunk_start, chunk) in names.chunks(limit).enumerate() {
208            let base = chunk_start * limit;
209            let mut set = tokio::task::JoinSet::new();
210
211            for (i, name) in chunk.iter().enumerate() {
212                let client = self.clone();
213                let name = name.clone();
214                let idx = base + i;
215                set.spawn(async move { (idx, client.domain_available(&name).await) });
216            }
217
218            while let Some(res) = set.join_next().await {
219                if let Ok((idx, result)) = res {
220                    output[idx] = Some(result);
221                }
222            }
223        }
224
225        output.into_iter().flatten().collect()
226    }
227
228    // ── Streaming API ─────────────────────────────────────────────────────────
229
230    pub fn stream_domain(
231        &self,
232        names: Vec<String>,
233        config: StreamConfig,
234    ) -> ReceiverStream<DomainEvent> {
235        let (tx, rx) = mpsc::channel(config.buffer_size);
236        let client = self.clone();
237
238        tokio::spawn(async move {
239            for name in names {
240                let event = match client.domain(&name).await {
241                    Ok(r) => DomainEvent::Result(Box::new(r)),
242                    Err(e) => DomainEvent::Error { query: name, error: e },
243                };
244                if tx.send(event).await.is_err() {
245                    break;
246                }
247            }
248        });
249
250        ReceiverStream::new(rx)
251    }
252
253    pub fn stream_ip(
254        &self,
255        addresses: Vec<String>,
256        config: StreamConfig,
257    ) -> ReceiverStream<IpEvent> {
258        let (tx, rx) = mpsc::channel(config.buffer_size);
259        let client = self.clone();
260
261        tokio::spawn(async move {
262            for addr in addresses {
263                let event = match client.ip(&addr).await {
264                    Ok(r) => IpEvent::Result(Box::new(r)),
265                    Err(e) => IpEvent::Error { query: addr, error: e },
266                };
267                if tx.send(event).await.is_err() {
268                    break;
269                }
270            }
271        });
272
273        ReceiverStream::new(rx)
274    }
275
276    pub fn stream_asn(&self, asns: Vec<String>, config: StreamConfig) -> ReceiverStream<AsnEvent> {
277        let (tx, rx) = mpsc::channel(config.buffer_size);
278        let client = self.clone();
279
280        tokio::spawn(async move {
281            for asn in asns {
282                let event = match client.asn(&asn).await {
283                    Ok(r) => AsnEvent::Result(Box::new(r)),
284                    Err(e) => AsnEvent::Error { query: asn, error: e },
285                };
286                if tx.send(event).await.is_err() {
287                    break;
288                }
289            }
290        });
291
292        ReceiverStream::new(rx)
293    }
294
295    pub fn stream_nameserver(
296        &self,
297        nameservers: Vec<String>,
298        config: StreamConfig,
299    ) -> ReceiverStream<NameserverEvent> {
300        let (tx, rx) = mpsc::channel(config.buffer_size);
301        let client = self.clone();
302
303        tokio::spawn(async move {
304            for ns in nameservers {
305                let event = match client.nameserver(&ns).await {
306                    Ok(r) => NameserverEvent::Result(Box::new(r)),
307                    Err(e) => NameserverEvent::Error { query: ns, error: e },
308                };
309                if tx.send(event).await.is_err() {
310                    break;
311                }
312            }
313        });
314
315        ReceiverStream::new(rx)
316    }
317
318    // ── Cache management ──────────────────────────────────────────────────────
319
320    pub async fn clear_cache(&self) {
321        if let Some(cache) = &self.cache {
322            cache.clear();
323        }
324        self.bootstrap.clear_cache().await;
325    }
326
327    pub fn cache_size(&self) -> usize {
328        self.cache.as_ref().map(|c| c.len()).unwrap_or(0)
329    }
330
331    // ── Private helpers ───────────────────────────────────────────────────────
332
333    async fn fetch_with_cache(&self, url: &str) -> Result<(serde_json::Value, bool)> {
334        if let Some(cache) = &self.cache {
335            if let Some(cached) = cache.get(url) {
336                return Ok((cached, true));
337            }
338        }
339
340        let value = self.fetcher.fetch(url).await?;
341
342        if let Some(cache) = &self.cache {
343            cache.set(url.to_string(), value.clone());
344        }
345
346        Ok((value, false))
347    }
348}
349
350impl Default for RdapClient {
351    fn default() -> Self {
352        Self::new().expect("Default RdapClient construction failed")
353    }
354}
355
356// ── Domain normalisation ──────────────────────────────────────────────────────
357
358fn normalise_domain(domain: &str) -> Result<String> {
359    let domain = domain.trim().trim_end_matches('.').to_lowercase();
360
361    if domain.is_empty() {
362        return Err(RdapError::InvalidInput(
363            "Domain name must not be empty".to_string(),
364        ));
365    }
366
367    if domain.is_ascii() {
368        return Ok(domain);
369    }
370
371    domain_to_ascii(&domain).map_err(|_| {
372        RdapError::InvalidInput(format!("Invalid internationalised domain name: {domain}"))
373    })
374}