fastly 0.12.0

Fastly Compute API
Documentation
//! Interface to the Fastly security products.
//!
//! The Fastly security products allow Compute services to [inspect] requests and make informed
//! decisions about how to block or redirect them.
//!
//! See the [Next-Gen WAF] documentation for more information.
//!
//! [Next-Gen WAF]: https://docs.fastly.com/en/ngwaf/
use std::net::IpAddr;
use std::str::FromStr;
use std::time::Duration;

use crate::abi::{self, FastlyStatus};
use crate::http::body::handle::BodyHandle;
use crate::http::request::handle::RequestHandle;
use crate::http::request::Request;
use crate::Response;
use lazy_static::lazy_static;
use serde::Deserialize;

lazy_static! {
    static ref NULL_BODY_HANDLE: BodyHandle = BodyHandle::new();
}

/// Errors that may occur when interacting with Security.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum InspectError {
    /// Failed to deserialize the JSON returned by Security.
    #[error("Failed to deserialize security response: {0}")]
    DeserializeError(serde_json::Error),

    /// The response to the inspection request contained invalid UTF-8.
    #[error("Response from Security was not valid UTF-8")]
    InvalidBytes(std::str::Utf8Error, Vec<u8>),

    /// [InspectConfig] contained an invalid configuration.
    #[error("Invalid Argument in Configuration")]
    InvalidConfig,

    /// Successfully sent the inspection request to Security, but failed to get back any verdict.
    #[error("Inspection request completed without any verdict")]
    NoVerdict,

    /// Failed to send an inspection request to the security module.
    #[error("Failed to send request to security module: {0:?}")]
    RequestError(FastlyStatus),

    /// BufferSizeError
    #[error("Buffer ({0} bytes) was not large enough to fit response")]
    BufferSizeError(usize),
}

/// The outcome of inspecting a [`Request`] with Security.
#[derive(Debug)]
pub enum InspectVerdict {
    /// Security indicated that this request is allowed.
    Allow,
    /// Security indicated that this request should be blocked.
    Block,
    /// Security indicated that this service is not authorized to inspect a request.
    Unauthorized,

    /// Security returned an unrecognized verdict.
    ///
    /// This variant exists to allow for the possibility of future additions, but should normally
    /// not be seen.
    Other(String),
}

impl FromStr for InspectVerdict {
    type Err = std::convert::Infallible;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        let verdict = match input {
            "allow" => InspectVerdict::Allow,
            "block" => InspectVerdict::Block,
            "unauthorized" => InspectVerdict::Unauthorized,
            _ => InspectVerdict::Other(input.to_string()),
        };

        Ok(verdict)
    }
}

impl<'de> Deserialize<'de> for InspectVerdict {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        Self::from_str(&s).map_err(serde::de::Error::custom)
    }
}

/// Results of asking Security to inspect a [`Request`].
#[derive(Deserialize, Debug)]
pub struct InspectResponse {
    #[serde(alias = "waf_response")]
    response: i16,
    redirect_url: String,
    tags: Vec<String>,
    verdict: InspectVerdict,
    decision_ms: u64,
}

impl InspectResponse {
    #[deprecated(note = "Please use InspectResponse::status instead")]
    #[doc(hidden)]
    pub fn waf_response(&self) -> i16 {
        self.response
    }
    /// Security status code.
    pub fn status(&self) -> i16 {
        self.response
    }

    /// A redirect URL returned from Security.
    pub fn redirect_url(&self) -> Option<&str> {
        if matches!(self.verdict, InspectVerdict::Block) && !self.redirect_url.is_empty() {
            Some(self.redirect_url.as_str())
        } else {
            None
        }
    }

    /// Tags returned by Security.
    pub fn tags(&self) -> Vec<&str> {
        self.tags.iter().map(String::as_str).collect()
    }

    /// Get Security's verdict on how to handle this request.
    pub fn verdict(&self) -> &InspectVerdict {
        &self.verdict
    }

    /// How long Security spent determining its verdict.
    pub fn decision_ms(&self) -> Duration {
        Duration::from_millis(self.decision_ms)
    }

    /// A redirect URI returned by Security.
    pub fn is_redirect(&self) -> bool {
        (300..=309).contains(&self.response) && self.redirect_url().is_some()
    }

    /// Convert a redirect URI returned by Security into a [Response].
    pub fn into_redirect(self) -> Option<Response> {
        let response = self.response as u16;
        self.redirect_url()
            .map(|url| Response::from_status(response).with_header(http::header::LOCATION, url))
    }
}

/// Configuration for inspecting a [`Request`] using Security.
pub struct InspectConfig<'a> {
    corp: Option<String>,
    workspace: Option<String>,
    req_handle: &'a RequestHandle,
    body_handle: &'a BodyHandle,
    buffer_size: usize,
    override_client_ip: Option<IpAddr>,
}

