1use super::pxconfig::{PXConfig, PXCustomParams};
2pub use crate::handlers::pxagentic_trust::AgenticTrustData;
3pub use crate::handlers::pxcredentials_intelligence::PXCredentialIntelligenceData;
4use crate::handlers::pxcrypto;
5use crate::handlers::pxgraphql::PXGraphQLExtractedItem;
6use crate::modules::{pxconstants::*, pxutils};
7use crate::px_debug;
8use base64::{Engine as _, engine::general_purpose};
9use fastly::Request;
10use fastly::http::header::COOKIE;
11use serde::Deserialize;
12use serde::Serialize;
13use std::collections::HashMap;
14use std::fmt;
15use strum_macros::{AsRefStr, Display, EnumString};
16use uuid::Uuid;
17
18#[derive(Debug, PartialEq, Default)]
19pub enum CallReason {
20 #[default]
21 None,
22 NoCookie,
23 NoCookieWVid,
24 CookieDecryptionFailed,
25 CookieValidationFailed,
26 CookieExpired,
27 SensitiveRoute,
28 MobileSdkConnectionError,
29 MobileError1,
30 MobileError2,
31 MobileError3,
32 MobileError4,
33}
34impl fmt::Display for CallReason {
35 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
36 match self {
37 CallReason::None => {
38 write!(f, "none")
39 }
40 CallReason::NoCookie => {
41 write!(f, "no_cookie")
42 }
43 CallReason::NoCookieWVid => {
44 write!(f, "no_cookie_w_vid")
45 }
46 CallReason::CookieDecryptionFailed => {
47 write!(f, "cookie_decryption_failed")
48 }
49 CallReason::CookieValidationFailed => {
50 write!(f, "cookie_validation_failed")
51 }
52 CallReason::CookieExpired => {
53 write!(f, "cookie_expired")
54 }
55 CallReason::SensitiveRoute => {
56 write!(f, "sensitive_route")
57 }
58 CallReason::MobileSdkConnectionError => {
59 write!(f, "mobile_sdk_connection_error")
60 }
61 CallReason::MobileError1 => {
62 write!(f, "mobile_error_1")
63 }
64 CallReason::MobileError2 => {
65 write!(f, "mobile_error_2")
66 }
67 CallReason::MobileError3 => {
68 write!(f, "mobile_error_3")
69 }
70 CallReason::MobileError4 => {
71 write!(f, "mobile_error_4")
72 }
73 }
74 }
75}
76
77impl CallReason {
78 pub(crate) fn is_mobile_sdk_error(&self) -> bool {
79 matches!(
80 self,
81 CallReason::MobileError1
82 | CallReason::MobileError2
83 | CallReason::MobileError3
84 | CallReason::MobileError4
85 | CallReason::MobileSdkConnectionError
86 )
87 }
88}
89
90#[derive(PartialEq, Default)]
91pub enum PassReason {
92 #[default]
93 None,
94 Cookie,
95 Error,
96 S2s,
97}
98
99impl fmt::Display for PassReason {
100 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
101 match self {
102 PassReason::None => {
103 write!(f, "none")
104 }
105 PassReason::Cookie => {
106 write!(f, "cookie")
107 }
108 PassReason::Error => {
109 write!(f, "s2s_error")
110 }
111 PassReason::S2s => {
112 write!(f, "s2s")
113 }
114 }
115 }
116}
117
118#[derive(PartialEq, Default)]
119pub enum BlockReason {
120 #[default]
121 None,
122 CookieScore,
123 ServerScore,
124 Challenge,
125}
126impl fmt::Display for BlockReason {
127 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
128 match self {
129 BlockReason::None => {
130 write!(f, "none")
131 }
132 BlockReason::CookieScore => {
133 write!(f, "cookie_high_score")
134 }
135 BlockReason::ServerScore => {
136 write!(f, "s2s_high_score")
137 }
138 BlockReason::Challenge => {
139 write!(f, "challenge")
140 }
141 }
142 }
143}
144
145#[derive(Default, PartialEq)]
146pub enum S2sErrorReason {
147 #[default]
148 None,
149 FailedOnServer,
150 InvalidResponse,
151 BadRequest,
152 ServerError,
153 Unknown,
154}
155impl fmt::Display for S2sErrorReason {
156 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
157 match self {
158 S2sErrorReason::None => {
159 write!(f, "none")
160 }
161 S2sErrorReason::FailedOnServer => {
162 write!(f, "request_failed_on_server")
163 }
164 S2sErrorReason::InvalidResponse => {
165 write!(f, "invalid_response")
166 }
167 S2sErrorReason::BadRequest => {
168 write!(f, "bad_request")
169 }
170 S2sErrorReason::ServerError => {
171 write!(f, "server_error")
172 }
173 S2sErrorReason::Unknown => {
174 write!(f, "unknown_error")
175 }
176 }
177 }
178}
179
180#[derive(Debug, Default, PartialEq, Clone, Copy, AsRefStr, Display, EnumString)]
182pub enum PXModuleMode {
183 #[default]
184 #[strum(serialize = "monitor")]
185 Monitor,
186 #[strum(
187 serialize = "active_blocking",
188 serialize = "blocking",
189 serialize = "blocked"
190 )]
191 Blocking,
192}
193
194#[derive(Debug, Default, PartialEq, Clone, Copy, AsRefStr, Display, EnumString)]
195pub enum TokenVersion {
196 #[strum(serialize = "2")]
197 V2,
198 #[default]
199 #[strum(serialize = "3")]
200 V3,
201}
202
203#[derive(Debug, Default, PartialEq)]
204pub enum CookieOrigin {
205 #[default]
206 Cookie,
207 Header,
208}
209
210#[derive(Default, PartialEq)]
211pub enum CookieVersion {
212 V2,
213 #[default]
214 V3,
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, AsRefStr, Display, EnumString)]
219pub enum VidSource {
220 #[strum(serialize = "vid_cookie")]
221 VidCookie,
222 #[strum(serialize = "risk_cookie")]
223 RiskCookie,
224}
225
226#[derive(Debug, Serialize)]
227pub(crate) struct RiskHeader {
228 pub name: String,
229 pub value: serde_json::Value,
230}
231
232#[derive(Deserialize, Debug, Default)]
234pub struct PXDataEnrichment {
235 pub(crate) timestamp: Option<i64>,
236 pub(crate) f_kb: Option<i8>,
237 pub(crate) f_type: Option<String>,
238 pub(crate) f_id: Option<String>,
239 pub(crate) f_origin: Option<String>,
240 pub(crate) ipc_id: Option<Vec<i32>>,
241 pub(crate) breached_account: Option<i8>,
242 pub(crate) f_access_token: Option<String>,
243 pub(crate) inc_id: Option<Vec<i32>>,
244}
245
246#[allow(dead_code)]
250#[derive(Default)]
251pub struct PXContext {
252 pub(crate) http_method: String,
253 pub(crate) http_version: String,
255 pub(crate) headers: serde_json::value::Value,
257 pub(crate) cookies: HashMap<String, String>,
259 pub(crate) request_cookie_names: Vec<String>,
261 pub(crate) access_cookies: HashMap<String, String>,
263 pub(crate) hostname: String,
265 pub(crate) full_url: String,
267 pub(crate) user_agent: String,
269 pub(crate) ip: String,
271 pub(crate) request_id: Uuid,
273
274 pub(crate) is_sensitive_route: bool,
276 pub(crate) is_enforced_request: bool,
278 pub(crate) is_monitored_request: bool,
280 pub(crate) risk_mode: PXModuleMode,
282 pub(crate) is_simulated_block: bool,
284
285 pub(crate) vid: Option<String>,
287 pub(crate) uuid: Option<String>,
289 pub(crate) pxhd_cookie: Option<String>,
291 pub(crate) pxhd_risk: Option<String>,
293 pub(crate) pxhd_domain: Option<String>,
295 pub(crate) vid_source: Option<VidSource>,
297 pub(crate) orig_cookie_vid: Option<String>,
299
300 pub(crate) s2s_error_http_status: Option<u16>,
302 pub(crate) s2s_error_message: Option<String>,
304 pub(crate) risk_rtt: Option<i64>,
306 pub(crate) block_action: Option<String>,
308 pub(crate) score: Option<u8>,
310 pub(crate) custom_params: PXCustomParams,
312 pub(crate) enforcer_start_time: Option<std::time::SystemTime>,
314 pub(crate) additional_risk_info: Option<String>,
316 pub(crate) additional_token_info: Option<String>,
318 pub(crate) cookie_json: Option<String>,
320
321 pub(crate) cookie_version: Option<CookieVersion>,
323 pub(crate) cookie_origin: Option<CookieOrigin>,
325 pub(crate) original_token: Option<String>,
329 pub(crate) original_token_error: Option<CallReason>,
331
332 pub(crate) v2_cookie_hash: Option<String>,
334 pub(crate) decoded_v2_cookie: Option<String>,
336
337 pub(crate) s2s_call_reason: Option<CallReason>,
339 pub(crate) s2s_error_reason: Option<S2sErrorReason>,
341 pub(crate) pass_reason: Option<PassReason>,
343 pub(crate) block_reason: Option<BlockReason>,
345
346 pub(crate) graphql_extracted_items: Vec<PXGraphQLExtractedItem>,
348
349 pub(crate) agentic_trust_data: Option<AgenticTrustData>,
352
353 pub(crate) data_enrichment: Option<PXDataEnrichment>,
355 pub(crate) pxde: Option<String>,
357 pub(crate) pxde_verified: bool,
359
360 pub(crate) pxcts_cookie: Option<String>,
362 pub(crate) app_user_id: Option<String>,
364 pub(crate) jwt_additional_fields: Option<serde_json::Map<String, serde_json::Value>>,
366
367 pub(crate) telemetry_requested: bool,
369
370 pub(crate) postpone_activities: bool,
372
373 pub(crate) credential_intelligence: Option<PXCredentialIntelligenceData>,
375}
376
377impl PXContext {
378 pub fn new(req: &Request, conf: &PXConfig) -> Self {
379 let mut cookie_origin = CookieOrigin::Cookie;
380 let mut cookies = HashMap::new();
381 let mut access_cookies = HashMap::new();
382 let mut request_cookie_names: Vec<String> = vec![];
383 let mut original_token = String::new();
384 let mut s2s_call_reason = None;
385 let mut risk_mode = conf.module_mode;
386
387 let should_bypass_monitor = conf.module_mode == PXModuleMode::Monitor
389 && !conf.bypass_monitor_header.is_empty()
390 && req
391 .get_header_str_lossy(&conf.bypass_monitor_header)
392 .map(|v| v.into_owned())
393 .unwrap_or_default()
394 == "1";
395
396 if conf.module_mode == PXModuleMode::Monitor && should_bypass_monitor {
397 risk_mode = PXModuleMode::Blocking;
398 px_debug!("Bypass monitor header set in monitor mode, forcing risk mode to blocking");
399 }
400
401 let is_enforced_request = risk_mode == PXModuleMode::Monitor
402 && (pxutils::verify_route(&conf.enforced_routes, req.get_path())
403 || conf
404 .is_enforced_request_fn
405 .map(|f| f(req, conf))
406 .unwrap_or(false));
407
408 if conf.module_mode == PXModuleMode::Monitor && is_enforced_request {
409 px_debug!("Enforced request detected in monitor mode, forcing risk mode to blocking");
410 }
411
412 let is_monitored_request = risk_mode == PXModuleMode::Blocking
413 && (pxutils::verify_route(&conf.monitored_routes, req.get_path())
414 || conf
415 .is_monitored_request_fn
416 .map(|f| f(req, conf))
417 .unwrap_or(false));
418
419 let risk_mode = if (risk_mode == PXModuleMode::Monitor && !is_enforced_request)
420 || is_monitored_request
421 {
422 PXModuleMode::Monitor
423 } else {
424 PXModuleMode::Blocking
425 };
426
427 if let Some(mobile_sdk_header) = req.get_header_str_lossy(MOBILE_SDK_HEADER) {
428 px_debug!("Mobile SDK token detected");
429 cookie_origin = CookieOrigin::Header;
430 let orig_token_header = req
431 .get_header_str_lossy(MOBILE_SDK_ORIGINAL_TOKEN_HEADER)
432 .map(|v| v.into_owned())
433 .unwrap_or_default();
434
435 let authorization_header = mobile_sdk_header.trim();
436
437 if pxutils::is_mobile_sdk_error_code(authorization_header) {
438 match authorization_header {
439 "1" => s2s_call_reason = Some(CallReason::MobileError1),
440 "2" => s2s_call_reason = Some(CallReason::MobileError2),
441 "3" => s2s_call_reason = Some(CallReason::MobileError3),
442 "4" => s2s_call_reason = Some(CallReason::MobileError4),
443 _ => {
444 s2s_call_reason = Some(CallReason::MobileSdkConnectionError);
445 }
446 }
447 } else {
448 if let Some((cookie_name, cookie_contents)) =
449 pxutils::parse_versioned_mobile_token(authorization_header)
450 {
451 cookies.insert(cookie_name, cookie_contents);
452 }
453 }
454
455 original_token = orig_token_header.trim().to_owned();
456 } else {
457 let cookie_header_value = req
458 .get_header_str_lossy(COOKIE)
459 .map(|v| v.into_owned())
460 .unwrap_or_default();
461
462 let custom_cookie_header_value = {
463 let custom_header_name = conf.custom_cookie_header.as_str();
464 if custom_header_name.is_empty() {
465 String::new()
466 } else {
467 req.get_header_str_lossy(custom_header_name)
468 .map(|v| v.into_owned())
469 .unwrap_or_default()
470 }
471 };
472
473 cookies = pxutils::build_merged_request_cookies(
474 cookie_header_value.as_str(),
475 custom_cookie_header_value.as_str(),
476 );
477 }
478 request_cookie_names = cookies.keys().cloned().collect();
479
480 let pxvid = pxutils::extract_cookie_value(&cookies, "_pxvid");
481 let (vid, vid_source, orig_cookie_vid) = if pxutils::is_valid_uuid(&pxvid) {
482 (Some(pxvid), Some(VidSource::VidCookie), None)
483 } else if !pxvid.is_empty() {
484 (None, None, Some(pxvid))
485 } else {
486 (None, None, None)
487 };
488 let pxhd_cookie = pxutils::extract_cookie_value(&cookies, "_pxhd");
489 let pxcts_cookie = pxutils::extract_cookie_value(&cookies, "pxcts");
490
491 let access_cookie = pxutils::extract_cookie_value(&cookies, "_pxac");
492 if !access_cookie.is_empty() {
493 access_cookies.insert("access_cookie".to_string(), access_cookie);
494 }
495 for key in &conf.extracted_cookies {
496 let value = pxutils::extract_cookie_value(&cookies, key);
497 if !value.is_empty() {
498 access_cookies.insert(key.clone(), value);
499 }
500 }
501
502 PXContext {
503 cookie_origin: Some(cookie_origin),
504 user_agent: req
505 .get_header_str_lossy("user-agent")
506 .map(|v| v.into_owned())
507 .unwrap_or_default(),
508 http_method: req.get_method_str().to_string(),
509 http_version: pxutils::get_fastly_version_str(req).into(),
510 cookies,
511 access_cookies,
512 headers: pxutils::get_headers_as_json(req),
513 hostname: req.get_url().host_str().unwrap_or_default().to_string(),
514 full_url: req.get_url_str().to_string(),
515 is_sensitive_route: pxutils::verify_route(&conf.sensitive_routes, req.get_path()),
516 is_enforced_request,
517 is_monitored_request,
518 risk_mode,
519 original_token: if original_token.is_empty() {
520 None
521 } else {
522 Some(original_token)
523 },
524 s2s_call_reason,
525 request_cookie_names,
526 vid_source,
527 vid,
528 orig_cookie_vid,
529 pxhd_cookie: if pxhd_cookie.is_empty() {
530 None
531 } else {
532 Some(pxhd_cookie)
533 },
534 ip: pxutils::extract_ip_from_configured_headers(req, &conf.ip_headers)
535 .or_else(|| req.get_client_ip_addr().map(|ip| ip.to_string()))
536 .unwrap_or_default(),
537 block_action: Some("c".to_string()),
538 request_id: Uuid::new_v4(),
539 pxde_verified: false,
540 pxcts_cookie: if pxcts_cookie.is_empty() {
541 None
542 } else {
543 Some(pxcts_cookie)
544 },
545 enforcer_start_time: Some(std::time::SystemTime::now()),
546 postpone_activities: false,
547 ..Default::default()
548 }
549 }
550
551 pub fn extract_pxde_cookie(&mut self, conf: &PXConfig) {
552 if let Some(pxde) = self.cookies.get("_pxde") {
553 let fields = pxde.split(':').collect::<Vec<&str>>();
554 if fields.len() != 2 {
555 px_debug!("_pxde cookie validation failed");
556 return;
557 }
558
559 let ehmac = match fields.first() {
560 Some(h) => h,
561 None => {
562 px_debug!("_pxde cookie validation failed");
563 return;
564 }
565 };
566 let pxde_val = fields.get(1..).map(|s| s.join(":")).unwrap_or_default();
567 let expected_hmac = pxcrypto::create_hmac(&pxde_val, &conf.cookie_secret);
568
569 if ehmac.to_lowercase() != expected_hmac.unwrap_or_default().to_lowercase() {
570 px_debug!("_pxde cookie HMAC validation failed");
571 return;
572 }
573
574 let pxde_val = match general_purpose::STANDARD.decode(pxde_val) {
575 Ok(s) => s,
576 Err(e) => {
577 px_debug!("_pxde cookie validation failed: {}", e);
578 return;
579 }
580 };
581 let pxde_val = match std::str::from_utf8(&pxde_val) {
582 Ok(s) => s,
583 Err(_) => {
584 px_debug!("_pxde cookie validation failed: invalid UTF-8");
585 return;
586 }
587 };
588
589 let data_enrichment: PXDataEnrichment =
590 serde_json::from_str::<PXDataEnrichment>(pxde_val).unwrap_or_default();
591 self.data_enrichment = Some(data_enrichment);
592 self.pxde = Some(pxde_val.to_string());
593 self.pxde_verified = true;
594 }
595 }
596
597 pub fn get_is_sensitive_route(&self) -> bool {
601 self.is_sensitive_route
602 }
603
604 pub fn get_request_cookie_names(&self) -> &Vec<String> {
606 &self.request_cookie_names
607 }
608
609 pub fn get_vid(&self) -> Option<&str> {
611 self.vid.as_deref()
612 }
613
614 pub fn get_uuid(&self) -> Option<&str> {
616 self.uuid.as_deref()
617 }
618
619 pub fn get_pxhd_cookie(&self) -> Option<&str> {
621 self.pxhd_cookie.as_deref()
622 }
623
624 pub fn get_pxhd_risk(&self) -> Option<&str> {
626 self.pxhd_risk.as_deref()
627 }
628
629 pub fn get_pxhd_domain(&self) -> Option<&str> {
631 self.pxhd_domain.as_deref()
632 }
633
634 pub fn get_pxhd(&self) -> Option<&str> {
636 if let Some(pxhd) = self.pxhd_risk.as_deref() {
637 return Some(pxhd);
638 }
639 if let Some(pxhd) = self.pxhd_cookie.as_deref() {
640 return Some(pxhd);
641 }
642 None
643 }
644
645 pub fn get_vid_source(&self) -> Option<&VidSource> {
647 self.vid_source.as_ref()
648 }
649
650 pub fn get_user_agent(&self) -> &str {
652 &self.user_agent
653 }
654
655 pub fn get_s2s_error_http_status(&self) -> Option<u16> {
657 self.s2s_error_http_status
658 }
659
660 pub fn get_s2s_error_message(&self) -> Option<&str> {
662 self.s2s_error_message.as_deref()
663 }
664
665 pub fn get_ip(&self) -> &str {
667 &self.ip
668 }
669
670 pub fn get_v2_cookie_hash(&self) -> Option<&str> {
672 self.v2_cookie_hash.as_deref()
673 }
674
675 pub fn get_decoded_v2_cookie(&self) -> Option<&str> {
677 self.decoded_v2_cookie.as_deref()
678 }
679
680 pub fn get_risk_rtt(&self) -> Option<i64> {
682 self.risk_rtt
683 }
684
685 pub fn get_block_action(&self) -> Option<&str> {
687 self.block_action.as_deref()
688 }
689
690 pub fn get_score(&self) -> Option<u8> {
692 self.score
693 }
694
695 pub fn get_s2s_call_reason(&self) -> Option<&CallReason> {
697 self.s2s_call_reason.as_ref()
698 }
699
700 pub fn get_s2s_error_reason(&self) -> Option<&S2sErrorReason> {
702 self.s2s_error_reason.as_ref()
703 }
704
705 pub fn get_pass_reason(&self) -> Option<&PassReason> {
707 self.pass_reason.as_ref()
708 }
709
710 pub fn get_block_reason(&self) -> Option<&BlockReason> {
712 self.block_reason.as_ref()
713 }
714
715 pub fn get_request_id(&self) -> &Uuid {
717 &self.request_id
718 }
719
720 pub fn get_data_enrichment(&self) -> Option<&PXDataEnrichment> {
722 self.data_enrichment.as_ref()
723 }
724
725 pub fn get_pxcts_cookie(&self) -> Option<&str> {
727 self.pxcts_cookie.as_deref()
728 }
729
730 pub fn get_agentic_trust_data(&self) -> Option<&AgenticTrustData> {
732 self.agentic_trust_data.as_ref()
733 }
734}
735
736static EMPTY_VEC: Vec<i32> = Vec::new();
737impl PXDataEnrichment {
738 pub fn get_timestamp(&self) -> i64 {
740 self.timestamp.unwrap_or_default()
741 }
742
743 pub fn get_f_kb(&self) -> i8 {
745 self.f_kb.unwrap_or_default()
746 }
747
748 pub fn get_f_type(&self) -> &str {
750 self.f_type.as_deref().unwrap_or("")
751 }
752
753 pub fn get_f_id(&self) -> &str {
755 self.f_id.as_deref().unwrap_or("")
756 }
757
758 pub fn get_f_origin(&self) -> &str {
760 self.f_origin.as_deref().unwrap_or("")
761 }
762
763 pub fn get_ipc_id(&self) -> &Vec<i32> {
765 self.ipc_id.as_ref().unwrap_or(&EMPTY_VEC)
766 }
767
768 pub fn get_breached_account(&self) -> i8 {
770 self.breached_account.unwrap_or_default()
771 }
772
773 pub fn get_f_access_token(&self) -> &str {
775 self.f_access_token.as_deref().unwrap_or("")
776 }
777
778 pub fn get_inc_id(&self) -> &Vec<i32> {
780 self.inc_id.as_ref().unwrap_or(&EMPTY_VEC)
781 }
782}