use super::pxconfig::{PXConfig, PXCustomParams};
pub use crate::handlers::pxagentic_trust::AgenticTrustData;
pub use crate::handlers::pxcredentials_intelligence::PXCredentialIntelligenceData;
use crate::handlers::pxcrypto;
use crate::handlers::pxgraphql::PXGraphQLExtractedItem;
use crate::modules::{pxconstants::*, pxutils};
use crate::px_debug;
use base64::{Engine as _, engine::general_purpose};
use fastly::Request;
use fastly::http::header::COOKIE;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::fmt;
use strum_macros::{AsRefStr, Display, EnumString};
use uuid::Uuid;
#[derive(Debug, PartialEq, Default)]
pub enum CallReason {
#[default]
None,
NoCookie,
NoCookieWVid,
CookieDecryptionFailed,
CookieValidationFailed,
CookieExpired,
SensitiveRoute,
MobileSdkConnectionError,
MobileError1,
MobileError2,
MobileError3,
MobileError4,
}
impl fmt::Display for CallReason {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
CallReason::None => {
write!(f, "none")
}
CallReason::NoCookie => {
write!(f, "no_cookie")
}
CallReason::NoCookieWVid => {
write!(f, "no_cookie_w_vid")
}
CallReason::CookieDecryptionFailed => {
write!(f, "cookie_decryption_failed")
}
CallReason::CookieValidationFailed => {
write!(f, "cookie_validation_failed")
}
CallReason::CookieExpired => {
write!(f, "cookie_expired")
}
CallReason::SensitiveRoute => {
write!(f, "sensitive_route")
}
CallReason::MobileSdkConnectionError => {
write!(f, "mobile_sdk_connection_error")
}
CallReason::MobileError1 => {
write!(f, "mobile_error_1")
}
CallReason::MobileError2 => {
write!(f, "mobile_error_2")
}
CallReason::MobileError3 => {
write!(f, "mobile_error_3")
}
CallReason::MobileError4 => {
write!(f, "mobile_error_4")
}
}
}
}
impl CallReason {
pub(crate) fn is_mobile_sdk_error(&self) -> bool {
matches!(
self,
CallReason::MobileError1
| CallReason::MobileError2
| CallReason::MobileError3
| CallReason::MobileError4
| CallReason::MobileSdkConnectionError
)
}
}
#[derive(PartialEq, Default)]
pub enum PassReason {
#[default]
None,
Cookie,
Error,
S2s,
}
impl fmt::Display for PassReason {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
PassReason::None => {
write!(f, "none")
}
PassReason::Cookie => {
write!(f, "cookie")
}
PassReason::Error => {
write!(f, "s2s_error")
}
PassReason::S2s => {
write!(f, "s2s")
}
}
}
}
#[derive(PartialEq, Default)]
pub enum BlockReason {
#[default]
None,
CookieScore,
ServerScore,
Challenge,
}
impl fmt::Display for BlockReason {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
BlockReason::None => {
write!(f, "none")
}
BlockReason::CookieScore => {
write!(f, "cookie_high_score")
}
BlockReason::ServerScore => {
write!(f, "s2s_high_score")
}
BlockReason::Challenge => {
write!(f, "challenge")
}
}
}
}
#[derive(Default, PartialEq)]
pub enum S2sErrorReason {
#[default]
None,
FailedOnServer,
InvalidResponse,
BadRequest,
ServerError,
Unknown,
}
impl fmt::Display for S2sErrorReason {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
S2sErrorReason::None => {
write!(f, "none")
}
S2sErrorReason::FailedOnServer => {
write!(f, "request_failed_on_server")
}
S2sErrorReason::InvalidResponse => {
write!(f, "invalid_response")
}
S2sErrorReason::BadRequest => {
write!(f, "bad_request")
}
S2sErrorReason::ServerError => {
write!(f, "server_error")
}
S2sErrorReason::Unknown => {
write!(f, "unknown_error")
}
}
}
}
#[derive(Debug, Default, PartialEq, Clone, Copy, AsRefStr, Display, EnumString)]
pub enum PXModuleMode {
#[default]
#[strum(serialize = "monitor")]
Monitor,
#[strum(
serialize = "active_blocking",
serialize = "blocking",
serialize = "blocked"
)]
Blocking,
}
#[derive(Debug, Default, PartialEq, Clone, Copy, AsRefStr, Display, EnumString)]
pub enum TokenVersion {
#[strum(serialize = "2")]
V2,
#[default]
#[strum(serialize = "3")]
V3,
}
#[derive(Debug, Default, PartialEq)]
pub enum CookieOrigin {
#[default]
Cookie,
Header,
}
#[derive(Default, PartialEq)]
pub enum CookieVersion {
V2,
#[default]
V3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, AsRefStr, Display, EnumString)]
pub enum VidSource {
#[strum(serialize = "vid_cookie")]
VidCookie,
#[strum(serialize = "risk_cookie")]
RiskCookie,
}
#[derive(Debug, Serialize)]
pub(crate) struct RiskHeader {
pub name: String,
pub value: serde_json::Value,
}
#[derive(Deserialize, Debug, Default)]
pub struct PXDataEnrichment {
pub(crate) timestamp: Option<i64>,
pub(crate) f_kb: Option<i8>,
pub(crate) f_type: Option<String>,
pub(crate) f_id: Option<String>,
pub(crate) f_origin: Option<String>,
pub(crate) ipc_id: Option<Vec<i32>>,
pub(crate) breached_account: Option<i8>,
pub(crate) f_access_token: Option<String>,
pub(crate) inc_id: Option<Vec<i32>>,
}
#[allow(dead_code)]
#[derive(Default)]
pub struct PXContext {
pub(crate) http_method: String,
pub(crate) http_version: String,
pub(crate) headers: serde_json::value::Value,
pub(crate) cookies: HashMap<String, String>,
pub(crate) request_cookie_names: Vec<String>,
pub(crate) access_cookies: HashMap<String, String>,
pub(crate) hostname: String,
pub(crate) full_url: String,
pub(crate) user_agent: String,
pub(crate) ip: String,
pub(crate) request_id: Uuid,
pub(crate) is_sensitive_route: bool,
pub(crate) is_enforced_request: bool,
pub(crate) is_monitored_request: bool,
pub(crate) risk_mode: PXModuleMode,
pub(crate) is_simulated_block: bool,
pub(crate) vid: Option<String>,
pub(crate) uuid: Option<String>,
pub(crate) pxhd_cookie: Option<String>,
pub(crate) pxhd_risk: Option<String>,
pub(crate) pxhd_domain: Option<String>,
pub(crate) vid_source: Option<VidSource>,
pub(crate) orig_cookie_vid: Option<String>,
pub(crate) s2s_error_http_status: Option<u16>,
pub(crate) s2s_error_message: Option<String>,
pub(crate) risk_rtt: Option<i64>,
pub(crate) block_action: Option<String>,
pub(crate) score: Option<u8>,
pub(crate) custom_params: PXCustomParams,
pub(crate) enforcer_start_time: Option<std::time::SystemTime>,
pub(crate) additional_risk_info: Option<String>,
pub(crate) additional_token_info: Option<String>,
pub(crate) cookie_json: Option<String>,
pub(crate) cookie_version: Option<CookieVersion>,
pub(crate) cookie_origin: Option<CookieOrigin>,
pub(crate) original_token: Option<String>,
pub(crate) original_token_error: Option<CallReason>,
pub(crate) v2_cookie_hash: Option<String>,
pub(crate) decoded_v2_cookie: Option<String>,
pub(crate) s2s_call_reason: Option<CallReason>,
pub(crate) s2s_error_reason: Option<S2sErrorReason>,
pub(crate) pass_reason: Option<PassReason>,
pub(crate) block_reason: Option<BlockReason>,
pub(crate) graphql_extracted_items: Vec<PXGraphQLExtractedItem>,
pub(crate) agentic_trust_data: Option<AgenticTrustData>,
pub(crate) data_enrichment: Option<PXDataEnrichment>,
pub(crate) pxde: Option<String>,
pub(crate) pxde_verified: bool,
pub(crate) pxcts_cookie: Option<String>,
pub(crate) app_user_id: Option<String>,
pub(crate) jwt_additional_fields: Option<serde_json::Map<String, serde_json::Value>>,
pub(crate) telemetry_requested: bool,
pub(crate) postpone_activities: bool,
pub(crate) credential_intelligence: Option<PXCredentialIntelligenceData>,
}
impl PXContext {
pub fn new(req: &Request, conf: &PXConfig) -> Self {
let mut cookie_origin = CookieOrigin::Cookie;
let mut cookies = HashMap::new();
let mut access_cookies = HashMap::new();
let mut request_cookie_names: Vec<String> = vec![];
let mut original_token = String::new();
let mut s2s_call_reason = None;
let mut risk_mode = conf.module_mode;
let should_bypass_monitor = conf.module_mode == PXModuleMode::Monitor
&& !conf.bypass_monitor_header.is_empty()
&& req
.get_header_str_lossy(&conf.bypass_monitor_header)
.map(|v| v.into_owned())
.unwrap_or_default()
== "1";
if conf.module_mode == PXModuleMode::Monitor && should_bypass_monitor {
risk_mode = PXModuleMode::Blocking;
px_debug!("Bypass monitor header set in monitor mode, forcing risk mode to blocking");
}
let is_enforced_request = risk_mode == PXModuleMode::Monitor
&& (pxutils::verify_route(&conf.enforced_routes, req.get_path())
|| conf
.is_enforced_request_fn
.map(|f| f(req, conf))
.unwrap_or(false));
if conf.module_mode == PXModuleMode::Monitor && is_enforced_request {
px_debug!("Enforced request detected in monitor mode, forcing risk mode to blocking");
}
let is_monitored_request = risk_mode == PXModuleMode::Blocking
&& (pxutils::verify_route(&conf.monitored_routes, req.get_path())
|| conf
.is_monitored_request_fn
.map(|f| f(req, conf))
.unwrap_or(false));
let risk_mode = if (risk_mode == PXModuleMode::Monitor && !is_enforced_request)
|| is_monitored_request
{
PXModuleMode::Monitor
} else {
PXModuleMode::Blocking
};
if let Some(mobile_sdk_header) = req.get_header_str_lossy(MOBILE_SDK_HEADER) {
px_debug!("Mobile SDK token detected");
cookie_origin = CookieOrigin::Header;
let orig_token_header = req
.get_header_str_lossy(MOBILE_SDK_ORIGINAL_TOKEN_HEADER)
.map(|v| v.into_owned())
.unwrap_or_default();
let authorization_header = mobile_sdk_header.trim();
if pxutils::is_mobile_sdk_error_code(authorization_header) {
match authorization_header {
"1" => s2s_call_reason = Some(CallReason::MobileError1),
"2" => s2s_call_reason = Some(CallReason::MobileError2),
"3" => s2s_call_reason = Some(CallReason::MobileError3),
"4" => s2s_call_reason = Some(CallReason::MobileError4),
_ => {
s2s_call_reason = Some(CallReason::MobileSdkConnectionError);
}
}
} else {
if let Some((cookie_name, cookie_contents)) =
pxutils::parse_versioned_mobile_token(authorization_header)
{
cookies.insert(cookie_name, cookie_contents);
}
}
original_token = orig_token_header.trim().to_owned();
} else {
let cookie_header_value = req
.get_header_str_lossy(COOKIE)
.map(|v| v.into_owned())
.unwrap_or_default();
let custom_cookie_header_value = {
let custom_header_name = conf.custom_cookie_header.as_str();
if custom_header_name.is_empty() {
String::new()
} else {
req.get_header_str_lossy(custom_header_name)
.map(|v| v.into_owned())
.unwrap_or_default()
}
};
cookies = pxutils::build_merged_request_cookies(
cookie_header_value.as_str(),
custom_cookie_header_value.as_str(),
);
}
request_cookie_names = cookies.keys().cloned().collect();
let pxvid = pxutils::extract_cookie_value(&cookies, "_pxvid");
let (vid, vid_source, orig_cookie_vid) = if pxutils::is_valid_uuid(&pxvid) {
(Some(pxvid), Some(VidSource::VidCookie), None)
} else if !pxvid.is_empty() {
(None, None, Some(pxvid))
} else {
(None, None, None)
};
let pxhd_cookie = pxutils::extract_cookie_value(&cookies, "_pxhd");
let pxcts_cookie = pxutils::extract_cookie_value(&cookies, "pxcts");
let access_cookie = pxutils::extract_cookie_value(&cookies, "_pxac");
if !access_cookie.is_empty() {
access_cookies.insert("access_cookie".to_string(), access_cookie);
}
for key in &conf.extracted_cookies {
let value = pxutils::extract_cookie_value(&cookies, key);
if !value.is_empty() {
access_cookies.insert(key.clone(), value);
}
}
PXContext {
cookie_origin: Some(cookie_origin),
user_agent: req
.get_header_str_lossy("user-agent")
.map(|v| v.into_owned())
.unwrap_or_default(),
http_method: req.get_method_str().to_string(),
http_version: pxutils::get_fastly_version_str(req).into(),
cookies,
access_cookies,
headers: pxutils::get_headers_as_json(req),
hostname: req.get_url().host_str().unwrap_or_default().to_string(),
full_url: req.get_url_str().to_string(),
is_sensitive_route: pxutils::verify_route(&conf.sensitive_routes, req.get_path()),
is_enforced_request,
is_monitored_request,
risk_mode,
original_token: if original_token.is_empty() {
None
} else {
Some(original_token)
},
s2s_call_reason,
request_cookie_names,
vid_source,
vid,
orig_cookie_vid,
pxhd_cookie: if pxhd_cookie.is_empty() {
None
} else {
Some(pxhd_cookie)
},
ip: pxutils::extract_ip_from_configured_headers(req, &conf.ip_headers)
.or_else(|| req.get_client_ip_addr().map(|ip| ip.to_string()))
.unwrap_or_default(),
block_action: Some("c".to_string()),
request_id: Uuid::new_v4(),
pxde_verified: false,
pxcts_cookie: if pxcts_cookie.is_empty() {
None
} else {
Some(pxcts_cookie)
},
enforcer_start_time: Some(std::time::SystemTime::now()),
postpone_activities: false,
..Default::default()
}
}
pub fn extract_pxde_cookie(&mut self, conf: &PXConfig) {
if let Some(pxde) = self.cookies.get("_pxde") {
let fields = pxde.split(':').collect::<Vec<&str>>();
if fields.len() != 2 {
px_debug!("_pxde cookie validation failed");
return;
}
let ehmac = match fields.first() {
Some(h) => h,
None => {
px_debug!("_pxde cookie validation failed");
return;
}
};
let pxde_val = fields.get(1..).map(|s| s.join(":")).unwrap_or_default();
let expected_hmac = pxcrypto::create_hmac(&pxde_val, &conf.cookie_secret);
if ehmac.to_lowercase() != expected_hmac.unwrap_or_default().to_lowercase() {
px_debug!("_pxde cookie HMAC validation failed");
return;
}
let pxde_val = match general_purpose::STANDARD.decode(pxde_val) {
Ok(s) => s,
Err(e) => {
px_debug!("_pxde cookie validation failed: {}", e);
return;
}
};
let pxde_val = match std::str::from_utf8(&pxde_val) {
Ok(s) => s,
Err(_) => {
px_debug!("_pxde cookie validation failed: invalid UTF-8");
return;
}
};
let data_enrichment: PXDataEnrichment =
serde_json::from_str::<PXDataEnrichment>(pxde_val).unwrap_or_default();
self.data_enrichment = Some(data_enrichment);
self.pxde = Some(pxde_val.to_string());
self.pxde_verified = true;
}
}
pub fn get_is_sensitive_route(&self) -> bool {
self.is_sensitive_route
}
pub fn get_request_cookie_names(&self) -> &Vec<String> {
&self.request_cookie_names
}
pub fn get_vid(&self) -> Option<&str> {
self.vid.as_deref()
}
pub fn get_uuid(&self) -> Option<&str> {
self.uuid.as_deref()
}
pub fn get_pxhd_cookie(&self) -> Option<&str> {
self.pxhd_cookie.as_deref()
}
pub fn get_pxhd_risk(&self) -> Option<&str> {
self.pxhd_risk.as_deref()
}
pub fn get_pxhd_domain(&self) -> Option<&str> {
self.pxhd_domain.as_deref()
}
pub fn get_pxhd(&self) -> Option<&str> {
if let Some(pxhd) = self.pxhd_risk.as_deref() {
return Some(pxhd);
}
if let Some(pxhd) = self.pxhd_cookie.as_deref() {
return Some(pxhd);
}
None
}
pub fn get_vid_source(&self) -> Option<&VidSource> {
self.vid_source.as_ref()
}
pub fn get_user_agent(&self) -> &str {
&self.user_agent
}
pub fn get_s2s_error_http_status(&self) -> Option<u16> {
self.s2s_error_http_status
}
pub fn get_s2s_error_message(&self) -> Option<&str> {
self.s2s_error_message.as_deref()
}
pub fn get_ip(&self) -> &str {
&self.ip
}
pub fn get_v2_cookie_hash(&self) -> Option<&str> {
self.v2_cookie_hash.as_deref()
}
pub fn get_decoded_v2_cookie(&self) -> Option<&str> {
self.decoded_v2_cookie.as_deref()
}
pub fn get_risk_rtt(&self) -> Option<i64> {
self.risk_rtt
}
pub fn get_block_action(&self) -> Option<&str> {
self.block_action.as_deref()
}
pub fn get_score(&self) -> Option<u8> {
self.score
}
pub fn get_s2s_call_reason(&self) -> Option<&CallReason> {
self.s2s_call_reason.as_ref()
}
pub fn get_s2s_error_reason(&self) -> Option<&S2sErrorReason> {
self.s2s_error_reason.as_ref()
}
pub fn get_pass_reason(&self) -> Option<&PassReason> {
self.pass_reason.as_ref()
}
pub fn get_block_reason(&self) -> Option<&BlockReason> {
self.block_reason.as_ref()
}
pub fn get_request_id(&self) -> &Uuid {
&self.request_id
}
pub fn get_data_enrichment(&self) -> Option<&PXDataEnrichment> {
self.data_enrichment.as_ref()
}
pub fn get_pxcts_cookie(&self) -> Option<&str> {
self.pxcts_cookie.as_deref()
}
pub fn get_agentic_trust_data(&self) -> Option<&AgenticTrustData> {
self.agentic_trust_data.as_ref()
}
}
static EMPTY_VEC: Vec<i32> = Vec::new();
impl PXDataEnrichment {
pub fn get_timestamp(&self) -> i64 {
self.timestamp.unwrap_or_default()
}
pub fn get_f_kb(&self) -> i8 {
self.f_kb.unwrap_or_default()
}
pub fn get_f_type(&self) -> &str {
self.f_type.as_deref().unwrap_or("")
}
pub fn get_f_id(&self) -> &str {
self.f_id.as_deref().unwrap_or("")
}
pub fn get_f_origin(&self) -> &str {
self.f_origin.as_deref().unwrap_or("")
}
pub fn get_ipc_id(&self) -> &Vec<i32> {
self.ipc_id.as_ref().unwrap_or(&EMPTY_VEC)
}
pub fn get_breached_account(&self) -> i8 {
self.breached_account.unwrap_or_default()
}
pub fn get_f_access_token(&self) -> &str {
self.f_access_token.as_deref().unwrap_or("")
}
pub fn get_inc_id(&self) -> &Vec<i32> {
self.inc_id.as_ref().unwrap_or(&EMPTY_VEC)
}
}