fix(cdp): apply Fetch.continueRequest overrides (#373)
handle_fetch_resolution built InterceptResolution::Continue with url, method,
headers, and body hardcoded to None, so Fetch.continueRequest silently sent the
request unmodified. Playwright route.continue({ headers, url, postData }) and
Puppeteer request.continue reported success while their overrides were dropped.
op_fetch_url already applies all four (and re-validates a rewritten URL through
the SSRF gate); only the CDP layer was discarding them. Read url, method,
headers, and postData from the continueRequest params and pass them through.
Fixes #365.
This commit is contained in:
1 parent
daa0d62fbd
commit
68234970b1
1 file changed
+50
-2
@@ -469,6 +469,24 @@ fn is_navigate_method(text: &str) -> bool {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse a CDP header list (`[{"name":..,"value":..}, ..]`, as used by
|
||||||
|
// Fetch.continueRequest / fulfillRequest) into a map. Returns None when the
|
||||||
|
// `headers` field is absent, so the caller can leave the request's headers
|
||||||
|
// untouched rather than clearing them.
|
||||||
|
fn parse_cdp_headers(params: &serde_json::Value) -> Option<HashMap<String, String>> {
|
||||||
|
let arr = params.get("headers")?.as_array()?;
|
||||||
|
Some(
|
||||||
|
arr.iter()
|
||||||
|
.filter_map(|h| {
|
||||||
|
Some((
|
||||||
|
h.get("name")?.as_str()?.to_string(),
|
||||||
|
h.get("value")?.as_str()?.to_string(),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_fetch_resolution(
|
fn handle_fetch_resolution(
|
||||||
text: &str,
|
text: &str,
|
||||||
_ctx: &mut CdpContext,
|
_ctx: &mut CdpContext,
|
||||||
@@ -484,7 +502,14 @@ fn handle_fetch_resolution(
|
|||||||
tracing::info!("INTERCEPTION resolved: {}", request_id);
|
tracing::info!("INTERCEPTION resolved: {}", request_id);
|
||||||
let resolution = match method {
|
let resolution = match method {
|
||||||
"Fetch.continueRequest" => obscura_js::ops::InterceptResolution::Continue {
|
"Fetch.continueRequest" => obscura_js::ops::InterceptResolution::Continue {
|
||||||
url: None, method: None, headers: None, body: None,
|
// Honor the client's overrides (Playwright route.continue,
|
||||||
|
// Puppeteer request.continue). op_fetch_url applies each and
|
||||||
|
// re-validates a rewritten URL through the SSRF gate. Leaving
|
||||||
|
// these None silently sent the request unmodified (issue #365).
|
||||||
|
url: req.params.get("url").and_then(|v| v.as_str()).map(|s| s.to_string()),
|
||||||
|
method: req.params.get("method").and_then(|v| v.as_str()).map(|s| s.to_string()),
|
||||||
|
headers: parse_cdp_headers(&req.params),
|
||||||
|
body: req.params.get("postData").and_then(|v| v.as_str()).map(|s| s.to_string()),
|
||||||
},
|
},
|
||||||
"Fetch.fulfillRequest" => {
|
"Fetch.fulfillRequest" => {
|
||||||
let status = req.params.get("responseCode").and_then(|v| v.as_u64()).unwrap_or(200) as u16;
|
let status = req.params.get("responseCode").and_then(|v| v.as_u64()).unwrap_or(200) as u16;
|
||||||
@@ -1009,7 +1034,8 @@ async fn handle_connection_ws(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::is_navigate_method;
|
use super::{is_navigate_method, parse_cdp_headers};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
// Issue #363: only an exact Page.navigate may take the spawn-and-defer
|
// Issue #363: only an exact Page.navigate may take the spawn-and-defer
|
||||||
// navigation path. A substring match also caught Page.navigateToHistoryEntry
|
// navigation path. A substring match also caught Page.navigateToHistoryEntry
|
||||||
@@ -1036,4 +1062,26 @@ mod tests {
|
|||||||
));
|
));
|
||||||
assert!(!is_navigate_method("not json"));
|
assert!(!is_navigate_method("not json"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Issue #365: Fetch.continueRequest header overrides must be parsed from the
|
||||||
|
// CDP `[{name, value}]` list so they can be applied to the outgoing request.
|
||||||
|
#[test]
|
||||||
|
fn parse_cdp_headers_reads_name_value_pairs() {
|
||||||
|
let params = json!({
|
||||||
|
"headers": [
|
||||||
|
{"name": "X-A", "value": "1"},
|
||||||
|
{"name": "X-B", "value": "2"},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
let headers = parse_cdp_headers(¶ms).expect("headers present");
|
||||||
|
assert_eq!(headers.get("X-A").map(String::as_str), Some("1"));
|
||||||
|
assert_eq!(headers.get("X-B").map(String::as_str), Some("2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// No `headers` field means "leave the request's headers untouched", which is
|
||||||
|
// None, not an empty map that would clear them.
|
||||||
|
#[test]
|
||||||
|
fn parse_cdp_headers_absent_is_none() {
|
||||||
|
assert!(parse_cdp_headers(&json!({"url": "https://example.com"})).is_none());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user