use std::fmt;
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use arc_swap::ArcSwap;
use maxminddb::{PathElement, Reader};
use super::{IpInfo, IpIntel};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum MaxMindDatabase {
Country,
Asn,
}
impl fmt::Display for MaxMindDatabase {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Country => "country",
Self::Asn => "ASN",
})
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum MaxMindError {
#[error("cannot read MaxMind {database} database {}: {source}", path.display())]
Io {
database: MaxMindDatabase,
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("invalid MaxMind {database} database: {source}")]
Invalid {
database: MaxMindDatabase,
#[source]
source: Box<dyn std::error::Error + Send + Sync + 'static>,
},
#[error("expected a MaxMind {database} database, found {found:?}")]
WrongDatabase {
database: MaxMindDatabase,
found: String,
},
}
pub struct MaxMindIntel {
databases: ArcSwap<Databases>,
}
struct Databases {
country: Reader<Vec<u8>>,
asn: Option<Reader<Vec<u8>>>,
}
impl Databases {
fn read(country: &Path, asn: Option<impl AsRef<Path>>) -> Result<Self, MaxMindError> {
let read = |database, path: &Path| {
std::fs::read(path).map_err(|source| MaxMindError::Io {
database,
path: path.to_owned(),
source,
})
};
let country = read(MaxMindDatabase::Country, country)?;
let asn = asn.map(|path| read(MaxMindDatabase::Asn, path.as_ref())).transpose()?;
Self::parse(country, asn)
}
fn parse(country: Vec<u8>, asn: Option<Vec<u8>>) -> Result<Self, MaxMindError> {
Ok(Self {
country: reader(MaxMindDatabase::Country, country)?,
asn: asn.map(|bytes| reader(MaxMindDatabase::Asn, bytes)).transpose()?,
})
}
}
fn reader(database: MaxMindDatabase, bytes: Vec<u8>) -> Result<Reader<Vec<u8>>, MaxMindError> {
let reader = Reader::from_source(bytes).map_err(|source| MaxMindError::Invalid {
database,
source: Box::new(source),
})?;
let kind = reader.metadata().database_type.to_ascii_lowercase();
let has_country = kind.contains("country") || kind.contains("city");
let has_asn = kind.contains("asn");
let fits = match database {
MaxMindDatabase::Country => has_country || !has_asn,
MaxMindDatabase::Asn => has_asn || !has_country,
};
if !fits {
return Err(MaxMindError::WrongDatabase {
database,
found: reader.metadata().database_type.clone(),
});
}
Ok(reader)
}
impl MaxMindIntel {
pub fn open(country_db: impl AsRef<Path>, asn_db: Option<impl AsRef<Path>>) -> Result<Self, MaxMindError> {
let databases = Databases::read(country_db.as_ref(), asn_db)?;
Ok(Self {
databases: ArcSwap::from_pointee(databases),
})
}
pub fn from_bytes(country_db: Vec<u8>, asn_db: Option<Vec<u8>>) -> Result<Self, MaxMindError> {
Ok(Self {
databases: ArcSwap::from_pointee(Databases::parse(country_db, asn_db)?),
})
}
pub fn reload(&self, country_db: impl AsRef<Path>, asn_db: Option<impl AsRef<Path>>) -> Result<(), MaxMindError> {
let databases = Databases::read(country_db.as_ref(), asn_db)?;
self.databases.store(Arc::new(databases));
Ok(())
}
pub fn reload_from_bytes(&self, country_db: Vec<u8>, asn_db: Option<Vec<u8>>) -> Result<(), MaxMindError> {
self.databases.store(Arc::new(Databases::parse(country_db, asn_db)?));
Ok(())
}
}
const COUNTRY: [PathElement<'static>; 2] = [PathElement::Key("country"), PathElement::Key("iso_code")];
const REGISTERED_COUNTRY: [PathElement<'static>; 2] =
[PathElement::Key("registered_country"), PathElement::Key("iso_code")];
const ASN: [PathElement<'static>; 1] = [PathElement::Key("autonomous_system_number")];
impl IpIntel for MaxMindIntel {
fn lookup(&self, ip: IpAddr) -> IpInfo {
let ip = crate::client::canonical_ip(ip);
let databases = self.databases.load();
let country = databases.country.lookup(ip).ok().and_then(|found| {
[&COUNTRY[..], ®ISTERED_COUNTRY[..]]
.into_iter()
.find_map(|path| found.decode_path::<&str>(path).ok().flatten())
.map(str::to_owned)
});
let asn = databases
.asn
.as_ref()
.and_then(|reader| reader.lookup(ip).ok())
.and_then(|found| found.decode_path::<u32>(&ASN).ok().flatten());
IpInfo::new(country, asn)
}
}
impl fmt::Debug for MaxMindIntel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let databases = self.databases.load();
f.debug_struct("MaxMindIntel")
.field("country", &databases.country.metadata().database_type)
.field("asn", &databases.asn.as_ref().map(|r| &r.metadata().database_type))
.finish()
}
}
#[cfg(test)]
#[path = "maxmind_tests.rs"]
mod tests;