pub use time::UtcOffset;
use crate::abi::{self, FastlyStatus};
use crate::error::BufferSizeError;
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
pub fn geo_lookup(ip: IpAddr) -> Option<Geo> {
geo_lookup_raw(ip).map(Geo::from_raw)
}
fn geo_lookup_raw(ip: IpAddr) -> Option<RawGeo> {
use std::net::IpAddr::{V4, V6};
let ipv4_bytes;
let ipv6_bytes;
let addr_bytes = match ip {
V4(ip) => {
ipv4_bytes = ip.octets();
&ipv4_bytes[..]
}
V6(ip) => {
ipv6_bytes = ip.octets();
&ipv6_bytes[..]
}
};
const INITIAL_GEO_BUF_SIZE: usize = 1024;
let result = match geo_lookup_impl(addr_bytes, INITIAL_GEO_BUF_SIZE) {
Ok(g) => g,
Err(BufferSizeError {
needed_buf_size, ..
}) => geo_lookup_impl(addr_bytes, needed_buf_size).ok()?,
};
result.and_then(|geo_bytes| serde_json::from_slice::<'_, RawGeo>(&geo_bytes).ok())
}
pub(crate) fn geo_lookup_impl(
addr_bytes: &[u8],
max_length: usize,
) -> Result<Option<Vec<u8>>, BufferSizeError> {
let mut buf = Vec::with_capacity(max_length);
let mut nwritten: usize = 0;
let status = unsafe {
abi::fastly_geo::lookup(
addr_bytes.as_ptr(),
addr_bytes.len(),
buf.as_mut_ptr(),
buf.capacity(),
&mut nwritten,
)
};
match status.result() {
Ok(_) => {
assert!(
nwritten <= buf.capacity(),
"fastly_geo::lookup wrote too many bytes"
);
unsafe {
buf.set_len(nwritten);
}
Ok(Some(buf))
}
Err(FastlyStatus::BUFLEN) => Err(BufferSizeError::geo(max_length, nwritten)),
Err(_) => Ok(None),
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct RawGeo {
as_name: String,
as_number: u32,
area_code: u16,
city: String,
#[serde(deserialize_with = "deserialize_conn_speed")]
conn_speed: ConnSpeed,
#[serde(deserialize_with = "deserialize_conn_type")]
conn_type: ConnType,
#[serde(deserialize_with = "deserialize_continent")]
continent: Continent,
country_code: String,
country_code3: String,
country_name: String,
latitude: f64,
longitude: f64,
metro_code: i64,
postal_code: String,
#[serde(deserialize_with = "deserialize_proxy_description")]
proxy_description: ProxyDescription,
#[serde(deserialize_with = "deserialize_proxy_type")]
proxy_type: ProxyType,
region: Option<String>,
utc_offset: i32,
}
#[derive(Clone, Debug)]
pub struct Geo {
as_name: String,
as_number: u32,
area_code: u16,
city: String,
conn_speed: ConnSpeed,
conn_type: ConnType,
continent: Continent,
country_code: String,
country_code3: String,
country_name: String,
latitude: f64,
longitude: f64,
metro_code: i64,
postal_code: String,
proxy_description: ProxyDescription,
proxy_type: ProxyType,
region: Option<String>,
utc_offset: Option<UtcOffset>,
}
impl Geo {
fn from_raw(raw: RawGeo) -> Self {
let utc_offset = if raw.utc_offset != 9999 {
let hours = (raw.utc_offset as u16 / 100) as i8;
let minutes = (raw.utc_offset as u16 % 100) as i8;
UtcOffset::from_hms(hours, minutes, 0).ok()
} else {
None
};
Geo {
as_name: raw.as_name,
as_number: raw.as_number,
area_code: raw.area_code,
city: raw.city,
conn_speed: raw.conn_speed,
conn_type: raw.conn_type,
continent: raw.continent,
country_code: raw.country_code,
country_code3: raw.country_code3,
country_name: raw.country_name,
latitude: raw.latitude,
longitude: raw.longitude,
metro_code: raw.metro_code,
postal_code: raw.postal_code,
proxy_description: raw.proxy_description,
proxy_type: raw.proxy_type,
region: raw.region,
utc_offset,
}
}
pub fn as_name(&self) -> &str {
self.as_name.as_str()
}
pub fn as_number(&self) -> u32 {
self.as_number
}
pub fn area_code(&self) -> u16 {
self.area_code
}
pub fn city(&self) -> &str {
self.city.as_str()
}
pub fn conn_speed(&self) -> ConnSpeed {
self.conn_speed.clone()
}
pub fn conn_type(&self) -> ConnType {
self.conn_type.clone()
}
pub fn continent(&self) -> Continent {
self.continent.clone()
}
pub fn country_code(&self) -> &str {
self.country_code.as_str()
}
pub fn country_code3(&self) -> &str {
self.country_code3.as_str()
}
pub fn country_name(&self) -> &str {
self.country_name.as_str()
}
pub fn latitude(&self) -> f64 {
self.latitude
}
pub fn longitude(&self) -> f64 {
self.longitude
}
pub fn metro_code(&self) -> i64 {
self.metro_code
}
pub fn postal_code(&self) -> &str {
self.postal_code.as_str()
}
pub fn proxy_description(&self) -> ProxyDescription {
self.proxy_description.clone()
}
pub fn proxy_type(&self) -> ProxyType {
self.proxy_type.clone()
}
pub fn region(&self) -> Option<&str> {
self.region.as_deref()
}
pub fn utc_offset(&self) -> Option<UtcOffset> {
self.utc_offset
}
}
#[allow(missing_docs)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum ConnSpeed {
Broadband,
Cable,
Dialup,
Mobile,
Oc12,
Oc3,
Satellite,
T1,
T3,
#[serde(rename = "ultrabb")]
UltraBroadband,
Wireless,
Xdsl,
Other(String),
}
#[allow(missing_docs)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum ConnType {
Wired,
Wifi,
Mobile,
Dialup,
Satellite,
#[serde(rename = "?")]
Unknown,
Other(String),
}
#[allow(missing_docs)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub enum Continent {
#[serde(rename = "AF")]
Africa,
#[serde(rename = "AN")]
Antarctica,
#[serde(rename = "AS")]
Asia,
#[serde(rename = "EU")]
Europe,
#[serde(rename = "NA")]
NorthAmerica,
#[serde(rename = "OC")]
Oceania,
#[serde(rename = "SA")]
SouthAmerica,
Other(String),
}
impl Continent {
pub fn as_code(&self) -> &'static str {
match self {
Self::Africa => "AF",
Self::Antarctica => "AN",
Self::Asia => "AS",
Self::Europe => "EU",
Self::NorthAmerica => "NA",
Self::Oceania => "OC",
Self::SouthAmerica => "SA",
Self::Other(_) => "??",
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum ProxyDescription {
Cloud,
CloudSecurity,
Dns,
TorExit,
TorRelay,
Vpn,
WebBrowser,
#[serde(rename = "?")]
Unknown,
Other(String),
}
#[allow(missing_docs)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum ProxyType {
Anonymous,
Aol,
Blackberry,
Corporate,
Edu,
Hosting,
Public,
Transparent,
#[serde(rename = "?")]
Unknown,
Other(String),
}
use serde::Deserializer;
fn deserialize_conn_speed<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<ConnSpeed, D::Error> {
deserialize_with_unknown(deserializer, ConnSpeed::Other)
}
fn deserialize_conn_type<'de, D: Deserializer<'de>>(deserializer: D) -> Result<ConnType, D::Error> {
deserialize_with_unknown(deserializer, ConnType::Other)
}
fn deserialize_continent<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Continent, D::Error> {
deserialize_with_unknown(deserializer, Continent::Other)
}
fn deserialize_proxy_description<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<ProxyDescription, D::Error> {
deserialize_with_unknown(deserializer, ProxyDescription::Other)
}
fn deserialize_proxy_type<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<ProxyType, D::Error> {
deserialize_with_unknown(deserializer, ProxyType::Other)
}
fn deserialize_with_unknown<'de, D: Deserializer<'de>, T: Deserialize<'de>>(
deserializer: D,
catchall: fn(String) -> T,
) -> Result<T, D::Error> {
let s: String = String::deserialize(deserializer)?;
use serde::de::value::StringDeserializer;
use serde::de::IntoDeserializer;
let deserializer: StringDeserializer<D::Error> = s.clone().into_deserializer();
Ok(T::deserialize(deserializer).unwrap_or_else(|_| catchall(s)))
}
#[test]
fn deserialize_partial_geo_responses() {
let invalid_utc_offset = br#"
{
"as_name": "test as",
"as_number": 11111,
"area_code": 0,
"city": "?",
"conn_speed": "broadband",
"conn_type": "wired",
"continent": "OS",
"country_code": "EA",
"country_code3": "EAR",
"country_name": "the entire earth",
"latitude": 6.5,
"longitude": -28.8,
"metro_code": 0,
"postal_code": "?",
"proxy_description": "?",
"proxy_type": "hosting",
"region": "?",
"utc_offset": 9999
}
"#;
let deserialized = serde_json::from_slice::<'_, RawGeo>(invalid_utc_offset)
.ok()
.map(Geo::from_raw);
assert!(deserialized.is_some());
assert!(deserialized.unwrap().utc_offset.is_none());
let invalid_variant = br#"
{
"as_name": "test as",
"as_number": 11111,
"area_code": 0,
"city": "?",
"conn_speed": "super_broadband",
"conn_type": "wired",
"continent": "OS",
"country_code": "EA",
"country_code3": "EAR",
"country_name": "the entire earth",
"latitude": 6.5,
"longitude": -28.8,
"metro_code": 0,
"postal_code": "?",
"proxy_description": "?",
"proxy_type": "hosting",
"region": "?",
"utc_offset": 200
}
"#;
let deserialized = serde_json::from_slice::<'_, RawGeo>(invalid_variant)
.ok()
.map(Geo::from_raw);
assert!(deserialized.is_some());
assert_eq!(
deserialized.unwrap().conn_speed(),
ConnSpeed::Other("super_broadband".to_string())
);
}