diff --git a/explorer-api/src/geo_ip/GeoLite2-Country.mmdb b/explorer-api/src/geo_ip/GeoLite2-Country.mmdb new file mode 100644 index 0000000000..ef8c639a46 Binary files /dev/null and b/explorer-api/src/geo_ip/GeoLite2-Country.mmdb differ diff --git a/explorer-api/src/geo_ip/geo_ip.rs b/explorer-api/src/geo_ip/geo_ip.rs new file mode 100644 index 0000000000..1212961b5a --- /dev/null +++ b/explorer-api/src/geo_ip/geo_ip.rs @@ -0,0 +1,105 @@ +use isocountry::CountryCode; +use log::warn; +use maxminddb::{geoip2::Country, MaxMindDBError, Reader}; +use std::{net::IpAddr, str::FromStr, sync::Arc}; + +const GEOIP_DB_PATH: &str = "./src/geo_ip/GeoLite2-Country.mmdb"; + +#[derive(Debug)] +pub enum GeoIpError { + NoValidIP, + InternalError, +} + +// The current State implementation does not allow to fail on state +// creation, ie. returning Result<>. To avoid to use unwrap family, +// as a workaround, wrap the state inside an Option<> +// If Reader::open_readfile fails for some reason db will will be set to None +// and an error will be logged. +pub(crate) struct GeoIp { + pub(crate) db: Option>>, +} + +#[derive(Clone)] +pub(crate) struct ThreadsafeGeoIp(pub Arc); + +pub(crate) struct Location { + /// two-letter country code (ISO 3166-1 alpha-2) + pub(crate) iso_alpha2: String, + /// three-letter country code (ISO 3166-1 alpha-3) + pub(crate) iso_alpha3: String, + /// English country short name (ISO 3166-1) + pub(crate) name: String, +} + +impl GeoIp { + pub fn new() -> Self { + let reader = Reader::open_readfile(GEOIP_DB_PATH) + .map_err(|e| { + error!( + "Fail to open GeoLite2 database file {}: {}", + GEOIP_DB_PATH, e + ); + }) + .ok(); + GeoIp { db: reader } + } + + pub fn query(&self, address: &str) -> Result, GeoIpError> { + let ip: IpAddr = FromStr::from_str(address).map_err(|e| { + error!("Fail to create IpAddr from {}: {}", &address, e); + GeoIpError::NoValidIP + })?; + let result = self + .db + .as_ref() + .ok_or_else(|| { + error!("No registered GeoIP database"); + GeoIpError::InternalError + })? + .lookup::(ip); + match &result { + Ok(v) => Ok(Some( + Location::try_from(v).map_err(|_| GeoIpError::InternalError)?, + )), + Err(e) => match e { + MaxMindDBError::AddressNotFoundError(_) => Ok(None), + _ => Err(GeoIpError::InternalError), + }, + } + } +} + +impl<'a> TryFrom<&Country<'a>> for Location { + type Error = String; + + fn try_from(country: &Country) -> Result { + let data = country.country.as_ref().ok_or_else(|| { + warn!("No Country data found"); + "No Country data found" + })?; + let iso_alpha2 = String::from(data.iso_code.ok_or_else(|| { + warn!("No iso alpha-2 code found in Country data {:#?}", data); + "No iso alpha-2 code found in Country data" + })?); + let iso_codes = CountryCode::for_alpha2(&iso_alpha2).map_err(|e| { + let message = format!( + "Fail to get iso codes from iso alpha-2 country code {}: {}", + &iso_alpha2, e + ); + warn!("{}", &message); + message + })?; + Ok(Location { + iso_alpha2, + iso_alpha3: String::from(iso_codes.alpha3()), + name: String::from(iso_codes.name()), + }) + } +} + +impl ThreadsafeGeoIp { + pub fn new() -> Self { + ThreadsafeGeoIp(Arc::new(GeoIp::new())) + } +} diff --git a/explorer-api/src/main.rs b/explorer-api/src/main.rs index 47dddf9f54..20f341bbe2 100644 --- a/explorer-api/src/main.rs +++ b/explorer-api/src/main.rs @@ -27,6 +27,9 @@ mod ping; mod state; mod tasks; mod validators; +mod buy_terms; +mod geo_ip; +mod guards; const COUNTRY_DATA_REFRESH_INTERVAL: u64 = 60 * 15; // every 15 minutes diff --git a/explorer-api/src/mix_nodes/utils.rs b/explorer-api/src/mix_nodes/utils.rs deleted file mode 100644 index a151ec318a..0000000000 --- a/explorer-api/src/mix_nodes/utils.rs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2022 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::mix_nodes::location::GeoLocation; -use isocountry::CountryCode; - -pub(crate) fn map_2_letter_to_3_letter_country_code(geo: &GeoLocation) -> String { - match CountryCode::for_alpha2(&geo.country_code) { - Ok(three_letter_country_code) => three_letter_country_code.alpha3().to_string(), - Err(_e) => { - warn!( - "❌ Oh no! map_2_letter_to_3_letter_country_code failed for '{:#?}'", - geo - ); - "???".to_string() - } - } -}