use std::net::IpAddr;
use serde::{Deserialize, Serialize};
#[cfg(feature = "maxmind")]
#[cfg_attr(docsrs, doc(cfg(feature = "maxmind")))]
mod maxmind;
#[cfg(feature = "maxmind")]
#[cfg_attr(docsrs, doc(cfg(feature = "maxmind")))]
pub use maxmind::{MaxMindDatabase, MaxMindError, MaxMindIntel};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct IpInfo {
pub country: Option<String>,
pub asn: Option<u32>,
pub anonymity: Option<Anonymity>,
pub city: Option<String>,
pub network: Option<String>,
}
impl IpInfo {
pub fn new(country: Option<String>, asn: Option<u32>) -> Self {
Self {
country,
asn,
..Self::default()
}
}
#[must_use]
pub fn with_anonymity(mut self, anonymity: Anonymity) -> Self {
self.anonymity = Some(anonymity);
self
}
#[must_use]
pub fn with_city(mut self, city: impl Into<String>) -> Self {
self.city = Some(city.into());
self
}
#[must_use]
pub fn with_network(mut self, network: impl Into<String>) -> Self {
self.network = Some(network.into());
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum Anonymity {
Direct,
Vpn,
HostingProvider,
ResidentialProxy,
PublicProxy,
TorExitNode,
}
pub trait IpIntel: Send + Sync + 'static {
fn lookup(&self, ip: IpAddr) -> IpInfo;
}
#[derive(Clone, Copy, Debug, Default)]
pub struct NoIpIntel;
impl IpIntel for NoIpIntel {
fn lookup(&self, _ip: IpAddr) -> IpInfo {
IpInfo::default()
}
}
impl<F> IpIntel for F
where
F: Fn(IpAddr) -> IpInfo + Send + Sync + 'static,
{
fn lookup(&self, ip: IpAddr) -> IpInfo {
self(ip)
}
}
impl<T: IpIntel + ?Sized> IpIntel for std::sync::Arc<T> {
fn lookup(&self, ip: IpAddr) -> IpInfo {
(**self).lookup(ip)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn closures_are_sources() {
let intel = |_ip: IpAddr| IpInfo::new(Some("BE".into()), Some(5432));
assert_eq!(intel.lookup("1.2.3.4".parse().unwrap()).asn, Some(5432));
assert_eq!(NoIpIntel.lookup("1.2.3.4".parse().unwrap()), IpInfo::default());
}
#[test]
fn builders_set_city_and_network() {
let info = IpInfo::new(Some("BE".into()), Some(5432))
.with_city("Brussels")
.with_network(String::from("Example Telecom"));
assert_eq!(info.city.as_deref(), Some("Brussels"));
assert_eq!(info.network.as_deref(), Some("Example Telecom"));
assert_eq!(info.country.as_deref(), Some("BE"));
assert_eq!(info.asn, Some(5432));
assert_eq!(IpInfo::new(None, None).city, None);
assert_eq!(IpInfo::new(None, None).network, None);
}
#[test]
fn shared_sources_are_sources() {
fn source(intel: impl IpIntel) -> impl IpIntel {
intel
}
#[derive(Debug)]
struct Fixed;
impl IpIntel for Fixed {
fn lookup(&self, _ip: IpAddr) -> IpInfo {
IpInfo::new(Some("BE".into()), None)
}
}
let shared = std::sync::Arc::new(Fixed);
let intel = source(std::sync::Arc::clone(&shared));
assert_eq!(intel.lookup("1.2.3.4".parse().unwrap()).country.as_deref(), Some("BE"));
}
}