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,
}
}