Skip to main content

abuse_contact/
cache.rs

1//! The RDAP answers a [`crate::Finder`] holds, so it does not ask a registry again.
2
3use std::collections::HashMap;
4use std::net::IpAddr;
5use std::ops::RangeInclusive;
6use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
7use std::time::{Duration, Instant};
8
9use crate::contact::Contact;
10use crate::query::DomainName;
11
12/// The RDAP answers a finder holds, and for how long.
13///
14/// The regional registries limit how often you can ask, and a run that reports a
15/// spam wave asks about the same few networks again and again. An answer for an
16/// address is held for the whole network range the registry returned, so it also
17/// answers for every other address in that range. An answer for a domain is held for
18/// that name.
19///
20/// A clone shares the answers with the original. Give a clone to
21/// [`crate::Finder::with_cache`] and keep one to call [`Cache::clear`].
22///
23/// The cache drops an answer when its time is over, the next time it stores one. It
24/// sets no other limit on its size. Read [`Cache::len`] and call [`Cache::clear`]
25/// when you want one.
26///
27/// DNS answers are not held here. The resolver holds them for the TTL of the record.
28#[derive(Clone, Debug)]
29pub struct Cache {
30    hold: Duration,
31    entries: Arc<Mutex<Entries>>,
32}
33
34impl Cache {
35    /// How long [`Cache::default`] holds an answer.
36    pub const DEFAULT_HOLD: Duration = Duration::from_secs(60 * 60);
37
38    /// Returns an empty cache that holds each answer for this long.
39    ///
40    /// A hold of zero holds nothing, so every lookup asks the registry.
41    pub fn new(hold: Duration) -> Self {
42        Self {
43            hold,
44            entries: Arc::default(),
45        }
46    }
47
48    /// Returns how many answers the cache holds, including answers whose time is over
49    /// and that it has not dropped yet.
50    pub fn len(&self) -> usize {
51        self.lock().len()
52    }
53
54    /// Returns whether the cache holds no answers.
55    pub fn is_empty(&self) -> bool {
56        self.len() == 0
57    }
58
59    /// Drops every answer.
60    pub fn clear(&self) {
61        *self.lock() = Entries::default();
62    }
63
64    /// Returns the contacts held for the network that holds this address.
65    pub(crate) fn network(&self, ip: IpAddr) -> Option<Vec<Contact>> {
66        self.lock().network(ip, Instant::now())
67    }
68
69    /// Holds the contacts for a network range.
70    pub(crate) fn put_network(&self, range: RangeInclusive<IpAddr>, contacts: Vec<Contact>) {
71        let now = Instant::now();
72        self.lock()
73            .put_network(range, contacts, now, now + self.hold);
74    }
75
76    /// Returns the contacts held for a domain.
77    pub(crate) fn domain(&self, domain: &DomainName) -> Option<Vec<Contact>> {
78        self.lock().domain(domain, Instant::now())
79    }
80
81    /// Holds the contacts for a domain.
82    pub(crate) fn put_domain(&self, domain: DomainName, contacts: Vec<Contact>) {
83        let now = Instant::now();
84        self.lock()
85            .put_domain(domain, contacts, now, now + self.hold);
86    }
87
88    fn lock(&self) -> MutexGuard<'_, Entries> {
89        // A panic while the lock was held cannot leave an entry half written: each
90        // change is one push or one insert. The answers are still good to use.
91        self.entries.lock().unwrap_or_else(PoisonError::into_inner)
92    }
93}
94
95impl Default for Cache {
96    /// Returns an empty cache that holds each answer for [`Cache::DEFAULT_HOLD`].
97    fn default() -> Self {
98        Self::new(Self::DEFAULT_HOLD)
99    }
100}
101
102/// The answers, and the time each one is good until.
103#[derive(Debug, Default)]
104struct Entries {
105    networks: Vec<(RangeInclusive<IpAddr>, Held)>,
106    domains: HashMap<DomainName, Held>,
107}
108
109#[derive(Debug)]
110struct Held {
111    contacts: Vec<Contact>,
112    until: Instant,
113}
114
115impl Held {
116    fn is_live(&self, now: Instant) -> bool {
117        now < self.until
118    }
119}
120
121impl Entries {
122    fn len(&self) -> usize {
123        self.networks.len() + self.domains.len()
124    }
125
126    /// Returns the contacts of the narrowest live range that holds the address.
127    ///
128    /// Ranges can nest: a registry gives a large block to one holder, and a small
129    /// part of it to another. The narrowest range is the one the registry gives for
130    /// an address inside it.
131    fn network(&self, ip: IpAddr, now: Instant) -> Option<Vec<Contact>> {
132        self.networks
133            .iter()
134            .filter(|(range, held)| range.contains(&ip) && held.is_live(now))
135            .min_by_key(|(range, _)| width(range))
136            .map(|(_, held)| held.contacts.clone())
137    }
138
139    fn put_network(
140        &mut self,
141        range: RangeInclusive<IpAddr>,
142        contacts: Vec<Contact>,
143        now: Instant,
144        until: Instant,
145    ) {
146        self.drop_expired(now);
147        self.networks.retain(|(held, _)| *held != range);
148        self.networks.push((range, Held { contacts, until }));
149    }
150
151    fn domain(&self, domain: &DomainName, now: Instant) -> Option<Vec<Contact>> {
152        self.domains
153            .get(domain)
154            .filter(|held| held.is_live(now))
155            .map(|held| held.contacts.clone())
156    }
157
158    fn put_domain(
159        &mut self,
160        domain: DomainName,
161        contacts: Vec<Contact>,
162        now: Instant,
163        until: Instant,
164    ) {
165        self.drop_expired(now);
166        self.domains.insert(domain, Held { contacts, until });
167    }
168
169    fn drop_expired(&mut self, now: Instant) {
170        self.networks.retain(|(_, held)| held.is_live(now));
171        self.domains.retain(|_, held| held.is_live(now));
172    }
173}
174
175/// Returns how many addresses a range covers, less one.
176fn width(range: &RangeInclusive<IpAddr>) -> u128 {
177    number(*range.end()) - number(*range.start())
178}
179
180fn number(ip: IpAddr) -> u128 {
181    match ip {
182        IpAddr::V4(v4) => u128::from(u32::from(v4)),
183        IpAddr::V6(v6) => u128::from(v6),
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190    use crate::contact::{EmailAddress, Scope, Source};
191
192    const HOUR: Duration = Duration::from_secs(60 * 60);
193
194    fn contacts(email: &str) -> Vec<Contact> {
195        vec![Contact {
196            email: EmailAddress::new(email).unwrap(),
197            scope: Scope::Network,
198            source: Source::Rdap {
199                server: "rdap.example".to_owned(),
200            },
201        }]
202    }
203
204    fn range(start: &str, end: &str) -> RangeInclusive<IpAddr> {
205        start.parse().unwrap()..=end.parse().unwrap()
206    }
207
208    fn emails(found: Option<Vec<Contact>>) -> Option<Vec<String>> {
209        found.map(|contacts| contacts.iter().map(|c| c.email.to_string()).collect())
210    }
211
212    #[test]
213    fn a_range_answers_for_every_address_in_it() {
214        let now = Instant::now();
215        let mut entries = Entries::default();
216        entries.put_network(
217            range("8.8.8.0", "8.8.8.255"),
218            contacts("abuse@example.com"),
219            now,
220            now + HOUR,
221        );
222
223        for ip in ["8.8.8.0", "8.8.8.8", "8.8.8.255"] {
224            assert_eq!(
225                emails(entries.network(ip.parse().unwrap(), now)),
226                Some(vec!["abuse@example.com".to_owned()]),
227                "{ip}"
228            );
229        }
230        for ip in ["8.8.7.255", "8.8.9.0", "::ffff:808:808"] {
231            assert_eq!(entries.network(ip.parse().unwrap(), now), None, "{ip}");
232        }
233    }
234
235    #[test]
236    fn the_narrowest_range_answers_for_an_address_in_two() {
237        let now = Instant::now();
238        let mut entries = Entries::default();
239        entries.put_network(
240            range("8.8.8.0", "8.8.8.7"),
241            contacts("customer@example.com"),
242            now,
243            now + HOUR,
244        );
245        entries.put_network(
246            range("8.8.0.0", "8.8.255.255"),
247            contacts("isp@example.com"),
248            now,
249            now + HOUR,
250        );
251
252        assert_eq!(
253            emails(entries.network("8.8.8.1".parse().unwrap(), now)),
254            Some(vec!["customer@example.com".to_owned()])
255        );
256        assert_eq!(
257            emails(entries.network("8.8.9.1".parse().unwrap(), now)),
258            Some(vec!["isp@example.com".to_owned()])
259        );
260    }
261
262    #[test]
263    fn an_answer_whose_time_is_over_is_not_given() {
264        let now = Instant::now();
265        let mut entries = Entries::default();
266        entries.put_network(
267            range("8.8.8.0", "8.8.8.255"),
268            contacts("abuse@example.com"),
269            now,
270            now + HOUR,
271        );
272        let domain: DomainName = "example.com".parse().unwrap();
273        entries.put_domain(
274            domain.clone(),
275            contacts("abuse@example.com"),
276            now,
277            now + HOUR,
278        );
279
280        let later = now + HOUR;
281        assert_eq!(entries.network("8.8.8.8".parse().unwrap(), later), None);
282        assert_eq!(entries.domain(&domain, later), None);
283    }
284
285    #[test]
286    fn storing_an_answer_drops_the_answers_whose_time_is_over() {
287        let now = Instant::now();
288        let mut entries = Entries::default();
289        entries.put_network(
290            range("8.8.8.0", "8.8.8.255"),
291            contacts("old@example.com"),
292            now,
293            now + HOUR,
294        );
295        entries.put_domain(
296            "old.example".parse().unwrap(),
297            contacts("old@example.com"),
298            now,
299            now + HOUR,
300        );
301
302        let later = now + HOUR;
303        entries.put_domain(
304            "new.example".parse().unwrap(),
305            contacts("new@example.com"),
306            later,
307            later + HOUR,
308        );
309
310        assert_eq!(entries.len(), 1);
311    }
312
313    #[test]
314    fn storing_the_same_range_again_replaces_it() {
315        let now = Instant::now();
316        let mut entries = Entries::default();
317        for email in ["old@example.com", "new@example.com"] {
318            entries.put_network(
319                range("8.8.8.0", "8.8.8.255"),
320                contacts(email),
321                now,
322                now + HOUR,
323            );
324        }
325
326        assert_eq!(entries.len(), 1);
327        assert_eq!(
328            emails(entries.network("8.8.8.8".parse().unwrap(), now)),
329            Some(vec!["new@example.com".to_owned()])
330        );
331    }
332
333    #[test]
334    fn a_hold_of_zero_holds_nothing() {
335        let cache = Cache::new(Duration::ZERO);
336        cache.put_network(range("8.8.8.0", "8.8.8.255"), contacts("abuse@example.com"));
337
338        assert_eq!(cache.network("8.8.8.8".parse().unwrap()), None);
339    }
340
341    #[test]
342    fn a_clone_shares_the_answers_and_clear_drops_them() {
343        let cache = Cache::default();
344        let clone = cache.clone();
345        clone.put_domain(
346            "example.com".parse().unwrap(),
347            contacts("abuse@example.com"),
348        );
349
350        assert_eq!(cache.len(), 1);
351        cache.clear();
352        assert!(clone.is_empty());
353    }
354
355    #[test]
356    fn measures_the_width_of_a_range() {
357        assert_eq!(width(&range("8.8.8.0", "8.8.8.255")), 255);
358        assert_eq!(width(&range("2001:db8::", "2001:db8::ffff")), 0xffff);
359    }
360}