#[cfg(test)]
mod tests;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use crate::cookie::{CookieJar, SetCookie};
use crate::request::Request;
static SESSION_COUNTER: AtomicU64 = AtomicU64::new(0);
fn generate_id() -> String {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
let count = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed);
let mut x = nanos ^ count.wrapping_mul(0x9e3779b97f4a7c15);
x ^= x >> 30;
x = x.wrapping_mul(0xbf58476d1ce4e5b9);
x ^= x >> 27;
x = x.wrapping_mul(0x94d049bb133111eb);
x ^= x >> 31;
let mut y = count ^ nanos.wrapping_mul(0x517cc1b727220a95);
y ^= y >> 30;
y = y.wrapping_mul(0xbf58476d1ce4e5b9);
y ^= y >> 27;
y = y.wrapping_mul(0x94d049bb133111eb);
y ^= y >> 31;
format!("{:016x}{:016x}", x, y)
}
pub struct Session {
pub id: String,
pub(crate) data: HashMap<String, String>,
}
impl Session {
pub fn get(&self, key: &str) -> Option<&str> {
self.data.get(key).map(String::as_str)
}
pub fn set(&mut self, key: &str, value: impl Into<String>) {
self.data.insert(key.to_string(), value.into());
}
pub fn remove(&mut self, key: &str) {
self.data.remove(key);
}
pub fn contains(&self, key: &str) -> bool {
self.data.contains_key(key)
}
}
struct Entry {
data: HashMap<String, String>,
expires_at: Instant,
}
struct Inner {
sessions: HashMap<String, Entry>,
}
pub struct SessionStore {
inner: Arc<Mutex<Inner>>,
ttl: Duration,
}
impl Clone for SessionStore {
fn clone(&self) -> Self {
SessionStore { inner: Arc::clone(&self.inner), ttl: self.ttl }
}
}
impl SessionStore {
pub fn new(ttl_secs: u64) -> Self {
SessionStore {
inner: Arc::new(Mutex::new(Inner { sessions: HashMap::new() })),
ttl: Duration::from_secs(ttl_secs),
}
}
pub fn create(&self) -> Session {
self.create_with_id(generate_id())
}
pub fn create_with_id(&self, id: String) -> Session {
let entry = Entry {
data: HashMap::new(),
expires_at: Instant::now() + self.ttl,
};
self.inner.lock().unwrap().sessions.insert(id.clone(), entry);
Session { id, data: HashMap::new() }
}
pub fn load(&self, id: &str) -> Option<Session> {
let inner = self.inner.lock().unwrap();
let entry = inner.sessions.get(id)?;
if Instant::now() > entry.expires_at {
return None;
}
Some(Session { id: id.to_string(), data: entry.data.clone() })
}
pub fn save(&self, session: &Session) {
let mut inner = self.inner.lock().unwrap();
if let Some(entry) = inner.sessions.get_mut(&session.id) {
entry.data = session.data.clone();
}
}
pub fn destroy(&self, id: &str) {
self.inner.lock().unwrap().sessions.remove(id);
}
pub fn purge_expired(&self) {
let now = Instant::now();
self.inner.lock().unwrap().sessions.retain(|_, e| e.expires_at > now);
}
pub fn len(&self) -> usize {
self.inner.lock().unwrap().sessions.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
pub fn session_id_from_request(request: &Request, cookie_name: &str) -> Option<String> {
let header = request.get_header("Cookie".to_string())?;
let jar = CookieJar::parse(&header.value);
jar.get(cookie_name).map(|c| c.value.clone())
}
pub fn session_cookie(session_id: &str, cookie_name: &str, ttl_secs: u64) -> String {
SetCookie::new(cookie_name, session_id)
.path("/")
.http_only()
.same_site("Lax")
.max_age(ttl_secs as i64)
.build()
}
pub fn destroy_cookie(cookie_name: &str) -> String {
SetCookie::new(cookie_name, "").path("/").max_age(0).build()
}