1#![forbid(unsafe_code)]
11
12use std::collections::HashMap;
13use std::net::IpAddr;
14
15use idna::domain_to_ascii;
16
17use rdap_bootstrap::Bootstrap;
18use rdap_core::{Fetcher, FetcherConfig, Normalizer};
19use rdap_security::{SsrfConfig, SsrfGuard};
20use rdap_types::error::{RdapError, Result};
21use rdap_types::{
22 AsnResponse, AvailabilityResult, DomainResponse, EntityResponse, IpResponse, NameserverResponse,
23};
24
25#[cfg(feature = "memory-cache")]
26use rdap_cache::MemoryCache;
27
28#[cfg(feature = "stream")]
29use tokio::sync::mpsc;
30#[cfg(feature = "stream")]
31use tokio_stream::wrappers::ReceiverStream;
32
33#[cfg(feature = "stream")]
34pub use rdap_stream::{AsnEvent, DomainEvent, IpEvent, NameserverEvent, StreamConfig};
35
36#[derive(Debug, Clone)]
40pub struct ClientConfig {
41 pub fetcher: FetcherConfig,
43 pub ssrf: SsrfConfig,
45 pub cache: bool,
49 pub bootstrap_url: Option<String>,
51 pub custom_bootstrap_servers: HashMap<String, String>,
53 pub reuse_connections: bool,
55 pub max_connections_per_host: usize,
57}
58
59impl Default for ClientConfig {
60 fn default() -> Self {
61 Self {
62 fetcher: FetcherConfig::default(),
63 ssrf: SsrfConfig::default(),
64 cache: true,
65 bootstrap_url: None,
66 custom_bootstrap_servers: HashMap::new(),
67 reuse_connections: true,
68 max_connections_per_host: 10,
69 }
70 }
71}
72
73#[derive(Clone, Debug)]
79pub struct RdapClient {
80 fetcher: Fetcher,
81 bootstrap: Bootstrap,
82 normalizer: Normalizer,
83 #[cfg(feature = "memory-cache")]
84 cache: Option<MemoryCache>,
85}
86
87impl RdapClient {
88 pub fn new() -> Result<Self> {
90 Self::with_config(ClientConfig::default())
91 }
92
93 pub fn with_config(config: ClientConfig) -> Result<Self> {
95 let ssrf = SsrfGuard::with_config(config.ssrf);
96 let mut fetcher_config = config.fetcher;
97 fetcher_config.reuse_connections = config.reuse_connections;
98 fetcher_config.max_connections_per_host = config.max_connections_per_host;
99 let fetcher = Fetcher::with_config(ssrf, fetcher_config)?;
100 let reqwest_client = fetcher.reqwest_client();
101
102 let mut bootstrap = match config.bootstrap_url {
103 Some(url) => Bootstrap::with_base_url(url, reqwest_client),
104 None => Bootstrap::new(reqwest_client),
105 };
106
107 if !config.custom_bootstrap_servers.is_empty() {
108 bootstrap.set_custom_servers(config.custom_bootstrap_servers);
109 }
110
111 #[cfg(feature = "memory-cache")]
112 let cache = if config.cache {
113 Some(MemoryCache::new())
114 } else {
115 None
116 };
117
118 Ok(Self {
119 fetcher,
120 bootstrap,
121 normalizer: Normalizer::new(),
122 #[cfg(feature = "memory-cache")]
123 cache,
124 })
125 }
126
127 pub async fn domain(&self, domain: &str) -> Result<DomainResponse> {
131 let domain = normalise_domain(domain)?;
132 let server = self.bootstrap.for_domain(&domain).await?;
133 let url = format!("{}/domain/{}", server.trim_end_matches('/'), domain);
134 let (raw, cached) = self.fetch_with_cache(&url).await?;
135 self.normalizer.domain(&domain, raw, &server, cached)
136 }
137
138 pub async fn ip(&self, ip: &str) -> Result<IpResponse> {
140 let addr: IpAddr = ip
141 .parse()
142 .map_err(|_| RdapError::InvalidInput(format!("Invalid IP address: {ip}")))?;
143
144 let server = match addr {
145 IpAddr::V4(_) => self.bootstrap.for_ipv4(ip).await?,
146 IpAddr::V6(_) => self.bootstrap.for_ipv6(ip).await?,
147 };
148
149 let url = format!("{}/ip/{}", server.trim_end_matches('/'), ip);
150 let (raw, cached) = self.fetch_with_cache(&url).await?;
151 self.normalizer.ip(ip, raw, &server, cached)
152 }
153
154 pub async fn asn(&self, asn: impl AsRef<str>) -> Result<AsnResponse> {
156 let asn_str = asn
157 .as_ref()
158 .trim_start_matches("AS")
159 .trim_start_matches("as");
160 let asn_num: u32 = asn_str
161 .parse()
162 .map_err(|_| RdapError::InvalidInput(format!("Invalid ASN: {}", asn.as_ref())))?;
163
164 let server = self.bootstrap.for_asn(asn_num).await?;
165 let url = format!("{}/autnum/{}", server.trim_end_matches('/'), asn_num);
166 let (raw, cached) = self.fetch_with_cache(&url).await?;
167 self.normalizer.asn(asn_num, raw, &server, cached)
168 }
169
170 pub async fn nameserver(&self, hostname: &str) -> Result<NameserverResponse> {
172 let hostname = normalise_domain(hostname)?;
173 let server = self.bootstrap.for_domain(&hostname).await?;
174 let url = format!("{}/nameserver/{}", server.trim_end_matches('/'), hostname);
175 let (raw, cached) = self.fetch_with_cache(&url).await?;
176 self.normalizer.nameserver(&hostname, raw, &server, cached)
177 }
178
179 pub async fn entity(&self, handle: &str, server_url: &str) -> Result<EntityResponse> {
181 if handle.is_empty() {
182 return Err(RdapError::InvalidInput(
183 "Entity handle must not be empty".to_string(),
184 ));
185 }
186 if server_url.is_empty() {
187 return Err(RdapError::InvalidInput(
188 "Server URL must not be empty".to_string(),
189 ));
190 }
191
192 let url = format!("{}/entity/{}", server_url.trim_end_matches('/'), handle);
193 let (raw, cached) = self.fetch_with_cache(&url).await?;
194 self.normalizer.entity(handle, raw, server_url, cached)
195 }
196
197 pub async fn domain_available(&self, name: &str) -> Result<AvailabilityResult> {
199 let domain_name = normalise_domain(name)?;
200 match self.domain(name).await {
201 Ok(response) => Ok(AvailabilityResult {
202 domain: domain_name,
203 available: false,
204 expires_at: response.expiration_date().map(|s| s.to_string()),
205 }),
206 Err(RdapError::HttpStatus { status: 404, .. }) => Ok(AvailabilityResult {
207 domain: domain_name,
208 available: true,
209 expires_at: None,
210 }),
211 Err(e) => Err(e),
212 }
213 }
214
215 pub async fn domain_available_batch(
217 &self,
218 names: Vec<String>,
219 concurrency: Option<usize>,
220 ) -> Vec<Result<AvailabilityResult>> {
221 let limit = concurrency.unwrap_or(10).max(1);
222 let mut output: Vec<Option<Result<AvailabilityResult>>> =
223 (0..names.len()).map(|_| None).collect();
224
225 for (chunk_start, chunk) in names.chunks(limit).enumerate() {
226 let base = chunk_start * limit;
227 let mut set = tokio::task::JoinSet::new();
228
229 for (i, name) in chunk.iter().enumerate() {
230 let client = self.clone();
231 let name = name.clone();
232 let idx = base + i;
233 set.spawn(async move { (idx, client.domain_available(&name).await) });
234 }
235
236 while let Some(res) = set.join_next().await {
237 if let Ok((idx, result)) = res {
238 output[idx] = Some(result);
239 }
240 }
241 }
242
243 output.into_iter().flatten().collect()
244 }
245
246 #[cfg(feature = "stream")]
249 pub fn stream_domain(
250 &self,
251 names: Vec<String>,
252 config: StreamConfig,
253 ) -> ReceiverStream<DomainEvent> {
254 let (tx, rx) = mpsc::channel(config.buffer_size);
255 let client = self.clone();
256
257 tokio::spawn(async move {
258 for name in names {
259 let event = match client.domain(&name).await {
260 Ok(r) => DomainEvent::Result(Box::new(r)),
261 Err(e) => DomainEvent::Error {
262 query: name,
263 error: e,
264 },
265 };
266 if tx.send(event).await.is_err() {
267 break;
268 }
269 }
270 });
271
272 ReceiverStream::new(rx)
273 }
274
275 #[cfg(feature = "stream")]
276 pub fn stream_ip(
277 &self,
278 addresses: Vec<String>,
279 config: StreamConfig,
280 ) -> ReceiverStream<IpEvent> {
281 let (tx, rx) = mpsc::channel(config.buffer_size);
282 let client = self.clone();
283
284 tokio::spawn(async move {
285 for addr in addresses {
286 let event = match client.ip(&addr).await {
287 Ok(r) => IpEvent::Result(Box::new(r)),
288 Err(e) => IpEvent::Error {
289 query: addr,
290 error: e,
291 },
292 };
293 if tx.send(event).await.is_err() {
294 break;
295 }
296 }
297 });
298
299 ReceiverStream::new(rx)
300 }
301
302 #[cfg(feature = "stream")]
303 pub fn stream_asn(&self, asns: Vec<String>, config: StreamConfig) -> ReceiverStream<AsnEvent> {
304 let (tx, rx) = mpsc::channel(config.buffer_size);
305 let client = self.clone();
306
307 tokio::spawn(async move {
308 for asn in asns {
309 let event = match client.asn(&asn).await {
310 Ok(r) => AsnEvent::Result(Box::new(r)),
311 Err(e) => AsnEvent::Error {
312 query: asn,
313 error: e,
314 },
315 };
316 if tx.send(event).await.is_err() {
317 break;
318 }
319 }
320 });
321
322 ReceiverStream::new(rx)
323 }
324
325 #[cfg(feature = "stream")]
326 pub fn stream_nameserver(
327 &self,
328 nameservers: Vec<String>,
329 config: StreamConfig,
330 ) -> ReceiverStream<NameserverEvent> {
331 let (tx, rx) = mpsc::channel(config.buffer_size);
332 let client = self.clone();
333
334 tokio::spawn(async move {
335 for ns in nameservers {
336 let event = match client.nameserver(&ns).await {
337 Ok(r) => NameserverEvent::Result(Box::new(r)),
338 Err(e) => NameserverEvent::Error {
339 query: ns,
340 error: e,
341 },
342 };
343 if tx.send(event).await.is_err() {
344 break;
345 }
346 }
347 });
348
349 ReceiverStream::new(rx)
350 }
351
352 pub async fn clear_cache(&self) {
356 #[cfg(feature = "memory-cache")]
357 if let Some(cache) = &self.cache {
358 cache.clear();
359 }
360 self.bootstrap.clear_cache().await;
361 }
362
363 pub fn cache_size(&self) -> usize {
367 #[cfg(feature = "memory-cache")]
368 {
369 self.cache.as_ref().map(|c| c.len()).unwrap_or(0)
370 }
371 #[cfg(not(feature = "memory-cache"))]
372 {
373 0
374 }
375 }
376
377 async fn fetch_with_cache(&self, url: &str) -> Result<(serde_json::Value, bool)> {
380 #[cfg(feature = "memory-cache")]
381 if let Some(cache) = &self.cache {
382 if let Some(cached) = cache.get(url) {
383 return Ok((cached, true));
384 }
385 }
386
387 let value = self.fetcher.fetch(url).await?;
388
389 #[cfg(feature = "memory-cache")]
390 if let Some(cache) = &self.cache {
391 cache.set(url.to_string(), value.clone());
392 }
393
394 Ok((value, false))
395 }
396}
397
398impl Default for RdapClient {
399 fn default() -> Self {
400 Self::new().expect("Default RdapClient construction failed")
401 }
402}
403
404fn normalise_domain(domain: &str) -> Result<String> {
407 let domain = domain.trim().trim_end_matches('.').to_lowercase();
408
409 if domain.is_empty() {
410 return Err(RdapError::InvalidInput(
411 "Domain name must not be empty".to_string(),
412 ));
413 }
414
415 if domain.is_ascii() {
416 return Ok(domain);
417 }
418
419 domain_to_ascii(&domain).map_err(|_| {
420 RdapError::InvalidInput(format!("Invalid internationalised domain name: {domain}"))
421 })
422}