session_guard/intel/
maxmind.rs1use std::fmt;
4use std::net::IpAddr;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use arc_swap::ArcSwap;
9use maxminddb::{PathElement, Reader};
10
11use super::{IpInfo, IpIntel};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[non_exhaustive]
16pub enum MaxMindDatabase {
17 Country,
19 Asn,
21}
22
23impl fmt::Display for MaxMindDatabase {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 f.write_str(match self {
26 Self::Country => "country",
27 Self::Asn => "ASN",
28 })
29 }
30}
31
32#[derive(Debug, thiserror::Error)]
34#[non_exhaustive]
35pub enum MaxMindError {
36 #[error("cannot read MaxMind {database} database {}: {source}", path.display())]
38 Io {
39 database: MaxMindDatabase,
41 path: PathBuf,
43 #[source]
45 source: std::io::Error,
46 },
47 #[error("invalid MaxMind {database} database: {source}")]
49 Invalid {
50 database: MaxMindDatabase,
52 #[source]
54 source: Box<dyn std::error::Error + Send + Sync + 'static>,
55 },
56 #[error("expected a MaxMind {database} database, found {found:?}")]
58 WrongDatabase {
59 database: MaxMindDatabase,
61 found: String,
63 },
64}
65
66pub struct MaxMindIntel {
85 databases: ArcSwap<Databases>,
86}
87
88struct Databases {
89 country: Reader<Vec<u8>>,
90 asn: Option<Reader<Vec<u8>>>,
91}
92
93impl Databases {
94 fn read(country: &Path, asn: Option<impl AsRef<Path>>) -> Result<Self, MaxMindError> {
95 let read = |database, path: &Path| {
96 std::fs::read(path).map_err(|source| MaxMindError::Io {
97 database,
98 path: path.to_owned(),
99 source,
100 })
101 };
102 let country = read(MaxMindDatabase::Country, country)?;
103 let asn = asn.map(|path| read(MaxMindDatabase::Asn, path.as_ref())).transpose()?;
104 Self::parse(country, asn)
105 }
106
107 fn parse(country: Vec<u8>, asn: Option<Vec<u8>>) -> Result<Self, MaxMindError> {
108 Ok(Self {
109 country: reader(MaxMindDatabase::Country, country)?,
110 asn: asn.map(|bytes| reader(MaxMindDatabase::Asn, bytes)).transpose()?,
111 })
112 }
113}
114
115fn reader(database: MaxMindDatabase, bytes: Vec<u8>) -> Result<Reader<Vec<u8>>, MaxMindError> {
116 let reader = Reader::from_source(bytes).map_err(|source| MaxMindError::Invalid {
117 database,
118 source: Box::new(source),
119 })?;
120 let kind = reader.metadata().database_type.to_ascii_lowercase();
121 let has_country = kind.contains("country") || kind.contains("city");
122 let has_asn = kind.contains("asn");
123 let fits = match database {
124 MaxMindDatabase::Country => has_country || !has_asn,
125 MaxMindDatabase::Asn => has_asn || !has_country,
126 };
127 if !fits {
128 return Err(MaxMindError::WrongDatabase {
129 database,
130 found: reader.metadata().database_type.clone(),
131 });
132 }
133 Ok(reader)
134}
135
136impl MaxMindIntel {
137 pub fn open(country_db: impl AsRef<Path>, asn_db: Option<impl AsRef<Path>>) -> Result<Self, MaxMindError> {
144 let databases = Databases::read(country_db.as_ref(), asn_db)?;
145 Ok(Self {
146 databases: ArcSwap::from_pointee(databases),
147 })
148 }
149
150 pub fn from_bytes(country_db: Vec<u8>, asn_db: Option<Vec<u8>>) -> Result<Self, MaxMindError> {
156 Ok(Self {
157 databases: ArcSwap::from_pointee(Databases::parse(country_db, asn_db)?),
158 })
159 }
160
161 pub fn reload(&self, country_db: impl AsRef<Path>, asn_db: Option<impl AsRef<Path>>) -> Result<(), MaxMindError> {
171 let databases = Databases::read(country_db.as_ref(), asn_db)?;
172 self.databases.store(Arc::new(databases));
173 Ok(())
174 }
175
176 pub fn reload_from_bytes(&self, country_db: Vec<u8>, asn_db: Option<Vec<u8>>) -> Result<(), MaxMindError> {
182 self.databases.store(Arc::new(Databases::parse(country_db, asn_db)?));
183 Ok(())
184 }
185}
186
187const COUNTRY: [PathElement<'static>; 2] = [PathElement::Key("country"), PathElement::Key("iso_code")];
188const REGISTERED_COUNTRY: [PathElement<'static>; 2] =
189 [PathElement::Key("registered_country"), PathElement::Key("iso_code")];
190const ASN: [PathElement<'static>; 1] = [PathElement::Key("autonomous_system_number")];
191
192impl IpIntel for MaxMindIntel {
193 fn lookup(&self, ip: IpAddr) -> IpInfo {
194 let ip = crate::client::canonical_ip(ip);
195 let databases = self.databases.load();
196 let country = databases.country.lookup(ip).ok().and_then(|found| {
197 [&COUNTRY[..], ®ISTERED_COUNTRY[..]]
198 .into_iter()
199 .find_map(|path| found.decode_path::<&str>(path).ok().flatten())
200 .map(str::to_owned)
201 });
202 let asn = databases
203 .asn
204 .as_ref()
205 .and_then(|reader| reader.lookup(ip).ok())
206 .and_then(|found| found.decode_path::<u32>(&ASN).ok().flatten());
207 IpInfo::new(country, asn)
208 }
209}
210
211impl fmt::Debug for MaxMindIntel {
212 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213 let databases = self.databases.load();
214 f.debug_struct("MaxMindIntel")
215 .field("country", &databases.country.metadata().database_type)
216 .field("asn", &databases.asn.as_ref().map(|r| &r.metadata().database_type))
217 .finish()
218 }
219}
220
221#[cfg(test)]
222#[path = "maxmind_tests.rs"]
223mod tests;