use http::HeaderMap;
use super::cookie_pin::{
CookiePinStore, AKAMAI_ABCK_TTL_SECS, DATADOME_TTL_SECS, PERIMETERX_TTL_SECS,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BypassLevel {
#[default]
None,
Replay,
Aggressive,
}
impl BypassLevel {
pub fn as_str(&self) -> &'static str {
match self {
Self::None => "none",
Self::Replay => "replay",
Self::Aggressive => "aggressive",
}
}
pub fn parse(s: &str) -> Result<Self, String> {
match s.to_ascii_lowercase().as_str() {
"" | "none" | "off" | "disabled" => Ok(Self::None),
"replay" | "pin" | "cookie" => Ok(Self::Replay),
"aggressive" | "active" | "full" => Ok(Self::Aggressive),
other => Err(format!("unknown antibot-bypass level: {other}")),
}
}
pub fn allows_replay(&self) -> bool {
matches!(self, Self::Replay | Self::Aggressive)
}
pub fn allows_aggressive(&self) -> bool {
matches!(self, Self::Aggressive)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CapturedCookie {
pub vendor: &'static str,
pub name: String,
pub value: String,
pub ttl_secs: u64,
}
pub fn capture_from_headers(headers: &HeaderMap, status: u16) -> Vec<CapturedCookie> {
let mut out = Vec::new();
for raw in headers.get_all("set-cookie") {
let Some(line) = raw.to_str().ok() else {
continue;
};
let (head, _attrs) = line.split_once(';').unwrap_or((line, ""));
let Some((name_raw, value_raw)) = head.split_once('=') else {
continue;
};
let name = name_raw.trim();
let value = value_raw.trim();
if name.is_empty() || value.is_empty() {
continue;
}
if name == "_abck" && !is_akamai_unsolved(value) {
out.push(CapturedCookie {
vendor: "akamai",
name: name.into(),
value: value.into(),
ttl_secs: AKAMAI_ABCK_TTL_SECS,
});
continue;
}
if name == "datadome" && (status >= 400 || status == 0) {
out.push(CapturedCookie {
vendor: "datadome",
name: name.into(),
value: value.into(),
ttl_secs: DATADOME_TTL_SECS,
});
continue;
}
if is_perimeterx_name(name) {
out.push(CapturedCookie {
vendor: "perimeterx",
name: name.into(),
value: value.into(),
ttl_secs: PERIMETERX_TTL_SECS,
});
}
}
out
}
pub fn pin_captured(
store: &dyn CookiePinStore,
origin: &str,
captured: &[CapturedCookie],
) -> usize {
let mut n = 0;
for c in captured {
if store
.pin(c.vendor, origin, &c.name, &c.value, c.ttl_secs)
.is_ok()
{
n += 1;
}
}
n
}
fn is_akamai_unsolved(value: &str) -> bool {
value.contains("~-1~-1") || value.len() < 32
}
fn is_perimeterx_name(name: &str) -> bool {
match name {
"_pxvid" | "_pxhd" | "_pxde" => true,
n => n.starts_with("_px") && n.len() > 3 && n.as_bytes()[3].is_ascii_digit(),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TurnstileAttemptOutcome {
NotAttempted(&'static str),
Prepared {
sitekey: String,
endpoint: &'static str,
dummy_token: &'static str,
},
}
pub const TURNSTILE_DUMMY_TOKEN: &str = "XXXX.DUMMY.TOKEN.XXXX";
pub const TURNSTILE_CHALLENGE_ENDPOINT: &str =
"https://challenges.cloudflare.com/turnstile/v0/api.js";
#[derive(Debug, Clone)]
pub struct TurnstileAttempt {
pub outcome: TurnstileAttemptOutcome,
}
pub fn prepare_turnstile_attempt(
level: BypassLevel,
sitekey: Option<&str>,
invisible_widget: bool,
) -> TurnstileAttempt {
if !level.allows_aggressive() {
return TurnstileAttempt {
outcome: TurnstileAttemptOutcome::NotAttempted("level=<aggressive"),
};
}
if !invisible_widget {
return TurnstileAttempt {
outcome: TurnstileAttemptOutcome::NotAttempted("widget_not_invisible"),
};
}
let Some(key) = sitekey.filter(|s| !s.is_empty()) else {
return TurnstileAttempt {
outcome: TurnstileAttemptOutcome::NotAttempted("no_sitekey"),
};
};
TurnstileAttempt {
outcome: TurnstileAttemptOutcome::Prepared {
sitekey: key.into(),
endpoint: TURNSTILE_CHALLENGE_ENDPOINT,
dummy_token: TURNSTILE_DUMMY_TOKEN,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use http::HeaderMap;
fn headers(set_cookie: &[&str]) -> HeaderMap {
let mut h = HeaderMap::new();
for v in set_cookie {
h.append("set-cookie", v.parse().unwrap());
}
h
}
#[test]
fn bypass_level_parses_and_defaults_to_none() {
assert_eq!(BypassLevel::default(), BypassLevel::None);
assert_eq!(BypassLevel::parse("none").unwrap(), BypassLevel::None);
assert_eq!(BypassLevel::parse("replay").unwrap(), BypassLevel::Replay);
assert_eq!(
BypassLevel::parse("aggressive").unwrap(),
BypassLevel::Aggressive
);
assert!(BypassLevel::parse("garbage").is_err());
assert!(!BypassLevel::None.allows_replay());
assert!(BypassLevel::Replay.allows_replay());
assert!(!BypassLevel::Replay.allows_aggressive());
assert!(BypassLevel::Aggressive.allows_aggressive());
}
#[test]
fn capture_classifies_vendor_cookies() {
let h = headers(&[
"_abck=6A1B2C3D4E5F6A7B8C9D0123456789ABCDEF~challenge-solved~; Path=/",
"datadome=abc123def456; Path=/; Max-Age=3600",
"_px3=ABCDEF123456; Path=/",
"unrelated=value",
]);
let caps = capture_from_headers(&h, 403);
let vendors: Vec<_> = caps.iter().map(|c| c.vendor).collect();
assert!(vendors.contains(&"akamai"));
assert!(vendors.contains(&"datadome"));
assert!(vendors.contains(&"perimeterx"));
assert_eq!(caps.len(), 3);
}
#[test]
fn capture_rejects_unsolved_akamai_cookie() {
let h = headers(&["_abck=short~-1~-1~-1"]);
let caps = capture_from_headers(&h, 403);
assert!(caps.is_empty());
}
#[test]
fn capture_skips_datadome_on_2xx() {
let h = headers(&["datadome=abc123def456; Path=/"]);
let caps = capture_from_headers(&h, 200);
assert!(caps.is_empty(), "only pin datadome on 4xx retry loop");
}
#[test]
fn turnstile_attempt_requires_aggressive_level() {
let a = prepare_turnstile_attempt(BypassLevel::None, Some("0x4AAA"), true);
assert!(matches!(
a.outcome,
TurnstileAttemptOutcome::NotAttempted(_)
));
let b = prepare_turnstile_attempt(BypassLevel::Replay, Some("0x4AAA"), true);
assert!(matches!(
b.outcome,
TurnstileAttemptOutcome::NotAttempted(_)
));
let c = prepare_turnstile_attempt(BypassLevel::Aggressive, Some("0x4AAA"), true);
assert!(matches!(
c.outcome,
TurnstileAttemptOutcome::Prepared { .. }
));
}
#[test]
fn turnstile_attempt_needs_sitekey_and_invisible_flag() {
let no_key = prepare_turnstile_attempt(BypassLevel::Aggressive, None, true);
assert!(matches!(
no_key.outcome,
TurnstileAttemptOutcome::NotAttempted("no_sitekey")
));
let visible = prepare_turnstile_attempt(BypassLevel::Aggressive, Some("0x4AAA"), false);
assert!(matches!(
visible.outcome,
TurnstileAttemptOutcome::NotAttempted("widget_not_invisible")
));
}
#[test]
fn pin_captured_writes_to_store() {
use super::super::cookie_pin::InMemoryCookiePinStore;
let store = InMemoryCookiePinStore::new();
let h = headers(&[
"_abck=6A1B2C3D4E5F6A7B8C9D0123456789ABCDEF~solved~; Path=/",
"datadome=abc123def456; Path=/",
]);
let caps = capture_from_headers(&h, 403);
let n = pin_captured(&store, "https://a.test", &caps);
assert_eq!(n, 2);
let abck = store
.get_pinned("akamai", "https://a.test", "_abck")
.unwrap();
assert!(abck.is_some());
}
}