impl<'a> InspectConfig<'a> {
    /// Create a new configuration for inspecting a request with Security.
    #[deprecated(
        since = "0.11.7",
        note = "Use InspectConfig::from_handles or InspectConfig::from_request instead"
    )]
    pub fn new(req_handle: &'a RequestHandle, body_handle: &'a BodyHandle) -> Self {
        Self::from_handles(req_handle, body_handle)
    }

    /// Create a new configuration for inspecting an explicit pair of handles with Security.
    pub fn from_handles(req_handle: &'a RequestHandle, body_handle: &'a BodyHandle) -> Self {
        Self {
            corp: None,
            workspace: None,
            req_handle,
            body_handle,
            buffer_size: 16 * 1024, // Previously 2048, bumped up after some users got BUFLEN
            override_client_ip: None,
        }
    }

    /// Create a new configuration for inspecting the handles associated with a given [Request] with
    /// Security.
    pub fn from_request(req: &'a Request) -> Self {
        let (rh, bh) = req.get_handles();
        let bh = bh.unwrap_or(&NULL_BODY_HANDLE);
        Self::from_handles(rh, bh)
    }

    /// Specify an explicity client IP address to inspect.
    ///
    /// By default, [inspect] will use the IP address that made the request
    /// to the running Compute service, but you may want to use a different
    /// IP when service chaining or if requests are proxied from outside of
    /// Fastly's network.
    pub fn client_ip(mut self, ip: IpAddr) -> Self {
        self.override_client_ip = Some(ip);
        self
    }

    /// Set a corp name for the configuration.
    pub fn corp(mut self, name: impl ToString) -> Self {
        self.corp = name.to_string().into();
        self
    }

    /// Set a workspace name for the configuration.
    pub fn workspace(mut self, name: impl ToString) -> Self {
        self.workspace = name.to_string().into();
        self
    }

    /// Set a buffer size for the response.
    pub fn buffer_size(self, buffer_size: usize) -> Self {
        Self {
            buffer_size,
            ..self
        }
    }
}

/// Inspect a [`Request`] using the [Fastly Next-Gen WAF][NGWAF].
///
/// [NGWAF]: https://docs.fastly.com/en/ngwaf/
pub fn inspect(config: InspectConfig) -> Result<InspectResponse, InspectError> {
    use abi::fastly_http_req::{InspectInfo, InspectInfoMask};

    let mut add_info_mask = InspectInfoMask::empty();
    let mut add_info = InspectInfo::default();

    if let Some(corp) = config.corp.as_deref() {
        add_info.corp = corp.as_ptr();
        add_info.corp_len = corp.len() as u32;
        add_info_mask.insert(InspectInfoMask::CORP);
    }

    if let Some(workspace) = config.workspace.as_deref() {
        add_info.workspace = workspace.as_ptr();
        add_info.workspace_len = workspace.len() as u32;
        add_info_mask.insert(InspectInfoMask::WORKSPACE);
    }

    let ipv4_bytes;
    let ipv6_bytes;
    if let Some(ip) = config.override_client_ip {
        let bytes = match ip {
            IpAddr::V4(ip) => {
                ipv4_bytes = ip.octets();
                &ipv4_bytes[..]
            }
            IpAddr::V6(ip) => {
                ipv6_bytes = ip.octets();
                &ipv6_bytes[..]
            }
        };
        add_info.override_client_ip_ptr = bytes.as_ptr();
        add_info.override_client_ip_len = bytes.len() as u32;
        add_info_mask.insert(InspectInfoMask::OVERRIDE_CLIENT_IP);
    }

    let mut buf = vec![0u8; config.buffer_size];
    let mut nwritten = 0;

    let status = unsafe {
        abi::fastly_http_req::inspect(
            config.req_handle.as_u32(),
            config.body_handle.as_u32(),
            add_info_mask,
            &add_info,
            buf.as_mut_ptr(),
            buf.capacity(),
            &mut nwritten,
        )
    };

    match status {
        FastlyStatus::OK => {
            if nwritten == 0 {
                return Err(InspectError::NoVerdict);
            }

            unsafe {
                buf.set_len(nwritten);
            }
        }
        FastlyStatus::INVAL => return Err(InspectError::InvalidConfig),
        FastlyStatus::BUFLEN => return Err(InspectError::BufferSizeError(buf.capacity())),
        status => return Err(InspectError::RequestError(status)),
    }

    let s = match std::str::from_utf8(&buf) {
        Ok(s) => s,
        Err(e) => return Err(InspectError::InvalidBytes(e, buf)),
    };

    serde_json::from_str(s).map_err(InspectError::DeserializeError)
}