use crate::browser::views::{InterceptPattern, InterceptedRequest, MockResponse};
use crate::error::Result;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
#[derive(Debug, Clone)]
pub struct NetworkInterceptor {
client: Arc<crate::browser::cdp::CdpClient>,
pending: Arc<Mutex<HashMap<String, InterceptedRequest>>>,
}
impl NetworkInterceptor {
pub fn new(client: Arc<crate::browser::cdp::CdpClient>) -> Self {
Self {
client,
pending: Arc::new(Mutex::new(HashMap::new())),
}
}
pub async fn enable(&self, patterns: Option<Vec<InterceptPattern>>) -> Result<()> {
let patterns = patterns.unwrap_or_else(|| {
vec![InterceptPattern {
url_pattern: "*".to_string(),
resource_type: None,
interception_stage: Some("Request".to_string()),
}]
});
let cdp_patterns: Vec<serde_json::Value> = patterns
.into_iter()
.map(|p| {
let mut obj = serde_json::json!({
"urlPattern": p.url_pattern,
});
if let Some(rt) = p.resource_type {
obj["resourceType"] = serde_json::json!(rt);
}
if let Some(stage) = p.interception_stage {
obj["interceptionStage"] = serde_json::json!(stage);
}
obj
})
.collect();
self.client
.send_command("Fetch.enable", serde_json::json!({ "patterns": cdp_patterns }))
.await?;
let pending = Arc::clone(&self.pending);
let mut rx = self.client.subscribe_events().await?;
tokio::spawn(async move {
while let Ok(event) = rx.recv().await {
if event.method == "Fetch.requestPaused"
&& let Some(req_id) = event.params.get("requestId").and_then(|v| v.as_str()) {
let request = event.params.get("request");
let url = request
.and_then(|r| r.get("url"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let method = request
.and_then(|r| r.get("method"))
.and_then(|v| v.as_str())
.unwrap_or("GET")
.to_string();
let headers = parse_headers(request.and_then(|r| r.get("headers")));
let post_data = event
.params
.get("request")
.and_then(|r| r.get("postData"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let resource_type = event
.params
.get("resourceType")
.and_then(|v| v.as_str())
.unwrap_or("Other")
.to_string();
let intercepted = InterceptedRequest {
request_id: req_id.to_string(),
url,
method,
headers,
post_data,
resource_type,
};
pending.lock().await.insert(req_id.to_string(), intercepted);
}
}
});
Ok(())
}
pub async fn disable(&self) -> Result<()> {
self.client
.send_command("Fetch.disable", serde_json::json!({}))
.await?;
self.pending.lock().await.clear();
Ok(())
}
pub async fn continue_request(
&self,
request_id: &str,
url: Option<&str>,
method: Option<&str>,
headers: Option<Vec<(String, String)>>,
post_data: Option<&str>,
) -> Result<()> {
let mut params = serde_json::json!({ "requestId": request_id });
if let Some(u) = url {
params["url"] = serde_json::json!(u);
}
if let Some(m) = method {
params["method"] = serde_json::json!(m);
}
if let Some(h) = headers {
let headers_json: Vec<serde_json::Value> = h
.into_iter()
.map(|(name, value)| {
serde_json::json!({
"name": name,
"value": value,
})
})
.collect();
params["headers"] = serde_json::json!(headers_json);
}
if let Some(pd) = post_data {
params["postData"] = serde_json::json!(pd);
}
self.client
.send_command("Fetch.continueRequest", params)
.await?;
self.pending.lock().await.remove(request_id);
Ok(())
}
pub async fn fulfill_request(&self, request_id: &str, response: &MockResponse) -> Result<()> {
let headers_json: Vec<serde_json::Value> = response
.headers
.iter()
.map(|h| {
serde_json::json!({
"name": h.name,
"value": h.value,
})
})
.collect();
self.client
.send_command(
"Fetch.fulfillRequest",
serde_json::json!({
"requestId": request_id,
"responseCode": response.status,
"responsePhrase": response.status_text,
"responseHeaders": headers_json,
"body": response.body,
"base64Encoded": response.base64_encoded,
}),
)
.await?;
self.pending.lock().await.remove(request_id);
Ok(())
}
pub async fn fail_request(&self, request_id: &str, error_reason: &str) -> Result<()> {
self.client
.send_command(
"Fetch.failRequest",
serde_json::json!({
"requestId": request_id,
"errorReason": error_reason,
}),
)
.await?;
self.pending.lock().await.remove(request_id);
Ok(())
}
pub async fn get_intercepted_requests(&self) -> Vec<InterceptedRequest> {
self.pending.lock().await.values().cloned().collect()
}
pub async fn get_request(&self, request_id: &str) -> Option<InterceptedRequest> {
self.pending.lock().await.get(request_id).cloned()
}
}
fn parse_headers(headers_val: Option<&serde_json::Value>) -> Vec<crate::browser::views::HarHeader> {
let mut result = Vec::new();
if let Some(obj) = headers_val.and_then(|v| v.as_object()) {
for (name, value) in obj {
result.push(crate::browser::views::HarHeader {
name: name.clone(),
value: value.as_str().unwrap_or("").to_string(),
});
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_intercept_pattern_creation() {
let p = InterceptPattern {
url_pattern: "*://*.example.com/*".to_string(),
resource_type: Some("XHR".to_string()),
interception_stage: Some("Request".to_string()),
};
assert_eq!(p.url_pattern, "*://*.example.com/*");
}
#[test]
fn test_intercepted_request_creation() {
let req = InterceptedRequest {
request_id: "req-1".to_string(),
url: "https://example.com/api".to_string(),
method: "POST".to_string(),
headers: vec![],
post_data: Some("{}".to_string()),
resource_type: "XHR".to_string(),
};
assert_eq!(req.method, "POST");
assert_eq!(req.resource_type, "XHR");
}
#[test]
fn test_mock_response_creation() {
let resp = MockResponse {
status: 200,
status_text: "OK".to_string(),
headers: vec![crate::browser::views::HarHeader {
name: "Content-Type".to_string(),
value: "application/json".to_string(),
}],
body: "{}".to_string(),
base64_encoded: false,
};
assert_eq!(resp.status, 200);
assert!(!resp.base64_encoded);
}
#[test]
fn test_mock_response_serialization() {
let resp = MockResponse {
status: 404,
status_text: "Not Found".to_string(),
headers: vec![],
body: "not found".to_string(),
base64_encoded: false,
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("404"));
assert!(json.contains("Not Found"));
}
}