use super::super::*;
use crate::captcha_detect::DetectedCaptcha;
use std::time::{Duration, Instant};
const DEFAULT_MAX_WAIT_MS: u64 = 20_000;
const REESE84_MIN_LEN: usize = 60;
const INCAP_SES_MIN_LEN: usize = 20;
pub struct ImpervaSolver {
max_wait_ms: u64,
}
impl Default for ImpervaSolver {
fn default() -> Self {
Self::new()
}
}
impl ImpervaSolver {
pub fn new() -> Self {
Self {
max_wait_ms: DEFAULT_MAX_WAIT_MS,
}
}
pub fn with_max_wait_ms(mut self, ms: u64) -> Self {
self.max_wait_ms = ms;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImpervaClearance {
Missing,
Pending,
Valid,
}
pub fn classify_imperva(
reese84: &str,
incap_ses: &str,
challenge_present: bool,
) -> ImpervaClearance {
let reese = reese84.trim().trim_matches('"');
let ses = incap_ses.trim().trim_matches('"');
if reese.len() >= REESE84_MIN_LEN {
return ImpervaClearance::Valid;
}
if !challenge_present && ses.len() >= INCAP_SES_MIN_LEN {
return ImpervaClearance::Valid;
}
if reese.is_empty() && ses.is_empty() {
return ImpervaClearance::Missing;
}
ImpervaClearance::Pending
}
const SIGNAL_PROBE_JS: &str = r#"
(() => {
const body = document.body ? (document.body.innerText || '').toLowerCase() : '';
const blocked = body.includes('request unsuccessful') && body.includes('incapsula incident id');
let challenge = false;
try {
challenge =
!!document.querySelector('iframe[src*="_Incapsula_Resource"], script[src*="_Incapsula_Resource"], [src*="incapsula.com"]') ||
typeof window._Incapsula_Resource !== 'undefined' ||
typeof window.incap_ses !== 'undefined';
} catch (e) { challenge = false; }
let reese = '', incapSes = '', visid = '';
for (const c of (document.cookie || '').split(';')) {
const t = c.trim();
const eq = t.indexOf('=');
if (eq < 0) continue;
const name = t.slice(0, eq);
const val = t.slice(eq + 1);
if (name === 'reese84') reese = val;
else if (name.indexOf('incap_ses_') === 0) incapSes = val;
else if (name.indexOf('visid_incap_') === 0) visid = val;
}
return { blocked: blocked, challenge: challenge, reese84: reese, incapSes: incapSes, visid: visid };
})()
"#;
#[derive(Debug, serde::Deserialize)]
struct ImpervaSignals {
#[serde(default)]
blocked: bool,
#[serde(default)]
challenge: bool,
#[serde(default)]
reese84: String,
#[serde(default, rename = "incapSes")]
incap_ses: String,
#[serde(default)]
visid: String,
}
#[async_trait]
impl CaptchaSolver for ImpervaSolver {
fn name(&self) -> &'static str {
"ImpervaSolver"
}
fn method(&self) -> SolveMethod {
SolveMethod::AutoPass
}
fn supports(&self, kind: &DetectedCaptcha) -> bool {
matches!(kind, DetectedCaptcha::Custom(s)
if s == "imperva_incapsula" || s == "incapsula_protect")
}
async fn solve(&self, page: &Page, _info: &CaptchaInfo) -> Result<CaptchaSolveResult> {
let t0 = Instant::now();
let deadline = t0 + Duration::from_millis(self.max_wait_ms);
loop {
let raw = page
.evaluate(SIGNAL_PROBE_JS)
.await
.map_err(|e| anyhow!("Imperva probe failed: {e}"))?;
let sig: ImpervaSignals = raw.into_value().unwrap_or(ImpervaSignals {
blocked: false,
challenge: true,
reese84: String::new(),
incap_ses: String::new(),
visid: String::new(),
});
let _ = &sig.visid;
if sig.blocked {
return Ok(CaptchaSolveResult::failure(
SolveMethod::AutoPass,
t0.elapsed().as_millis() as u64,
));
}
if classify_imperva(&sig.reese84, &sig.incap_ses, sig.challenge)
== ImpervaClearance::Valid
{
let cookies = crate::cookies::capture_from_page(page)
.await
.unwrap_or_default();
return Ok(CaptchaSolveResult {
solution: "imperva:cookie".into(),
confidence: 1.0,
method: SolveMethod::AutoPass,
time_ms: t0.elapsed().as_millis() as u64,
success: true,
screenshot: None,
cookies,
verified_outcome: None,
});
}
if Instant::now() >= deadline {
return Ok(CaptchaSolveResult::failure(
SolveMethod::AutoPass,
t0.elapsed().as_millis() as u64,
));
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_and_builder() {
assert_eq!(ImpervaSolver::new().max_wait_ms, 20_000);
assert_eq!(
ImpervaSolver::new().with_max_wait_ms(8_000).max_wait_ms,
8_000
);
}
#[test]
fn name_method_stable() {
let s = ImpervaSolver::new();
assert_eq!(s.name(), "ImpervaSolver");
assert_eq!(s.method(), SolveMethod::AutoPass);
}
#[test]
fn supports_only_imperva_custom_kinds() {
let s = ImpervaSolver::new();
assert!(s.supports(&DetectedCaptcha::Custom("imperva_incapsula".into())));
assert!(s.supports(&DetectedCaptcha::Custom("incapsula_protect".into())));
assert!(!s.supports(&DetectedCaptcha::Custom("datadome".into())));
assert!(!s.supports(&DetectedCaptcha::Custom("kasada".into())));
assert!(!s.supports(&DetectedCaptcha::Turnstile));
assert!(!s.supports(&DetectedCaptcha::SliderCaptcha));
}
#[test]
fn reese84_long_is_valid_even_during_challenge() {
let reese = "A1b2C3d4".repeat(20); assert_eq!(classify_imperva(&reese, "", true), ImpervaClearance::Valid);
}
#[test]
fn reese84_short_is_pending() {
assert_eq!(
classify_imperva("short", "", true),
ImpervaClearance::Pending
);
}
#[test]
fn classic_pass_requires_challenge_gone_and_session() {
let ses = "TfjE92kfASdfKJasdf9012xyz"; assert_eq!(classify_imperva("", ses, false), ImpervaClearance::Valid);
assert_eq!(classify_imperva("", ses, true), ImpervaClearance::Pending);
}
#[test]
fn short_session_is_not_a_pass() {
assert_eq!(
classify_imperva("", "abc", false),
ImpervaClearance::Pending
);
}
#[test]
fn nothing_present_is_missing() {
assert_eq!(classify_imperva("", "", true), ImpervaClearance::Missing);
assert_eq!(classify_imperva("", "", false), ImpervaClearance::Missing);
}
#[test]
fn quoted_values_are_trimmed() {
let reese = format!("\"{}\"", "Q1w2E3r4".repeat(20));
assert_eq!(classify_imperva(&reese, "", true), ImpervaClearance::Valid);
}
#[test]
fn signal_probe_js_covers_documented_signals() {
for needle in [
"incapsula incident id",
"_incapsula_resource",
"reese84",
"incap_ses_",
"visid_incap_",
] {
assert!(
SIGNAL_PROBE_JS.to_lowercase().contains(needle),
"SIGNAL_PROBE_JS must reference: {needle}"
);
}
}
#[test]
fn signal_probe_js_is_bracket_balanced() {
let mut depth = 0i32;
for c in SIGNAL_PROBE_JS.chars() {
match c {
'(' | '{' | '[' => depth += 1,
')' | '}' | ']' => depth -= 1,
_ => {}
}
assert!(depth >= 0);
}
assert_eq!(depth, 0);
}
}