fastly 0.2.0-alpha4

Fastly Compute@Edge API
Documentation
//! Backend server.

use crate::body::Body;
use crate::request::RequestExt;
use crate::Error;
use anyhow::anyhow;
use http::{Request, Response};
use lazy_static::lazy_static;
use regex::Regex;
use std::str::FromStr;

pub(crate) mod error_messages {
    //! Error messages thrown by [`validate_backend`] when attempting to create backend objects, or
    //! sending requests to backends, using invalid strings.
    //!
    //! [`validate_backend`]: fn.validate_backend.html

    /// Error message used when a backend string is not valid because it is empty.
    pub(crate) const EMPTY_BACKEND_ERROR_MSG: &str = "an empty string is not a valid backend";

    /// Error message used when a backend string is not valid because it contains invalid
    /// characters. This includes Unicode characters, and control characters like `'\n'`.
    pub(crate) const INVALID_CHARS_ERROR_MSG: &str =
        "valid backend strings only contain ASCII characters";
}

/// A backend server.
///
/// This represents a backend associated with a service that we can send requests to, caching the
/// responses received.
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Backend(String);

impl Backend {
    /// Create a new backend object, given a string.
    ///
    /// This uses [`Backend`]'s implementation of the `std::string::FromStr` trait, so strings can also be
    /// parsed directly instead of calling this method. So either of the following examples will
    /// create a new [`Backend`].
    ///
    /// ```
    /// // Create `Backend`s using either of the following:
    /// use fastly::backend::Backend;
    /// let a = Backend::new("exampleOrigin").unwrap();
    /// let b = "exampleOrigin".parse::<Backend>().unwrap();
    /// assert_eq!(a, b);
    /// ```
    ///
    /// Backend strings cannot be empty, and must only contain ASCII characters, without any
    /// unprintable characters such as `'\n'`. See [`validate_backend`] for more information.
    ///
    /// [`Backend`]: struct.Backend.html
    /// [`validate_backend`]: fn.validate_backend.html
    pub fn new(s: &str) -> Result<Self, <Self as FromStr>::Err> {
        s.parse()
    }

    /// Send a [`http::Request`] to this [`Backend`].
    ///
    /// [`Backend`]: struct.Backend.html
    pub fn send(&self, req: Request<Body>) -> Result<Response<Body>, Error> {
        req.send(self.0.as_str())
    }
}

impl FromStr for Backend {
    type Err = Error;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        validate_backend(s)?;
        Ok(Self(s.into()))
    }
}

/// Validate that a string looks like an acceptable [`Backend`] value.
///
/// Note that this is *not* meant to be a filter for things that could cause security issues, it is
/// only meant to catch errors before the hostcalls do in order to yield friendlier error messages.
///
/// Backend strings cannot be empty, and must only contain alphanumeric characters or the
/// underscore `'_'`. Backend strings cannot begin with an underscore or number. Backend strings
/// cannot contain unprintable characters such as `'\n'`.
///
/// ```
/// use fastly::backend::{Backend, validate_backend};
/// let valid = validate_backend("valid_backend_1");
/// assert!(valid.is_ok());
/// let invalid = validate_backend("invalid-backend\n");
/// assert!(invalid.is_err());
/// ```
///
/// [`Backend`]: struct.Backend.html
// TODO KTM 2020-03-10: We should not allow VCL keywords like `if`, `now`, `true`, `urldecode`, and
// so on. Once we have better errors, let's make sure that these are caught in a pleasant manner.
pub fn validate_backend(backend: &str) -> Result<(), <Backend as FromStr>::Err> {
    lazy_static! {
        static ref VALID_BACKEND_RE: Regex = Regex::new("^[a-zA-Z][a-zA-Z0-9_]*$").unwrap();
    }
    match backend {
        b if b.is_empty() => Err(anyhow!(error_messages::EMPTY_BACKEND_ERROR_MSG)),
        b if !VALID_BACKEND_RE.is_match(b) => Err(anyhow!(error_messages::INVALID_CHARS_ERROR_MSG)),
        _ => Ok(()),
    }
}

#[cfg(test)]
mod validate_backend_tests {
    use crate::backend::{error_messages::*, *};

    #[test]
    fn valid_backend_name_is_accepted() -> Result<(), Error> {
        validate_backend("valid_backend_1")?;
        Ok(())
    }

    #[test]
    fn empty_str_is_not_accepted() -> Result<(), Error> {
        let invalid_backend = "";
        match validate_backend(invalid_backend).map_err(|e| e.to_string()) {
            Err(msg) if msg == EMPTY_BACKEND_ERROR_MSG => Ok(()),
            x => panic!("unexpected result: {:?}", x),
        }
    }

    #[test]
    fn unicode_is_not_accepted() -> Result<(), Error> {
        let invalid_backend = "";
        match validate_backend(invalid_backend).map_err(|e| e.to_string()) {
            Err(msg) if msg == INVALID_CHARS_ERROR_MSG => Ok(()),
            x => panic!("unexpected result: {:?}", x),
        }
    }

    #[test]
    fn unprintable_characters_are_not_accepted() -> Result<(), Error> {
        let invalid_backend = "\n";
        match validate_backend(invalid_backend).map_err(|e| e.to_string()) {
            Err(msg) if msg == INVALID_CHARS_ERROR_MSG => Ok(()),
            x => panic!("unexpected result: {:?}", x),
        }
    }

    #[test]
    fn dashes_are_not_accepted() -> Result<(), Error> {
        let invalid_backend = "kebab-case";
        match validate_backend(invalid_backend).map_err(|e| e.to_string()) {
            Err(msg) if msg == INVALID_CHARS_ERROR_MSG => Ok(()),
            x => panic!("unexpected result: {:?}", x),
        }
    }

    #[test]
    fn leading_integers_are_not_accepted() -> Result<(), Error> {
        let invalid_backend = "1_invalid_backend";
        match validate_backend(invalid_backend).map_err(|e| e.to_string()) {
            Err(msg) if msg == INVALID_CHARS_ERROR_MSG => Ok(()),
            x => panic!("unexpected result: {:?}", x),
        }
    }
}