monovm-whois-rust 1.0.0

Domain WHOIS and RDAP lookups with availability detection, structured record parsing, referral chasing, caching and rate limiting.
Documentation
//! Caching responses, so a repeated question costs nothing.
//!
//! Worth doing for two reasons beyond speed. Registries meter queries, and a
//! cache hit does not spend budget; and a bulk check of a hundred names under
//! four popular TLDs asks the same four servers twenty-five times each, which is
//! exactly the traffic pattern that gets an address blocked.
//!
//! A cache is a [`ResponseCache`], so a caller can supply Redis, a file, or
//! anything else in place of the bundled [`MemoryCache`]. When caching is not
//! wanted, [`NullCache`] is the no-op implementation rather than an `Option` to
//! branch on.

use std::fmt;
use std::sync::Arc;

use crate::transport::{Query, RawResponse};

mod memory;
mod null;

pub use memory::MemoryCache;
pub use null::NullCache;

/// What identifies one cached answer.
///
/// Two fields, not one: the same name asked of two endpoints is two different
/// answers, and conflating them would serve a registrar's record as if it were
/// the registry's.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct CacheKey {
    /// The endpoint address the question went to.
    pub endpoint: String,
    /// The exact wire name that was asked about.
    pub name: String,
}

impl CacheKey {
    /// Build a key.
    pub fn new(endpoint: impl Into<String>, name: impl Into<String>) -> Self {
        CacheKey {
            endpoint: endpoint.into(),
            name: name.into(),
        }
    }

    /// The key for a query.
    pub fn of(query: &Query) -> Self {
        CacheKey::new(query.endpoint.address(), query.wire_name.clone())
    }
}

impl fmt::Display for CacheKey {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}|{}", self.endpoint, self.name)
    }
}

/// Somewhere to keep responses between lookups.
///
/// Implementations must be safe to share across threads and must tolerate being
/// called concurrently for the same key; the worst a race may cost is a duplicate
/// fetch, never a wrong answer.
pub trait ResponseCache: fmt::Debug + Send + Sync {
    /// The stored response, if there is a live one.
    ///
    /// Expiry is the cache's business: a caller that gets `Some` may use it.
    fn get(&self, key: &CacheKey) -> Option<RawResponse>;

    /// Store a response.
    ///
    /// Failures — a full store, an unreachable Redis — must be swallowed. A cache
    /// that cannot write is a slow cache, not a broken lookup.
    fn put(&self, key: CacheKey, response: RawResponse);

    /// Drop everything.
    fn clear(&self);

    /// How many live entries are held, if the implementation can say cheaply.
    ///
    /// `None` means "cannot answer without doing real work" — a remote cache should
    /// not round-trip to satisfy a diagnostic.
    fn len(&self) -> Option<usize> {
        None
    }

    /// Whether no live entry is held, if the implementation can say cheaply.
    fn is_empty(&self) -> Option<bool> {
        self.len().map(|count| count == 0)
    }
}

impl<T: ResponseCache + ?Sized> ResponseCache for Arc<T> {
    fn get(&self, key: &CacheKey) -> Option<RawResponse> {
        (**self).get(key)
    }

    fn put(&self, key: CacheKey, response: RawResponse) {
        (**self).put(key, response)
    }

    fn clear(&self) {
        (**self).clear()
    }

    fn len(&self) -> Option<usize> {
        (**self).len()
    }

    fn is_empty(&self) -> Option<bool> {
        (**self).is_empty()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::domain::Tld;
    use crate::registry::Endpoint;

    #[test]
    fn a_key_distinguishes_endpoints() {
        let registry = CacheKey::new("whois.verisign-grs.com", "example.com");
        let registrar = CacheKey::new("whois.registrar.example", "example.com");
        assert_ne!(registry, registrar);
    }

    #[test]
    fn a_key_is_derived_from_the_query() {
        let query = Query::new(
            Endpoint::whois("whois.nic.uk"),
            "example.co.uk",
            Tld::parse("co.uk").unwrap(),
        );
        let key = CacheKey::of(&query);

        assert_eq!(key.endpoint, "whois.nic.uk");
        assert_eq!(key.name, "example.co.uk");
        assert_eq!(key.to_string(), "whois.nic.uk|example.co.uk");
    }
}