Skip to main content

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}