viewpoint_core/api/cookies/
mod.rs

1//! Cookie synchronization between browser and API contexts.
2
3use std::sync::Arc;
4
5use reqwest::cookie::{CookieStore, Jar};
6use reqwest::header::HeaderValue;
7use tracing::debug;
8
9use crate::context::Cookie;
10
11/// Sync cookies from browser context to a reqwest cookie jar.
12///
13/// This converts browser cookies to reqwest cookies and adds them to the jar.
14pub fn sync_to_jar(cookies: &[Cookie], jar: &Arc<Jar>) {
15    for cookie in cookies {
16        // Build a cookie URL for reqwest
17        let url = cookie_to_url(cookie);
18
19        if let Ok(parsed_url) = url::Url::parse(&url) {
20            // Build Set-Cookie header string
21            let cookie_str = cookie_to_string(cookie);
22
23            // Create header value and add to jar
24            if let Ok(header_value) = HeaderValue::from_str(&cookie_str) {
25                // Use a Vec to create a slice iterator that yields references
26                let headers = [header_value];
27                jar.set_cookies(&mut headers.iter(), &parsed_url);
28                debug!("Synced cookie {} to API jar for {}", cookie.name, url);
29            }
30        }
31    }
32}
33
34/// Convert a cookie to a URL for use with reqwest's jar.
35fn cookie_to_url(cookie: &Cookie) -> String {
36    if let Some(ref url) = cookie.url {
37        return url.clone();
38    }
39
40    let scheme = if cookie.secure.unwrap_or(false) {
41        "https"
42    } else {
43        "http"
44    };
45
46    let domain = cookie.domain.as_deref().unwrap_or("localhost");
47    let domain = domain.trim_start_matches('.');
48    let path = cookie.path.as_deref().unwrap_or("/");
49
50    format!("{scheme}://{domain}{path}")
51}
52
53/// Convert a cookie to a Set-Cookie header string.
54fn cookie_to_string(cookie: &Cookie) -> String {
55    let mut parts = vec![format!("{}={}", cookie.name, cookie.value)];
56
57    if let Some(ref domain) = cookie.domain {
58        parts.push(format!("Domain={domain}"));
59    }
60    if let Some(ref path) = cookie.path {
61        parts.push(format!("Path={path}"));
62    }
63    if cookie.secure.unwrap_or(false) {
64        parts.push("Secure".to_string());
65    }
66    if cookie.http_only.unwrap_or(false) {
67        parts.push("HttpOnly".to_string());
68    }
69    if let Some(same_site) = &cookie.same_site {
70        parts.push(format!(
71            "SameSite={}",
72            match same_site {
73                crate::context::SameSite::Strict => "Strict",
74                crate::context::SameSite::Lax => "Lax",
75                crate::context::SameSite::None => "None",
76            }
77        ));
78    }
79    if let Some(expires) = cookie.expires {
80        // Convert Unix timestamp to HTTP date
81        if let Some(dt) = chrono::DateTime::from_timestamp(expires as i64, 0) {
82            parts.push(format!(
83                "Expires={}",
84                dt.format("%a, %d %b %Y %H:%M:%S GMT")
85            ));
86        }
87    }
88
89    parts.join("; ")
90}
91
92/// Extract cookies from a reqwest cookie jar for a given URL.
93///
94/// Returns a list of cookies that would be sent for the given URL.
95pub fn extract_from_jar(jar: &Arc<Jar>, url: &str) -> Vec<Cookie> {
96    let mut cookies = Vec::new();
97
98    if let Ok(parsed_url) = url::Url::parse(url) {
99        if let Some(cookie_header) = jar.cookies(&parsed_url) {
100            // Parse the cookie header
101            let header_str = cookie_header.to_str().unwrap_or("");
102            for cookie_str in header_str.split("; ") {
103                if let Some((name, value)) = cookie_str.split_once('=') {
104                    cookies.push(Cookie {
105                        name: name.to_string(),
106                        value: value.to_string(),
107                        domain: parsed_url.host_str().map(String::from),
108                        path: Some(parsed_url.path().to_string()),
109                        url: Some(url.to_string()),
110                        expires: None,
111                        http_only: None,
112                        secure: Some(parsed_url.scheme() == "https"),
113                        same_site: None,
114                    });
115                }
116            }
117        }
118    }
119
120    cookies
121}
122
123#[cfg(test)]
124mod tests;