redirectionio 3.1.0

Redirection IO Library to handle matching rule, redirect and filtering headers and body.
Documentation
use std::{collections::BTreeMap, net::IpAddr, str::FromStr};

use chrono::{DateTime, Utc};
#[cfg(feature = "router")]
use http::Error;
use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
use serde::{Deserialize, Serialize};
use trusted_proxies::RequestInformation;
use url::form_urlencoded::parse as parse_query;

use super::{header::Header, query::PathAndQueryWithSkipped};
#[cfg(feature = "router")]
use crate::api::Example;
#[cfg(feature = "router")]
use crate::http::sanitize_url;
#[cfg(feature = "router")]
use crate::router_config::RouterConfig;

const QUERY_ENCODE_SET: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'#').add(b'<').add(b'>');

#[derive(Serialize, Deserialize, Debug, Clone, Hash)]
pub struct Request {
    #[serde(rename = "path_and_query")]
    pub path_and_query_skipped: PathAndQueryWithSkipped,
    #[serde(rename = "path_and_query_v2")]
    pub path_and_query: Option<String>,
    pub host: Option<String>,
    pub scheme: Option<String>,
    pub method: Option<String>,
    pub headers: Vec<Header>,
    pub remote_addr: Option<IpAddr>,
    pub created_at: Option<DateTime<Utc>>,
    pub sampling_override: Option<bool>,
}

impl FromStr for Request {
    type Err = http::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let http_request = http::Request::<()>::builder().uri(s).method("GET").body(())?;
        let path_and_query_str = match http_request.uri().path_and_query() {
            None => "",
            Some(path_and_query) => path_and_query.as_str(),
        };

        Ok(Request::new(
            PathAndQueryWithSkipped::from_static(path_and_query_str),
            path_and_query_str.to_string(),
            http_request.uri().authority().map(|s| s.to_string()),
            http_request.uri().scheme_str().map(|s| s.to_string()),
            None,
            None,
            None,
        ))
    }
}

impl Request {
    pub fn new(
        path_and_query_skipped: PathAndQueryWithSkipped,
        path_and_query: String,
        host: Option<String>,
        scheme: Option<String>,
        method: Option<String>,
        remote_addr: Option<IpAddr>,
        sampling_override: Option<bool>,
    ) -> Request {
        Request {
            path_and_query_skipped,
            path_and_query: Some(path_and_query),
            host,
            scheme,
            method,
            headers: Vec::new(),
            remote_addr,
            created_at: Some(Utc::now()),
            sampling_override,
        }
    }

    #[cfg(feature = "router")]
    pub fn from_config(
        config: &RouterConfig,
        path_and_query: String,
        host: Option<String>,
        scheme: Option<String>,
        method: Option<String>,
        remote_addr: Option<IpAddr>,
        sampling_override: Option<bool>,
    ) -> Request {
        Request {
            path_and_query_skipped: PathAndQueryWithSkipped::from_config(config, path_and_query.as_str()),
            path_and_query: Some(path_and_query),
            host: host.map(|s| if config.ignore_host_case { s.to_lowercase() } else { s }),
            scheme,
            method,
            remote_addr,
            headers: Vec::new(),
            created_at: Some(Utc::now()),
            sampling_override,
        }
    }

    #[cfg(feature = "router")]
    pub fn from_example(router_config: &RouterConfig, example: &Example) -> Result<Self, Error> {
        let method = example.method.as_deref().unwrap_or("GET");
        let url = sanitize_url(example.url.as_str());
        let http_request = http::Request::<()>::builder().uri(url.as_str()).method(method).body(())?;
        let path_and_query_str = match http_request.uri().path_and_query() {
            None => "",
            Some(path_and_query) => path_and_query.as_str(),
        };

        let mut request = Request::from_config(
            router_config,
            path_and_query_str.to_string(),
            http_request.uri().authority().map(|s| s.to_string()),
            http_request.uri().scheme_str().map(|s| s.to_string()),
            example.method.clone(),
            None,
            None,
        );

        if let Some(headers) = &example.headers {
            for header in headers {
                request.add_header(header.name.clone(), header.value.clone(), router_config.ignore_header_case);
            }
        }

        if let Some(ip) = &example.ip_address {
            request.remote_addr = Some(IpAddr::from_str(ip).unwrap());
        }

        if let Some(datetime) = &example.datetime {
            request.set_created_at(Some(datetime.to_string()));
        }

        Ok(request)
    }

    #[cfg(feature = "router")]
    pub fn rebuild_with_config(config: &RouterConfig, request: &Request) -> Self {
        let original_url = match &request.path_and_query {
            Some(str) => str.as_str(),
            None => request.path_and_query_skipped.original.as_str(),
        };

        let path_and_query_skipped = PathAndQueryWithSkipped::from_config(config, original_url);
        let mut headers = Vec::new();

        for header in &request.headers {
            headers.push(Header {
                name: header.name.clone(),
                value: if config.ignore_header_case {
                    header.value.to_lowercase()
                } else {
                    header.value.clone()
                },
            });
        }

        Request {
            path_and_query_skipped,
            path_and_query: Some(original_url.to_string()),
            host: match &request.host {
                Some(host) => {
                    if config.ignore_host_case {
                        Some(host.to_lowercase())
                    } else {
                        Some(host.clone())
                    }
                }
                None => None,
            },
            scheme: request.scheme.clone(),
            method: request.method.clone(),
            headers,
            remote_addr: request.remote_addr,
            created_at: request.created_at,
            sampling_override: request.sampling_override,
        }
    }

