Skip to main content

firefox_webdriver/browser/
network.rs

1//! Network interception types.
2//!
3//! Types for request/response interception callbacks.
4//!
5//! # Request Interception
6//!
7//! ```ignore
8//! use firefox_webdriver::RequestAction;
9//!
10//! let intercept_id = tab.intercept_request(|req| {
11//!     if req.url.contains("ads") {
12//!         RequestAction::block()
13//!     } else {
14//!         RequestAction::allow()
15//!     }
16//! }).await?;
17//! ```
18//!
19//! # Response Interception
20//!
21//! ```ignore
22//! use firefox_webdriver::BodyAction;
23//!
24//! let intercept_id = tab.intercept_response_body(|res| {
25//!     if res.url.contains("config.json") {
26//!         BodyAction::modify_body(r#"{"modified": true}"#)
27//!     } else {
28//!         BodyAction::allow()
29//!     }
30//! }).await?;
31//! ```
32
33// ============================================================================
34// Imports
35// ============================================================================
36
37use std::collections::HashMap;
38
39// ============================================================================
40// InterceptedRequest
41// ============================================================================
42
43/// Data about an intercepted network request.
44#[derive(Debug, Clone)]
45pub struct InterceptedRequest {
46    /// Unique request ID.
47    pub request_id: String,
48
49    /// Request URL.
50    pub url: String,
51
52    /// HTTP method (GET, POST, etc.).
53    pub method: String,
54
55    /// Request headers.
56    pub headers: HashMap<String, String>,
57
58    /// Resource type (document, script, xhr, etc.).
59    pub resource_type: String,
60
61    /// Tab ID where request originated.
62    pub tab_id: u32,
63
64    /// Frame ID where request originated.
65    pub frame_id: u64,
66
67    /// Request body (if available).
68    pub body: Option<RequestBody>,
69}
70
71// ============================================================================
72// RequestBody
73// ============================================================================
74
75/// Request body data.
76#[derive(Debug, Clone)]
77pub enum RequestBody {
78    /// Form data (application/x-www-form-urlencoded or multipart/form-data).
79    FormData(HashMap<String, Vec<String>>),
80
81    /// Raw bytes (base64 encoded).
82    Raw(Vec<u8>),
83
84    /// Error reading body.
85    Error(String),
86}
87
88// ============================================================================
89// RequestAction
90// ============================================================================
91
92/// Action to take for an intercepted request.
93#[derive(Debug, Clone)]
94pub enum RequestAction {
95    /// Allow the request to proceed.
96    Allow,
97
98    /// Block/cancel the request.
99    Block,
100
101    /// Redirect to a different URL.
102    Redirect(String),
103}
104
105// ============================================================================
106// RequestAction - Constructors
107// ============================================================================
108
109impl RequestAction {
110    /// Creates an Allow action.
111    #[inline]
112    #[must_use]
113    pub fn allow() -> Self {
114        Self::Allow
115    }
116
117    /// Creates a Block action.
118    #[inline]
119    #[must_use]
120    pub fn block() -> Self {
121        Self::Block
122    }
123
124    /// Creates a Redirect action.
125    #[inline]
126    #[must_use]
127    pub fn redirect(url: impl Into<String>) -> Self {
128        Self::Redirect(url.into())
129    }
130}
131
132// ============================================================================
133// InterceptedRequestBody
134// ============================================================================
135
136/// Data about an intercepted request body (read-only, cannot be modified).
137///
138/// Browser limitation: request body cannot be modified, only inspected.
139#[derive(Debug, Clone)]
140pub struct InterceptedRequestBody {
141    /// Unique request ID.
142    pub request_id: String,
143
144    /// Request URL.
145    pub url: String,
146
147    /// HTTP method.
148    pub method: String,
149
150    /// Resource type (document, script, xhr, etc.).
151    pub resource_type: String,
152
153    /// Tab ID.
154    pub tab_id: u32,
155
156    /// Frame ID.
157    pub frame_id: u64,
158
159    /// Request body (if available).
160    pub body: Option<RequestBody>,
161}
162
163// ============================================================================
164// InterceptedRequestHeaders
165// ============================================================================
166
167/// Data about intercepted request headers.
168#[derive(Debug, Clone)]
169pub struct InterceptedRequestHeaders {
170    /// Unique request ID.
171    pub request_id: String,
172
173    /// Request URL.
174    pub url: String,
175
176    /// HTTP method.
177    pub method: String,
178
179    /// Request headers.
180    pub headers: HashMap<String, String>,
181
182    /// Tab ID.
183    pub tab_id: u32,
184
185    /// Frame ID.
186    pub frame_id: u64,
187}
188
189// ============================================================================
190// HeadersAction
191// ============================================================================
192
193/// Action to take for intercepted headers.
194#[derive(Debug, Clone)]
195pub enum HeadersAction {
196    /// Allow headers to proceed unchanged.
197    Allow,
198
199    /// Modify headers with optional status code override.
200    Modify {
201        /// Headers to set/replace.
202        headers: HashMap<String, String>,
203        /// Optional HTTP status code override.
204        status_code: Option<u16>,
205    },
206
207    /// Block/cancel the request.
208    Block,
209}
210
211// ============================================================================
212// HeadersAction - Constructors
213// ============================================================================
214
215impl HeadersAction {
216    /// Creates an Allow action.
217    #[inline]
218    #[must_use]
219    pub fn allow() -> Self {
220        Self::Allow
221    }
222
223    /// Creates a Block action.
224    #[inline]
225    #[must_use]
226    pub fn block() -> Self {
227        Self::Block
228    }
229
230    /// Creates a Modify action with headers only (no status code change).
231    #[inline]
232    #[must_use]
233    pub fn modify_headers(headers: HashMap<String, String>) -> Self {
234        Self::Modify {
235            headers,
236            status_code: None,
237        }
238    }
239
240    /// Creates a Modify action with headers and status code override.
241    #[inline]
242    #[must_use]
243    pub fn modify_headers_with_status(
244        headers: HashMap<String, String>,
245        status_code: u16,
246    ) -> Self {
247        Self::Modify {
248            headers,
249            status_code: Some(status_code),
250        }
251    }
252}
253
254// ============================================================================
255// InterceptedResponse
256// ============================================================================
257
258/// Data about an intercepted network response.
259#[derive(Debug, Clone)]
260pub struct InterceptedResponse {
261    /// Unique request ID.
262    pub request_id: String,
263
264    /// Request URL.
265    pub url: String,
266
267    /// HTTP status code.
268    pub status: u16,
269
270    /// HTTP status text.
271    pub status_text: String,
272
273    /// Response headers.
274    pub headers: HashMap<String, String>,
275
276    /// Tab ID where request originated.
277    pub tab_id: u32,
278
279    /// Frame ID where request originated.
280    pub frame_id: u64,
281}
282
283// ============================================================================
284// ResponseAction
285// ============================================================================
286
287/// Action to take for intercepted response headers.
288#[derive(Debug, Clone)]
289pub enum ResponseAction {
290    /// Allow the response to proceed unchanged.
291    Allow,
292
293    /// Block/cancel the response.
294    Block,
295
296    /// Modify response body content.
297    Modify {
298        /// Replacement body content.
299        body: String,
300    },
301
302    /// Fulfill the response with fully synthetic data (complete response mocking).
303    Fulfill {
304        /// HTTP status code.
305        status: u16,
306        /// Response headers.
307        headers: HashMap<String, String>,
308        /// Response body.
309        body: String,
310    },
311}
312
313// ============================================================================
314// ResponseAction - Constructors
315// ============================================================================
316
317impl ResponseAction {
318    /// Creates an Allow action.
319    #[inline]
320    #[must_use]
321    pub fn allow() -> Self {
322        Self::Allow
323    }
324
325    /// Creates a Block action.
326    #[inline]
327    #[must_use]
328    pub fn block() -> Self {
329        Self::Block
330    }
331
332    /// Creates a Modify action that replaces the response body.
333    #[inline]
334    #[must_use]
335    pub fn modify(body: impl Into<String>) -> Self {
336        Self::Modify { body: body.into() }
337    }
338
339    /// Creates a Fulfill action for complete response mocking.
340    #[inline]
341    #[must_use]
342    pub fn fulfill(
343        status: u16,
344        headers: HashMap<String, String>,
345        body: impl Into<String>,
346    ) -> Self {
347        Self::Fulfill {
348            status,
349            headers,
350            body: body.into(),
351        }
352    }
353}
354
355// ============================================================================
356// InterceptedResponseBody
357// ============================================================================
358
359/// Data about an intercepted response body.
360#[derive(Debug, Clone)]
361pub struct InterceptedResponseBody {
362    /// Unique request ID.
363    pub request_id: String,
364
365    /// Request URL.
366    pub url: String,
367
368    /// HTTP status code.
369    pub status: u16,
370
371    /// Content-Type of the response.
372    pub content_type: String,
373
374    /// Response body data (text or binary).
375    pub body: ResponseBodyData,
376
377    /// Tab ID.
378    pub tab_id: u32,
379
380    /// Frame ID.
381    pub frame_id: u64,
382
383    /// Content length.
384    pub content_length: usize,
385}
386
387// ============================================================================
388// ResponseBodyData
389// ============================================================================
390
391/// Response body content, supporting both text and binary data.
392#[derive(Debug, Clone)]
393pub enum ResponseBodyData {
394    /// Text body content (UTF-8 string).
395    Text(String),
396
397    /// Binary body content (base64-decoded from extension).
398    Binary(Vec<u8>),
399}
400
401impl ResponseBodyData {
402    /// Returns the body as a string reference if it is text.
403    #[inline]
404    #[must_use]
405    pub fn as_text(&self) -> Option<&str> {
406        match self {
407            Self::Text(s) => Some(s),
408            Self::Binary(_) => None,
409        }
410    }
411
412    /// Returns the body as a byte slice if it is binary.
413    #[inline]
414    #[must_use]
415    pub fn as_bytes(&self) -> Option<&[u8]> {
416        match self {
417            Self::Text(_) => None,
418            Self::Binary(b) => Some(b),
419        }
420    }
421
422    /// Returns true if the body is text.
423    #[inline]
424    #[must_use]
425    pub fn is_text(&self) -> bool {
426        matches!(self, Self::Text(_))
427    }
428
429    /// Returns true if the body is binary.
430    #[inline]
431    #[must_use]
432    pub fn is_binary(&self) -> bool {
433        matches!(self, Self::Binary(_))
434    }
435}
436
437// ============================================================================
438// BodyAction
439// ============================================================================
440
441/// Action to take for intercepted response body.
442#[derive(Debug, Clone)]
443pub enum BodyAction {
444    /// Allow body to proceed unchanged.
445    Allow,
446
447    /// Modify body content.
448    ModifyBody(String),
449}
450
451// ============================================================================
452// BodyAction - Constructors
453// ============================================================================
454
455impl BodyAction {
456    /// Creates an Allow action.
457    #[inline]
458    #[must_use]
459    pub fn allow() -> Self {
460        Self::Allow
461    }
462
463    /// Creates a ModifyBody action.
464    #[inline]
465    #[must_use]
466    pub fn modify_body(body: impl Into<String>) -> Self {
467        Self::ModifyBody(body.into())
468    }
469}
470
471// ============================================================================
472// Tests
473// ============================================================================
474
475#[cfg(test)]
476mod tests {
477    use super::{BodyAction, HeadersAction, RequestAction, ResponseAction, ResponseBodyData};
478
479    use std::collections::HashMap;
480
481    #[test]
482    fn test_request_action_allow() {
483        let action = RequestAction::allow();
484        assert!(matches!(action, RequestAction::Allow));
485    }
486
487    #[test]
488    fn test_request_action_block() {
489        let action = RequestAction::block();
490        assert!(matches!(action, RequestAction::Block));
491    }
492
493    #[test]
494    fn test_request_action_redirect() {
495        let action = RequestAction::redirect("https://example.com");
496        if let RequestAction::Redirect(url) = action {
497            assert_eq!(url, "https://example.com");
498        } else {
499            panic!("Expected Redirect action");
500        }
501    }
502
503    #[test]
504    fn test_headers_action_allow() {
505        let action = HeadersAction::allow();
506        assert!(matches!(action, HeadersAction::Allow));
507    }
508
509    #[test]
510    fn test_headers_action_block() {
511        let action = HeadersAction::block();
512        assert!(matches!(action, HeadersAction::Block));
513    }
514
515    #[test]
516    fn test_headers_action_modify() {
517        let mut headers = HashMap::new();
518        headers.insert("X-Custom".to_string(), "value".to_string());
519
520        let action = HeadersAction::modify_headers(headers);
521        if let HeadersAction::Modify {
522            headers: h,
523            status_code,
524        } = action
525        {
526            assert_eq!(h.get("X-Custom"), Some(&"value".to_string()));
527            assert!(status_code.is_none());
528        } else {
529            panic!("Expected Modify action");
530        }
531    }
532
533    #[test]
534    fn test_headers_action_modify_with_status() {
535        let mut headers = HashMap::new();
536        headers.insert("X-Custom".to_string(), "value".to_string());
537
538        let action = HeadersAction::modify_headers_with_status(headers, 404);
539        if let HeadersAction::Modify {
540            headers: h,
541            status_code,
542        } = action
543        {
544            assert_eq!(h.get("X-Custom"), Some(&"value".to_string()));
545            assert_eq!(status_code, Some(404));
546        } else {
547            panic!("Expected Modify action with status");
548        }
549    }
550
551    #[test]
552    fn test_response_action_allow() {
553        let action = ResponseAction::allow();
554        assert!(matches!(action, ResponseAction::Allow));
555    }
556
557    #[test]
558    fn test_response_action_block() {
559        let action = ResponseAction::block();
560        assert!(matches!(action, ResponseAction::Block));
561    }
562
563    #[test]
564    fn test_response_action_modify() {
565        let action = ResponseAction::modify("new body");
566        if let ResponseAction::Modify { body } = action {
567            assert_eq!(body, "new body");
568        } else {
569            panic!("Expected Modify action");
570        }
571    }
572
573    #[test]
574    fn test_response_action_fulfill() {
575        let mut headers = HashMap::new();
576        headers.insert("Content-Type".to_string(), "application/json".to_string());
577
578        let action = ResponseAction::fulfill(200, headers, r#"{"ok": true}"#);
579        if let ResponseAction::Fulfill {
580            status,
581            headers: h,
582            body,
583        } = action
584        {
585            assert_eq!(status, 200);
586            assert_eq!(
587                h.get("Content-Type"),
588                Some(&"application/json".to_string())
589            );
590            assert_eq!(body, r#"{"ok": true}"#);
591        } else {
592            panic!("Expected Fulfill action");
593        }
594    }
595
596    #[test]
597    fn test_body_action_allow() {
598        let action = BodyAction::allow();
599        assert!(matches!(action, BodyAction::Allow));
600    }
601
602    #[test]
603    fn test_body_action_modify() {
604        let action = BodyAction::modify_body("new body");
605        if let BodyAction::ModifyBody(body) = action {
606            assert_eq!(body, "new body");
607        } else {
608            panic!("Expected ModifyBody action");
609        }
610    }
611
612    #[test]
613    fn test_response_body_data_text() {
614        let data = ResponseBodyData::Text("hello".to_string());
615        assert!(data.is_text());
616        assert!(!data.is_binary());
617        assert_eq!(data.as_text(), Some("hello"));
618        assert!(data.as_bytes().is_none());
619    }
620
621    #[test]
622    fn test_response_body_data_binary() {
623        let data = ResponseBodyData::Binary(vec![0x89, 0x50, 0x4E, 0x47]);
624        assert!(!data.is_text());
625        assert!(data.is_binary());
626        assert!(data.as_text().is_none());
627        assert_eq!(data.as_bytes(), Some(&[0x89, 0x50, 0x4E, 0x47][..]));
628    }
629}