1#![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#[derive(Debug, Clone)]
27pub struct ClientConfig {
28 pub fetcher: FetcherConfig,
30 pub ssrf: SsrfConfig,
32 pub cache: bool,
34 pub bootstrap_url: Option<String>,
36 pub custom_bootstrap_servers: HashMap<String, String>,
38 pub reuse_connections: bool,
40 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#[derive(Clone, Debug)]
64pub struct RdapClient {
65 fetcher: Fetcher,
66 bootstrap: Bootstrap,
67 normalizer: Normalizer,
68 cache: Option<MemoryCache>,
69}
70
71impl RdapClient {
72 pub fn new() -> Result<Self> {
74 Self::with_config(ClientConfig::default())
75 }
76
77 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 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 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 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 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 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 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 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 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 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 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
356fn 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}