    pub fn add_header(&mut self, name: String, value: String, ignore_case: bool) {
        self.headers.push(Header {
            name,
            value: if ignore_case { value.to_lowercase() } else { value },
        });
    }

    pub fn set_created_at(&mut self, created_at: Option<String>) {
        match created_at {
            None => (),
            Some(created_at) => match created_at.parse::<DateTime<Utc>>() {
                Ok(dt) => self.created_at = Some(dt),
                Err(err) => {
                    tracing::error!("cannot parse datetime {created_at}: {err}");
                }
            },
        };
    }

    pub fn method(&self) -> &str {
        match &self.method {
            None => "GET",
            Some(method) => method.as_str(),
        }
    }

    pub fn host(&self) -> Option<&str> {
        match &self.host {
            None => None,
            Some(host_str) => Some(host_str.as_str()),
        }
    }

    pub fn scheme(&self) -> Option<&str> {
        match &self.scheme {
            None => None,
            Some(scheme_str) => Some(scheme_str.as_str()),
        }
    }

    pub fn header_exists(&self, name: &str) -> bool {
        let lowercase_name = name.to_lowercase();

        for header in &self.headers {
            if header.name.to_lowercase() == lowercase_name {
                return true;
            }
        }

        false
    }

    pub fn set_remote_ip(&mut self, remote_ip: IpAddr) {
        self.remote_addr = Some(remote_ip);
    }

    pub fn header_values(&self, name: &str) -> Vec<&str> {
        let mut values = Vec::new();
        let lowercase_name = name.to_lowercase();

        for header in &self.headers {
            if header.name.to_lowercase() == lowercase_name {
                values.push(header.value.as_str());
            }
        }

        values
    }

    pub fn header_value(&self, name: &str) -> Option<String> {
        let values = self.header_values(name);

        if values.is_empty() { None } else { Some(values.join(",")) }
    }

    pub fn path_and_query(&self) -> String {
        match &self.path_and_query_skipped.path_and_query_matching {
            None => self.path_and_query_skipped.path_and_query.clone(),
            Some(path) => path.clone(),
        }
    }

    pub fn build_sorted_query(query: &str) -> Option<String> {
        let hash_query: BTreeMap<_, _> = parse_query(query.as_bytes()).into_owned().collect();

        let mut query_string = "".to_string();

        for (key, value) in &hash_query {
            query_string.push_str(&utf8_percent_encode(key, QUERY_ENCODE_SET).to_string());

            if !value.is_empty() {
                query_string.push('=');
                query_string.push_str(&utf8_percent_encode(value, QUERY_ENCODE_SET).to_string());
            }

            query_string.push('&');
        }

        query_string.pop();

        if query_string.is_empty() {
            return None;
        }

        Some(query_string)
    }
}

impl RequestInformation for Request {
    fn is_host_header_allowed(&self) -> bool {
        true
    }

    fn host_header(&self) -> Option<&str> {
        self.headers
            .iter()
            .find(|header| header.name.to_lowercase() == "host")
            .map(|header| header.value.as_str())
    }

    fn authority(&self) -> Option<&str> {
        self.host()
    }

    fn forwarded(&self) -> impl DoubleEndedIterator<Item = &str> {
        self.headers
            .iter()
            .filter(|header| header.name.to_lowercase() == "forwarded")
            .map(|header| header.value.as_str())
    }

    fn x_forwarded_for(&self) -> impl DoubleEndedIterator<Item = &str> {
        self.headers
            .iter()
            .filter(|header| header.name.to_lowercase() == "x-forwarded-for")
            .map(|header| header.value.as_str())
    }

    fn x_forwarded_host(&self) -> impl DoubleEndedIterator<Item = &str> {
        self.headers
            .iter()
            .filter(|header| header.name.to_lowercase() == "x-forwarded-host")
            .map(|header| header.value.as_str())
    }

    fn x_forwarded_proto(&self) -> impl DoubleEndedIterator<Item = &str> {
        self.headers
            .iter()
            .filter(|header| header.name.to_lowercase() == "x-forwarded-proto")
            .map(|header| header.value.as_str())
    }

    fn x_forwarded_by(&self) -> impl DoubleEndedIterator<Item = &str> {
        self.headers
            .iter()
            .filter(|header| header.name.to_lowercase() == "x-forwarded-by")
            .map(|header| header.value.as_str())
    }

    fn default_scheme(&self) -> Option<&str> {
        self.scheme()
    }
}