use std::time::{Duration, Instant};
use serde_json::Value;
use tokio::time::sleep;
use crate::Result;
use crate::cdp::ChromiumTab;
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');
function rectOf(el){var r=el.getBoundingClientRect();return {x:r.x,y:r.y,w:r.width,h:r.height};}
var box = null, kind = null;
// ① CF/Turnstile iframe:light DOM + 开放 shadow DOM(闭合 shadow 读不到,见 ②)。
var stack=[document];
while(stack.length && !box){
var root=stack.pop(); var els;
try{els=root.querySelectorAll('*');}catch(e){continue;}
for(var i=0;i<els.length;i++){var el=els[i];
if(el.shadowRoot){stack.push(el.shadowRoot);}
if(el.tagName==='IFRAME'){
var src=el.getAttribute('src')||'', id=el.id||'', ti=el.getAttribute('title')||'';
if(/challenges\.cloudflare\.com|turnstile|cdn-cgi\/challenge/i.test(src)
|| /cf-chl-widget/i.test(id)
|| /cloudflare|challenge|verify|验证/i.test(ti)){
var r=rectOf(el); if(r.w>0&&r.h>0){box=r;kind='iframe';break;}
}
}
}
}
// ② respbox:cf-turnstile-response 隐藏域的可见祖先盒(iframe 在闭合 shadow 时唯一可定位的小部件框)。
// CDP 合成鼠标按**屏幕坐标**命中能穿透闭合 shadow / 跨域 iframe 点到复选框。
var resp=document.querySelector('[name=cf-turnstile-response]')
|| document.querySelector('[id^=cf-chl-widget][id$=_response]');
if(!box && resp){
var p=resp.parentElement;
for(var k=0;k<5&&p;k++){var rr=rectOf(p); if(rr.w>=60&&rr.w<=520&&rr.h>=40&&rr.h<=140){box=rr;kind='respbox';break;} p=p.parentElement;}
}
// ③ host 容器兜底。
if(!box){
var host=document.querySelector('.cf-turnstile,[data-sitekey],#cf-turnstile,[class*="turnstile" i]');
if(host){var rh=rectOf(host); if(rh.w>0&&rh.h>0){box=rh;kind='host';}}
}
// 已产出的 Turnstile token(非空=已过盾;widget 过盾后仍留在 DOM,故必须据此判过,不能只看 challenge 消失)。
var token = resp ? (resp.value||'') : '';
var challenge = titleChallenge || optChallenge || box !== null;
return JSON.stringify({ challenge: challenge, box: box, kind: kind, token: token.length, title: document.title });
})()"#;
struct CfState {
challenge: bool,
checkbox: Option<(f64, f64)>,
has_token: bool,
kind: Option<String>,
}
impl ChromiumTab {
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.has_token {
return Ok(true);
}
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,
kind = st.kind.as_deref().unwrap_or("?"),
"点击 Cloudflare Turnstile 复选框(CDP)"
);
self.trusted_click(cx, cy).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)
}
async fn cf_state(&self) -> Result<CfState> {
let probe = parse_probe(self.run_js(PROBE_JS).await?);
let challenge = probe
.get("challenge")
.and_then(Value::as_bool)
.unwrap_or(false);
let has_token = probe.get("token").and_then(Value::as_u64).unwrap_or(0) > 20;
let kind = probe.get("kind").and_then(Value::as_str).map(String::from);
let checkbox = probe.get("box").filter(|b| b.is_object()).map(|b| {
let x = b.get("x").and_then(Value::as_f64).unwrap_or(0.0);
let y = b.get("y").and_then(Value::as_f64).unwrap_or(0.0);
let w = b.get("w").and_then(Value::as_f64).unwrap_or(0.0);
let h = b.get("h").and_then(Value::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,
has_token,
kind,
})
}
async fn trusted_click(&self, x: f64, y: f64) -> Result<()> {
self.mouse_move(x - 8.0, y - 5.0).await?;
sleep(Duration::from_millis(90)).await;
self.mouse_move(x - 2.0, y + 1.0).await?;
sleep(Duration::from_millis(70)).await;
self.mouse_move(x, y).await?;
sleep(Duration::from_millis(130)).await;
self.mouse_down(x, y).await?;
sleep(Duration::from_millis(60)).await;
self.mouse_up(x, y).await?;
Ok(())
}
}
fn parse_probe(v: Value) -> Value {
match v.as_str() {
Some(s) => serde_json::from_str(s).unwrap_or(Value::Null),
None => v,
}
}