use super::super::*;
use crate::captcha_detect::DetectedCaptcha;
use std::time::{Duration, Instant};
const DEFAULT_MAX_WAIT_MS: u64 = 20_000;
const KP_UIDZ_MIN_LEN: usize = 24;
pub struct KasadaSolver {
max_wait_ms: u64,
}
impl Default for KasadaSolver {
fn default() -> Self {
Self::new()
}
}
impl KasadaSolver {
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 KasadaClearance {
Missing,
Pending,
Valid,
}
pub fn classify_kasada(kp_uidz: &str, challenge_present: bool) -> KasadaClearance {
let kp = kp_uidz.trim().trim_matches('"');
if kp.is_empty() {
return KasadaClearance::Missing;
}
if kp.len() >= KP_UIDZ_MIN_LEN && !challenge_present {
return KasadaClearance::Valid;
}
KasadaClearance::Pending
}
const SIGNAL_PROBE_JS: &str = r#"
(() => {
let kpsdk = false;
try { kpsdk = typeof window.KPSDK !== 'undefined'; } catch (e) { kpsdk = false; }
// The interruption is "active" while ips.js is referenced AND KPSDK has not
// yet signalled ready. Once the real content loads, ips.js is no longer the
// document's controlling script.
let ipsPresent = false;
try {
ipsPresent = !!document.querySelector('script[src*="/ips.js"], script[src*="/149e"], [id*="kpsdk"]');
} catch (e) { ipsPresent = false; }
let kready = false;
try { kready = !!(window.KPSDK && (window.KPSDK.configured || window.KPSDK.message)); } catch (e) { kready = false; }
const challenge = ipsPresent && !kready;
let kp = '';
for (const c of (document.cookie || '').split(';')) {
const t = c.trim();
if (t.indexOf('KP_UIDz=') === 0) { kp = t.slice('KP_UIDz='.length); break; }
}
return { challenge: challenge, kpUidz: kp, kpsdk: kpsdk };
})()
"#;
#[derive(Debug, serde::Deserialize)]
struct KasadaSignals {
#[serde(default)]
challenge: bool,
#[serde(default, rename = "kpUidz")]
kp_uidz: String,
#[serde(default)]
kpsdk: bool,
}
#[async_trait]
impl CaptchaSolver for KasadaSolver {
fn name(&self) -> &'static str {
"KasadaSolver"
}
fn method(&self) -> SolveMethod {
SolveMethod::AutoPass
}
fn supports(&self, kind: &DetectedCaptcha) -> bool {
matches!(kind, DetectedCaptcha::Custom(s) if s == "kasada")
}
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!("Kasada probe failed: {e}"))?;
let sig: KasadaSignals = raw.into_value().unwrap_or(KasadaSignals {
challenge: true,
kp_uidz: String::new(),
kpsdk: false,
});
if sig.kpsdk && classify_kasada(&sig.kp_uidz, sig.challenge) == KasadaClearance::Valid {
let cookies = crate::cookies::capture_from_page(page)
.await
.unwrap_or_default();
return Ok(CaptchaSolveResult {
solution: "kasada: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!(KasadaSolver::new().max_wait_ms, 20_000);
assert_eq!(
KasadaSolver::new().with_max_wait_ms(9_000).max_wait_ms,
9_000
);
}
#[test]
fn name_method_stable() {
let s = KasadaSolver::new();
assert_eq!(s.name(), "KasadaSolver");
assert_eq!(s.method(), SolveMethod::AutoPass);
}
#[test]
fn supports_only_kasada_custom() {
let s = KasadaSolver::new();
assert!(s.supports(&DetectedCaptcha::Custom("kasada".into())));
assert!(!s.supports(&DetectedCaptcha::Custom("datadome".into())));
assert!(!s.supports(&DetectedCaptcha::Custom("imperva_incapsula".into())));
assert!(!s.supports(&DetectedCaptcha::Turnstile));
}
#[test]
fn long_token_no_challenge_is_valid() {
let tok = "kpuidz".repeat(6); assert_eq!(classify_kasada(&tok, false), KasadaClearance::Valid);
}
#[test]
fn long_token_with_challenge_is_pending() {
let tok = "kpuidz".repeat(6);
assert_eq!(classify_kasada(&tok, true), KasadaClearance::Pending);
}
#[test]
fn short_token_is_pending() {
assert_eq!(classify_kasada("short", false), KasadaClearance::Pending);
}
#[test]
fn empty_token_is_missing() {
assert_eq!(classify_kasada("", false), KasadaClearance::Missing);
assert_eq!(classify_kasada("\"\"", false), KasadaClearance::Missing);
}
#[test]
fn quoted_token_trimmed() {
let tok = format!("\"{}\"", "kpuidz".repeat(6));
assert_eq!(classify_kasada(&tok, false), KasadaClearance::Valid);
}
#[test]
fn signal_probe_js_covers_documented_signals() {
for needle in ["kpsdk", "ips.js", "kp_uidz="] {
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);
}
}