270 lines
7.2 KiB
Rust
Raw Normal View History

/*
* This module provides an abstraction over the maxmind geoip databases
* that provides the results ready for templating.
*/
use log::{debug,info,warn,error};
use maxminddb::geoip2;
use maxminddb::MaxMindDBError::AddressNotFoundError;
2023-03-26 14:27:16 +02:00
use parking_lot::RwLock;
use std::collections::BTreeMap;
use std::net::IpAddr;
2023-02-18 13:09:22 +01:00
use std::path::Path;
/* Datatypes */
2023-02-21 00:06:49 +01:00
#[derive(serde::Deserialize, serde::Serialize, Default, Clone)]
pub struct NamedLocation {
iso_code: Option<String>,
name: Option<String>,
geoname_id: Option<u32>,
}
2023-02-21 00:06:49 +01:00
#[derive(serde::Deserialize, serde::Serialize, Default, Copy, Clone)]
pub struct LocationCoordinates {
lat: f64,
lon: f64,
}
2023-02-21 00:06:49 +01:00
#[derive(serde::Deserialize, serde::Serialize, Default, Clone)]
pub struct LocationResult {
continent: Option<NamedLocation>,
country: Option<NamedLocation>,
registered_country: Option<NamedLocation>,
represented_country: Option<NamedLocation>,
subdivisions: Option<Vec<NamedLocation>>,
city: Option<NamedLocation>,
coordinates: Option<LocationCoordinates>,
accuracy: Option<u16>,
postal_code: Option<String>,
time_zone: Option<String>,
}
2023-02-21 00:06:49 +01:00
#[derive(serde::Deserialize, serde::Serialize, Default, Clone)]
pub struct AsnResult {
asn: Option<u32>,
name: Option<String>,
}
2023-02-18 13:09:22 +01:00
pub struct MMDBCarrier {
2023-03-26 14:27:16 +02:00
pub mmdb: RwLock<Option<maxminddb::Reader<Vec<u8>>>>,
2023-02-18 13:09:22 +01:00
pub name: String,
2023-03-26 14:27:16 +02:00
pub path: Option<String>,
}
2023-02-18 13:09:22 +01:00
pub trait QueryLocation {
fn query_location_for_ip(&self, address: &IpAddr, laguages: &[&str]) -> Option<LocationResult>;
}
2023-02-18 13:09:22 +01:00
pub trait QueryAsn {
2023-02-22 21:32:10 +01:00
fn query_asn_for_ip(&self, address: &IpAddr) -> Option<AsnResult>;
}
/* Converters */
pub fn extract_localized_name(
names: &Option<BTreeMap<&str, &str>>,
languages: &[&str])
-> Option<String> {
match names {
Some(names) => {
for language in languages {
if let Some(name) = names.get(language){
return Some(name.to_string())
}
}
return None
},
None => None
}
}
pub fn geoip2_city_to_named_location(item: geoip2::city::City, languages: &[&str]) -> NamedLocation {
NamedLocation {
iso_code: None,
geoname_id: item.geoname_id,
name: extract_localized_name(&item.names, languages),
}
}
pub fn geoip2_continent_to_named_location(item: geoip2::country::Continent, languages: &[&str]) -> NamedLocation {
NamedLocation {
iso_code: item.code.map(ToString::to_string),
geoname_id: item.geoname_id,
name: extract_localized_name(&item.names, languages),
}
}
pub fn geoip2_country_to_named_location(item: geoip2::country::Country, languages: &[&str]) -> NamedLocation {
NamedLocation {
iso_code: item.iso_code.map(ToString::to_string),
geoname_id: item.geoname_id,
name: extract_localized_name(&item.names, languages),
}
}
pub fn geoip2_represented_country_to_named_location(item: geoip2::country::RepresentedCountry, languages: &[&str]) -> NamedLocation {
NamedLocation {
iso_code: item.iso_code.map(ToString::to_string),
geoname_id: item.geoname_id,
name: extract_localized_name(&item.names, languages),
}
}
pub fn geoip2_subdivision_to_named_location(item: geoip2::city::Subdivision, languages: &[&str]) -> NamedLocation {
NamedLocation {
iso_code: item.iso_code.map(ToString::to_string),
geoname_id: item.geoname_id,
name: extract_localized_name(&item.names, languages),
}
}
/* Implementation */
impl QueryAsn for MMDBCarrier {
2023-02-22 21:32:10 +01:00
fn query_asn_for_ip(&self, address: &IpAddr) -> Option<AsnResult> {
2023-03-26 14:27:16 +02:00
let mmdb = self.mmdb.read();
match &*mmdb {
Some(mmdb) => {
2023-02-22 21:32:10 +01:00
match mmdb.lookup::<geoip2::Asn>(*address) {
Ok(res) => {
Some(AsnResult {
asn: res.autonomous_system_number,
name: res.autonomous_system_organization.map(ToString::to_string),
})
},
Err(AddressNotFoundError(_)) => {
// Log to the debug channel.
// This isn't severe, and shouldn't be logged in production.
debug!("ASN not found in database for {address}.");
None
},
Err(e) => {
2025-02-09 15:11:17 +01:00
error!("Error while looking up ASN for {address}: {e}");
None
}
}
},
None => None,
}
}
}
impl QueryLocation for MMDBCarrier {
fn query_location_for_ip(&self, address: &IpAddr, languages: &[&str]) -> Option<LocationResult> {
2023-03-26 14:27:16 +02:00
let mmdb = self.mmdb.read();
match &*mmdb {
Some(mmdb) => {
2023-02-22 21:32:10 +01:00
match mmdb.lookup::<geoip2::City>(*address) {
Ok(res) => {
Some(LocationResult {
continent:
res.continent.map(|c| geoip2_continent_to_named_location(c, languages)),
country:
res.country.map(|c| geoip2_country_to_named_location(c, languages)),
registered_country:
res.registered_country.map(|c| geoip2_country_to_named_location(c, languages)),
represented_country:
res.represented_country.map(|c| geoip2_represented_country_to_named_location(c, languages)),
city:
res.city.map(|c| geoip2_city_to_named_location(c, languages)),
subdivisions: match res.subdivisions {
Some(sds) => {
2023-02-18 13:09:22 +01:00
let mut subdivisions = Vec::new();
subdivisions.reserve_exact(sds.len());
for sd in sds {
subdivisions.push(geoip2_subdivision_to_named_location(sd, languages));
}
Some(subdivisions)
},
None => None,
},
coordinates: match &res.location {
Some(loc) => {
if loc.latitude.is_some() && loc.longitude.is_some() {
Some(LocationCoordinates {
lat: loc.latitude.unwrap_or(0.0),
lon: loc.longitude.unwrap_or(0.0),
})
} else {
None
}
},
None => None,
},
accuracy: match &res.location {
Some(loc) => loc.accuracy_radius,
None => None,
},
postal_code: match res.postal {
Some(p) => p.code.map(ToString::to_string),
None => None,
},
time_zone: match res.location {
Some(loc) => loc.time_zone.map(ToString::to_string),
None => None,
},
})
},
Err(AddressNotFoundError(_)) => {
// Log to the debug channel.
// This isn't severe, and shouldn't be logged in production.
debug!("IP location not found in database for {address}");
None
},
Err(e) => {
error!("Error while looking up IP location for {address}: {e}");
None
}
}
},
None => None,
}
}
}
2023-02-18 13:09:22 +01:00
impl MMDBCarrier {
2023-03-26 14:27:16 +02:00
pub fn new(name: String, path: Option<String>) -> MMDBCarrier {
MMDBCarrier {
mmdb: RwLock::new(None),
name: name,
path: path,
}
}
pub fn reload_database(&self) -> Result<(),maxminddb::MaxMindDBError> {
match &self.path {
Some(path) => self.load_database_from_path(Path::new(&path)),
None => Ok(()),
}
}
pub fn load_database_from_path(&self, path: &Path) -> Result<(),maxminddb::MaxMindDBError> {
let mut mmdb = self.mmdb.write();
2025-02-09 15:11:17 +01:00
info!("Loading {} from '{}' ...", &self.name, path.display());
2023-02-18 13:09:22 +01:00
match maxminddb::Reader::open_readfile(path) {
Ok(reader) => {
2023-03-26 14:27:16 +02:00
let wording = if mmdb.is_some() {
2023-02-18 13:09:22 +01:00
"Replaced old"
} else {
"Loaded new"
};
2023-03-26 14:27:16 +02:00
*mmdb = Some(reader);
2025-02-09 15:11:17 +01:00
info!("{} {} with new one.", wording, &self.name);
2023-02-18 13:09:22 +01:00
Ok(())
},
Err(e) => {
2025-02-09 15:11:17 +01:00
error!("Error while reading {}: {}", &self.name, &e);
2023-03-26 14:27:16 +02:00
if mmdb.is_some() {
2025-02-09 15:11:17 +01:00
warn!("Not replacing old database.");
2023-02-18 13:09:22 +01:00
}
Err(e)
},
}
}
}