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(
28                    &mut headers.iter(),
29                    &parsed_url,
30                );
31                debug!("Synced cookie {} to API jar for {}", cookie.name, url);
32            }
33        }
34    }
35}
36
37/// Convert a cookie to a URL for use with reqwest's jar.
38fn cookie_to_url(cookie: &Cookie) -> String {
39    if let Some(ref url) = cookie.url {
40        return url.clone();
41    }
42
43    let scheme = if cookie.secure.unwrap_or(false) {
44        "https"
45    } else {
46        "http"
47    };
48
49    let domain = cookie.domain.as_deref().unwrap_or("localhost");
50    let domain = domain.trim_start_matches('.');
51    let path = cookie.path.as_deref().unwrap_or("/");
52
53    format!("{scheme}://{domain}{path}")
54}
55
56/// Convert a cookie to a Set-Cookie header string.
57fn cookie_to_string(cookie: &Cookie) -> String {
58    let mut parts = vec![format!("{}={}", cookie.name, cookie.value)];
59
60    if let Some(ref domain) = cookie.domain {
61        parts.push(format!("Domain={domain}"));
62    }
63    if let Some(ref path) = cookie.path {
64        parts.push(format!("Path={path}"));
65    }
66    if cookie.secure.unwrap_or(false) {
67        parts.push("Secure".to_string());
68    }
69    if cookie.http_only.unwrap_or(false) {
70        parts.push("HttpOnly".to_string());
71    }
72    if let Some(same_site) = &cookie.same_site {
73        parts.push(format!("SameSite={}", match same_site {
74            crate::context::SameSite::Strict => "Strict",
75            crate::context::SameSite::Lax => "Lax",
76            crate::context::SameSite::None => "None",
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!("Expires={}", dt.format("%a, %d %b %Y %H:%M:%S GMT")));
83        }
84    }
85
86    parts.join("; ")
87}
88
89/// Extract cookies from a reqwest cookie jar for a given URL.
90///
91/// Returns a list of cookies that would be sent for the given URL.
92pub fn extract_from_jar(jar: &Arc<Jar>, url: &str) -> Vec<Cookie> {
93    let mut cookies = Vec::new();
94
95    if let Ok(parsed_url) = url::Url::parse(url) {
96        if let Some(cookie_header) = jar.cookies(&parsed_url) {
97            // Parse the cookie header
98            let header_str = cookie_header.to_str().unwrap_or("");
99            for cookie_str in header_str.split("; ") {
100                if let Some((name, value)) = cookie_str.split_once('=') {
101                    cookies.push(Cookie {
102                        name: name.to_string(),
103                        value: value.to_string(),
104                        domain: parsed_url.host_str().map(String::from),
105                        path: Some(parsed_url.path().to_string()),
106                        url: Some(url.to_string()),
107                        expires: None,
108                        http_only: None,
109                        secure: Some(parsed_url.scheme() == "https"),
110                        same_site: None,
111                    });
112                }
113            }
114        }
115    }
116
117    cookies
118}
119
120#[cfg(test)]
121mod tests;