Skip to main content

cortex_runtime/acquisition/
http_session.rs

1//! HTTP session management for authenticated requests.
2//!
3//! An `HttpSession` stores cookies, auth headers, and CSRF tokens obtained
4//! during authentication. It can be applied to any `HttpClient` request to
5//! make authenticated API calls and page fetches.
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::time::{SystemTime, UNIX_EPOCH};
11
12/// Monotonic counter for generating unique session IDs.
13static SESSION_COUNTER: AtomicU64 = AtomicU64::new(0);
14
15/// An authenticated HTTP session.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct HttpSession {
18    /// Unique session identifier.
19    pub session_id: String,
20    /// Domain this session is valid for.
21    pub domain: String,
22    /// Session cookies (name -> value).
23    pub cookies: HashMap<String, String>,
24    /// Authentication headers to include (header-name -> value).
25    pub auth_headers: HashMap<String, String>,
26    /// CSRF token if discovered.
27    pub csrf_token: Option<String>,
28    /// Type of authentication used.
29    pub auth_type: AuthType,
30    /// Unix timestamp when session was created.
31    pub created_at: f64,
32    /// Unix timestamp when session expires, if known.
33    pub expires_at: Option<f64>,
34}
35
36/// Type of authentication used to establish a session.
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
38pub enum AuthType {
39    /// Password-based login (form POST).
40    Password,
41    /// OAuth flow (browser-assisted).
42    OAuth(String), // provider name
43    /// API key in header.
44    ApiKey,
45    /// Bearer token.
46    Bearer,
47    /// No authentication.
48    None,
49}
50
51impl HttpSession {
52    /// Create a new session for the given domain with the specified auth type.
53    ///
54    /// Generates a unique session ID from the current timestamp and an atomic
55    /// counter. Cookies and auth headers start empty.
56    pub fn new(domain: &str, auth_type: AuthType) -> Self {
57        let ts = SystemTime::now()
58            .duration_since(UNIX_EPOCH)
59            .unwrap_or_default()
60            .as_millis();
61        let counter = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed);
62        let session_id = format!("sess-{ts}-{counter}");
63        let created_at = ts as f64 / 1000.0;
64
65        Self {
66            session_id,
67            domain: domain.to_string(),
68            cookies: HashMap::new(),
69            auth_headers: HashMap::new(),
70            csrf_token: None,
71            auth_type,
72            created_at,
73            expires_at: None,
74        }
75    }
76
77    /// Format cookies as a `Cookie` header value.
78    ///
79    /// Returns a string like `name1=val1; name2=val2`. The order of cookies
80    /// is sorted by name for deterministic output.
81    pub fn cookie_header(&self) -> String {
82        let mut pairs: Vec<_> = self.cookies.iter().collect();
83        pairs.sort_by_key(|(k, _)| (*k).clone());
84        pairs
85            .iter()
86            .map(|(k, v)| format!("{k}={v}"))
87            .collect::<Vec<_>>()
88            .join("; ")
89    }
90
91    /// Check whether this session has expired.
92    ///
93    /// Returns `false` if no expiry is set.
94    pub fn is_expired(&self) -> bool {
95        if let Some(expires) = self.expires_at {
96            let now = SystemTime::now()
97                .duration_since(UNIX_EPOCH)
98                .unwrap_or_default()
99                .as_secs_f64();
100            now >= expires
101        } else {
102            false
103        }
104    }
105
106    /// Add a cookie to this session.
107    pub fn add_cookie(&mut self, name: &str, value: &str) {
108        self.cookies.insert(name.to_string(), value.to_string());
109    }
110
111    /// Add an authentication header to this session.
112    pub fn add_auth_header(&mut self, name: &str, value: &str) {
113        self.auth_headers
114            .insert(name.to_string(), value.to_string());
115    }
116
117    /// Set the expiry timestamp for this session.
118    pub fn set_expires(&mut self, unix_timestamp: f64) {
119        self.expires_at = Some(unix_timestamp);
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_session_creation() {
129        let session = HttpSession::new("example.com", AuthType::Password);
130
131        assert!(session.session_id.starts_with("sess-"));
132        assert_eq!(session.domain, "example.com");
133        assert_eq!(session.auth_type, AuthType::Password);
134        assert!(session.cookies.is_empty());
135        assert!(session.auth_headers.is_empty());
136        assert!(session.csrf_token.is_none());
137        assert!(session.expires_at.is_none());
138        assert!(session.created_at > 0.0);
139    }
140
141    #[test]
142    fn test_cookie_header_format() {
143        let mut session = HttpSession::new("example.com", AuthType::None);
144        session.add_cookie("session_id", "abc123");
145        session.add_cookie("csrftoken", "xyz789");
146
147        let header = session.cookie_header();
148        // Sorted by name: csrftoken comes before session_id
149        assert_eq!(header, "csrftoken=xyz789; session_id=abc123");
150    }
151
152    #[test]
153    fn test_is_expired() {
154        let mut session = HttpSession::new("example.com", AuthType::Bearer);
155
156        // No expiry set — not expired.
157        assert!(!session.is_expired());
158
159        // Expiry in the past — expired.
160        session.set_expires(0.0);
161        assert!(session.is_expired());
162
163        // Expiry far in the future — not expired.
164        session.set_expires(f64::MAX);
165        assert!(!session.is_expired());
166    }
167
168    #[test]
169    fn test_add_cookies_and_headers() {
170        let mut session = HttpSession::new("example.com", AuthType::ApiKey);
171
172        session.add_cookie("sid", "value1");
173        session.add_cookie("pref", "dark");
174        assert_eq!(session.cookies.len(), 2);
175        assert_eq!(session.cookies.get("sid").unwrap(), "value1");
176        assert_eq!(session.cookies.get("pref").unwrap(), "dark");
177
178        session.add_auth_header("X-Api-Key", "key123");
179        session.add_auth_header("X-Custom", "custom_val");
180        assert_eq!(session.auth_headers.len(), 2);
181        assert_eq!(session.auth_headers.get("X-Api-Key").unwrap(), "key123");
182        assert_eq!(session.auth_headers.get("X-Custom").unwrap(), "custom_val");
183    }
184}