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}