Skip to main content

glass/browser/session/
intercept.rs

1//! CDP Fetch domain request interception.
2//!
3//! Provides scoped network request interception via the CDP `Fetch`
4//! domain. Use [`BrowserSession::intercept_request`] to enable
5//! interception with a [`RequestPattern`] and obtain an
6//! [`InterceptGuard`] that disables interception on drop.
7//!
8//! This is distinct from the built-in policy Fetch interception used
9//! for URL filtering.
10
11use super::*;
12
13/// Pattern for intercepting network requests via CDP Fetch domain.
14#[derive(Debug, Clone)]
15pub struct RequestPattern {
16    /// Glob pattern or exact URL to match. "*" matches all URLs.
17    pub url_pattern: String,
18    /// Optional resource type filter (e.g. "Document", "XHR", "Script").
19    /// When None, all resource types are intercepted.
20    pub resource_type: Option<String>,
21    /// When to intercept: "Request" or "Response". Defaults to "Request".
22    pub request_stage: String,
23}
24
25impl Default for RequestPattern {
26    fn default() -> Self {
27        Self {
28            url_pattern: "*".to_string(),
29            resource_type: None,
30            request_stage: "Request".to_string(),
31        }
32    }
33}
34
35/// Scoped lease that enables CDP Fetch domain interception.
36///
37/// While this guard is alive, the Fetch domain is enabled for the
38/// active session with the configured patterns. Paused requests
39/// fire Fetch.requestPaused events observable via diagnostics.
40///
41/// On drop, Fetch is disabled for the session.
42pub struct InterceptGuard {
43    cdp: CdpClient,
44    session_id: String,
45    armed: bool,
46}
47
48impl InterceptGuard {
49    /// Enable the Fetch domain and begin interception with the given pattern.
50    async fn enable(cdp: CdpClient, pattern: &RequestPattern) -> BrowserResult<Self> {
51        let session_id = cdp
52            .current_session_id()
53            .ok_or("intercept requires an active page session")?;
54        let resource_type = pattern.resource_type.as_deref().unwrap_or("*");
55        cdp.send_to_session(
56            &session_id,
57            "Fetch.enable",
58            Some(serde_json::json!({
59                "patterns": [{
60                    "urlPattern": pattern.url_pattern,
61                    "resourceType": resource_type,
62                    "requestStage": pattern.request_stage
63                }]
64            })),
65        )
66        .await?;
67        Ok(Self {
68            cdp,
69            session_id,
70            armed: true,
71        })
72    }
73
74    /// Manually disable interception before the guard drops.
75    pub async fn disable(mut self) -> BrowserResult<()> {
76        self.armed = false;
77        disable_fetch_for(&self.cdp, Some(&self.session_id)).await
78    }
79}
80
81impl Drop for InterceptGuard {
82    fn drop(&mut self) {
83        if self.armed {
84            let cdp = self.cdp.clone();
85            let session_id = self.session_id.clone();
86            tokio::spawn(async move {
87                let _ = disable_fetch_for(&cdp, Some(&session_id)).await;
88            });
89        }
90    }
91}
92
93impl BrowserSession {
94    /// Enable CDP Fetch domain interception for the active page session.
95    ///
96    /// Returns a scoped guard that disables interception on drop.
97    /// While active, matching requests fire Fetch.requestPaused events.
98    /// Distinct from the built-in policy Fetch interception.
99    pub async fn intercept_request(
100        &self,
101        pattern: &RequestPattern,
102    ) -> BrowserResult<InterceptGuard> {
103        self.cdp
104            .with_current_route(async { InterceptGuard::enable(self.cdp.clone(), pattern).await })
105            .await
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn request_pattern_default_matches_all_urls() {
115        let pattern = RequestPattern::default();
116        assert_eq!(pattern.url_pattern, "*");
117        assert_eq!(pattern.request_stage, "Request");
118        assert!(pattern.resource_type.is_none());
119    }
120
121    #[test]
122    fn request_pattern_custom_resource_type() {
123        let pattern = RequestPattern {
124            url_pattern: "https://example.com/*".to_string(),
125            resource_type: Some("XHR".to_string()),
126            request_stage: "Response".to_string(),
127        };
128        assert_eq!(pattern.url_pattern, "https://example.com/*");
129        assert_eq!(pattern.resource_type.as_deref(), Some("XHR"));
130        assert_eq!(pattern.request_stage, "Response");
131    }
132
133    #[test]
134    fn request_pattern_is_cloneable() {
135        let pattern = RequestPattern {
136            url_pattern: "/api/*".to_string(),
137            resource_type: Some("Fetch".to_string()),
138            request_stage: "Request".to_string(),
139        };
140        let cloned = pattern.clone();
141        assert_eq!(cloned.url_pattern, "/api/*");
142        assert_eq!(cloned.resource_type.as_deref(), Some("Fetch"));
143        assert_eq!(cloned.request_stage, "Request");
144        // Verify original is unmodified
145        assert_eq!(pattern.url_pattern, "/api/*");
146    }
147
148    #[test]
149    fn request_pattern_debug_output_includes_fields() {
150        let pattern = RequestPattern::default();
151        let debug = format!("{:?}", pattern);
152        assert!(debug.contains("*"));
153        assert!(debug.contains("Request"));
154    }
155}