use crate::{
error::{GeoError, GeoResult},
geoip::{Asn, Code, GeoLocation, IpGeoData, Locale, Location, Provider, ProviderDataLicense},
};
use lat_long::{Coordinate, Latitude, Longitude};
use rfham_core::error::CoreError;
use serde::{Deserialize, Serialize};
use std::{fmt::Debug, net::IpAddr};
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct GeoIpLookup();
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct NoOp();
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
struct GeoIpResponse {
ip: IpAddr,
isp: String,
org: String,
hostname: String,
latitude: Latitude,
longitude: Longitude,
postal_code: String,
city: String,
country_code: String,
country_name: String,
continent_code: String,
continent_name: String,
region: String,
district: String,
timezone_name: String,
connection_type: String,
asn_number: u64,
asn_org: String,
asn: String,
currency_name: String,
currency_code: String,
language_code: String,
language_name: String,
success: bool,
premium: bool,
}
const GEOIP_LOOKUP_URL: &str = "https://json.geoiplookup.io/";
impl Provider for GeoIpLookup {
fn lookup(&self, address: &std::net::IpAddr) -> GeoResult<Option<IpGeoData>> {
let response = reqwest::blocking::get(format!("{GEOIP_LOOKUP_URL}{address}"))?;
if response.status().is_success() {
let body = response.text()?;
let parsed: GeoIpResponse = serde_json::from_str(&body)?;
if parsed.success {
Ok(Some(parsed.try_into()?))
} else {
Ok(None)
}
} else {
Err(GeoError::Http(response.status()))
}
}
fn license(&self) -> ProviderDataLicense {
ProviderDataLicense::Public
}
}
impl TryFrom<GeoIpResponse> for IpGeoData {
type Error = CoreError;
fn try_from(response: GeoIpResponse) -> Result<Self, Self::Error> {
let location = Location::new(
Code::new(response.continent_code.parse()?, response.continent_name),
Code::new(response.country_code.parse()?, response.country_name),
);
let location = if !response.region.is_empty() {
location.with_region(response.region)
} else {
location
};
let location = if !response.city.is_empty() {
location.with_city(response.city)
} else {
location
};
let location = if !response.district.is_empty() {
location.with_district(response.district)
} else {
location
};
let location = if !response.postal_code.is_empty() {
location.with_postal_code(response.postal_code)
} else {
location
};
let location = location.with_location(GeoLocation::new(Coordinate::new(
response.latitude,
response.longitude,
)));
let data = IpGeoData::new(response.ip, location);
let data = if !response.timezone_name.is_empty()
|| !response.currency_code.is_empty()
|| !response.language_code.is_empty()
{
let locale = Locale::default();
let locale = if !response.timezone_name.is_empty() {
locale.with_timezone(response.timezone_name)
} else {
locale
};
let locale = if !response.currency_code.is_empty() {
locale.with_currency(Code::new(
response.currency_code.parse()?,
response.currency_name,
))
} else {
locale
};
let locale = if !response.language_code.is_empty() {
locale.with_language(Code::new(
response.language_code.parse()?,
response.language_name,
))
} else {
locale
};
data.with_locale(locale)
} else {
data
};
let data = if response.asn_number != 0 {
data.with_asn(Asn::new(
response.asn_number,
response.asn,
response.asn_org,
))
} else {
data
};
Ok(data)
}
}
impl Provider for NoOp {
fn lookup(&self, _: &std::net::IpAddr) -> GeoResult<Option<IpGeoData>> {
Ok(None)
}
fn license(&self) -> ProviderDataLicense {
ProviderDataLicense::Public
}
}
pub mod local;
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_parse_geoip_response() {
const RESPONSE: &str = r##"{
"ip": "23.64.167.34",
"isp": "Akamai Technologies, Inc.",
"org": "Akamai Technologies, Inc.",
"hostname": "",
"latitude": 32.814,
"longitude": -96.9489,
"postal_code": "",
"city": "Irving",
"country_code": "US",
"country_name": "United States",
"continent_code": "NA",
"continent_name": "North America",
"region": "Texas",
"district": "",
"timezone_name": "America/Chicago",
"connection_type": "Corporate",
"asn_number": 16625,
"asn_org": "Akamai Technologies, Inc.",
"asn": "AS16625 - Akamai Technologies, Inc.",
"currency_code": "USD",
"currency_name": "United States Dollar",
"language_code": "en",
"language_name": "English",
"success": true,
"premium": false
}"##;
let parsed: Result<GeoIpResponse, serde_json::Error> = serde_json::from_str(RESPONSE);
assert!(parsed.is_ok());
let parsed = parsed.unwrap();
assert_eq!("23.64.167.34".to_string(), parsed.ip.to_string());
}
#[test]
fn test_geoip_response_to_data() {
const RESPONSE: &str = r##"{
"ip": "23.64.167.34",
"isp": "Akamai Technologies, Inc.",
"org": "Akamai Technologies, Inc.",
"hostname": "",
"latitude": 32.814,
"longitude": -96.9489,
"postal_code": "",
"city": "Irving",
"country_code": "US",
"country_name": "United States",
"continent_code": "NA",
"continent_name": "North America",
"region": "Texas",
"district": "",
"timezone_name": "America/Chicago",
"connection_type": "Corporate",
"asn_number": 16625,
"asn_org": "Akamai Technologies, Inc.",
"asn": "AS16625 - Akamai Technologies, Inc.",
"currency_code": "USD",
"currency_name": "United States Dollar",
"language_code": "en",
"language_name": "English",
"success": true,
"premium": false
}"##;
let parsed: GeoIpResponse = serde_json::from_str(RESPONSE).unwrap();
let data: IpGeoData = parsed.try_into().unwrap();
assert_eq!("NA", data.location().continent().code().to_string());
assert_eq!("US", data.location().country().code().to_string());
assert_eq!(16625, data.asn().unwrap().number());
}
#[test]
fn test_noop_provider() {
let provider = NoOp::default();
let ip_address: IpAddr = "23.64.167.34".parse().unwrap();
assert_eq!(None, provider.lookup(&ip_address).unwrap())
}
}