fastly 0.7.3

Fastly Compute@Edge API
Documentation
//! Geographic data for IP addresses.

pub use chrono::FixedOffset;

use crate::abi::{self, FastlyStatus};
use crate::error::BufferSizeError;
use crate::limits;
use serde::{Deserialize, Serialize};
use std::net::IpAddr;

/// Look up the geographic data associated with a particular IP address.
///
/// Returns `None` if no geographic data is available, such as when the IP address is reserved for
/// private use.
///
/// # Examples
///
/// To get geographic information for the downstream client:
///
/// ```no_run
/// let client_ip = fastly::Request::from_client().get_client_ip_addr().unwrap();
/// let geo = fastly::geo::geo_lookup(client_ip).unwrap();
/// if let fastly::geo::ConnType::Satellite = geo.conn_type() {
///     println!("receiving a request from outer space 🛸");
/// }
/// ```
pub fn geo_lookup(ip: IpAddr) -> Option<Geo> {
    use std::net::IpAddr::{V4, V6};
    let (addr_bytes, addr_len) = match ip {
        V4(ip) => (ip.octets().to_vec(), 4),
        V6(ip) => (ip.octets().to_vec(), 16),
    };

    let result = match geo_lookup_impl(&addr_bytes, addr_len, limits::INITIAL_GEO_BUF_SIZE) {
        Ok(g) => g,
        Err(BufferSizeError {
            needed_buf_size, ..
        }) => geo_lookup_impl(&addr_bytes, addr_len, needed_buf_size).ok()?,
    };

    // Try to parse any non-null response, returning `None` otherwise.
    result.and_then(|geo_bytes| {
        serde_json::from_slice::<'_, RawGeo>(&geo_bytes)
            .ok()
            .and_then(Geo::try_from_raw)
    })
}

pub(crate) fn geo_lookup_impl(
    addr_bytes: &[u8],
    addr_len: usize,
    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_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,
    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: i32,
}

/// The geographic data associated with a particular IP address.
// TODO ACF 2020-04-20: make a nicer type for the AS fields once the IANA licensing question is
// sorted out. https://www.iana.org/assignments/as-numbers/as-numbers.xhtml
//
// TODO ACF 2020-04-20: we should be able to represent the continent, country, region, etc much more
// nicely than this, however the licensing for ISO data appears to be fraught. The `locale-codes`
// crate looks very nice, but it sources its data from a CC BY-SA 4.0 repo despite being
// MIT-licensed, which in turn scrapes wikipedia and other sources. For now, just use strings.
#[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: FixedOffset,
}

