abuse_contact/client.rs
1//! The RDAP client.
2//!
3//! The client picks the server from the IANA bootstrap registries and fetches the
4//! record. It returns the record with the server that answered, and
5//! [`Record::abuse_contacts`] reads the contacts out of it.
6
7use std::net::IpAddr;
8use std::sync::Arc;
9use std::time::Duration;
10
11use crate::bootstrap::{Bootstrap, DNS_URL, IPV4_URL, IPV6_URL, Registry};
12use crate::contact::{Contact, Scope};
13use crate::destination::{self, Destinations, PublicResolver};
14use crate::error::Error;
15use crate::query::{DomainName, Query};
16use crate::rdap::Response;
17
18/// How long to wait for one request.
19const TIMEOUT: Duration = Duration::from_secs(30);
20
21/// What the crate calls itself to a registry.
22///
23/// Registries ask for an agent that names the caller, and some refuse a request
24/// without one.
25const USER_AGENT: &str = concat!("abuse-contact/", env!("CARGO_PKG_VERSION"));
26
27/// The media type an RDAP server answers with.
28const RDAP_MEDIA_TYPE: &str = "application/rdap+json";
29
30/// The most the client reads of a record, in bytes.
31///
32/// The largest record in the test fixtures is 95 KiB: a LACNIC network that lists the
33/// name servers of 63 reverse DNS zones. The limit leaves room for a larger network
34/// and stops a server that sends without end.
35pub const MAX_RECORD_BYTES: usize = 1024 * 1024;
36
37/// The most the client reads of a bootstrap registry, in bytes.
38///
39/// The largest registry, for domain names, is 71 KiB.
40pub const MAX_BOOTSTRAP_BYTES: usize = 4 * 1024 * 1024;
41
42/// Fetches RDAP records.
43///
44/// Build one and keep it. It holds the bootstrap registries and a connection pool,
45/// and both are wasted when a client is built for one lookup.
46///
47/// The client connects to public addresses only, unless it is built with
48/// [`Destinations::Any`]. A record and a redirect come from outside the process, and
49/// this keeps either from sending the client to a service inside your network.
50///
51/// ```no_run
52/// use abuse_contact::{Client, Scope, rank};
53///
54/// # async fn run() -> Result<(), abuse_contact::Error> {
55/// let client = Client::new().await?;
56///
57/// if let Some(record) = client.lookup_ip("8.8.8.8".parse().unwrap()).await? {
58/// for contact in rank(record.abuse_contacts(Scope::Network)) {
59/// println!("{} ({:?})", contact.email, contact.scope);
60/// }
61/// }
62/// # Ok(())
63/// # }
64/// ```
65#[derive(Clone, Debug)]
66pub struct Client {
67 http: reqwest::Client,
68 bootstrap: Bootstrap,
69 destinations: Destinations,
70}
71
72impl Client {
73 /// Fetches the three bootstrap registries and returns a client.
74 ///
75 /// This makes three requests to IANA. Build the client one time. The client
76 /// connects to public addresses only.
77 ///
78 /// # Errors
79 ///
80 /// Returns [`Error::Transport`] when a registry cannot be fetched,
81 /// [`Error::TooLarge`] when one is past [`MAX_BOOTSTRAP_BYTES`], and
82 /// [`Error::Decode`] when one is not a bootstrap file.
83 pub async fn new() -> Result<Self, Error> {
84 let destinations = Destinations::Public;
85 let http = Self::http_client(destinations)?;
86 let bootstrap = Bootstrap {
87 ipv4: fetch_registry(&http, IPV4_URL).await?,
88 ipv6: fetch_registry(&http, IPV6_URL).await?,
89 dns: fetch_registry(&http, DNS_URL).await?,
90 };
91
92 Ok(Self {
93 http,
94 bootstrap,
95 destinations,
96 })
97 }
98
99 /// Returns a client that uses registries you already hold.
100 ///
101 /// Use this to keep the registries between runs, so a short-lived process does
102 /// not fetch them again. Give [`Destinations::Public`] unless every server in the
103 /// registries is one you run.
104 ///
105 /// # Errors
106 ///
107 /// Returns [`Error::Transport`] when the HTTP client cannot be built.
108 pub fn with_bootstrap(bootstrap: Bootstrap, destinations: Destinations) -> Result<Self, Error> {
109 Ok(Self {
110 http: Self::http_client(destinations)?,
111 bootstrap,
112 destinations,
113 })
114 }
115
116 /// Returns the registries this client holds.
117 pub fn bootstrap(&self) -> &Bootstrap {
118 &self.bootstrap
119 }
120
121 /// Fetches the record for an address or a name.
122 ///
123 /// Returns `None` when the registry holds no record for it.
124 ///
125 /// # Errors
126 ///
127 /// Returns [`Error::NotPublic`] for a private or reserved address,
128 /// [`Error::NoServer`] when no registry answers for the target, and the errors of
129 /// [`Client::fetch`].
130 pub async fn lookup(&self, query: impl Into<Query>) -> Result<Option<Record>, Error> {
131 match query.into() {
132 Query::Ip(ip) => self.lookup_ip(ip).await,
133 Query::Domain(domain) => self.lookup_domain(&domain).await,
134 }
135 }
136
137 /// Fetches the record for an address.
138 ///
139 /// # Errors
140 ///
141 /// The same errors as [`Client::lookup`].
142 pub async fn lookup_ip(&self, ip: IpAddr) -> Result<Option<Record>, Error> {
143 // The IPv6 registry holds no record for `::ffff:8.8.8.8`, so ask about the
144 // IPv4 address it carries. The check and the lookup must use the same form.
145 let ip = crate::query::unmap(ip);
146 let target = ip.to_string();
147
148 // A private address sits in a block a registry describes, so the bootstrap
149 // finds a server and the answer names IANA. Stop before that happens.
150 if !crate::is_public(ip) {
151 return Err(Error::NotPublic { target });
152 }
153
154 let server = self
155 .bootstrap
156 .server_for_ip(ip)
157 .ok_or_else(|| Error::NoServer {
158 target: target.clone(),
159 })?;
160
161 self.fetch(&record_url(server, "ip", &target), &target)
162 .await
163 }
164
165 /// Fetches the record for a name.
166 ///
167 /// The record names the registrar and carries its abuse address. Follow
168 /// [`Response::related_href`] on [`Record::response`] with [`Client::fetch`] only
169 /// when you want what the registry leaves out.
170 ///
171 /// # Errors
172 ///
173 /// The same errors as [`Client::lookup`].
174 pub async fn lookup_domain(&self, domain: &DomainName) -> Result<Option<Record>, Error> {
175 let target = domain.as_str().to_owned();
176 let server = self
177 .bootstrap
178 .server_for_domain(domain)
179 .ok_or_else(|| Error::NoServer {
180 target: target.clone(),
181 })?;
182
183 self.fetch(&record_url(server, "domain", &target), &target)
184 .await
185 }
186
187 /// Fetches one RDAP record by its URL.
188 ///
189 /// Use it to follow a link out of a record you already hold. The URL is used as it
190 /// is given, so it must come from a record and not from outside input.
191 ///
192 /// # Errors
193 ///
194 /// Returns [`Error::Refused`] when the URL, or a redirect from it, goes where the
195 /// client does not connect, [`Error::Transport`] when the request does not
196 /// complete, [`Error::Status`] when the server refuses, [`Error::TooLarge`] when the
197 /// body is past [`MAX_RECORD_BYTES`], and [`Error::Decode`] when the body is not
198 /// RDAP.
199 pub async fn fetch(&self, url: &str, target: &str) -> Result<Option<Record>, Error> {
200 let parsed = reqwest::Url::parse(url).map_err(|problem| Error::Refused {
201 server: url.to_owned(),
202 reason: format!("it is not a URL: {problem}"),
203 })?;
204 destination::check_url(&parsed, self.destinations).map_err(|refusal| Error::Refused {
205 server: url.to_owned(),
206 reason: refusal.reason,
207 })?;
208
209 let answer = self
210 .http
211 .get(parsed)
212 .header(reqwest::header::ACCEPT, RDAP_MEDIA_TYPE)
213 .send()
214 .await
215 .map_err(|source| request_error(url, source))?;
216
217 // Read where the answer came from before the body is read, which uses up the
218 // answer. After a redirect this is the last server, not the first.
219 let url_answered = answer.url().clone();
220
221 let status = answer.status();
222 // A registry answers 404 when it holds no record. That is an answer, not a
223 // failure, and a caller asking several sources must not stop on it.
224 if status == reqwest::StatusCode::NOT_FOUND {
225 return Ok(None);
226 }
227 if !status.is_success() {
228 return Err(Error::Status {
229 server: url.to_owned(),
230 status: status.as_u16(),
231 target: target.to_owned(),
232 });
233 }
234
235 let body = read_capped(answer, url, MAX_RECORD_BYTES).await?;
236
237 let response = serde_json::from_slice(&body).map_err(|source| Error::Decode {
238 server: url.to_owned(),
239 source,
240 })?;
241
242 Ok(Some(Record {
243 response,
244 server: server_of(&url_answered),
245 url: url_answered.into(),
246 }))
247 }
248
249 /// Builds the HTTP client the crate uses.
250 fn http_client(destinations: Destinations) -> Result<reqwest::Client, Error> {
251 let mut builder = reqwest::Client::builder()
252 .user_agent(USER_AGENT)
253 .timeout(TIMEOUT)
254 .redirect(destination::redirect_policy(destinations));
255
256 if destinations == Destinations::Public {
257 // A proxy resolves names where the resolver cannot drop private addresses,
258 // so a public-only client does not use one.
259 builder = builder.dns_resolver(Arc::new(PublicResolver)).no_proxy();
260 }
261
262 builder.build().map_err(|source| Error::Transport {
263 server: "the HTTP client".to_owned(),
264 source: Box::new(source),
265 })
266 }
267}
268
269/// A record the client fetched, with the server that answered.
270#[derive(Clone, Debug)]
271pub struct Record {
272 /// The record.
273 pub response: Response,
274 /// The server that answered: the host, with the port when the URL names one.
275 ///
276 /// After a redirect this is the last server, which is the one that sent the record.
277 pub server: String,
278 /// The URL that answered, after every redirect.
279 pub url: String,
280}
281
282impl Record {
283 /// Returns every abuse contact in the record, each one time.
284 ///
285 /// The source of each contact names the server that answered.
286 pub fn abuse_contacts(&self, scope: Scope) -> Vec<Contact> {
287 self.response.abuse_contacts(scope, &self.server)
288 }
289}
290
291/// Returns the server part of a URL: the host, with the port when the URL names one.
292///
293/// A URL for the default port of its scheme names no port.
294fn server_of(url: &reqwest::Url) -> String {
295 let host = url.host_str().unwrap_or_default();
296 match url.port() {
297 Some(port) => format!("{host}:{port}"),
298 None => host.to_owned(),
299 }
300}
301
302/// Joins a server base URL, the kind of record, and the target.
303///
304/// A base URL in the registry ends with a slash, but not every entry does, so the
305/// join does not trust it.
306fn record_url(server: &str, kind: &str, target: &str) -> String {
307 format!("{}/{kind}/{target}", server.trim_end_matches('/'))
308}
309
310/// Returns the error for a request that did not complete.
311///
312/// The resolver and the redirect policy report a refusal through the HTTP client. It
313/// comes back here as a transport error, and is turned back into [`Error::Refused`].
314fn request_error(url: &str, source: reqwest::Error) -> Error {
315 match destination::refusal_in(&source) {
316 Some(refusal) => Error::Refused {
317 server: url.to_owned(),
318 reason: refusal.reason.clone(),
319 },
320 None => Error::Transport {
321 server: url.to_owned(),
322 source: Box::new(source),
323 },
324 }
325}
326
327/// Fetches one bootstrap registry.
328async fn fetch_registry(http: &reqwest::Client, url: &str) -> Result<Registry, Error> {
329 let answer = http
330 .get(url)
331 .send()
332 .await
333 .and_then(reqwest::Response::error_for_status)
334 .map_err(|source| request_error(url, source))?;
335
336 let body = read_capped(answer, url, MAX_BOOTSTRAP_BYTES).await?;
337
338 Registry::from_slice(&body).map_err(|source| Error::Decode {
339 server: url.to_owned(),
340 source,
341 })
342}
343
344/// Reads a body, and stops with [`Error::TooLarge`] past `limit` bytes.
345///
346/// The body is read in chunks and counted as it arrives. `Content-Length` alone does
347/// not bound it: a chunked answer does not send one, and a server can send more than
348/// it announced.
349async fn read_capped(
350 mut answer: reqwest::Response,
351 url: &str,
352 limit: usize,
353) -> Result<Vec<u8>, Error> {
354 let too_large = || Error::TooLarge {
355 server: url.to_owned(),
356 limit,
357 };
358
359 // A server that announces too much is refused before any of the body is read.
360 if answer
361 .content_length()
362 .is_some_and(|length| length > limit as u64)
363 {
364 return Err(too_large());
365 }
366
367 let mut body = Vec::new();
368 while let Some(chunk) = answer.chunk().await.map_err(|source| Error::Transport {
369 server: url.to_owned(),
370 source: Box::new(source),
371 })? {
372 if body.len() + chunk.len() > limit {
373 return Err(too_large());
374 }
375 body.extend_from_slice(&chunk);
376 }
377
378 Ok(body)
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn joins_a_base_url_that_ends_with_a_slash() {
387 assert_eq!(
388 record_url("https://rdap.arin.net/registry/", "ip", "8.8.8.8"),
389 "https://rdap.arin.net/registry/ip/8.8.8.8"
390 );
391 }
392
393 #[test]
394 fn joins_a_base_url_that_does_not_end_with_a_slash() {
395 assert_eq!(
396 record_url("https://rdap.example.net", "domain", "example.com"),
397 "https://rdap.example.net/domain/example.com"
398 );
399 }
400
401 #[test]
402 fn the_server_of_a_url_is_its_host() {
403 let url = |value: &str| reqwest::Url::parse(value).unwrap();
404
405 assert_eq!(
406 server_of(&url("https://rdap.arin.net/registry/ip/8.8.8.8")),
407 "rdap.arin.net"
408 );
409 assert_eq!(
410 server_of(&url("https://rdap.arin.net:443/registry/")),
411 "rdap.arin.net"
412 );
413 assert_eq!(
414 server_of(&url("http://127.0.0.1:8080/ip/8.8.8.8")),
415 "127.0.0.1:8080"
416 );
417 assert_eq!(
418 server_of(&url("https://[2001:db8::1]:8443/")),
419 "[2001:db8::1]:8443"
420 );
421 }
422
423 #[test]
424 fn names_the_crate_and_its_version_to_a_registry() {
425 assert!(USER_AGENT.starts_with("abuse-contact/"));
426 }
427}