use crate::px_debug;
use crate::pxcontext::RiskHeader;
use base64::{Engine as _, engine::general_purpose};
use fastly::{Request, http::Version};
use regex::Regex;
use std::collections::HashMap;
use std::net::Ipv4Addr;
use std::sync::OnceLock;
const UUID_PATTERN: &str =
r"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$";
static UUID_VALIDATOR: OnceLock<Regex> = OnceLock::new();
fn uuid_validator() -> Option<&'static Regex> {
if let Some(re) = UUID_VALIDATOR.get() {
return Some(re);
}
let Ok(re) = Regex::new(UUID_PATTERN) else {
return None;
};
let _ = UUID_VALIDATOR.set(re);
UUID_VALIDATOR.get()
}
pub fn is_valid_uuid(uuid: &str) -> bool {
!uuid.is_empty() && uuid_validator().is_some_and(|re| re.is_match(uuid))
}
pub fn get_risk_headers(
headers_json: &serde_json::value::Value,
sensitive_headers: &[String],
) -> Vec<RiskHeader> {
let mut sanitized_headers = vec![];
if let Some(headers_obj) = headers_json.as_object() {
for (k, v) in headers_obj {
if !sensitive_headers.iter().any(|h| h.eq_ignore_ascii_case(k)) {
sanitized_headers.push(RiskHeader {
name: k.into(),
value: v.to_owned(),
})
}
}
}
sanitized_headers
}
pub fn filter_headers(req: &mut Request, sensitive_headers: &Vec<String>) {
for h in sensitive_headers {
req.remove_header(h);
}
}
pub fn parse_cookie_header(cookie_header: &str) -> HashMap<String, String> {
cookie_header
.split(";")
.filter_map(|kv| {
let kv = kv.trim();
if kv.is_empty() {
return None;
}
let (key, value) = kv.split_once('=')?;
let key = key.trim();
if key.is_empty() {
return None;
}
Some((key.to_string(), value.trim().to_string()))
})
.collect()
}
pub fn merge_cookie_headers(
base: HashMap<String, String>,
override_cookies: HashMap<String, String>,
) -> HashMap<String, String> {
let mut merged = base;
merged.extend(override_cookies);
merged
}
pub fn build_merged_request_cookies(
cookie_header: &str,
custom_cookie_header: &str,
) -> HashMap<String, String> {
merge_cookie_headers(
parse_cookie_header(cookie_header),
parse_cookie_header(custom_cookie_header),
)
}
pub fn extract_cookie_value(cookies: &HashMap<String, String>, name: &str) -> String {
cookies.get(name).cloned().unwrap_or_default()
}
pub(crate) fn first_ip_from_header_value(value: &str) -> Option<String> {
value
.split(',')
.map(str::trim)
.find(|part| !part.is_empty())
.map(str::to_owned)
}
pub(crate) fn extract_ip_from_configured_headers(
req: &Request,
ip_headers: &[String],
) -> Option<String> {
extract_ip_from_header_values(ip_headers, |name| {
req.get_header_str_lossy(name).map(|v| v.into_owned())
})
}
pub(crate) fn extract_ip_from_header_values(
ip_headers: &[String],
get_header: impl Fn(&str) -> Option<String>,
) -> Option<String> {
for header_name in ip_headers {
if header_name.is_empty() {
continue;
}
if let Some(header_value) = get_header(header_name.as_str()) {
if let Some(ip) = first_ip_from_header_value(header_value.as_str()) {
return Some(ip);
}
}
}
None
}
fn ipv4_cidr_mask(prefix: u8) -> Option<u32> {
match prefix {
0 => Some(0),
8 => Some(0xFF00_0000),
16 => Some(0xFFFF_0000),
24 => Some(0xFFFF_FF00),
32 => Some(0xFFFF_FFFF),
_ => None,
}
}
pub(crate) fn ip_matches_filter(client_ip: &str, filter: &str) -> bool {
let Ok(client) = client_ip.trim().parse::<Ipv4Addr>() else {
return false;
};
let filter = filter.trim();
if let Some((network_str, prefix_str)) = filter.split_once('/') {
let Ok(prefix) = prefix_str.trim().parse::<u8>() else {
return false;
};
let Some(mask) = ipv4_cidr_mask(prefix) else {
return false;
};
let Ok(network) = network_str.trim().parse::<Ipv4Addr>() else {
return false;
};
let client_bits = u32::from(client);
let network_bits = u32::from(network);
(client_bits & mask) == (network_bits & mask)
} else {
filter
.parse::<Ipv4Addr>()
.is_ok_and(|expected| client == expected)
}
}
pub(crate) fn is_mobile_sdk_error_code(value: &str) -> bool {
!value.is_empty() && value.chars().all(|c| c.is_ascii_digit())
}
pub(crate) fn parse_versioned_mobile_token(value: &str) -> Option<(String, String)> {
let value = value.trim();
let (version, contents) = value.split_once(':')?;
if contents.is_empty() {
return None;
}
match version {
"2" => Some(("_px2".to_owned(), contents.to_owned())),
"3" => Some(("_px3".to_owned(), contents.to_owned())),
_ => None,
}
}
pub fn verify_route(routes: &[String], path: &str) -> bool {
routes
.iter()
.any(|prefix| !prefix.is_empty() && path.starts_with(prefix))
}
pub fn get_headers_as_json(req: &Request) -> serde_json::value::Value {
let mut headers_json = serde_json::Map::new();
for h in req.get_header_names_str() {
headers_json.insert(
h.to_string(),
serde_json::Value::String(
req.get_header_str_lossy(h)
.map(|v| v.into_owned())
.unwrap_or_default(),
),
);
}
serde_json::Value::Object(headers_json)
}
pub fn decode_jwt_payload(jwt: &str) -> Option<serde_json::Value> {
let mut segments = jwt.split('.');
let _header = segments.next()?;
let encoded_payload = segments.next()?;
let _signature = segments.next()?;
if encoded_payload.is_empty() {
return None;
}
let payload_bytes = general_purpose::URL_SAFE_NO_PAD
.decode(encoded_payload)
.or_else(|_| {
let mut normalized = encoded_payload.replace('-', "+").replace('_', "/");
let rem = normalized.len() % 4;
if rem > 0 {
normalized.push_str(&"=".repeat(4 - rem));
}
general_purpose::STANDARD.decode(normalized)
})
.ok()?;
serde_json::from_slice(&payload_bytes).ok()
}
pub fn get_value_at_path<'a>(
value: &'a serde_json::Value,
path: &str,
) -> Option<&'a serde_json::Value> {
if path.is_empty() {
return None;
}
let mut current = value;
for part in path.split('.') {
if part.is_empty() {
return None;
}
current = current.get(part)?;
}
Some(current)
}
pub fn is_truthy_json_value(value: &serde_json::Value) -> bool {
match value {
serde_json::Value::Null => false,
serde_json::Value::Bool(b) => *b,
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
i != 0
} else if let Some(u) = n.as_u64() {
u != 0
} else if let Some(f) = n.as_f64() {
f != 0.0
} else {
true
}
}
serde_json::Value::String(s) => !s.is_empty(),
serde_json::Value::Array(a) => !a.is_empty(),
serde_json::Value::Object(o) => !o.is_empty(),
}
}
pub fn get_cookie_json(px_cookie: &str) -> serde_json::Value {
let default_json = serde_json::json!({});
let px_cookie_bytes = general_purpose::STANDARD
.decode(px_cookie)
.unwrap_or_default();
let decoded_px_cookie = std::str::from_utf8(&px_cookie_bytes).unwrap_or_default();
if !decoded_px_cookie.is_empty() {
let cookie_json: serde_json::Value =
serde_json::from_str(decoded_px_cookie).unwrap_or_default();
if !cookie_json.is_object() {
px_debug!(
"cookie value is not a valid json, value: {}",
decoded_px_cookie
);
return default_json;
}
return cookie_json;
};
default_json
}
pub fn get_fastly_version_str(req: &Request) -> &'static str {
match req.get_version() {
Version::HTTP_09 => "0.9",
Version::HTTP_10 => "1.0",
Version::HTTP_11 => "1.1",
Version::HTTP_2 => "2.0",
Version::HTTP_3 => "3.0",
_ => "unknown",
}
}
pub fn set_json_str_at_path(root: &mut serde_json::Value, path: &str, value: &str) {
if value.is_empty() {
return;
}
let parts: Vec<&str> = path.split('.').collect();
let mut current: &mut serde_json::Value = root;
for (i, part) in parts.iter().enumerate() {
if i == parts.len() - 1 {
current[*part] = serde_json::Value::String(value.to_string());
} else {
if !current.get(*part).is_some_and(|v| v.is_object()) {
current[*part] = serde_json::json!({});
}
current = &mut current[*part];
}
}
}
macro_rules! set_json_str {
($root:expr, $path:expr; $value:expr) => {{
use $crate::modules::pxutils::set_json_str_at_path;
let str_value: String = $value.to_string();
set_json_str_at_path($root, $path, &str_value);
}};
}
pub(crate) use set_json_str;
pub fn set_json_int_at_path(root: &mut serde_json::Value, path: &str, value: i64) {
let parts: Vec<&str> = path.split('.').collect();
let mut current: &mut serde_json::Value = root;
for (i, part) in parts.iter().enumerate() {
if i == parts.len() - 1 {
current[*part] = serde_json::Value::Number(serde_json::Number::from(value));
} else {
if !current.get(*part).is_some_and(|v| v.is_object()) {
current[*part] = serde_json::json!({});
}
current = &mut current[*part];
}
}
}
macro_rules! set_json_int {
($root:expr, $path:expr; $value:expr) => {{
use $crate::modules::pxutils::set_json_int_at_path;
let int_value: i64 = $value as i64;
set_json_int_at_path($root, $path, int_value);
}};
}
pub(crate) use set_json_int;