use std::time::{Duration, Instant};
use serde_json::Value;
use tokio::time::sleep;
use crate::Result;
use crate::browser::tab::Tab;
const PROBE_JS: &str = r#"(function () {
var t = (document.title || '').toLowerCase();
var titleChallenge = t.indexOf('just a moment') >= 0
|| t.indexOf('attention required') >= 0
|| t.indexOf('checking your browser') >= 0
|| t.indexOf('verifying') >= 0
|| t.indexOf('请稍候') >= 0;
var optChallenge = (typeof window._cf_chl_opt !== 'undefined');
var box = null;
var ifr = document.querySelector(
'iframe[src*="challenges.cloudflare.com"], iframe[id^="cf-chl-widget"], iframe[title*="Cloudflare"], iframe[title*="challenge"], iframe[title*="验证"]'
);
if (ifr) {
var r = ifr.getBoundingClientRect();
if (r.width > 0 && r.height > 0) {
box = { x: r.x, y: r.y, w: r.width, h: r.height };
}
}
var challenge = titleChallenge || optChallenge || box !== null;
return JSON.stringify({ challenge: challenge, box: box, title: document.title });
})()"#;
const DEBUG_JS: &str = r#"(function () {
var ifrs = [];
var list = document.querySelectorAll('iframe');
for (var i = 0; i < list.length; i++) {
var f = list[i];
var r = f.getBoundingClientRect();
ifrs.push({
src: (f.getAttribute('src') || '').slice(0, 100),
id: f.id || '',
title: f.title || '',
x: r.x, y: r.y, w: r.width, h: r.height
});
}
return JSON.stringify({
title: document.title,
iframes: ifrs,
hasChallengeStage: !!document.querySelector('#challenge-stage, #challenge-form, .cf-turnstile, #turnstile-wrapper'),
cfOpt: (typeof window._cf_chl_opt !== 'undefined')
});
})()"#;
struct CfState {
challenge: bool,
checkbox: Option<(f64, f64)>,
}
impl Tab {
pub async fn pass_cloudflare_default(&self) -> Result<bool> {
self.pass_cloudflare(Duration::from_secs(30)).await
}
pub async fn pass_cloudflare(&self, timeout: Duration) -> Result<bool> {
let deadline = Instant::now() + timeout;
let mut clicked = 0u32;
loop {
let st = self.cf_state().await?;
if !st.challenge {
return Ok(true);
}
if Instant::now() >= deadline {
return Ok(false);
}
match st.checkbox {
Some((cx, cy)) => {
tracing::debug!(x = cx, y = cy, "点击 Cloudflare Turnstile 复选框");
self.actions()
.move_to(cx, cy, 0.6)
.wait(0.2)
.click()
.perform()
.await?;
clicked += 1;
sleep(Duration::from_millis(2500)).await;
}
None => {
sleep(Duration::from_millis(1000)).await;
}
}
if clicked > 8 {
sleep(Duration::from_millis(500)).await;
}
}
}
pub async fn is_cloudflare(&self) -> Result<bool> {
Ok(self.cf_state().await?.challenge)
}
pub async fn cloudflare_debug(&self) -> Result<Value> {
let v = self.run_js(DEBUG_JS).await?;
Ok(parse_probe(v))
}
async fn cf_state(&self) -> Result<CfState> {
let v = self.run_js(PROBE_JS).await?;
let probe = parse_probe(v);
let challenge = probe
.get("challenge")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let checkbox = probe.get("box").filter(|b| b.is_object()).map(|b| {
let x = b.get("x").and_then(|v| v.as_f64()).unwrap_or(0.0);
let y = b.get("y").and_then(|v| v.as_f64()).unwrap_or(0.0);
let w = b.get("w").and_then(|v| v.as_f64()).unwrap_or(0.0);
let h = b.get("h").and_then(|v| v.as_f64()).unwrap_or(0.0);
let cx = x + 30.0_f64.min(w / 2.0).max(8.0);
let cy = y + h / 2.0;
(cx, cy)
});
Ok(CfState {
challenge,
checkbox,
})
}
}
fn parse_probe(v: Value) -> Value {
match v.as_str() {
Some(s) => serde_json::from_str(s).unwrap_or(Value::Null),
None => v,
}
}