perimeterx-fastly-enforcer 2.2.2

PerimeterX Fastly Compute@Edge Rust Enforcer
Documentation
use crate::px_debug;
use crate::pxconfig::PXConfig;
use crate::pxcontext::PXContext;
use crate::{handlers::pxcrypto, modules::pxconstants::*};
use base64::{Engine as _, engine::general_purpose};
use fastly::{Error, Request, Response};
use serde_json::json;
use std::time::{SystemTime, UNIX_EPOCH};

pub(crate) enum TelemetryUpdateReason {
    Command,
    RiskApi,
}

impl TelemetryUpdateReason {
    fn as_str(&self) -> &str {
        match self {
            TelemetryUpdateReason::Command => "command",
            TelemetryUpdateReason::RiskApi => "risk",
        }
    }
}

pub(crate) fn send_telemetry_activity(
    conf: &PXConfig,
    ctx: &PXContext,
    update_reason: TelemetryUpdateReason,
) -> Result<Response, Error> {
    let current_timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis() as i64;

    let configs = json!({
        "active_config": conf.build_config_json(),
        "static_config": conf.build_static_json(),
        "remote_config": json!({})
    });

    let payload = json!({
        "type": "enforcer_telemetry",
        "px_app_id": &conf.app_id,
        "timestamp": current_timestamp,
        "details": {
            "enforcer_configs": configs,
            "module_version": PX_MODULE_VERSION,
            "update_reason": update_reason.as_str(),
            "node_name": fastly::compute_runtime::hostname(),
            "request_id": ctx.request_id.to_string()
        }
    });

    px_debug!("Sending telemetry payload: {}", payload.to_string());

    //let body = payload.as_bytes();
    let url = format!("https://{}{}", conf.human_sapi_host, TELEMETRY_PATH);

    let req = Request::post(url)
        .with_header("Authorization", format!("Bearer {}", conf.auth_token))
        .with_header("Content-Type", APPLICATION_JSON)
        .with_body(payload.to_string());

    Ok(req.send(&conf.human_sapi_backend)?)
}

pub fn handle_telemetry(req: &Request, conf: &PXConfig, ctx: &PXContext) {
    px_debug!("Received command to send enforcer telemetry");

    let header_value: String = req
        .get_header_str_lossy(TELEMETRY_HEADER)
        .map(|v| v.into_owned())
        .unwrap_or_default();
    let decoded_header_base64 = general_purpose::STANDARD
        .decode(header_value)
        .unwrap_or_default();
    let decoded_header = std::str::from_utf8(&decoded_header_base64).unwrap_or_default();
    let splitted_header: Vec<&str> = decoded_header.split(':').collect();
    if splitted_header.len() != 2 {
        px_debug!("Malformed {} header: {}", TELEMETRY_HEADER, decoded_header);
        return;
    }

    let Some(timestamp_str) = splitted_header.first() else {
        px_debug!("Missing timestamp in {} header", TELEMETRY_HEADER);
        return;
    };
    let Some(hmac_str) = splitted_header.get(1) else {
        px_debug!("Missing hmac in {} header", TELEMETRY_HEADER);
        return;
    };

    if !pxcrypto::is_hmac_valid(timestamp_str, hmac_str, &conf.cookie_secret) {
        px_debug!(
            "{} hmac validation failed. original hmac: {}, timestamp: {}.",
            TELEMETRY_HEADER,
            hmac_str,
            timestamp_str
        );
        return;
    }

    let current_timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_millis() as i64;

    let parsed_timestamp = timestamp_str.parse::<i64>().unwrap_or_default();
    if parsed_timestamp == 0 || parsed_timestamp < current_timestamp {
        px_debug!(
            "{} timestamp expired: {} < {}",
            TELEMETRY_HEADER,
            parsed_timestamp,
            current_timestamp
        );
        return;
    }
    let telemetry_result = send_telemetry_activity(conf, ctx, TelemetryUpdateReason::Command);

    match telemetry_result {
        Ok(response) => {
            px_debug!("telemetry status: {}", response.get_status());
        }
        Err(e) => {
            px_debug!("error sending telemetry: {}", e);
        }
    }
}