1use std::net::IpAddr;
4
5use serde::{Deserialize, Serialize};
6
7#[cfg(feature = "maxmind")]
8#[cfg_attr(docsrs, doc(cfg(feature = "maxmind")))]
9mod maxmind;
10#[cfg(feature = "maxmind")]
11#[cfg_attr(docsrs, doc(cfg(feature = "maxmind")))]
12pub use maxmind::{MaxMindDatabase, MaxMindError, MaxMindIntel};
13
14#[derive(Clone, Debug, Default, PartialEq, Eq)]
16#[non_exhaustive]
17pub struct IpInfo {
18 pub country: Option<String>,
20 pub asn: Option<u32>,
22 pub anonymity: Option<Anonymity>,
25 pub city: Option<String>,
27 pub network: Option<String>,
30}
31
32impl IpInfo {
33 pub fn new(country: Option<String>, asn: Option<u32>) -> Self {
35 Self {
36 country,
37 asn,
38 ..Self::default()
39 }
40 }
41
42 #[must_use]
44 pub fn with_anonymity(mut self, anonymity: Anonymity) -> Self {
45 self.anonymity = Some(anonymity);
46 self
47 }
48
49 #[must_use]
51 pub fn with_city(mut self, city: impl Into<String>) -> Self {
52 self.city = Some(city.into());
53 self
54 }
55
56 #[must_use]
58 pub fn with_network(mut self, network: impl Into<String>) -> Self {
59 self.network = Some(network.into());
60 self
61 }
62}
63
64#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
70#[serde(rename_all = "snake_case")]
71#[non_exhaustive]
72pub enum Anonymity {
73 Direct,
75 Vpn,
77 HostingProvider,
79 ResidentialProxy,
81 PublicProxy,
83 TorExitNode,
85}
86
87pub trait IpIntel: Send + Sync + 'static {
92 fn lookup(&self, ip: IpAddr) -> IpInfo;
94}
95
96#[derive(Clone, Copy, Debug, Default)]
98pub struct NoIpIntel;
99
100impl IpIntel for NoIpIntel {
101 fn lookup(&self, _ip: IpAddr) -> IpInfo {
102 IpInfo::default()
103 }
104}
105
106impl<F> IpIntel for F
107where
108 F: Fn(IpAddr) -> IpInfo + Send + Sync + 'static,
109{
110 fn lookup(&self, ip: IpAddr) -> IpInfo {
111 self(ip)
112 }
113}
114
115impl<T: IpIntel + ?Sized> IpIntel for std::sync::Arc<T> {
118 fn lookup(&self, ip: IpAddr) -> IpInfo {
119 (**self).lookup(ip)
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn closures_are_sources() {
129 let intel = |_ip: IpAddr| IpInfo::new(Some("BE".into()), Some(5432));
130 assert_eq!(intel.lookup("1.2.3.4".parse().unwrap()).asn, Some(5432));
131 assert_eq!(NoIpIntel.lookup("1.2.3.4".parse().unwrap()), IpInfo::default());
132 }
133
134 #[test]
135 fn builders_set_city_and_network() {
136 let info = IpInfo::new(Some("BE".into()), Some(5432))
137 .with_city("Brussels")
138 .with_network(String::from("Example Telecom"));
139 assert_eq!(info.city.as_deref(), Some("Brussels"));
140 assert_eq!(info.network.as_deref(), Some("Example Telecom"));
141 assert_eq!(info.country.as_deref(), Some("BE"));
142 assert_eq!(info.asn, Some(5432));
143 assert_eq!(IpInfo::new(None, None).city, None);
144 assert_eq!(IpInfo::new(None, None).network, None);
145 }
146
147 #[test]
148 fn shared_sources_are_sources() {
149 fn source(intel: impl IpIntel) -> impl IpIntel {
150 intel
151 }
152 #[derive(Debug)]
153 struct Fixed;
154 impl IpIntel for Fixed {
155 fn lookup(&self, _ip: IpAddr) -> IpInfo {
156 IpInfo::new(Some("BE".into()), None)
157 }
158 }
159 let shared = std::sync::Arc::new(Fixed);
160 let intel = source(std::sync::Arc::clone(&shared));
161 assert_eq!(intel.lookup("1.2.3.4".parse().unwrap()).country.as_deref(), Some("BE"));
162 }
163}