Skip to main content

tako_rs_plugins/middleware/session/
data.rs

1//! The per-request [`Session`] handle injected into request extensions.
2
3use std::sync::Arc;
4use std::sync::atomic::AtomicBool;
5use std::sync::atomic::AtomicU64;
6use std::sync::atomic::Ordering;
7
8use parking_lot::Mutex;
9use serde::Serialize;
10use serde::de::DeserializeOwned;
11
12/// A session handle injected into request extensions.
13#[derive(Clone)]
14pub struct Session {
15  data: Arc<Mutex<serde_json::Map<String, serde_json::Value>>>,
16  dirty: Arc<AtomicBool>,
17  rotation_counter: Arc<AtomicU64>,
18  destroyed: Arc<AtomicBool>,
19}
20
21impl Session {
22  pub(crate) fn new(data: serde_json::Map<String, serde_json::Value>) -> Self {
23    Self {
24      data: Arc::new(Mutex::new(data)),
25      dirty: Arc::new(AtomicBool::new(false)),
26      rotation_counter: Arc::new(AtomicU64::new(0)),
27      destroyed: Arc::new(AtomicBool::new(false)),
28    }
29  }
30
31  /// Reads a value from the session.
32  pub fn get<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
33    self
34      .data
35      .lock()
36      .get(key)
37      .and_then(|v| serde_json::from_value(v.clone()).ok())
38  }
39
40  /// Stores a value in the session, marking it dirty.
41  pub fn set<T: Serialize>(&self, key: &str, value: T) {
42    if let Ok(v) = serde_json::to_value(value) {
43      self.data.lock().insert(key.to_string(), v);
44      self.dirty.store(true, Ordering::Relaxed);
45    }
46  }
47
48  /// Removes a key from the session.
49  pub fn remove(&self, key: &str) {
50    if self.data.lock().remove(key).is_some() {
51      self.dirty.store(true, Ordering::Relaxed);
52    }
53  }
54
55  /// Empties the session keeping its id stable. Use this when you want the
56  /// session to live on (e.g. clearing temporary state) but the cookie should
57  /// keep being refreshed. For logout flows that should remove the cookie
58  /// from the browser, use [`Self::destroy`] instead.
59  pub fn clear(&self) {
60    let mut guard = self.data.lock();
61    if !guard.is_empty() {
62      guard.clear();
63      self.dirty.store(true, Ordering::Relaxed);
64    }
65  }
66
67  /// Marks the session for destruction: the server-side entry is removed and
68  /// the response Set-Cookie carries `Max-Age=0` with a past `Expires` so the
69  /// user agent drops it. Pair this with whatever logout response your
70  /// application returns.
71  pub fn destroy(&self) {
72    self.data.lock().clear();
73    self.destroyed.store(true, Ordering::Release);
74    self.dirty.store(true, Ordering::Relaxed);
75  }
76
77  pub(crate) fn is_destroyed(&self) -> bool {
78    self.destroyed.load(Ordering::Acquire)
79  }
80
81  /// Forces a fresh session id on the next response. Call this after
82  /// privilege transitions (login / role change) to defend against
83  /// fixation attacks.
84  pub fn rotate(&self) {
85    self.rotation_counter.fetch_add(1, Ordering::AcqRel);
86    self.dirty.store(true, Ordering::Relaxed);
87  }
88
89  pub(crate) fn is_dirty(&self) -> bool {
90    self.dirty.load(Ordering::Relaxed)
91  }
92
93  /// True if [`Session::rotate`] has been called on this handle since the
94  /// session middleware created it. Surfaced as public API so paired
95  /// middleware (notably CSRF) can mint fresh derivative tokens on the same
96  /// response that emits the rotated session id.
97  pub fn rotation_requested(&self) -> bool {
98    self.rotation_counter.load(Ordering::Acquire) > 0
99  }
100
101  pub(crate) fn snapshot(&self) -> serde_json::Map<String, serde_json::Value> {
102    self.data.lock().clone()
103  }
104}