redirectionio 3.1.0

Redirection IO Library to handle matching rule, redirect and filtering headers and body.
Documentation
use serde::{Deserialize, Serialize};

use crate::http::{Addr, Header, Request};

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Log {
    code: u16,
    to: String,
    time: u128,
    proxy: String,
    ips: Option<Vec<String>>,
    from: FromLog,
    duration: Option<u128>,
    match_duration: Option<u128>,
    proxy_duration: Option<u128>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
struct FromLog {
    #[serde(rename = "ruleIds")]
    rule_ids: Option<Vec<String>>,
    url: String,
    method: Option<String>,
    scheme: Option<String>,
    host: Option<String>,
    referer: Option<String>,
    #[serde(rename = "userAgent")]
    user_agent: Option<String>,
    #[serde(rename = "contentType")]
    content_type: Option<String>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct LegacyLog {
    status_code: u16,
    host: Option<String>,
    method: Option<String>,
    request_uri: Option<String>,
    user_agent: Option<String>,
    referer: Option<String>,
    scheme: Option<String>,
    use_json: Option<bool>,
    target: Option<String>,
    rule_id: Option<String>,
}

impl Log {
    pub fn from_legacy(legacy: LegacyLog, proxy: String) -> Self {
        let now = chrono::Utc::now().timestamp() as u128;

        Log {
            code: legacy.status_code,
            to: legacy.target.unwrap_or_default(),
            time: now,
            proxy,
            ips: None,
            from: FromLog {
                rule_ids: legacy.rule_id.map(|id| vec![id]),
                url: legacy.request_uri.unwrap_or_default(),
                method: legacy.method,
                scheme: legacy.scheme,
                host: legacy.host,
                referer: legacy.referer,
                user_agent: legacy.user_agent,
                content_type: None,
            },
            duration: None,
            match_duration: None,
            proxy_duration: None,
        }
    }

    #[allow(clippy::too_many_arguments)]
    pub fn from_proxy(
        request: &Request,
        code: u16,
        response_headers: &[Header],
        rule_ids: Vec<String>,
        proxy: &str,
        request_start_time: u128,
        action_match_time: u128,
        proxy_response_time: Option<u128>,
        client_ip: &str,
    ) -> Log {
        let mut location = None;
        let mut user_agent = None;
        let mut referer = None;
        let mut content_type = None;
        let mut ips = Vec::new();
        let now = chrono::Utc::now().timestamp_millis() as u128;
        let duration = now.checked_sub(request_start_time);
        let match_duration = action_match_time.checked_sub(request_start_time);
        let proxy_duration = proxy_response_time.and_then(|ms| ms.checked_sub(action_match_time));

        if let Ok(addr) = client_ip.parse::<Addr>() {
            ips.push(addr.addr);
        }

        for header in &request.headers {
            if header.name.to_lowercase() == "user-agent" {
                user_agent = Some(header.value.clone())
            }

            if header.name.to_lowercase() == "referer" {
                referer = Some(header.value.clone())
            }

            if header.name.to_lowercase() == "x-forwarded-for" {
                let forwarded_ips = header.value.split(',');

                for forwarded_ip in forwarded_ips {
                    if let Ok(addr) = forwarded_ip.parse::<Addr>() {
                        ips.push(addr.addr);
                    }
                }
            }

            if header.name.to_lowercase() == "forwarded" {
                for (name, val) in header.value.split(';').flat_map(|val| val.split(',')).flat_map(|pair| {
                    let mut items = pair.trim().splitn(2, '=');
                    Some((items.next()?, items.next()?))
                }) {
                    if name.trim().to_lowercase().as_str() == "for" {
                        let ip = val.trim().trim_start_matches('"').trim_end_matches('"').to_string();

                        if let Ok(ip) = ip.parse::<Addr>() {
                            ips.push(ip.addr);
                        }
                    }
                }
            }
        }

        for header in response_headers {
            if header.name.to_lowercase() == "location" {
                location = Some(header.value.clone())
            }

            if header.name.to_lowercase() == "content-type" {
                content_type = Some(header.value.clone())
            }
        }

        let from = FromLog {
            rule_ids: Some(rule_ids),
            url: request.path_and_query_skipped.original.clone(),
            method: request.method.clone(),
            scheme: request.scheme.clone(),
            host: request.host.clone(),
            referer,
            user_agent,
            content_type,
        };

        Log {
            code,
            from,
            proxy: proxy.to_string(),
            time: request_start_time,
            ips: Some(ips.iter().map(|ip| ip.to_string()).collect()),
            to: location.unwrap_or_default(),
            duration,
            match_duration,
            proxy_duration,
        }
    }
}