use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct CapturedCookie {
pub name: String,
pub value: String,
pub domain: String,
pub path: String,
pub expires: Option<i64>,
pub secure: bool,
pub http_only: bool,
pub same_site: Option<String>,
}
impl CapturedCookie {
pub fn is_expired_now(&self) -> bool {
let Some(exp) = self.expires else {
return false;
};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
exp <= now
}
pub fn keep_persistent_alive(input: &[CapturedCookie]) -> Vec<CapturedCookie> {
input
.iter()
.filter(|c| c.expires.is_some() && !c.is_expired_now())
.cloned()
.collect()
}
}
pub async fn capture_from_page(page: &chromiumoxide::Page) -> anyhow::Result<Vec<CapturedCookie>> {
use chromiumoxide::cdp::browser_protocol::network::GetCookiesParams;
let resp = page.execute(GetCookiesParams::default()).await?;
let mut out = Vec::with_capacity(resp.cookies.len());
for c in &resp.cookies {
out.push(CapturedCookie {
name: c.name.clone(),
value: c.value.clone(),
domain: c.domain.clone(),
path: c.path.clone(),
expires: cdp_expiry_to_epoch(c.expires),
secure: c.secure,
http_only: c.http_only,
same_site: c.same_site.as_ref().map(|s| {
format!("{s:?}").to_lowercase()
}),
});
}
Ok(out)
}
pub async fn apply_to_page(
page: &chromiumoxide::Page,
cookies: &[CapturedCookie],
) -> anyhow::Result<usize> {
use chromiumoxide::cdp::browser_protocol::network::{
CookieParam, SetCookiesParams, TimeSinceEpoch,
};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let mut to_install = Vec::with_capacity(cookies.len());
for c in cookies {
if let Some(exp) = c.expires {
if exp <= now {
continue; }
}
let mut builder = CookieParam::builder()
.name(c.name.clone())
.value(c.value.clone())
.domain(c.domain.clone())
.path(c.path.clone())
.secure(c.secure)
.http_only(c.http_only);
if let Some(exp) = c.expires {
builder = builder.expires(TimeSinceEpoch::new(exp as f64));
}
let cookie = builder
.build()
.map_err(|e| anyhow::anyhow!("cookie builder: {e}"))?;
to_install.push(cookie);
}
let count = to_install.len();
if !to_install.is_empty() {
page.execute(SetCookiesParams::new(to_install)).await?;
}
Ok(count)
}
fn cdp_expiry_to_epoch(raw: f64) -> Option<i64> {
if raw <= 0.0 {
None
} else {
Some(raw as i64)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cookie(name: &str, expires: Option<i64>) -> CapturedCookie {
CapturedCookie {
name: name.into(),
value: "v".into(),
domain: ".example.com".into(),
path: "/".into(),
expires,
secure: true,
http_only: false,
same_site: None,
}
}
#[test]
fn is_expired_now_true_for_past_expires() {
let c = cookie("c", Some(1)); assert!(c.is_expired_now());
}
#[test]
fn is_expired_now_false_for_far_future_expires() {
let c = cookie("c", Some(i64::MAX));
assert!(!c.is_expired_now());
}
#[test]
fn is_expired_now_false_for_session_cookie() {
let c = cookie("c", None);
assert!(!c.is_expired_now());
}
#[test]
fn keep_persistent_alive_drops_session_cookies() {
let cookies = vec![
cookie("session", None),
cookie("persistent", Some(i64::MAX)),
];
let kept = CapturedCookie::keep_persistent_alive(&cookies);
assert_eq!(kept.len(), 1);
assert_eq!(kept[0].name, "persistent");
}
#[test]
fn keep_persistent_alive_drops_expired_cookies() {
let cookies = vec![cookie("expired", Some(1)), cookie("alive", Some(i64::MAX))];
let kept = CapturedCookie::keep_persistent_alive(&cookies);
assert_eq!(kept.len(), 1);
assert_eq!(kept[0].name, "alive");
}
#[test]
fn cdp_expiry_negative_one_becomes_none() {
assert_eq!(cdp_expiry_to_epoch(-1.0), None);
}
#[test]
fn cdp_expiry_zero_becomes_none() {
assert_eq!(cdp_expiry_to_epoch(0.0), None);
}
#[test]
fn cdp_expiry_positive_becomes_some_truncated() {
assert_eq!(cdp_expiry_to_epoch(1234567890.7), Some(1234567890));
}
#[test]
fn captured_cookie_serde_roundtrip() {
let c = cookie("cf_clearance", Some(1234567890));
let json = serde_json::to_string(&c).unwrap();
let back: CapturedCookie = serde_json::from_str(&json).unwrap();
assert_eq!(c, back);
}
}