drission/cdp/oopif.rs
1//! 附着 **OOPIF(out-of-process iframe,跨域 iframe)/ worker** 子 target,把它当成可独立逆向的 tab。
2//!
3//! 站点隔离(site-isolation)下,跨域 iframe(典型如 Cloudflare Turnstile 的
4//! `challenges.cloudflare.com`)跑在**独立进程**,主 target 的 `Runtime.evaluate` /
5//! `Page.createIsolatedWorld{frameId}` 都进不去([`ChromiumFrame`](crate::cdp::ChromiumFrame)
6//! 适用于**同进程**子帧)。要真正进到这种 iframe 内部逆向,必须用
7//! `Target.setAutoAttach{flatten}` 拿到子 target 的独立 `sessionId`。
8//!
9//! 拿到 `sessionId` 后,本库的 [`CdpCore`] 完全按 session 工作,于是可以把这个 iframe **当成一个
10//! 普通 [`ChromiumTab`]** 来用——在它上面 `scripts()`(dump challenge 脚本)/ `debugger()`(下断点抠
11//! 解扰逻辑)/ `hook()`(偷 key/iv)/ `listen()`(抓包)/ `run_js()`。
12//!
13//! ```no_run
14//! use drission::prelude::*;
15//! use std::time::Duration;
16//! # async fn f(tab: ChromiumTab) -> drission::Result<()> {
17//! // 触发 Turnstile 后,等它的跨域 iframe 被附着:
18//! if let Some(cf) = tab.wait_oopif("challenges.cloudflare.com", Some(Duration::from_secs(8))).await? {
19//! // 进到 iframe 内部 dump 它的 challenge 脚本:
20//! for s in cf.tab().scripts().list().await? {
21//! println!("{} ({}B)", s.url, s.length);
22//! }
23//! }
24//! # Ok(()) }
25//! ```
26
27use std::time::Duration;
28
29use serde_json::{Value, json};
30use tokio::sync::broadcast::error::RecvError;
31use tokio::time::{Instant, timeout};
32
33use super::core::CdpCore;
34use super::tab::ChromiumTab;
35use crate::Result;
36
37/// 一个被附着的子 target(跨域 iframe / worker),带一个可独立驱动 **且可逆向** 的 [`ChromiumTab`]。
38pub struct ChildTarget {
39 /// 子 target id。
40 pub target_id: String,
41 /// 附着会话 id(`flatten` 模式,与主连接复用同一条传输)。
42 pub session_id: String,
43 /// 类型:`iframe` / `worker` / `shared_worker` / `service_worker` / `page` 等。
44 pub kind: String,
45 /// 子 target 的 URL(如 `https://challenges.cloudflare.com/cdn-cgi/challenge-platform/...`)。
46 /// iframe 刚附着、尚未导航时可能为空。
47 pub url: String,
48 tab: ChromiumTab,
49}
50
51impl ChildTarget {
52 /// 该子 target 的可驱动 tab。可在其上 `scripts()` / `debugger()` / `hook()` / `listen()` /
53 /// `run_js()` —— 即把这个跨域 iframe **当成一个普通标签**逆向。
54 pub fn tab(&self) -> &ChromiumTab {
55 &self.tab
56 }
57
58 /// 取走 tab(消费 `self`)。
59 pub fn into_tab(self) -> ChromiumTab {
60 self.tab
61 }
62
63 /// URL 是否包含子串(过滤目标 iframe 用)。
64 pub fn url_contains(&self, s: &str) -> bool {
65 self.url.contains(s)
66 }
67
68 /// 是否是 iframe 类型(过滤掉 worker 等)。
69 pub fn is_iframe(&self) -> bool {
70 self.kind == "iframe"
71 }
72}
73
74impl ChromiumTab {
75 /// 开启对子 target(OOPIF 跨域 iframe / 专用 worker)的**自动附着**,收集 `settle` 时间窗内
76 /// 已附着的全部子 target;每个都带一个可独立逆向的 [`ChromiumTab`]。
77 ///
78 /// 站点隔离下,跨域 iframe(如 Cloudflare Turnstile)跑在独立进程、主 target 进不去;本方法用
79 /// `Target.setAutoAttach{flatten:true}` 拿到它们的独立 `sessionId`,从而能在 iframe 内部
80 /// `scripts()`(dump challenge 脚本)/ `debugger()`(下断点)/ `hook()`(偷 key)/ `listen()`(抓包)。
81 ///
82 /// 语义:先订阅事件再开启 auto-attach —— 开启那一刻会对**当前已存在**的子 target 补发
83 /// `attachedToTarget`,故能拿到调用时刻已存在的子 target;`settle` 是等这些事件汇齐的静默窗
84 /// (典型 1–2s)。**调用时机**:应在「目标 iframe 已出现」之后(如触发 Turnstile、页面渲染完)再调。
85 /// 嵌套 OOPIF 直接对返回项的 `child.tab().attach_oopifs(..)` 继续下钻即可。
86 pub async fn attach_oopifs(&self, settle: Duration) -> Result<Vec<ChildTarget>> {
87 let mut events = self.core.conn.subscribe();
88 self.enable_auto_attach().await?;
89 let mut out: Vec<ChildTarget> = Vec::new();
90 let deadline = Instant::now() + settle;
91 loop {
92 let remain = deadline.saturating_duration_since(Instant::now());
93 if remain.is_zero() {
94 break;
95 }
96 let ev = match timeout(remain, events.recv()).await {
97 Ok(Ok(ev)) => ev,
98 Ok(Err(RecvError::Lagged(_))) => continue,
99 Ok(Err(RecvError::Closed)) => break,
100 Err(_) => break, // settle 到点:正常收尾
101 };
102 if ev.method != "Target.attachedToTarget" {
103 continue;
104 }
105 if let Some(child) = self.child_from_event(&ev.params) {
106 if !out.iter().any(|c| c.session_id == child.session_id) {
107 out.push(child);
108 }
109 }
110 }
111 Ok(out)
112 }
113
114 /// 等待一个 URL 含 `url_substr` 的子 target 被附着(如等 Turnstile 的
115 /// `challenges.cloudflare.com` iframe 出现),返回其可逆向 tab。超时返回 `None`。
116 /// 会先开启 auto-attach;`timeout_dur` 为 `None` 时用本标签默认超时。
117 pub async fn wait_oopif(
118 &self,
119 url_substr: &str,
120 timeout_dur: Option<Duration>,
121 ) -> Result<Option<ChildTarget>> {
122 let mut events = self.core.conn.subscribe();
123 self.enable_auto_attach().await?;
124 let dur = timeout_dur.unwrap_or_else(|| self.core.timeout());
125 let deadline = Instant::now() + dur;
126 loop {
127 let remain = deadline.saturating_duration_since(Instant::now());
128 if remain.is_zero() {
129 return Ok(None);
130 }
131 let ev = match timeout(remain, events.recv()).await {
132 Ok(Ok(ev)) => ev,
133 Ok(Err(RecvError::Lagged(_))) => continue,
134 Ok(Err(RecvError::Closed)) => return Ok(None),
135 Err(_) => return Ok(None),
136 };
137 if ev.method != "Target.attachedToTarget" {
138 continue;
139 }
140 if let Some(child) = self.child_from_event(&ev.params) {
141 if child.url.contains(url_substr) {
142 return Ok(Some(child));
143 }
144 }
145 }
146 }
147
148 /// 在本标签 session 上开启 `flatten` 自动附着(对已存在 + 之后出现的子 target 都发
149 /// `attachedToTarget`)。`waitForDebuggerOnStart:false`:不在子 target 第一行暂停。
150 async fn enable_auto_attach(&self) -> Result<()> {
151 self.core
152 .send(
153 "Target.setAutoAttach",
154 json!({
155 "autoAttach": true,
156 "waitForDebuggerOnStart": false,
157 "flatten": true,
158 }),
159 )
160 .await?;
161 Ok(())
162 }
163
164 /// 从 `Target.attachedToTarget` 事件参数造一个 [`ChildTarget`](含可驱动 tab)。
165 /// **不**预先开任何 CDP 域(`scripts()`/`debugger()` 自己开 `Debugger`、`listen()` 自己开
166 /// `Network`),从而对 iframe 与 worker 都通用(worker 不支持 `Page` 域)。
167 fn child_from_event(&self, params: &Value) -> Option<ChildTarget> {
168 let session_id = params["sessionId"].as_str()?.to_string();
169 let ti = ¶ms["targetInfo"];
170 let target_id = ti["targetId"].as_str().unwrap_or_default().to_string();
171 let kind = ti["type"].as_str().unwrap_or_default().to_string();
172 let url = ti["url"].as_str().unwrap_or_default().to_string();
173 let core = CdpCore::new(
174 self.core.conn.clone(),
175 session_id.clone(),
176 target_id.clone(),
177 self.core.download_dir(),
178 self.core.browser_context_id.clone(),
179 );
180 if !self.core.should_dispose_context_on_close() {
181 core.keep_context_on_close();
182 }
183 Some(ChildTarget {
184 target_id,
185 session_id,
186 kind,
187 url,
188 tab: ChromiumTab::new(core),
189 })
190 }
191}