feat(cdp): implement Network.setCookie + deleteCookies, full field fidelity (#170)

* feat(cdp): implement Network.setCookie + deleteCookies, full field fidelity

Closes #168.

Adds the missing Network.setCookie (singular) and Network.deleteCookies
methods and upgrades the existing Network.setCookies/getCookies handlers
so sameSite, expires, httpOnly, secure, and the url-fallback parameter
all round-trip correctly.

Network and Storage cookie handlers share a single parse_cdp_cookie /
parse_delete_cookies_params helper to avoid drift.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* fix(cdp): fall back to default cookie jar, add Network.getAllCookies

Addresses @SGavrl review on PR #170:

- Network.{set,get,delete}Cookie(s)/clearBrowserCookies now resolve the
  cookie jar via a shared helper that prefers the session page's jar and
  falls back to ctx.default_context.cookie_jar when no target has been
  attached. Mirrors how Storage.setCookies already routes when there is
  no session, so Puppeteer/Playwright flows that issue cookie methods
  before Target.attachToTarget no longer hit "No page".
- Add Network.getAllCookies (separate CDP method per spec); aliases the
  existing getCookies handler so both arms return the full jar.

Tests: 6 new #[tokio::test] cases in domains::network::tests cover the
no-session fallback for set/get/setMany/delete/clear and the new
getAllCookies arm. RED on prod-reverted file (6/6 fail with "No page" /
"Unknown Network method: getAllCookies"), GREEN on patched
(22 -> 30 obscura-cdp lib tests, obscura-net 22/22 baseline preserved).
This commit is contained in:
Ben Younes authored and GitHub committed 2026-05-25 17:58:48 +02:00
1 parent 52fcf2a35a
commit 1da6032639
5 files changed
+492 -127

No files matched your search

+11 -90
View File
@@ -1,6 +1,8 @@
use serde_json::{json, Value};
use crate::cookie_params::{parse_cdp_cookie, parse_delete_cookies_params};
use crate::dispatch::CdpContext;
use crate::domains::network::cookie_info_to_cdp_json;
pub async fn handle(
method: &str,
@@ -11,104 +13,23 @@ pub async fn handle(
match method {
"getCookies" => {
let cookies = ctx.default_context.cookie_jar.get_all_cookies();
let cdp_cookies: Vec<Value> = cookies
.iter()
.map(|c| {
json!({
"name": c.name,
"value": c.value,
"domain": c.domain,
"path": c.path,
"expires": -1,
"size": c.name.len() + c.value.len(),
"httpOnly": c.http_only,
"secure": c.secure,
"session": true,
"priority": "Medium",
"sameParty": false,
"sourceScheme": if c.secure { "Secure" } else { "NonSecure" },
"sourcePort": 80,
})
})
.collect();
let cdp_cookies: Vec<Value> = cookies.iter().map(cookie_info_to_cdp_json).collect();
Ok(json!({ "cookies": cdp_cookies }))
}
"setCookies" => {
if let Some(cookies) = params.get("cookies").and_then(|v| v.as_array()) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs_f64();
for c in cookies {
let name = match c.get("name").and_then(|v| v.as_str()) {
Some(n) => n.to_string(),
None => continue,
};
let value = c.get("value").and_then(|v| v.as_str()).unwrap_or("").to_string();
let domain = c
.get("domain")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or_else(|| {
c.get("url")
.and_then(|v| v.as_str())
.and_then(|u| url::Url::parse(u).ok())
.and_then(|u| u.host_str().map(|h| h.to_string()))
})
.unwrap_or_default();
let expires = c.get("expires").and_then(|v| v.as_f64());
if let Some(exp) = expires {
if exp > 0.0 && exp < now {
ctx.default_context.cookie_jar.delete_cookie(&name, &domain);
continue;
}
}
let path = c
.get("path")
.and_then(|v| v.as_str())
.unwrap_or("/")
.to_string();
let secure =
c.get("secure").and_then(|v| v.as_bool()).unwrap_or(false);
let http_only =
c.get("httpOnly").and_then(|v| v.as_bool()).unwrap_or(false);
ctx.default_context.cookie_jar.set_cookies_from_cdp(vec![
obscura_net::CookieInfo {
name,
value,
domain,
path,
secure,
http_only,
},
]);
}
let parsed: Vec<_> = cookies.iter().filter_map(parse_cdp_cookie).collect();
ctx.default_context.cookie_jar.set_cookies_from_cdp(parsed);
}
Ok(json!({}))
}
"deleteCookies" => {
let name = params.get("name").and_then(|v| v.as_str()).unwrap_or("");
let domain_from_url = params
.get("url")
.and_then(|v| v.as_str())
.and_then(|u| url::Url::parse(u).ok())
.and_then(|u| u.host_str().map(|h| h.to_string()));
let domain = params
.get("domain")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.or(domain_from_url)
.unwrap_or_default();
if !name.is_empty() {
ctx.default_context
.cookie_jar
.delete_cookie(name, &domain);
if let Some(filter) = parse_delete_cookies_params(params) {
ctx.default_context.cookie_jar.delete_cookies_filtered(
&filter.name,
&filter.domain,
filter.path.as_deref(),
);
}
Ok(json!({}))
}