impl Geo {
    fn try_from_raw(raw: RawGeo) -> Option<Self> {
        let hours = raw.utc_offset / 100;
        let minutes = raw.utc_offset % 100;
        let seconds = (hours * 60 * 60) + (minutes * 60);
        FixedOffset::east_opt(seconds).map(|utc_offset| 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,
        })
    }

    /// The name of the organization associated with `as_number`.
    ///
    /// For example, `fastly` is the value given for IP addresses under AS-54113.
    pub fn as_name(&self) -> &str {
        self.as_name.as_str()
    }

    /// [Autonomous system](https://en.wikipedia.org/wiki/Autonomous_system_(Internet)) (AS) number.
    pub fn as_number(&self) -> u32 {
        self.as_number
    }

    /// The telephone area code associated with an IP address.
    ///
    /// These are only available for IP addresses in the United States, its territories, and Canada.
    pub fn area_code(&self) -> u16 {
        self.area_code
    }

    /// City or town name.
    pub fn city(&self) -> &str {
        self.city.as_str()
    }

    /// Connection speed.
    pub fn conn_speed(&self) -> ConnSpeed {
        self.conn_speed
    }

    /// Connection type.
    pub fn conn_type(&self) -> ConnType {
        self.conn_type
    }

    /// Continent.
    pub fn continent(&self) -> Continent {
        self.continent
    }

    /// A two-character [ISO 3166-1][iso] country code for the country associated with an IP address.
    ///
    /// The US country code is returned for IP addresses associated with overseas United States military bases.
    ///
    /// These values include subdivisions that are assigned their own country codes in ISO
    /// 3166-1. For example, subdivisions NO-21 and NO-22 are presented with the country code SJ for
    /// Svalbard and the Jan Mayen Islands.
    ///
    /// [iso]: https://en.wikipedia.org/wiki/ISO_3166-1
    pub fn country_code(&self) -> &str {
        self.country_code.as_str()
    }

    /// A three-character [ISO 3166-1 alpha-3][iso] country code for the country associated with the IP address.
    ///
    /// The USA country code is returned for IP addresses associated with overseas United States
    /// military bases.
    ///
    /// [iso]: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3
    pub fn country_code3(&self) -> &str {
        self.country_code3.as_str()
    }

    /// Country name.
    ///
    /// This field is the [ISO 3166-1][iso] English short name for a country.
    ///
    /// [iso]: https://en.wikipedia.org/wiki/ISO_3166-1
    pub fn country_name(&self) -> &str {
        self.country_name.as_str()
    }

    /// Latitude, in units of degrees from the equator.
    ///
    /// Values range from -90.0 to +90.0 inclusive, and are based on the [WGS 84][wgs84] coordinate
    /// reference system.
    ///
    /// [wgs84]: https://en.wikipedia.org/wiki/World_Geodetic_System
    pub fn latitude(&self) -> f64 {
        self.latitude
    }

    /// Longitude, in units of degrees from the [IERS Reference Meridian][iers].
    ///
    /// Values range from -180.0 to +180.0 inclusive, and are based on the [WGS 84][wgs84]
    /// coordinate reference system.
    ///
    /// [iers]: https://en.wikipedia.org/wiki/IERS_Reference_Meridian
    /// [wgs84]: https://en.wikipedia.org/wiki/World_Geodetic_System
    pub fn longitude(&self) -> f64 {
        self.longitude
    }

    /// Metro code, representing designated market areas (DMAs) in the United States.
    pub fn metro_code(&self) -> i64 {
        self.metro_code
    }

    /// The postal code associated with the IP address.
    ///
    /// These are available for some IP addresses in Australia, Canada, France, Germany, Italy,
    /// Spain, Switzerland, the United Kingdom, and the United States.
    ///
    /// For Canadian postal codes, this is the first 3 characters. For the United Kingdom, this is
    /// the first 2-4 characters (outward code). For countries with alphanumeric postal codes, this
    /// field is a lowercase transliteration.
    pub fn postal_code(&self) -> &str {
        self.postal_code.as_str()
    }

    /// Client proxy description.
    pub fn proxy_description(&self) -> ProxyDescription {
        self.proxy_description
    }

    /// Client proxy type.
    pub fn proxy_type(&self) -> ProxyType {
        self.proxy_type
    }

    /// [ISO 3166-2][iso] country subdivision code.
    ///
    /// For countries with multiple levels of subdivision (for example, nations within the United
    /// Kingdom), this variable gives the more specific subdivision.
    ///
    /// This field can be `None` for countries that do not have ISO country subdivision codes. For
    /// example, `None` is given for IP addresses assigned to the Åland Islands (country code AX,
    /// illustrated below).
    ///
    /// # Examples
    ///
    /// Region values are the subdivision part only. For typical use, a subdivision is normally
    /// formatted with its associated country code. The following example illustrates constructing
    /// an [ISO 3166-2][iso] two-part country and subdivision code from the respective fields:
    ///
    /// ```no_run
    /// # let client_ip = fastly::Request::from_client().get_client_ip_addr().unwrap();
    /// # let geo = fastly::geo::geo_lookup(client_ip).unwrap();
    /// let code = if let Some(region) = geo.region() {
    ///     format!("{}-{}", geo.country_code(), region);
    /// } else {
    ///     format!("{}", geo.country_code());
    /// };
    /// ```
    ///
    /// | `code`     | Region Name       | Country            | ISO 3166-2 subdivision |
    /// | ---------- | ----------------- | ------------------ | ---------------------- |
    /// | `AX`       | Ödkarby           | Åland Islands      | (none)                 |
    /// | `DE-BE`    | Berlin	         | Germany            | Land (State)           |
    /// | `GB-BNH`   | Brighton and Hove | United Kingdom     | Unitary authority      |
    /// | `JP-13`    | 東京都 (Tōkyō-to)  | Japan              | Prefecture             |
    /// | `RU-MOW`   | Москва́ (Moscow)   | Russian Federation | Federal city           |
    /// | `SE-AB`    | Stockholms län    | Sweden             | Län (County)           |
    /// | `US-CA`    | California        | United States      | State                  |
    ///
    /// [iso]: https://en.wikipedia.org/wiki/ISO_3166-2
    pub fn region(&self) -> Option<&str> {
        self.region.as_ref().map(|s| s.as_str())
    }

    /// Time zone offset from coordinated universal time (UTC) for `city`.
    ///
    /// This is represented using the [`FixedOffset`] type from the [`chrono`] library. See
    /// `chrono`'s documentation for more details on how to format this type or use it in time
    /// calculations.
    pub fn utc_offset(&self) -> FixedOffset {
        self.utc_offset
    }
}

