use std::fmt;
use std::net::IpAddr;
use crate::cache::Cache;
use crate::client::Client;
use crate::contact::{Contact, Scope, rank};
use crate::error::Error;
use crate::query::{DomainName, Query};
use crate::resolver::Resolver;
#[derive(Clone, Debug)]
pub struct Finder {
client: Client,
resolver: Resolver,
cache: Cache,
}
impl Finder {
pub fn new(client: Client, resolver: Resolver) -> Self {
Self {
client,
resolver,
cache: Cache::default(),
}
}
pub fn with_cache(self, cache: Cache) -> Self {
Self { cache, ..self }
}
pub async fn lookup(&self, query: impl Into<Query>) -> Result<Found, Error> {
match query.into() {
Query::Ip(ip) => self.lookup_ip(ip).await,
Query::Domain(domain) => Ok(self.lookup_domain(&domain).await),
}
}
async fn lookup_ip(&self, ip: IpAddr) -> Result<Found, Error> {
let ip = crate::query::unmap(ip);
if !crate::is_public(ip) {
return Err(Error::NotPublic {
target: ip.to_string(),
});
}
let (rdap, abusix) = tokio::join!(self.rdap_ip(ip), self.resolver.abusix(ip));
Ok(merge([(Origin::Rdap, rdap), (Origin::Abusix, abusix)]))
}
async fn lookup_domain(&self, domain: &DomainName) -> Found {
let (rdap, abuse_net, rfc2142) = tokio::join!(
self.rdap_domain(domain),
self.resolver.abuse_net(domain),
self.resolver.rfc2142(domain),
);
merge([
(Origin::Rdap, rdap),
(Origin::AbuseNet, abuse_net),
(Origin::Rfc2142, rfc2142.map(Vec::from_iter)),
])
}
async fn rdap_ip(&self, ip: IpAddr) -> Result<Vec<Contact>, Error> {
if let Some(contacts) = self.cache.network(ip) {
return Ok(contacts);
}
let Some(record) = self.client.lookup_ip(ip).await? else {
return Ok(Vec::new());
};
let contacts = record.abuse_contacts(Scope::Network);
if let Some(range) = record.response.range()
&& range.contains(&ip)
{
self.cache.put_network(range, contacts.clone());
}
Ok(contacts)
}
async fn rdap_domain(&self, domain: &DomainName) -> Result<Vec<Contact>, Error> {
if let Some(contacts) = self.cache.domain(domain) {
return Ok(contacts);
}
let contacts = self
.client
.lookup_domain(domain)
.await?
.map(|record| record.abuse_contacts(Scope::Registrar))
.unwrap_or_default();
self.cache.put_domain(domain.clone(), contacts.clone());
Ok(contacts)
}
}
#[derive(Debug, Default)]
pub struct Found {
pub contacts: Vec<Contact>,
pub failures: Vec<Failure>,
}
#[derive(Debug)]
pub struct Failure {
pub origin: Origin,
pub error: Error,
}
impl fmt::Display for Failure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} did not answer: {}", self.origin, self.error)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Origin {
Rdap,
Abusix,
AbuseNet,
Rfc2142,
}
impl fmt::Display for Origin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Origin::Rdap => "RDAP",
Origin::Abusix => "Abusix",
Origin::AbuseNet => "abuse.net",
Origin::Rfc2142 => "the RFC 2142 check",
})
}
}
fn merge(answers: impl IntoIterator<Item = (Origin, Result<Vec<Contact>, Error>)>) -> Found {
let mut contacts = Vec::new();
let mut failures = Vec::new();
for (origin, answer) in answers {
match answer {
Ok(found) => contacts.extend(found),
Err(error) => failures.push(Failure { origin, error }),
}
}
Found {
contacts: rank(contacts),
failures,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::contact::{EmailAddress, Source};
fn contact(email: &str, scope: Scope, source: Source) -> Contact {
Contact {
email: EmailAddress::new(email).unwrap(),
scope,
source,
}
}
fn timeout(name: &str) -> Error {
Error::Dns {
name: name.to_owned(),
source: "timed out".into(),
}
}
#[test]
fn a_failed_source_keeps_the_contacts_of_the_others() {
let rdap = contact(
"abuse@registrar.example",
Scope::Registrar,
Source::Rdap {
server: "rdap.example".to_owned(),
},
);
let found = merge([
(Origin::Rdap, Ok(vec![rdap])),
(
Origin::AbuseNet,
Err(timeout("example.com.contacts.abuse.net.")),
),
]);
let emails: Vec<&str> = found.contacts.iter().map(|c| c.email.as_str()).collect();
assert_eq!(emails, ["abuse@registrar.example"]);
assert_eq!(found.failures.len(), 1);
assert_eq!(found.failures[0].origin, Origin::AbuseNet);
}
#[test]
fn the_contacts_are_ranked_and_a_repeat_is_dropped() {
let found = merge([
(
Origin::Abusix,
Ok(vec![contact(
"abuse@example.net",
Scope::Network,
Source::Abusix,
)]),
),
(
Origin::Rdap,
Ok(vec![contact(
"abuse@example.net",
Scope::Network,
Source::Rdap {
server: "rdap.example".to_owned(),
},
)]),
),
]);
assert_eq!(
found.contacts,
[contact(
"abuse@example.net",
Scope::Network,
Source::Rdap {
server: "rdap.example".to_owned()
},
)]
);
assert_eq!(found.failures.len(), 0);
}
#[test]
fn every_source_failing_gives_no_contacts_and_every_failure() {
let found = merge([
(Origin::Rdap, Err(timeout("rdap.example"))),
(Origin::Abusix, Err(timeout("abusix.example"))),
]);
assert_eq!(found.contacts, []);
let origins: Vec<Origin> = found.failures.iter().map(|f| f.origin).collect();
assert_eq!(origins, [Origin::Rdap, Origin::Abusix]);
}
#[test]
fn a_failure_says_which_source_failed_and_why() {
let failure = Failure {
origin: Origin::Rdap,
error: Error::NoServer {
target: "example.invalid".to_owned(),
},
};
assert!(
failure
.to_string()
.starts_with("RDAP did not answer: no RDAP server answers for example.invalid"),
"{failure}"
);
}
}