/* * This module provides an abstraction over the maxmind geoip databases * that provides the results ready for templating. */ use maxminddb; use maxminddb::geoip2; use std::collections::BTreeMap; use std::net::IpAddr; use std::path::Path; /* Datatypes */ #[derive(serde::Deserialize, serde::Serialize, Default)] pub struct NamedLocation { iso_code: Option, name: Option, geoname_id: Option, } #[derive(serde::Deserialize, serde::Serialize, Default)] pub struct LocationCoordinates { latitude: f64, logtitude: f64, } #[derive(serde::Deserialize, serde::Serialize, Default)] pub struct LocationResult { continent: Option, country: Option, registered_country: Option, represented_country: Option, subdivisions: Option>, city: Option, postal_code: Option, time_zone: Option, } #[derive(serde::Deserialize, serde::Serialize, Default)] pub struct AsnResult { asn: Option, name: Option, } pub struct MMDBCarrier { pub mmdb: Option>>, pub name: String, } pub trait QueryLocation { fn query_location_for_ip(&self, address: IpAddr, laguages: &Vec) -> Option; } pub trait QueryAsn { fn query_asn_for_ip(&self, address: IpAddr) -> Option; } /* Converters */ pub fn extract_localized_name( names: &Option>, languages: &Vec) -> 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::model::City, languages: &Vec) -> 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::model::Continent, languages: &Vec) -> 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::model::Country, languages: &Vec) -> 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::model::RepresentedCountry, languages: &Vec) -> 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::model::Subdivision, languages: &Vec) -> 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 { match &self.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) -> Option { match &self.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, }, 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 load_database_from_path(&mut self, path: &Path) -> Result<(),maxminddb::MaxMindDBError> { println!("Loading {} from '{}' ...", &self.name, path.display()); match maxminddb::Reader::open_readfile(path) { Ok(reader) => { let wording = if self.mmdb.is_some() { "Replaced old" } else { "Loaded new" }; self.mmdb = Some(reader); println!("{} {} with new one.", wording, &self.name); Ok(()) }, Err(e) => { println!("Error while reading {}: {}", &self.name, &e); if self.mmdb.is_some() { println!("Not replacing old database."); } Err(e) }, } } }