Skip to main content

session_guard/
intel.rs

1//! Country and ASN lookups for client IPs.
2
3use std::net::IpAddr;
4
5#[cfg(feature = "maxmind")]
6#[cfg_attr(docsrs, doc(cfg(feature = "maxmind")))]
7mod maxmind;
8#[cfg(feature = "maxmind")]
9#[cfg_attr(docsrs, doc(cfg(feature = "maxmind")))]
10pub use maxmind::{MaxMindDatabase, MaxMindError, MaxMindIntel};
11
12/// What an [`IpIntel`] source knows about an address.
13#[derive(Clone, Debug, Default, PartialEq, Eq)]
14#[non_exhaustive]
15pub struct IpInfo {
16    /// ISO 3166-1 alpha-2 country code.
17    pub country: Option<String>,
18    /// Autonomous system number.
19    pub asn: Option<u32>,
20}
21
22impl IpInfo {
23    /// Builds an `IpInfo`.
24    pub fn new(country: Option<String>, asn: Option<u32>) -> Self {
25        Self { country, asn }
26    }
27}
28
29/// A geolocation / ASN source. Without one, IP changes are judged by network prefix only.
30///
31/// Lookups run on the request path and must be fast and non-blocking. With the `maxmind`
32/// feature, `MaxMindIntel` reads `GeoLite2` / `GeoIP2` databases held in memory.
33pub trait IpIntel: Send + Sync + 'static {
34    /// Looks up `ip`. Return empty fields for unknown addresses; never panic.
35    fn lookup(&self, ip: IpAddr) -> IpInfo;
36}
37
38/// The default source: knows nothing.
39#[derive(Clone, Copy, Debug, Default)]
40pub struct NoIpIntel;
41
42impl IpIntel for NoIpIntel {
43    fn lookup(&self, _ip: IpAddr) -> IpInfo {
44        IpInfo::default()
45    }
46}
47
48impl<F> IpIntel for F
49where
50    F: Fn(IpAddr) -> IpInfo + Send + Sync + 'static,
51{
52    fn lookup(&self, ip: IpAddr) -> IpInfo {
53        self(ip)
54    }
55}
56
57/// Lets the guard share a source the app keeps a handle to, for example to reload `MaxMind`
58/// databases.
59impl<T: IpIntel + ?Sized> IpIntel for std::sync::Arc<T> {
60    fn lookup(&self, ip: IpAddr) -> IpInfo {
61        (**self).lookup(ip)
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn closures_are_sources() {
71        let intel = |_ip: IpAddr| IpInfo::new(Some("BE".into()), Some(5432));
72        assert_eq!(intel.lookup("1.2.3.4".parse().unwrap()).asn, Some(5432));
73        assert_eq!(NoIpIntel.lookup("1.2.3.4".parse().unwrap()), IpInfo::default());
74    }
75
76    #[test]
77    fn shared_sources_are_sources() {
78        fn source(intel: impl IpIntel) -> impl IpIntel {
79            intel
80        }
81        #[derive(Debug)]
82        struct Fixed;
83        impl IpIntel for Fixed {
84            fn lookup(&self, _ip: IpAddr) -> IpInfo {
85                IpInfo::new(Some("BE".into()), None)
86            }
87        }
88        let shared = std::sync::Arc::new(Fixed);
89        let intel = source(std::sync::Arc::clone(&shared));
90        assert_eq!(intel.lookup("1.2.3.4".parse().unwrap()).country.as_deref(), Some("BE"));
91    }
92}