From c19a21c14289378c1bed042986bcf52692862ec7 Mon Sep 17 00:00:00 2001 From: SGavrl <277843151+SGavrl@users.noreply.github.com> Date: Wed, 24 Jun 2026 21:51:21 +0000 Subject: [PATCH] api: honor interception Continue overrides and fix pre-navigation enable op_fetch_url ignored the url/method/headers/body fields on InterceptResolution::Continue, so the interception channel could pass, mock, or block a request but not rewrite it. Apply them: shadow url/method/body and derive custom_headers from the override after the channel resolves Continue. Also fix enable_interception() called before the first navigation: init_js propagated intercept_tx to the new runtime but not intercept_enabled, so the runtime started with interception disabled and op_fetch_url never intercepted. Propagate intercept_enabled in init_js as well. Add a test that rewrites a fetch's url via Continue and asserts the rewritten response is captured. --- crates/obscura-browser/src/page.rs | 5 +++ crates/obscura-js/src/ops.rs | 26 ++++++++++-- crates/obscura/tests/interception.rs | 60 ++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) diff --git a/crates/obscura-browser/src/page.rs b/crates/obscura-browser/src/page.rs index 9e0079f..6e2c098 100644 --- a/crates/obscura-browser/src/page.rs +++ b/crates/obscura-browser/src/page.rs @@ -327,6 +327,11 @@ impl Page { if let Some(tx) = &self.intercept_tx { rt.set_intercept_tx(tx.clone()); } + // Re-apply intercept_enabled: enable_interception()/enable_intercept() + // called before the first navigation sets this on the Page while the + // runtime does not exist yet, so the new runtime would otherwise start + // with interception disabled and op_fetch_url would never intercept. + rt.set_intercept_enabled(self.intercept_enabled); if let Some(dom) = self.dom.take() { rt.set_dom(dom); diff --git a/crates/obscura-js/src/ops.rs b/crates/obscura-js/src/ops.rs index a2b64f1..63b6e4f 100644 --- a/crates/obscura-js/src/ops.rs +++ b/crates/obscura-js/src/ops.rs @@ -654,6 +654,13 @@ async fn op_fetch_url( (jar, in_flight, itx, proxy_url, gs.http_client.clone()) }; + // Slots the interception channel can override via Continue so a consumer + // can rewrite url/method/headers/body before the request goes out. + let mut override_url: Option = None; + let mut override_method: Option = None; + let mut override_headers: Option> = None; + let mut override_body: Option = None; + if let Some((tx, request_id)) = intercept_tx { let custom_headers: HashMap = serde_json::from_str(&headers_json).unwrap_or_default(); let (resolve_tx, resolve_rx) = tokio::sync::oneshot::channel(); @@ -686,8 +693,16 @@ async fn op_fetch_url( "error": reason, }).to_string()); } - Ok(InterceptResolution::Continue { url: _new_url, method: _new_method, headers: _new_headers, body: _new_body }) => { - tracing::debug!("Interception: continue request {}", url); + Ok(InterceptResolution::Continue { url, method, headers, body }) => { + override_url = url; + override_method = method; + override_headers = headers; + override_body = body; + tracing::debug!( + "Interception: continue (overrides url={} method={} headers={} body={})", + override_url.is_some(), override_method.is_some(), + override_headers.is_some(), override_body.is_some() + ); } Err(_) => { } @@ -695,6 +710,11 @@ async fn op_fetch_url( } } + // Apply interception overrides (shadow the params for the rest of the op). + let url = override_url.unwrap_or(url); + let method = override_method.unwrap_or(method); + let body = override_body.unwrap_or(body); + let client = cached_request_client(proxy_url.as_deref()) .map_err(deno_error::JsErrorBox::generic)?; @@ -714,7 +734,7 @@ async fn op_fetch_url( let req_method: reqwest::Method = method.parse().unwrap_or(reqwest::Method::GET); let custom_headers: std::collections::HashMap = - serde_json::from_str(&headers_json).unwrap_or_default(); + override_headers.unwrap_or_else(|| serde_json::from_str(&headers_json).unwrap_or_default()); // Passive request observation (non-blocking). Fires for every request that // reaches the network (Fulfill/Fail from the interception channel short- diff --git a/crates/obscura/tests/interception.rs b/crates/obscura/tests/interception.rs index acdc80a..cb13499 100644 --- a/crates/obscura/tests/interception.rs +++ b/crates/obscura/tests/interception.rs @@ -25,6 +25,8 @@ fn spawn_echo_server() -> String { let path = req.split_whitespace().nth(1).unwrap_or("/"); let (ct, body) = if path.starts_with("/api") { ("application/json", "{\"hello\":\"world\"}".to_string()) + } else if path.starts_with("/modified") { + ("text/plain", "REWRITTEN".to_string()) } else { ("text/html", "".to_string()) }; @@ -105,3 +107,61 @@ async fn page_intercepts_and_observes_js_fetch() { body ); } + +#[tokio::test] +async fn page_rewrites_request_url_via_interception() { + std::env::set_var("OBSCURA_ALLOW_PRIVATE_NETWORK", "1"); + let base = spawn_echo_server(); + let modified = format!("{}/modified", base); + + let browser = Browser::new().unwrap(); + let mut page = browser.new_page().await.unwrap(); + + let captured = Arc::new(Mutex::new(String::new())); + let cap = captured.clone(); + page.on_response(Arc::new(move |info, resp| { + if info.resource_type == ResourceType::Fetch { + *cap.lock().unwrap() = String::from_utf8_lossy(&resp.body).into_owned(); + } + })); + + // Intercept and rewrite the /api request to /modified via Continue. + let mut rx = page.enable_interception(); + let modified_for_task = modified.clone(); + tokio::spawn(async move { + while let Some(req) = rx.recv().await { + let new_url = if req.url.contains("/api") { + Some(modified_for_task.clone()) + } else { + None + }; + if req + .resolver + .send(InterceptResolution::Continue { + url: new_url, + method: None, + headers: None, + body: None, + }) + .is_err() + { + break; + } + } + }); + + page.goto(&base).await.unwrap(); + for _ in 0..20 { + page.settle(500).await; + if captured.lock().unwrap().contains("REWRITTEN") { + break; + } + } + + let body = captured.lock().unwrap().clone(); + assert!( + body.contains("REWRITTEN"), + "interception Continue url-rewrite did not take effect; captured: {:?}", + body + ); +}