typed_session/session.rs
1use chrono::{DateTime, Duration, Utc};
2use secure_string::SecureArray;
3use std::fmt::Debug;
4use std::mem;
5
6/// A session with a client.
7/// This type handles the creation, updating and deletion of sessions.
8/// It is marked `#[must_use]`, as dropping it will not update the session store.
9/// Instead, it should be passed to [`SessionStore::store_session`](crate::session_store::SessionStore::store_session).
10///
11/// `SessionData` is the data associated with a session.
12/// `COOKIE_LENGTH` is the length of the session cookie, in characters.
13/// The default choice is 32, which is secure.
14/// It should be a multiple of 32, which is the block size of blake3.
15#[derive(Debug, Clone)]
16#[must_use]
17pub struct Session<SessionData, const COOKIE_LENGTH: usize = 32> {
18 pub(crate) state: SessionState<SessionData>,
19}
20
21#[derive(Debug, Clone)]
22pub(crate) enum SessionState<SessionData> {
23 /// The session was newly generated for this request, and at most the expiry was written to.
24 /// In this state, the session does not necessarily need to be communicated to the client.
25 NewUnchanged {
26 expiry: SessionExpiry,
27 data: SessionData,
28 },
29 /// The session was newly generated for this request, and the data was written to.
30 /// In this state, the session must be communicated to the client.
31 NewChanged {
32 expiry: SessionExpiry,
33 data: SessionData,
34 },
35 /// The session was loaded from the session store, and was not changed.
36 Unchanged {
37 current_id: SessionId,
38 expiry: SessionExpiry,
39 data: SessionData,
40 },
41 /// The session was loaded from the session store, and was changed.
42 /// Either the expiry datetime or the data have changed.
43 Changed {
44 current_id: SessionId,
45 expiry: SessionExpiry,
46 data: SessionData,
47 },
48 /// The session was marked for deletion.
49 Deleted { current_id: SessionId },
50 /// The session was marked for deletion before it was ever communicated to database or client.
51 NewDeleted,
52 /// Used internally to avoid unsafe code when replacing the session state through a mutable reference.
53 Invalid,
54}
55
56/// The expiry of a session.
57/// Either a given date and time, or never.
58#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
59pub enum SessionExpiry {
60 /// The session expires at the given date and time.
61 DateTime(DateTime<Utc>),
62 /// The session never expires, unless it is explicitly deleted.
63 Never,
64}
65
66/// The type of a session id.
67pub type SessionIdType = SecureArray<u8, { blake3::OUT_LEN }>;
68
69/// A session id.
70#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
71pub struct SessionId(Box<SessionIdType>);
72
73impl<SessionData, const COOKIE_LENGTH: usize> Session<SessionData, COOKIE_LENGTH> {
74 /// Extract the optionally associated data and expiry while consuming the session.
75 ///
76 /// **This function is supposed to be used in tests only.**
77 /// This loses the association of the data to the actual session, making it useless for most
78 /// purposes.
79 pub fn into_data_expiry_pair(self) -> (Option<SessionData>, Option<SessionExpiry>) {
80 self.state.into_data_expiry_pair()
81 }
82}
83
84impl<SessionData: Default, const COOKIE_LENGTH: usize> Session<SessionData, COOKIE_LENGTH> {
85 /// Create a new session with default data. Does not set an expiry.
86 /// Using this method does not mark the session as changed, i.e. it will be silently dropped if
87 /// neither the data nor the expiry are accessed mutably.
88 ///
89 /// # Example
90 ///
91 /// ```rust
92 /// # use typed_session::Session;
93 /// # fn main() -> Result<(), typed_session::Error<()>> { use typed_session::SessionExpiry;
94 /// # async_std::task::block_on(async {
95 /// let session: Session<i32> = Session::new();
96 /// assert_eq!(&SessionExpiry::Never, session.expiry());
97 /// assert_eq!(i32::default(), *session.data());
98 /// # Ok(()) }) }
99 pub fn new() -> Self {
100 Self {
101 state: SessionState::new(),
102 }
103 }
104}
105
106impl<SessionData, const COOKIE_LENGTH: usize> Session<SessionData, COOKIE_LENGTH> {
107 /// Create a new session with the given session data. Does not set an expiry.
108 /// Using this method marks the session as changed, i.e. it will be stored in the backend and
109 /// communicated to the client even if it was created with default data and never accessed mutably.
110 ///
111 /// # Example
112 ///
113 /// ```rust
114 /// # use typed_session::Session;
115 /// # fn main() -> Result<(), typed_session::Error<()>> { use typed_session::SessionExpiry;
116 /// # async_std::task::block_on(async {
117 /// let session: Session<_> = Session::new_with_data(4);
118 /// assert_eq!(&SessionExpiry::Never, session.expiry());
119 /// assert_eq!(4, *session.data());
120 /// # Ok(()) }) }
121 pub fn new_with_data(data: SessionData) -> Self {
122 Self {
123 state: SessionState::new_with_data(data),
124 }
125 }
126
127 /// **This method should only be called by a session store!**
128 ///
129 /// Create a session instance from parts loaded by a session store.
130 /// The session state will be `Unchanged`.
131 pub fn new_from_session_store(
132 current_id: SessionId,
133 expiry: SessionExpiry,
134 data: SessionData,
135 ) -> Self {
136 Self {
137 state: SessionState::new_from_session_store(current_id, expiry, data),
138 }
139 }
140
141 /// Returns true if this session is marked for destruction.
142 ///
143 /// # Example
144 ///
145 /// ```rust
146 /// # use typed_session::Session;
147 /// # fn main() -> Result<(), typed_session::Error<()>> { async_std::task::block_on(async {
148 /// let mut session: Session<()> = Session::new();
149 /// assert!(!session.is_deleted());
150 /// session.delete();
151 /// assert!(session.is_deleted());
152 /// # Ok(()) }) }
153 pub fn is_deleted(&self) -> bool {
154 self.state.is_deleted()
155 }
156
157 /// Returns true if this session was changed since it was loaded from the session store.
158 ///
159 /// # Example
160 ///
161 /// ```rust
162 /// # use typed_session::Session;
163 /// # fn main() -> Result<(), typed_session::Error<()>> { async_std::task::block_on(async {
164 /// let mut session: Session<()> = Session::new();
165 /// assert!(!session.is_changed());
166 /// session.data_mut();
167 /// assert!(session.is_changed());
168 /// # Ok(()) }) }
169 pub fn is_changed(&self) -> bool {
170 self.state.is_changed()
171 }
172
173 /// Returns true if this session was changed since it was loaded from the session store, or if it is marked for destruction.
174 ///
175 /// # Example
176 ///
177 /// ```rust
178 /// # use typed_session::Session;
179 /// # fn main() -> Result<(), typed_session::Error<()>> { async_std::task::block_on(async {
180 /// let mut session: Session<()> = Session::new();
181 /// assert!(!session.is_changed_or_deleted());
182 /// session.data_mut();
183 /// assert!(session.is_changed_or_deleted());
184 /// let mut session: Session<()> = Session::new();
185 /// assert!(!session.is_changed_or_deleted());
186 /// session.delete();
187 /// assert!(session.is_changed_or_deleted());
188 /// # Ok(()) }) }
189 pub fn is_changed_or_deleted(&self) -> bool {
190 self.state.is_changed_or_deleted()
191 }
192}
193
194impl<SessionData: Debug, const COOKIE_LENGTH: usize> Session<SessionData, COOKIE_LENGTH> {
195 /// Returns the expiry timestamp of this session, if there is one.
196 ///
197 /// # Example
198 ///
199 /// ```rust
200 /// # use typed_session::Session;
201 /// # fn main() -> Result<(), typed_session::Error<()>> { use chrono::Utc;
202 /// # use typed_session::SessionExpiry;
203 /// # async_std::task::block_on(async {
204 /// let mut session: Session<()> = Session::new();
205 /// assert_eq!(&SessionExpiry::Never, session.expiry());
206 /// session.expire_in(Utc::now(), std::time::Duration::from_secs(1));
207 /// assert!(matches!(session.expiry(), SessionExpiry::DateTime { .. }));
208 /// # Ok(()) }) }
209 /// ```
210 pub fn expiry(&self) -> &SessionExpiry {
211 self.state.expiry()
212 }
213
214 /// Returns a reference to the data associated with this session.
215 /// This does not mark the session as changed.
216 pub fn data(&self) -> &SessionData {
217 self.state.data()
218 }
219
220 /// Returns a mutable reference to the data associated with this session,
221 /// and marks the session as changed.
222 ///
223 /// Note that the session gets marked as changed, even if the returned reference is never written to.
224 ///
225 /// **Panics** if the session was marked for deletion before.
226 pub fn data_mut(&mut self) -> &mut SessionData {
227 self.state.data_mut()
228 }
229
230 /// Mark this session for destruction.
231 /// Further access to this session will result in a panic.
232 /// Note that the session is only deleted from the session store if [`SessionStore::store_session`](crate::session_store::SessionStore::store_session) is called.
233 ///
234 /// # Example
235 ///
236 /// ```rust
237 /// # use typed_session::{Session, Error};
238 /// # fn main() -> Result<(), Error<()>> { async_std::task::block_on(async {
239 /// let mut session: Session<()> = Session::new();
240 /// assert!(!session.is_deleted());
241 /// session.delete();
242 /// assert!(session.is_deleted());
243 /// # Ok(()) }) }
244 pub fn delete(&mut self) {
245 self.state.delete();
246 }
247
248 /// Forces the generation of a new id and cookie for this session, unless the session is new and its data was not accessed mutably.
249 pub fn regenerate(&mut self) {
250 // Calling this marks the state as changed, unless it is new and its data was not accessed mutably.
251 self.state.change_expiry();
252 }
253
254 /// Updates the expiry timestamp of this session.
255 ///
256 /// # Example
257 ///
258 /// ```rust
259 /// # use typed_session::Session;
260 /// # fn main() -> Result<(), typed_session::Error<()>> { use typed_session::SessionExpiry;
261 /// # async_std::task::block_on(async {
262 /// let mut session: Session<()> = Session::new();
263 /// assert_eq!(&SessionExpiry::Never, session.expiry());
264 /// session.set_expiry(chrono::Utc::now());
265 /// assert!(matches!(session.expiry(), SessionExpiry::DateTime { .. }));
266 /// # Ok(()) }) }
267 /// ```
268 pub fn set_expiry(&mut self, expiry: DateTime<Utc>) {
269 *self.state.expiry_mut() = SessionExpiry::DateTime(expiry);
270 }
271
272 /// Sets this session to never expire.
273 ///
274 /// # Example
275 ///
276 /// ```rust
277 /// # use typed_session::{Session, Error};
278 /// # fn main() -> Result<(), Error<()>> { use typed_session::SessionExpiry;
279 /// # async_std::task::block_on(async {
280 /// let mut session: Session<()> = Session::new();
281 /// assert_eq!(&SessionExpiry::Never, session.expiry());
282 /// session.set_expiry(chrono::Utc::now());
283 /// assert!(matches!(session.expiry(), SessionExpiry::DateTime { .. }));
284 /// session.do_not_expire();
285 /// assert!(matches!(session.expiry(), SessionExpiry::Never));
286 /// # Ok(()) }) }
287 /// ```
288 pub fn do_not_expire(&mut self) {
289 *self.state.expiry_mut() = SessionExpiry::Never;
290 }
291
292 /// Sets this session to expire `ttl` time into the future.
293 ///
294 /// # Example
295 ///
296 /// ```rust
297 /// # use typed_session::Session;
298 /// # fn main() -> Result<(), typed_session::Error<()>> { use chrono::Utc;
299 /// # use typed_session::SessionExpiry;
300 /// # async_std::task::block_on(async {
301 /// let mut session: Session<()> = Session::new();
302 /// assert_eq!(&SessionExpiry::Never, session.expiry());
303 /// session.expire_in(Utc::now(), std::time::Duration::from_secs(1));
304 /// assert!(matches!(session.expiry(), SessionExpiry::DateTime { .. }));
305 /// # Ok(()) }) }
306 /// ```
307 pub fn expire_in(&mut self, now: DateTime<Utc>, ttl: std::time::Duration) {
308 *self.state.expiry_mut() = SessionExpiry::DateTime(now + Duration::from_std(ttl).unwrap());
309 }
310
311 /// Return true if the session is expired.
312 /// The session is expired if it has an expiry timestamp that is in the future.
313 ///
314 /// # Example
315 ///
316 /// ```rust
317 /// # use typed_session::Session;
318 /// # use std::time::Duration;
319 /// # use async_std::task;
320 /// # fn main() -> Result<(), typed_session::Error<()>> { use chrono::Utc;
321 /// # use typed_session::SessionExpiry;
322 /// # async_std::task::block_on(async {
323 /// let mut session: Session<()> = Session::new();
324 /// assert_eq!(&SessionExpiry::Never, session.expiry());
325 /// assert!(!session.is_expired(Utc::now()));
326 /// session.expire_in(Utc::now(), Duration::from_secs(1));
327 /// assert!(!session.is_expired(Utc::now()));
328 /// task::sleep(Duration::from_secs(2)).await;
329 /// assert!(session.is_expired(Utc::now()));
330 /// # Ok(()) }) }
331 /// ```
332 pub fn is_expired(&self, now: DateTime<Utc>) -> bool {
333 match self.state.expiry() {
334 SessionExpiry::DateTime(expiry) => *expiry < now,
335 SessionExpiry::Never => false,
336 }
337 }
338
339 /// Returns the duration from now to the expiry time of this session.
340 /// Returns `None` if it is expired.
341 ///
342 /// # Example
343 ///
344 /// ```rust
345 /// # use typed_session::Session;
346 /// # use std::time::Duration;
347 /// # use async_std::task;
348 /// # fn main() -> Result<(), typed_session::Error<()>> { use chrono::Utc;
349 /// # async_std::task::block_on(async {
350 /// let mut session: Session<()> = Session::new();
351 /// session.expire_in(Utc::now(), Duration::from_secs(123));
352 /// let expires_in = session.expires_in(Utc::now()).unwrap();
353 /// assert!(123 - expires_in.as_secs() < 2);
354 /// # Ok(()) }) }
355 /// ```
356 pub fn expires_in(&self, now: DateTime<Utc>) -> Option<std::time::Duration> {
357 match self.state.expiry() {
358 SessionExpiry::DateTime(date_time) => {
359 let duration = date_time.signed_duration_since(now);
360 if duration > Duration::zero() {
361 Some(duration.to_std().unwrap())
362 } else {
363 None
364 }
365 }
366 SessionExpiry::Never => None,
367 }
368 }
369}
370
371impl<SessionData: Default, const COOKIE_LENGTH: usize> Default
372 for Session<SessionData, COOKIE_LENGTH>
373{
374 fn default() -> Self {
375 Self::new()
376 }
377}
378
379impl<SessionData: Default> SessionState<SessionData> {
380 fn new() -> Self {
381 Self::NewUnchanged {
382 expiry: SessionExpiry::Never,
383 data: Default::default(),
384 }
385 }
386}
387
388impl<SessionData> SessionState<SessionData> {
389 fn new_with_data(data: SessionData) -> Self {
390 Self::NewChanged {
391 expiry: SessionExpiry::Never,
392 data,
393 }
394 }
395
396 fn new_from_session_store(
397 current_id: SessionId,
398 expiry: SessionExpiry,
399 data: SessionData,
400 ) -> Self {
401 Self::Unchanged {
402 current_id,
403 expiry,
404 data,
405 }
406 }
407
408 fn is_deleted(&self) -> bool {
409 matches!(self, Self::Deleted { .. } | Self::NewDeleted)
410 }
411
412 fn is_changed(&self) -> bool {
413 matches!(self, Self::Changed { .. } | Self::NewChanged { .. })
414 }
415
416 fn is_changed_or_deleted(&self) -> bool {
417 self.is_changed() || self.is_deleted()
418 }
419
420 fn into_data_expiry_pair(self) -> (Option<SessionData>, Option<SessionExpiry>) {
421 match self {
422 SessionState::NewUnchanged { data, expiry }
423 | SessionState::NewChanged { data, expiry }
424 | SessionState::Unchanged { data, expiry, .. }
425 | SessionState::Changed { data, expiry, .. } => (Some(data), Some(expiry)),
426 SessionState::Deleted { .. } | SessionState::NewDeleted => (None, None),
427 SessionState::Invalid => unreachable!("Invalid state is used internally only"),
428 }
429 }
430}
431
432impl<SessionData: Debug> SessionState<SessionData> {
433 fn expiry(&self) -> &SessionExpiry {
434 match self {
435 Self::NewUnchanged { expiry, .. }
436 | Self::NewChanged { expiry, .. }
437 | Self::Unchanged { expiry, .. }
438 | Self::Changed { expiry, .. } => expiry,
439 Self::Deleted { .. } | Self::NewDeleted => {
440 panic!("Attempted to retrieve the expiry of a purged session {self:?}")
441 }
442 Self::Invalid => unreachable!("Invalid state is used internally only"),
443 }
444 }
445
446 fn expiry_mut(&mut self) -> &mut SessionExpiry {
447 self.change_expiry();
448
449 match self {
450 Self::NewUnchanged { expiry, .. }
451 | Self::NewChanged { expiry, .. }
452 | Self::Changed { expiry, .. } => expiry,
453 Self::Deleted { .. } | Self::NewDeleted => {
454 panic!("Attempted to retrieve the expiry of a purged session {self:?}")
455 }
456 Self::Unchanged { .. } => {
457 unreachable!("Cannot be unchanged after explicitly changing expiry")
458 }
459 Self::Invalid => unreachable!("Invalid state is used internally only"),
460 }
461 }
462
463 fn data(&self) -> &SessionData {
464 match self {
465 Self::NewUnchanged { data, .. }
466 | Self::NewChanged { data, .. }
467 | Self::Unchanged { data, .. }
468 | Self::Changed { data, .. } => data,
469 Self::Deleted { .. } | Self::NewDeleted => {
470 panic!("Attempted to retrieve the data of a purged session {self:?}")
471 }
472 Self::Invalid => unreachable!("Invalid state is used internally only"),
473 }
474 }
475
476 fn data_mut(&mut self) -> &mut SessionData {
477 self.change_data();
478
479 match self {
480 Self::NewChanged { data, .. } | Self::Changed { data, .. } => data,
481 Self::Deleted { .. } | Self::NewDeleted => {
482 panic!("Attempted to retrieve the data of a purged session {self:?}")
483 }
484 Self::NewUnchanged { .. } | Self::Unchanged { .. } => {
485 unreachable!("Cannot be unchanged after explicitly changing")
486 }
487 Self::Invalid => unreachable!("Invalid state is used internally only"),
488 }
489 }
490
491 fn change_expiry(&mut self) {
492 match self {
493 Self::Unchanged { .. } => {
494 let Self::Unchanged {
495 current_id,
496 expiry,
497 data,
498 } = mem::replace(self, Self::Invalid)
499 else {
500 unreachable!()
501 };
502 *self = Self::Changed {
503 current_id,
504 expiry,
505 data,
506 };
507 }
508 Self::Changed { .. } | Self::NewChanged { .. } => { /* Already changed. */ }
509 Self::NewUnchanged { .. } => { /* Changing expiry is not enough reason to store the session. */
510 }
511 Self::Deleted { .. } | Self::NewDeleted => {
512 panic!("Attempted to change purged session {self:?}")
513 }
514 Self::Invalid => unreachable!("Invalid state is used internally only"),
515 }
516 }
517
518 fn change_data(&mut self) {
519 match self {
520 Self::NewUnchanged { .. } => {
521 let Self::NewUnchanged { expiry, data } = mem::replace(self, Self::Invalid) else {
522 unreachable!()
523 };
524 *self = Self::NewChanged { expiry, data };
525 }
526 Self::Unchanged { .. } => {
527 let Self::Unchanged {
528 current_id,
529 expiry,
530 data,
531 } = mem::replace(self, Self::Invalid)
532 else {
533 unreachable!()
534 };
535 *self = Self::Changed {
536 current_id,
537 expiry,
538 data,
539 };
540 }
541 Self::Changed { .. } | Self::NewChanged { .. } => { /* Already changed. */ }
542 Self::Deleted { .. } | Self::NewDeleted => {
543 panic!("Attempted to change purged session {self:?}")
544 }
545 Self::Invalid => unreachable!("Invalid state is used internally only"),
546 }
547 }
548
549 fn delete(&mut self) {
550 match self {
551 Self::NewUnchanged { .. } | Self::NewChanged { .. } => {
552 *self = Self::NewDeleted;
553 }
554 Self::Unchanged { .. } => {
555 let Self::Unchanged { current_id, .. } = mem::replace(self, Self::Invalid) else {
556 unreachable!()
557 };
558 *self = Self::Deleted { current_id };
559 }
560 Self::Changed { .. } => {
561 let Self::Changed { current_id, .. } = mem::replace(self, Self::Invalid) else {
562 unreachable!()
563 };
564 *self = Self::Deleted { current_id };
565 }
566 Self::Deleted { .. } | Self::NewDeleted => {
567 panic!("Attempted to purge a purged session {self:?}")
568 }
569 Self::Invalid => unreachable!("Invalid state is used internally only"),
570 }
571 }
572}
573
574impl SessionId {
575 /// Applies a cryptographic hash function on a cookie value to obtain the session id for that cookie.
576 ///
577 /// This is automatically done by the [`SessionStore`](crate::SessionStore), and this function is only public for test purposes.
578 pub fn from_cookie_value(cookie_value: &str) -> Self {
579 // The original code used base64 encoded binary ids of length of a multiple of the blake3 block size.
580 // We do the same, but instead of base64 encoding a binary ids, we use normal alphanumerical ids with a length multiple of the blake3 block size.
581 // This gives less entropy, but still more than enough to be secure (see crate-level documentation).
582 let hash = blake3::hash(cookie_value.as_bytes());
583 Self(Box::new((<[u8; blake3::OUT_LEN]>::from(hash)).into()))
584 }
585}
586
587impl AsRef<[u8]> for SessionId {
588 fn as_ref(&self) -> &[u8] {
589 self.0.as_ref().unsecure()
590 }
591}
592
593impl From<SessionId> for SessionIdType {
594 fn from(id: SessionId) -> Self {
595 *id.0
596 }
597}