/// Connection speed.
///
/// These connection speeds imply different latencies, as well as throughput.
///
/// See [OC rates][oc] and [T-carrier][t] for background on OC- and T- connections.
///
/// [oc]: https://en.wikipedia.org/wiki/Optical_Carrier_transmission_rates
/// [t]: https://en.wikipedia.org/wiki/T-carrier
#[allow(missing_docs)]
#[serde(rename_all = "kebab-case")]
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub enum ConnSpeed {
    Broadband,
    Cable,
    Dialup,
    Mobile,
    Oc12,
    Oc3,
    T1,
    T3,
    Satellite,
    Wireless,
    Xdsl,
}

/// Connection type.
///
/// Defaults to `Unknown` when the connection type is not known.
#[allow(missing_docs)]
#[serde(rename_all = "kebab-case")]
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub enum ConnType {
    Wired,
    Wifi,
    Mobile,
    Dialup,
    Satellite,
    #[serde(other)]
    #[serde(rename = "?")]
    Unknown,
}

/// Continent.
#[allow(missing_docs)]
#[derive(Clone, Copy, 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,
}

impl Continent {
    /// Get the two-letter continent code.
    ///
    /// | Continent     | Code |
    /// | ------------- | ---- |
    /// | Africa        | `AF` |
    /// | Asia          | `AS` |
    /// | Europe        | `EU` |
    /// | North America | `NA` |
    /// | South America | `SA` |
    /// | Oceania       | `OC` |
    /// | Antarctica    | `AN` |
    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",
        }
    }
}

/// Client proxy description.
///
/// Defaults to `Unknown` when an IP address is not known to be a proxy or VPN.
#[serde(rename_all = "kebab-case")]
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub enum ProxyDescription {
    /// Enables ubiquitous network access to a shared pool of configurable computing resources.
    Cloud,
    /// A host accessing the internet via a web security and data protection cloud provider.
    ///
    /// Example providers with this type of service are Zscaler, Scansafe, and Onavo.
    CloudSecurity,
    /// A proxy used by overriding the client's DNS value for an endpoint host to that of the proxy
    /// instead of the actual DNS value.
    Dns,
    /// The gateway nodes where encrypted or anonymous Tor traffic hits the internet.
    TorExit,
    /// Receives traffic on the Tor network and passes it along; also referred to as "routers".
    TorRelay,
    /// Virtual private network that encrypts and routes all traffic through the VPN server,
    /// including programs and applications.
    Vpn,
    /// Connectivity that is taking place through mobile device web browser software that proxies
    /// the user through a centralized location.
    ///
    /// Examples of such browsers are Opera mobile browsers and UCBrowser.
    WebBrowser,
    /// An IP address that is not known to be a proxy or VPN.
    #[serde(other)]
    #[serde(rename = "?")]
    Unknown,
}

/// Client proxy type.
///
/// Defaults to `Unknown` when an IP address is not known to be a proxy or VPN.
// TODO ACF 2020-04-22: the docs on https://docs.fastly.com/vcl/variables/client-geo-proxy-type/
// look like they need a refresher, so I did not transcribe them for the individual variants.
#[allow(missing_docs)]
#[serde(rename_all = "kebab-case")]
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub enum ProxyType {
    Anonymous,
    Aol,
    Blackberry,
    Corporate,
    Edu,
    Hosting,
    Public,
    Transparent,
    #[serde(other)]
    #[serde(rename = "?")]
    Unknown,
}