/* * This module provides an abstraction over the maxmind geoip databases * that provides the results ready for templating. */ use maxminddb; use maxminddb::geoip2; use parking_lot::RwLock; use std::collections::BTreeMap; use std::net::IpAddr; use std::path::Path; /* Datatypes */ #[derive(serde::Deserialize, serde::Serialize, Default, Clone)] pub struct NamedLocation { iso_code: Option, name: Option, geoname_id: Option, } #[derive(serde::Deserialize, serde::Serialize, Default, Copy, Clone)] pub struct LocationCoordinates { lat: f64, lon: f64, } #[derive(serde::Deserialize, serde::Serialize, Default, Clone)] pub struct LocationResult { continent: Option, country: Option, registered_country: Option, represented_country: Option, subdivisions: Option>, city: Option, coordinates: Option, accuracy: Option, postal_code: Option, time_zone: Option, } #[derive(serde::Deserialize, serde::Serialize, Default, Clone)] pub struct AsnResult { asn: Option, name: Option, } pub struct MMDBCarrier { pub mmdb: RwLock>>>, pub name: String, pub path: Option, } pub trait QueryLocation { fn query_location_for_ip(&self, address: &IpAddr, laguages: &Vec<&String>) -> Option; } pub trait QueryAsn { fn query_asn_for_ip(&self, address: &IpAddr) -> Option; } /* Converters */ pub fn extract_localized_name( names: &Option>, languages: &Vec<&String>) -> Option { match names { Some(names) => { for language in languages { if let Some(name) = names.get(language.as_str()){ return Some(name.to_string()) } } return None }, None => None } } pub fn geoip2_city_to_named_location(item: geoip2::city::City, languages: &Vec<&String>) -> 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: &Vec<&String>) -> 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: &Vec<&String>) -> 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: &Vec<&String>) -> 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: &Vec<&String>) -> 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 { fn query_asn_for_ip(&self, address: &IpAddr) -> Option { let mmdb = self.mmdb.read(); match &*mmdb { Some(mmdb) => { match mmdb.lookup::(*address) { Ok(res) => { Some(AsnResult { asn: res.autonomous_system_number, name: res.autonomous_system_organization.map(ToString::to_string), }) }, Err(e) => { println!("Error while looking up ASN for {address}: {e}"); Default::default() } } }, None => None, } } } impl QueryLocation for MMDBCarrier { fn query_location_for_ip(&self, address: &IpAddr, languages: &Vec<&String>) -> Option { let mmdb = self.mmdb.read(); match &*mmdb { Some(mmdb) => { match mmdb.lookup::(*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) => { 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(e) => { println!("Error while looking up ASN for {address}: {e}"); Default::default() } } }, None => None, } } } impl MMDBCarrier { pub fn new(name: String, path: Option) -> 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(); println!("Loading {} from '{}' ...", &self.name, path.display()); match maxminddb::Reader::open_readfile(path) { Ok(reader) => { let wording = if mmdb.is_some() { "Replaced old" } else { "Loaded new" }; *mmdb = Some(reader); println!("{} {} with new one.", wording, &self.name); Ok(()) }, Err(e) => { println!("Error while reading {}: {}", &self.name, &e); if mmdb.is_some() { println!("Not replacing old database."); } Err(e) }, } } }