1use std::fmt;
4use std::net::IpAddr;
5
6use crate::cache::Cache;
7use crate::client::Client;
8use crate::contact::{Contact, Scope, rank};
9use crate::error::Error;
10use crate::query::{DomainName, Query};
11use crate::resolver::Resolver;
12
13#[derive(Clone, Debug)]
39pub struct Finder {
40 client: Client,
41 resolver: Resolver,
42 cache: Cache,
43}
44
45impl Finder {
46 pub fn new(client: Client, resolver: Resolver) -> Self {
50 Self {
51 client,
52 resolver,
53 cache: Cache::default(),
54 }
55 }
56
57 pub fn with_cache(self, cache: Cache) -> Self {
61 Self { cache, ..self }
62 }
63
64 pub async fn lookup(&self, query: impl Into<Query>) -> Result<Found, Error> {
74 match query.into() {
75 Query::Ip(ip) => self.lookup_ip(ip).await,
76 Query::Domain(domain) => Ok(self.lookup_domain(&domain).await),
77 }
78 }
79
80 async fn lookup_ip(&self, ip: IpAddr) -> Result<Found, Error> {
81 let ip = crate::query::unmap(ip);
84 if !crate::is_public(ip) {
85 return Err(Error::NotPublic {
86 target: ip.to_string(),
87 });
88 }
89
90 let (rdap, abusix) = tokio::join!(self.rdap_ip(ip), self.resolver.abusix(ip));
91
92 Ok(merge([(Origin::Rdap, rdap), (Origin::Abusix, abusix)]))
93 }
94
95 async fn lookup_domain(&self, domain: &DomainName) -> Found {
96 let (rdap, abuse_net, rfc2142) = tokio::join!(
97 self.rdap_domain(domain),
98 self.resolver.abuse_net(domain),
99 self.resolver.rfc2142(domain),
100 );
101
102 merge([
103 (Origin::Rdap, rdap),
104 (Origin::AbuseNet, abuse_net),
105 (Origin::Rfc2142, rfc2142.map(Vec::from_iter)),
106 ])
107 }
108
109 async fn rdap_ip(&self, ip: IpAddr) -> Result<Vec<Contact>, Error> {
114 if let Some(contacts) = self.cache.network(ip) {
115 return Ok(contacts);
116 }
117
118 let Some(record) = self.client.lookup_ip(ip).await? else {
119 return Ok(Vec::new());
120 };
121 let contacts = record.abuse_contacts(Scope::Network);
122
123 if let Some(range) = record.response.range()
124 && range.contains(&ip)
125 {
126 self.cache.put_network(range, contacts.clone());
127 }
128 Ok(contacts)
129 }
130
131 async fn rdap_domain(&self, domain: &DomainName) -> Result<Vec<Contact>, Error> {
135 if let Some(contacts) = self.cache.domain(domain) {
136 return Ok(contacts);
137 }
138
139 let contacts = self
140 .client
141 .lookup_domain(domain)
142 .await?
143 .map(|record| record.abuse_contacts(Scope::Registrar))
144 .unwrap_or_default();
145
146 self.cache.put_domain(domain.clone(), contacts.clone());
147 Ok(contacts)
148 }
149}
150
151#[derive(Debug, Default)]
153pub struct Found {
154 pub contacts: Vec<Contact>,
156 pub failures: Vec<Failure>,
161}
162
163#[derive(Debug)]
165pub struct Failure {
166 pub origin: Origin,
168 pub error: Error,
170}
171
172impl fmt::Display for Failure {
173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174 write!(f, "{} did not answer: {}", self.origin, self.error)
175 }
176}
177
178#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
184pub enum Origin {
185 Rdap,
187 Abusix,
189 AbuseNet,
191 Rfc2142,
193}
194
195impl fmt::Display for Origin {
196 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197 f.write_str(match self {
198 Origin::Rdap => "RDAP",
199 Origin::Abusix => "Abusix",
200 Origin::AbuseNet => "abuse.net",
201 Origin::Rfc2142 => "the RFC 2142 check",
202 })
203 }
204}
205
206fn merge(answers: impl IntoIterator<Item = (Origin, Result<Vec<Contact>, Error>)>) -> Found {
208 let mut contacts = Vec::new();
209 let mut failures = Vec::new();
210
211 for (origin, answer) in answers {
212 match answer {
213 Ok(found) => contacts.extend(found),
214 Err(error) => failures.push(Failure { origin, error }),
215 }
216 }
217
218 Found {
219 contacts: rank(contacts),
220 failures,
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227 use crate::contact::{EmailAddress, Source};
228
229 fn contact(email: &str, scope: Scope, source: Source) -> Contact {
230 Contact {
231 email: EmailAddress::new(email).unwrap(),
232 scope,
233 source,
234 }
235 }
236
237 fn timeout(name: &str) -> Error {
238 Error::Dns {
239 name: name.to_owned(),
240 source: "timed out".into(),
241 }
242 }
243
244 #[test]
245 fn a_failed_source_keeps_the_contacts_of_the_others() {
246 let rdap = contact(
247 "abuse@registrar.example",
248 Scope::Registrar,
249 Source::Rdap {
250 server: "rdap.example".to_owned(),
251 },
252 );
253
254 let found = merge([
255 (Origin::Rdap, Ok(vec![rdap])),
256 (
257 Origin::AbuseNet,
258 Err(timeout("example.com.contacts.abuse.net.")),
259 ),
260 ]);
261
262 let emails: Vec<&str> = found.contacts.iter().map(|c| c.email.as_str()).collect();
263 assert_eq!(emails, ["abuse@registrar.example"]);
264 assert_eq!(found.failures.len(), 1);
265 assert_eq!(found.failures[0].origin, Origin::AbuseNet);
266 }
267
268 #[test]
269 fn the_contacts_are_ranked_and_a_repeat_is_dropped() {
270 let found = merge([
271 (
272 Origin::Abusix,
273 Ok(vec![contact(
274 "abuse@example.net",
275 Scope::Network,
276 Source::Abusix,
277 )]),
278 ),
279 (
280 Origin::Rdap,
281 Ok(vec![contact(
282 "abuse@example.net",
283 Scope::Network,
284 Source::Rdap {
285 server: "rdap.example".to_owned(),
286 },
287 )]),
288 ),
289 ]);
290
291 assert_eq!(
292 found.contacts,
293 [contact(
294 "abuse@example.net",
295 Scope::Network,
296 Source::Rdap {
297 server: "rdap.example".to_owned()
298 },
299 )]
300 );
301 assert_eq!(found.failures.len(), 0);
302 }
303
304 #[test]
305 fn every_source_failing_gives_no_contacts_and_every_failure() {
306 let found = merge([
307 (Origin::Rdap, Err(timeout("rdap.example"))),
308 (Origin::Abusix, Err(timeout("abusix.example"))),
309 ]);
310
311 assert_eq!(found.contacts, []);
312 let origins: Vec<Origin> = found.failures.iter().map(|f| f.origin).collect();
313 assert_eq!(origins, [Origin::Rdap, Origin::Abusix]);
314 }
315
316 #[test]
317 fn a_failure_says_which_source_failed_and_why() {
318 let failure = Failure {
319 origin: Origin::Rdap,
320 error: Error::NoServer {
321 target: "example.invalid".to_owned(),
322 },
323 };
324
325 assert!(
326 failure
327 .to_string()
328 .starts_with("RDAP did not answer: no RDAP server answers for example.invalid"),
329 "{failure}"
330 );
331 }
332}