actix_cloud/session/
session.rs

1use std::{
2    cell::{Ref, RefCell},
3    collections::HashMap,
4    mem,
5    rc::Rc,
6};
7
8use actix_utils::future::{ready, Ready};
9use actix_web::{
10    dev::{Extensions, Payload, ServiceRequest, ServiceResponse},
11    error::Error,
12    FromRequest, HttpMessage, HttpRequest,
13};
14use serde::{de::DeserializeOwned, Serialize};
15
16use crate::Result;
17
18/// The primary interface to access and modify session state.
19///
20/// [`Session`] is an [extractor](#impl-FromRequest)—you can specify it as an input type for your
21/// request handlers and it will be automatically extracted from the incoming request.
22///
23/// You can also retrieve a [`Session`] object from an `HttpRequest` or a `ServiceRequest` using
24/// [`SessionExt`].
25///
26/// [`SessionExt`]: super::SessionExt
27#[derive(Clone)]
28pub struct Session(Rc<RefCell<SessionInner>>);
29
30/// Status of a [`Session`].
31#[derive(Debug, Clone, Default, PartialEq, Eq)]
32pub enum SessionStatus {
33    /// Session state has been updated - the changes will have to be persisted to the backend.
34    Changed,
35
36    /// The session has been flagged for deletion - the session cookie will be removed from
37    /// the client and the session state will be deleted from the session store.
38    ///
39    /// Most operations on the session after it has been marked for deletion will have no effect.
40    Purged,
41
42    /// The session has been flagged for renewal.
43    ///
44    /// The session key will be regenerated and the time-to-live of the session state will be
45    /// extended.
46    Renewed,
47
48    /// The session state has not been modified since its creation/retrieval.
49    #[default]
50    Unchanged,
51}
52
53#[derive(Default)]
54struct SessionInner {
55    state: HashMap<String, String>,
56    status: SessionStatus,
57}
58
59impl Session {
60    /// Get a `value` from the session.
61    ///
62    /// It returns an error if it fails to deserialize as `T` the JSON value associated with `key`.
63    pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
64        if let Some(val_str) = self.0.borrow().state.get(key) {
65            Ok(Some(serde_json::from_str(val_str)?))
66        } else {
67            Ok(None)
68        }
69    }
70
71    /// Get all raw key-value data from the session.
72    ///
73    /// Note that values are JSON encoded.
74    pub fn entries(&self) -> Ref<'_, HashMap<String, String>> {
75        Ref::map(self.0.borrow(), |inner| &inner.state)
76    }
77
78    /// Returns session status.
79    pub fn status(&self) -> SessionStatus {
80        Ref::map(self.0.borrow(), |inner| &inner.status).clone()
81    }
82
83    /// Inserts a key-value pair into the session.
84    ///
85    /// Any serializable value can be used and will be encoded as JSON in session data, hence why
86    /// only a reference to the value is taken.
87    ///
88    /// It returns an error if it fails to serialize `value` to JSON.
89    pub fn insert<T: Serialize>(&self, key: impl Into<String>, value: T) -> Result<()> {
90        let mut inner = self.0.borrow_mut();
91
92        if inner.status != SessionStatus::Purged {
93            if inner.status != SessionStatus::Renewed {
94                inner.status = SessionStatus::Changed;
95            }
96
97            let key = key.into();
98            let val = serde_json::to_string(&value)?;
99
100            inner.state.insert(key, val);
101        }
102
103        Ok(())
104    }
105
106    /// Remove value from the session.
107    ///
108    /// If present, the JSON encoded value is returned.
109    pub fn remove(&self, key: &str) -> Option<String> {
110        let mut inner = self.0.borrow_mut();
111
112        if inner.status != SessionStatus::Purged {
113            if inner.status != SessionStatus::Renewed {
114                inner.status = SessionStatus::Changed;
115            }
116            return inner.state.remove(key);
117        }
118
119        None
120    }
121
122    /// Remove value from the session and deserialize.
123    ///
124    /// Returns `None` if key was not present in session. Returns `T` if deserialization succeeds,
125    /// otherwise returns un-deserialized JSON string.
126    pub fn remove_as<T: DeserializeOwned>(&self, key: &str) -> Option<Result<T, String>> {
127        self.remove(key)
128            .map(|val_str| match serde_json::from_str(&val_str) {
129                Ok(val) => Ok(val),
130                Err(_err) => Err(val_str),
131            })
132    }
133
134    /// Clear the session.
135    pub fn clear(&self) {
136        let mut inner = self.0.borrow_mut();
137
138        if inner.status != SessionStatus::Purged {
139            if inner.status != SessionStatus::Renewed {
140                inner.status = SessionStatus::Changed;
141            }
142            inner.state.clear()
143        }
144    }
145
146    /// Removes session both client and server side.
147    pub fn purge(&self) {
148        let mut inner = self.0.borrow_mut();
149        inner.status = SessionStatus::Purged;
150        inner.state.clear();
151    }
152
153    /// Renews the session key, assigning existing session state to new key.
154    pub fn renew(&self) {
155        let mut inner = self.0.borrow_mut();
156
157        if inner.status != SessionStatus::Purged {
158            inner.status = SessionStatus::Renewed;
159        }
160    }
161
162    /// Adds the given key-value pairs to the session on the request.
163    ///
164    /// Values that match keys already existing on the session will be overwritten. Values should
165    /// already be JSON serialized.
166    #[allow(clippy::needless_pass_by_ref_mut)]
167    pub(crate) fn set_session(
168        req: &mut ServiceRequest,
169        data: impl IntoIterator<Item = (String, String)>,
170    ) {
171        let session = Session::get_session(&mut req.extensions_mut());
172        let mut inner = session.0.borrow_mut();
173        inner.state.extend(data);
174    }
175
176    /// Returns session status and iterator of key-value pairs of changes.
177    ///
178    /// This is a destructive operation - the session state is removed from the request extensions
179    /// typemap, leaving behind a new empty map. It should only be used when the session is being
180    /// finalised (i.e. in `SessionMiddleware`).
181    #[allow(clippy::needless_pass_by_ref_mut)]
182    pub(crate) fn get_changes<B>(
183        res: &mut ServiceResponse<B>,
184    ) -> (SessionStatus, HashMap<String, String>) {
185        if let Some(s_impl) = res
186            .request()
187            .extensions()
188            .get::<Rc<RefCell<SessionInner>>>()
189        {
190            let state = mem::take(&mut s_impl.borrow_mut().state);
191            (s_impl.borrow().status.clone(), state)
192        } else {
193            (SessionStatus::Unchanged, HashMap::new())
194        }
195    }
196
197    pub(crate) fn get_session(extensions: &mut Extensions) -> Session {
198        if let Some(s_impl) = extensions.get::<Rc<RefCell<SessionInner>>>() {
199            return Session(Rc::clone(s_impl));
200        }
201
202        let inner = Rc::new(RefCell::new(SessionInner::default()));
203        extensions.insert(inner.clone());
204
205        Session(inner)
206    }
207}
208
209/// Extractor implementation for [`Session`]s.
210impl FromRequest for Session {
211    type Error = Error;
212    type Future = Ready<Result<Session, Error>>;
213
214    #[inline]
215    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
216        ready(Ok(Session::get_session(&mut req.extensions_mut())))
217    }
218}