Skip to main content

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 = &params["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}