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:
1 parent
52fcf2a35a
commit
1da6032639
5 files changed
+492
-127
No files matched your search
@@ -2,6 +2,8 @@ use std::collections::HashMap;
|
||||
use std::sync::RwLock;
|
||||
use url::Url;
|
||||
|
||||
const DEFAULT_SAME_SITE: &str = "Lax";
|
||||
|
||||
pub struct CookieJar {
|
||||
cookies: RwLock<HashMap<String, HashMap<String, CookieEntry>>>,
|
||||
}
|
||||
@@ -15,7 +17,6 @@ struct CookieEntry {
|
||||
secure: bool,
|
||||
http_only: bool,
|
||||
expires: Option<u64>,
|
||||
#[allow(dead_code)]
|
||||
same_site: String,
|
||||
}
|
||||
|
||||
@@ -165,6 +166,8 @@ impl CookieJar {
|
||||
path: entry.path.clone(),
|
||||
secure: entry.secure,
|
||||
http_only: entry.http_only,
|
||||
same_site: entry.same_site.clone(),
|
||||
expires: entry.expires.map(|e| e as i64),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -174,6 +177,12 @@ impl CookieJar {
|
||||
pub fn set_cookies_from_cdp(&self, cookies: Vec<CookieInfo>) {
|
||||
let mut jar = self.cookies.write().unwrap();
|
||||
for cookie in cookies {
|
||||
let same_site = if cookie.same_site.is_empty() {
|
||||
DEFAULT_SAME_SITE.to_string()
|
||||
} else {
|
||||
cookie.same_site
|
||||
};
|
||||
let expires = cookie.expires.and_then(|e| if e > 0 { Some(e as u64) } else { None });
|
||||
let entry = CookieEntry {
|
||||
name: cookie.name.clone(),
|
||||
value: cookie.value,
|
||||
@@ -181,8 +190,8 @@ impl CookieJar {
|
||||
domain: cookie.domain.clone(),
|
||||
secure: cookie.secure,
|
||||
http_only: cookie.http_only,
|
||||
expires: None,
|
||||
same_site: "Lax".to_string(),
|
||||
expires,
|
||||
same_site,
|
||||
};
|
||||
jar.entry(cookie.domain).or_default().insert(cookie.name, entry);
|
||||
}
|
||||
@@ -336,6 +345,30 @@ impl CookieJar {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_cookies_filtered(&self, name: &str, domain: &str, path: Option<&str>) {
|
||||
let mut cookies = self.cookies.write().unwrap();
|
||||
let matches_path = |entry_path: &str| match path {
|
||||
Some(p) => entry_path == p,
|
||||
None => true,
|
||||
};
|
||||
if domain.is_empty() {
|
||||
for domain_cookies in cookies.values_mut() {
|
||||
domain_cookies.retain(|n, e| !(n == name && matches_path(&e.path)));
|
||||
}
|
||||
} else {
|
||||
let domains_to_try = [
|
||||
domain.to_string(),
|
||||
format!(".{}", domain.trim_start_matches('.')),
|
||||
domain.trim_start_matches('.').to_string(),
|
||||
];
|
||||
for d in &domains_to_try {
|
||||
if let Some(domain_cookies) = cookies.get_mut(d.as_str()) {
|
||||
domain_cookies.retain(|n, e| !(n == name && matches_path(&e.path)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&self) {
|
||||
self.cookies.write().unwrap().clear();
|
||||
}
|
||||
@@ -356,6 +389,10 @@ pub struct CookieInfo {
|
||||
pub secure: bool,
|
||||
#[serde(rename = "httpOnly")]
|
||||
pub http_only: bool,
|
||||
#[serde(default, rename = "sameSite")]
|
||||
pub same_site: String,
|
||||
#[serde(default)]
|
||||
pub expires: Option<i64>,
|
||||
}
|
||||
|
||||
fn parse_http_date(s: &str) -> Result<u64, ()> {
|
||||
@@ -438,6 +475,8 @@ mod tests {
|
||||
path: "/".to_string(),
|
||||
secure: false,
|
||||
http_only: false,
|
||||
same_site: String::new(),
|
||||
expires: None,
|
||||
}]);
|
||||
|
||||
let apex_url = Url::parse("https://example.com/").unwrap();
|
||||
@@ -509,4 +548,105 @@ mod tests {
|
||||
jar.clear();
|
||||
assert!(jar.get_cookie_header(&url).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_cookies_from_cdp_preserves_same_site_and_expires() {
|
||||
let jar = CookieJar::new();
|
||||
let future_expiry = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64
|
||||
+ 3600;
|
||||
jar.set_cookies_from_cdp(vec![CookieInfo {
|
||||
name: "sid".to_string(),
|
||||
value: "abc".to_string(),
|
||||
domain: "example.com".to_string(),
|
||||
path: "/".to_string(),
|
||||
secure: true,
|
||||
http_only: true,
|
||||
same_site: "Strict".to_string(),
|
||||
expires: Some(future_expiry),
|
||||
}]);
|
||||
|
||||
let cookies = jar.get_all_cookies();
|
||||
assert_eq!(cookies.len(), 1);
|
||||
assert_eq!(cookies[0].same_site, "Strict");
|
||||
assert_eq!(cookies[0].expires, Some(future_expiry));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_cookies_from_cdp_session_when_expires_none() {
|
||||
let jar = CookieJar::new();
|
||||
jar.set_cookies_from_cdp(vec![CookieInfo {
|
||||
name: "n".to_string(),
|
||||
value: "v".to_string(),
|
||||
domain: "example.com".to_string(),
|
||||
path: "/".to_string(),
|
||||
secure: false,
|
||||
http_only: false,
|
||||
same_site: String::new(),
|
||||
expires: None,
|
||||
}]);
|
||||
let cookies = jar.get_all_cookies();
|
||||
assert_eq!(cookies[0].expires, None);
|
||||
assert_eq!(cookies[0].same_site, DEFAULT_SAME_SITE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_cookies_filtered_path_mismatch_preserves_cookie() {
|
||||
let jar = CookieJar::new();
|
||||
jar.set_cookies_from_cdp(vec![CookieInfo {
|
||||
name: "sid".to_string(),
|
||||
value: "v".to_string(),
|
||||
domain: "example.com".to_string(),
|
||||
path: "/admin".to_string(),
|
||||
secure: false,
|
||||
http_only: false,
|
||||
same_site: String::new(),
|
||||
expires: None,
|
||||
}]);
|
||||
jar.delete_cookies_filtered("sid", "example.com", Some("/other"));
|
||||
assert_eq!(jar.get_all_cookies().len(), 1);
|
||||
|
||||
jar.delete_cookies_filtered("sid", "example.com", Some("/admin"));
|
||||
assert!(jar.get_all_cookies().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_cookies_filtered_no_path_deletes_regardless() {
|
||||
let jar = CookieJar::new();
|
||||
jar.set_cookies_from_cdp(vec![CookieInfo {
|
||||
name: "sid".to_string(),
|
||||
value: "v".to_string(),
|
||||
domain: "example.com".to_string(),
|
||||
path: "/admin".to_string(),
|
||||
secure: false,
|
||||
http_only: false,
|
||||
same_site: String::new(),
|
||||
expires: None,
|
||||
}]);
|
||||
jar.delete_cookies_filtered("sid", "example.com", None);
|
||||
assert!(jar.get_all_cookies().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_cookies_from_cdp_expired_does_not_persist() {
|
||||
let jar = CookieJar::new();
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as i64;
|
||||
jar.set_cookies_from_cdp(vec![CookieInfo {
|
||||
name: "old".to_string(),
|
||||
value: "v".to_string(),
|
||||
domain: "example.com".to_string(),
|
||||
path: "/".to_string(),
|
||||
secure: false,
|
||||
http_only: false,
|
||||
same_site: String::new(),
|
||||
expires: Some(now - 1),
|
||||
}]);
|
||||
let url = Url::parse("https://example.com/").unwrap();
|
||||
assert!(jar.get_cookie_header(&url).is_empty());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user