perimeterx-fastly-enforcer 2.2.2

PerimeterX Fastly Compute@Edge Rust Enforcer
Documentation
use crate::modules::pxconstants::*;
use crate::px_debug;
use crate::pxconfig::PXConfig;
use crate::pxcontext::{CookieOrigin, PXContext};
use base64::{Engine as _, engine::general_purpose};
use fastly::http::StatusCode;
use serde_json::json;

pub(crate) struct BlockResponse {
    pub body: String,
    pub content_type: String,
    pub status_code: StatusCode,
}

struct BlockData {
    host_url: String,
    js_client_src: String,
    block_script: String,
    alt_block_script: String,
}

const BLOCK_TEMPLATE: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/resources/block_template.tmpl"
));
const RATE_LIMIT: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/resources/rate_limit.tmpl"
));

const HARD_BLOCK_TEMPLATE: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/resources/block_page_hard_block.tmpl"
));

fn parse_action(action: &str) -> String {
    match action {
        "c" => String::from("captcha"),
        "b" => String::from("block"),
        "r" => String::from("ratelimit"),
        _ => String::from("captcha"),
    }
}

fn get_block_data(conf: &PXConfig, ctx: &PXContext) -> BlockData {
    let mut js_client_src = format!(
        "https://{}/{}/main.min.js",
        conf.human_client_host, conf.app_id
    );
    let captcha_params = format!(
        "/captcha.js?a={}&u={}&v={}&m={}",
        ctx.block_action.as_deref().unwrap_or("c"),
        ctx.uuid.as_deref().unwrap_or_default(),
        ctx.vid.as_deref().unwrap_or_default(),
        if ctx.cookie_origin == Some(CookieOrigin::Header) {
            "1"
        } else {
            "0"
        }
    );
    let mut captcha_src = format!(
        "https://{}/{}{}",
        conf.human_captcha_host, conf.app_id, captcha_params
    );

    let alt_block_script = format!(
        "{}/{}{}",
        PX_BACKUP_CAPTCHA_HOST, conf.app_id, captcha_params
    );

    let mut host_url = format!("https://{}", conf.human_collector_host);

    if conf.first_party_enabled && matches!(ctx.cookie_origin, Some(CookieOrigin::Cookie) | None) {
        js_client_src = format!("/{}/init.js", &conf.app_id[2..]);
        captcha_src = format!("/{}/captcha{}", &conf.app_id[2..], captcha_params);
        host_url = format!("/{}/xhr", &conf.app_id[2..]);
    }

    BlockData {
        host_url,
        js_client_src,
        block_script: captcha_src,
        alt_block_script,
    }
}

pub fn get_block_response(conf: &PXConfig, ctx: &PXContext) -> BlockResponse {
    let block_data = get_block_data(conf, ctx);

    if ctx.block_action.as_deref() == Some("r") {
        px_debug!("Enforcing action: ratelimit page is served");
        return BlockResponse {
            body: RATE_LIMIT.into(),
            content_type: "text/html".into(),
            status_code: StatusCode::TOO_MANY_REQUESTS,
        };
    } else if ctx.block_action.as_deref() == Some("b") {
        px_debug!("Enforcing action: hard block page is served");
        return BlockResponse {
            body: HARD_BLOCK_TEMPLATE.into(),
            content_type: "text/html".into(),
            status_code: StatusCode::FORBIDDEN,
        };
    }
    let mut accept_header_value = ctx
        .headers
        .get("accept")
        .map(|h| h.to_string())
        .unwrap_or_default();
    if accept_header_value.is_empty() {
        accept_header_value = ctx
            .headers
            .get("content-type")
            .map(|h| h.to_string())
            .unwrap_or_default()
    }
    let is_json_response = matches!(ctx.cookie_origin, Some(CookieOrigin::Cookie) | None)
        && ctx.block_action.as_deref() != Some("r")
        && !accept_header_value.is_empty()
        && accept_header_value.contains(APPLICATION_JSON);

    px_debug!(
        "Enforcing action: {} page is served {}",
        parse_action(ctx.block_action.as_deref().unwrap_or("c")),
        if is_json_response {
            "using advanced protection mode"
        } else {
            ""
        }
    );

    if is_json_response {
        return BlockResponse {
            body: json!({
                "appId": &conf.app_id,
                "jsClientSrc": block_data.js_client_src,
                "firstPartyEnabled": &conf.first_party_enabled,
                "vid": ctx.vid.as_deref().unwrap_or_default(),
                "uuid": ctx.uuid.as_deref().unwrap_or_default(),
                "hostUrl": block_data.host_url,
                "blockScript": block_data.block_script,
            })
            .to_string(),
            content_type: APPLICATION_JSON.into(),
            status_code: StatusCode::FORBIDDEN,
        };
    };

    let mut tera = tera::Tera::default();
    if let Err(e) = tera.add_raw_template("block_template", BLOCK_TEMPLATE) {
        let mut msg = format!("{e}");
        let mut source: Option<&dyn std::error::Error> = std::error::Error::source(&e);
        while let Some(cause) = source {
            msg.push_str(&format!(" caused by: {cause}"));
            source = std::error::Error::source(cause);
        }
        px_debug!("Failed to add block template: {}", msg);
        return BlockResponse {
            body: "ERROR".into(),
            content_type: "text/plain".into(),
            status_code: StatusCode::INTERNAL_SERVER_ERROR,
        };
    }

    let is_mobile = ctx.cookie_origin == Some(CookieOrigin::Header);
    let mut template_context = tera::Context::new();
    template_context.insert("cssRef", &conf.css_ref);
    template_context.insert("customLogo", &conf.custom_logo);
    template_context.insert("appId", &conf.app_id);
    template_context.insert("jsClientSrc", &block_data.js_client_src);
    template_context.insert("firstPartyEnabled", &conf.first_party_enabled);
    template_context.insert("vid", ctx.vid.as_deref().unwrap_or_default());
    template_context.insert("uuid", ctx.uuid.as_deref().unwrap_or_default());
    template_context.insert("hostUrl", &block_data.host_url);
    template_context.insert("blockScript", &block_data.block_script);
    template_context.insert("altBlockScript", &block_data.alt_block_script);
    template_context.insert("jsRef", &conf.js_ref);
    template_context.insert("isMobile", &is_mobile);

    let tmpl = tera.render("block_template", &template_context);
    let body = match tmpl {
        Ok(s) => s,
        Err(e) => {
            px_debug!("Failed to render template: {}", e);
            e.to_string()
        }
    };

    if ctx.cookie_origin == Some(CookieOrigin::Header) {
        return BlockResponse {
            body: json!({
                "action": parse_action(ctx.block_action.as_deref().unwrap_or("c")),
                "uuid": ctx.uuid.as_deref().unwrap_or_default(),
                "vid": ctx.vid.as_deref().unwrap_or_default(),
                "appId": &conf.app_id,
                "page": general_purpose::STANDARD.encode(body)
            })
            .to_string(),
            content_type: APPLICATION_JSON.into(),
            status_code: StatusCode::FORBIDDEN,
        };
    }

    BlockResponse {
        body,
        content_type: String::from("text/html"),
        status_code: StatusCode::FORBIDDEN,
    }
}