1use crate::config::SessionConfig;
4use crate::id::{generate, id_hash};
5use crate::store::SessionRecord;
6use crate::token;
7use http::request::Parts;
8use serde::Serialize;
9use serde::de::DeserializeOwned;
10use serde_json::{Value, json};
11use siderite_core::{ApiError, FromRequestParts};
12use siderite_openapi::{Operation, SchemaRegistry};
13use std::collections::BTreeMap;
14use std::fmt;
15use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
16use std::time::Duration;
17
18const SCHEME: &str = "sessionCookie";
20const DOCUMENTED_COOKIE: &str = "__Host-session";
22
23pub(crate) struct Loaded {
25 pub(crate) hash: String,
26 pub(crate) record: SessionRecord,
27 pub(crate) touch: bool,
29}
30
31pub(crate) struct Plan {
33 pub(crate) failed: bool,
35 pub(crate) save: Option<Save>,
36 pub(crate) delete: Vec<String>,
38 pub(crate) fresh: Option<String>,
40 pub(crate) login: Option<String>,
42 pub(crate) cookie: CookieAction,
43}
44
45pub(crate) struct Save {
47 pub(crate) hash: String,
48 pub(crate) record: SessionRecord,
49 pub(crate) ttl: Duration,
50 pub(crate) required: bool,
52 pub(crate) existing: bool,
55}
56
57pub(crate) enum CookieAction {
59 Keep,
60 Set { value: String, max_age: Duration },
61 Expire,
62}
63
64struct State {
65 record: SessionRecord,
66 existing: Option<String>,
68 fresh: Option<(String, String)>,
70 stale: Vec<String>,
72 dirty: bool,
73 touch: bool,
74 login: Option<String>,
75 failed: bool,
76 expire_cookie: bool,
77}
78
79impl State {
80 fn current_hash(&self) -> Option<&str> {
81 match &self.fresh {
82 Some((_, hash)) => Some(hash),
83 None => self.existing.as_deref(),
84 }
85 }
86
87 fn ensure_id(&mut self) {
89 if self.existing.is_none() && self.fresh.is_none() {
90 self.issue();
91 }
92 }
93
94 fn issue(&mut self) {
95 match generate() {
96 Ok(raw) => {
97 let hash = id_hash(&raw);
98 self.fresh = Some((raw, hash));
99 }
100 Err(_) => {
101 tracing::error!("session id generation failed");
102 self.failed = true;
103 }
104 }
105 }
106
107 fn rotate(&mut self) {
109 if let Some(old) = self.existing.take() {
110 self.stale.push(old);
111 }
112 self.issue();
113 self.dirty = true;
114 }
115}
116
117#[derive(Clone)]
128pub struct Session {
129 state: Arc<Mutex<State>>,
130 config: Arc<SessionConfig>,
131}
132
133impl fmt::Debug for Session {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 f.debug_struct("Session").finish_non_exhaustive()
137 }
138}
139
140impl Session {
141 pub(crate) fn new(
142 config: Arc<SessionConfig>,
143 loaded: Option<Loaded>,
144 expire_cookie: bool,
145 ) -> Self {
146 let mut state = State {
147 record: SessionRecord::default(),
148 existing: None,
149 fresh: None,
150 stale: Vec::new(),
151 dirty: false,
152 touch: false,
153 login: None,
154 failed: false,
155 expire_cookie,
156 };
157 if let Some(loaded) = loaded {
158 state.record = loaded.record;
159 state.existing = Some(loaded.hash);
160 state.touch = loaded.touch;
161 }
162 Self {
163 state: Arc::new(Mutex::new(state)),
164 config,
165 }
166 }
167
168 fn lock(&self) -> MutexGuard<'_, State> {
169 self.state.lock().unwrap_or_else(PoisonError::into_inner)
170 }
171
172 #[must_use]
175 pub fn get<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
176 let value = self.lock().record.data.get(key)?.clone();
177 serde_json::from_value(value).ok()
178 }
179
180 pub fn insert<T: Serialize>(
185 &self,
186 key: impl Into<String>,
187 value: T,
188 ) -> Result<(), serde_json::Error> {
189 let value = serde_json::to_value(value)?;
190 let mut state = self.lock();
191 state.record.data.insert(key.into(), value);
192 state.dirty = true;
193 state.ensure_id();
194 Ok(())
195 }
196
197 pub fn remove(&self, key: &str) -> Option<Value> {
199 let mut state = self.lock();
200 let removed = state.record.data.remove(key)?;
201 state.dirty = true;
202 Some(removed)
203 }
204
205 #[must_use]
207 pub fn user(&self) -> Option<String> {
208 self.lock().record.user.clone()
209 }
210
211 pub fn login(&self, user: impl Into<String>) {
218 let user = user.into();
219 let mut state = self.lock();
220 if state.record.user.as_ref().is_some_and(|prev| *prev != user) {
221 state.record.data.clear();
222 }
223 state.record.user = Some(user.clone());
224 state.login = Some(user);
225 state.rotate();
226 }
227
228 pub fn logout(&self) {
233 let mut state = self.lock();
234 if let Some(old) = state.existing.take() {
235 state.stale.push(old);
236 }
237 state.fresh = None;
238 state.record = SessionRecord::default();
239 state.dirty = false;
240 state.touch = false;
241 state.login = None;
242 state.expire_cookie = true;
243 }
244
245 pub fn rotate(&self) {
249 self.lock().rotate();
250 }
251
252 #[must_use]
259 pub fn csrf_token(&self) -> Option<String> {
260 let key = self.config.csrf_key.as_deref()?;
261 let mut state = self.lock();
262 state.ensure_id();
263 token::issue(key, state.current_hash()?)
264 }
265
266 #[must_use]
269 pub fn verify_csrf(&self, token: &str) -> bool {
270 let Some(key) = self.config.csrf_key.as_deref() else {
271 return false;
272 };
273 let state = self.lock();
274 state
275 .current_hash()
276 .is_some_and(|hash| token::verify(key, hash, token))
277 }
278
279 pub(crate) fn is_loaded(&self) -> bool {
281 self.lock().existing.is_some()
282 }
283
284 pub(crate) fn plan(&self, now: u64) -> Plan {
286 let mut state = self.lock();
287 let mut plan = Plan {
288 failed: state.failed,
289 save: None,
290 delete: std::mem::take(&mut state.stale),
291 fresh: None,
292 login: state.login.take(),
293 cookie: CookieAction::Keep,
294 };
295 if state.failed {
296 return plan;
297 }
298 let mut record = state.record.clone();
299 if plan.login.is_some() || record.created_at == 0 {
300 record.created_at = now;
301 }
302 record.last_seen = now;
303 let ttl = self.config.ttl(&record, now);
304 if let Some((value, hash)) = state.fresh.take() {
305 plan.fresh = Some(hash.clone());
306 let max_age = self.config.remaining(&record, now);
307 plan.cookie = CookieAction::Set { value, max_age };
308 plan.save = Some(Save {
309 hash,
310 record,
311 ttl,
312 required: true,
313 existing: false,
314 });
315 } else if let Some(hash) = state.existing.clone()
316 && (state.dirty || state.touch)
317 {
318 let required = state.dirty;
319 plan.save = Some(Save {
320 hash,
321 record,
322 ttl,
323 required,
324 existing: true,
325 });
326 }
327 if plan.save.is_none() && state.expire_cookie {
328 plan.cookie = CookieAction::Expire;
329 }
330 plan
331 }
332}
333
334impl SessionConfig {
335 pub(crate) fn remaining(
337 &self,
338 record: &SessionRecord,
339 now: u64,
340 ) -> Duration {
341 let age = now.saturating_sub(record.created_at);
342 let left = self.absolute.as_secs().saturating_sub(age);
343 Duration::from_secs(left.max(1))
344 }
345
346 fn ttl(&self, record: &SessionRecord, now: u64) -> Duration {
350 let idle = self.idle + self.effective_write_interval();
351 idle.min(self.remaining(record, now))
352 }
353}
354
355impl FromRequestParts for Session {
356 async fn from_request_parts(parts: &mut Parts) -> Result<Self, ApiError> {
357 parts
358 .extensions
359 .get::<Session>()
360 .cloned()
361 .ok_or_else(|| ApiError::internal("SessionLayer is not installed"))
362 }
363
364 fn describe(op: &mut Operation, registry: &mut SchemaRegistry) {
367 let definition = json!({
368 "type": "apiKey",
369 "in": "cookie",
370 "name": DOCUMENTED_COOKIE,
371 });
372 registry.add_security_scheme(SCHEME, definition);
373 if op.security.is_empty() {
374 op.security.push(BTreeMap::new());
375 }
376 let requirement = BTreeMap::from([(SCHEME.to_owned(), Vec::new())]);
377 if !op.security.contains(&requirement) {
378 op.security.push(requirement);
379 }
380 }
381}