Skip to content
Clone

fix(session): vendor axum_session with multi-replica session fixes

Sessions were lost when several replicas and concurrent requests were
involved: the SPA signs in (200), fires /me, /me/spaces, /chat/dms and
/auth/jwt-request in parallel, and part of them answers 401, which bounces
the user back to the login page. Three defects in axum_session 0.21.0 cause
this and are fixed in a vendored copy under lib/axum-session:

- SessionData.expires / autoremove were #[serde(skip)], so a session loaded
  from Redis came back with 1970 timestamps. The load path only reset
  autoremove, leaving expired() true, and service_clear() then wiped the
  whole session payload and wrote the empty result back. Serialize both
  fields with sane defaults and repair a stale expires on load.
- The response phase re-read storable from the in-memory map and reported
  false when a parallel request had already unloaded the entry. In opt-in
  mode that deleted the database session and expired the cookies of a valid
  session. Fall back to the storable parsed from the request instead.
- memory_lifespan = 0 turns the first defect from occasional into frequent,
  so the chart now pins APP_SESSION__MEMORY_LIFESPAN_SECS to 0: sessions stay
  in Redis only and every replica reads the same state. The resulting warn
  noise on session-less probes is muted via RUST_LOG.

Verified with a two-pod port-forward harness (parallel bursts across both
replicas) and three real browser sign-ins: every follow-up call returns 200
and the SPA stays on /app/@me.
ZhenYi committed
parent 0a9ab7b commit 1c140d5 Browse files

23 files changed

+4503 −3

File tree 23
Modified chart/values.yaml View chart/values.yaml
@@ -38,6 +38,11 @@
3838 APP_IMPORT__ENABLED: "false"
3939 # 自建 forge 的内网地址需显式加白,例如:
4040 # APP_IMPORT__ALLOWED_HOSTS: '["gitea.internal","gitlab.svc.cluster.local"]'
41 # 多副本必须置 0:axum_session 默认在每个 pod 内存缓存会话最长 60 分钟,
42 # 副本间内存快照互不可见,会导致跨副本丢失 pow challenge / 2FA / 登录态。
43 APP_SESSION__MEMORY_LIFESPAN_SECS: "0"
44 # 上面的 0 会让无会话请求(/healthz、扫描器)产生一条无害的 warn 噪音,压掉它。
45 RUST_LOG: "info,axum_session::session_store=error"
4146
4247# 敏感配置(渲染为 Secret)。注意 AppConfig 的 Debug 会带出密钥,
4348# 切勿在任何日志里 {:?} 打印配置。
Added lib/axum-session/src/databases/any_db.rs View lib/axum-session/src/databases/any_db.rs
@@ -0,0 +1,83 @@
1use crate::{DatabaseError, DatabasePool, Session, SessionStore};
2use async_trait::async_trait;
3use std::fmt::Debug;
4use std::sync::Arc;
5///Any Session Helper type for the DatabasePool.
6pub type SessionAnySession = Session<SessionAnyPool>;
7///Any Session Store Helper type for the DatabasePool.
8pub type SessionAnySessionStore = SessionStore<SessionAnyPool>;
9
10/// [SessionAnyPool] is effectively a `dyn DatabasePool`. It can be useful if your application
11///
12/// requires a runtime decision between multiple database backends. For example using `sqlite`
13/// in development builds but `postgres` in production builds.
14#[derive(Clone)]
15pub struct SessionAnyPool {
16 pool: Arc<dyn DatabasePool + Send + Sync>,
17}
18
19impl SessionAnyPool {
20 pub fn new<Pool>(pool: Pool) -> Self
21 where
22 Pool: 'static + DatabasePool + Send + Sync,
23 {
24 Self {
25 pool: Arc::new(pool),
26 }
27 }
28}
29
30impl Debug for SessionAnyPool {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 f.debug_struct("SessionAnyPool").finish()
33 }
34}
35
36#[async_trait]
37impl DatabasePool for SessionAnyPool {
38 async fn initiate(&self, table_name: &str) -> Result<(), DatabaseError> {
39 self.pool.initiate(table_name).await
40 }
41
42 async fn count(&self, table_name: &str) -> Result<i64, DatabaseError> {
43 self.pool.count(table_name).await
44 }
45
46 async fn store(
47 &self,
48 id: &str,
49 session: &str,
50 expires: i64,
51 table_name: &str,
52 ) -> Result<(), DatabaseError> {
53 self.pool.store(id, session, expires, table_name).await
54 }
55
56 async fn load(&self, id: &str, table_name: &str) -> Result<Option<String>, DatabaseError> {
57 self.pool.load(id, table_name).await
58 }
59
60 async fn delete_one_by_id(&self, id: &str, table_name: &str) -> Result<(), DatabaseError> {
61 self.pool.delete_one_by_id(id, table_name).await
62 }
63
64 async fn exists(&self, id: &str, table_name: &str) -> Result<bool, DatabaseError> {
65 self.pool.exists(id, table_name).await
66 }
67
68 async fn delete_by_expiry(&self, table_name: &str) -> Result<Vec<String>, DatabaseError> {
69 self.pool.delete_by_expiry(table_name).await
70 }
71
72 async fn delete_all(&self, table_name: &str) -> Result<(), DatabaseError> {
73 self.pool.delete_all(table_name).await
74 }
75
76 async fn get_ids(&self, table_name: &str) -> Result<Vec<String>, DatabaseError> {
77 self.pool.get_ids(table_name).await
78 }
79
80 fn auto_handles_expiry(&self) -> bool {
81 self.pool.auto_handles_expiry()
82 }
83}
Added lib/axum-session/src/databases/database.rs View lib/axum-session/src/databases/database.rs
@@ -0,0 +1,72 @@
1use async_trait::async_trait;
2use thiserror::Error;
3
4/// The Trait used to identify a database pool.
5///
6/// This can be freely implemented but default implementations for the supported database types are already included
7/// If you're using a custom database library than you should use the Generic*Error in the DatabaseError enum to indicate an error.
8#[async_trait]
9pub trait DatabasePool {
10 /// This is called to create the table in the database using the given table name.
11 /// if an error occurs it should be propagated to the caller.
12 async fn initiate(&self, table_name: &str) -> Result<(), DatabaseError>;
13
14 /// This is called to receive the session count in the database using the given table name.
15 /// if an error occurs it should be propagated to the caller.
16 async fn count(&self, table_name: &str) -> Result<i64, DatabaseError>;
17
18 /// This is called to store a session in the database using the given table name.
19 /// The session is a string and should be stored in its own field.
20 /// if an error occurs it should be propagated to the caller.
21 /// expires is a unix timestamp(number of non-leap seconds since January 1, 1970 0:00:00 UTC)
22 /// which is set to UTC::now() + the expiration time.
23 async fn store(
24 &self,
25 id: &str,
26 session: &str,
27 expires: i64,
28 table_name: &str,
29 ) -> Result<(), DatabaseError>;
30
31 /// This is called to receive the session from the database using the given table name.
32 /// if an error occurs it should be propagated to the caller.
33 async fn load(&self, id: &str, table_name: &str) -> Result<Option<String>, DatabaseError>;
34
35 /// This is called to delete one session from the database using the given table name.
36 /// if an error occurs it should be propagated to the caller.
37 async fn delete_one_by_id(&self, id: &str, table_name: &str) -> Result<(), DatabaseError>;
38
39 /// This is called to check if the id exists in the database using the given table name.
40 /// if an error occurs it should be propagated to the caller.
41 async fn exists(&self, id: &str, table_name: &str) -> Result<bool, DatabaseError>;
42
43 /// This is called to delete all sessions that expired from the database using the given table name.
44 /// if an error occurs it should be propagated to the caller.
45 async fn delete_by_expiry(&self, table_name: &str) -> Result<Vec<String>, DatabaseError>;
46
47 /// This is called to delete all sessions from the database using the given table name.
48 /// if an error occurs it should be propagated to the caller.
49 async fn delete_all(&self, table_name: &str) -> Result<(), DatabaseError>;
50
51 /// This is called to get all id's in the database from the last run.
52 /// if an error occurs it should be propagated to the caller.
53 async fn get_ids(&self, table_name: &str) -> Result<Vec<String>, DatabaseError>;
54
55 fn auto_handles_expiry(&self) -> bool;
56}
57
58#[derive(Error, Debug)]
59pub enum DatabaseError {
60 #[error("Database insert error {0}")]
61 GenericAcquire(String),
62 #[error("Database insert error {0}")]
63 GenericInsertError(String),
64 #[error("Database select error {0}")]
65 GenericSelectError(String),
66 #[error("Database create error {0}")]
67 GenericCreateError(String),
68 #[error("Database delete error {0}")]
69 GenericDeleteError(String),
70 #[error("{0}")]
71 GenericNotSupportedError(String),
72}
Added lib/axum-session/src/databases/null.rs View lib/axum-session/src/databases/null.rs
@@ -0,0 +1,61 @@
1use crate::{DatabaseError, DatabasePool, Session, SessionStore};
2use async_trait::async_trait;
3
4///Null's Session Helper type for a DatabaseLess Session.
5pub type SessionNullSession = Session<SessionNullPool>;
6///Null's Session Store Helper type for a DatabaseLess Session.
7pub type SessionNullSessionStore = SessionStore<SessionNullPool>;
8
9/// Null Pool type for a DatabaseLess Session.
10/// Use this when you do not want to load any database.
11#[derive(Debug, Clone)]
12pub struct SessionNullPool;
13
14#[async_trait]
15impl DatabasePool for SessionNullPool {
16 async fn initiate(&self, _table_name: &str) -> Result<(), DatabaseError> {
17 Ok(())
18 }
19
20 async fn delete_by_expiry(&self, _table_name: &str) -> Result<Vec<String>, DatabaseError> {
21 Ok(Vec::new())
22 }
23
24 async fn count(&self, _table_name: &str) -> Result<i64, DatabaseError> {
25 return Ok(0);
26 }
27
28 async fn store(
29 &self,
30 _id: &str,
31 _session: &str,
32 _expires: i64,
33 _table_name: &str,
34 ) -> Result<(), DatabaseError> {
35 Ok(())
36 }
37
38 async fn load(&self, _id: &str, _table_name: &str) -> Result<Option<String>, DatabaseError> {
39 Ok(None)
40 }
41
42 async fn delete_one_by_id(&self, _id: &str, _table_name: &str) -> Result<(), DatabaseError> {
43 Ok(())
44 }
45
46 async fn exists(&self, _id: &str, _table_name: &str) -> Result<bool, DatabaseError> {
47 Ok(false)
48 }
49
50 async fn delete_all(&self, _table_name: &str) -> Result<(), DatabaseError> {
51 Ok(())
52 }
53
54 async fn get_ids(&self, _table_name: &str) -> Result<Vec<String>, DatabaseError> {
55 Ok(Vec::new())
56 }
57
58 fn auto_handles_expiry(&self) -> bool {
59 false
60 }
61}
Added lib/axum-session/src/sec/encrypt.rs View lib/axum-session/src/sec/encrypt.rs
@@ -0,0 +1,65 @@
1use crate::SessionError;
2use aes_gcm::aead::common::array::Array;
3use aes_gcm::aead::{Aead, AeadInPlace, KeyInit, Payload};
4use aes_gcm::Aes256Gcm;
5use base64::{engine::general_purpose, Engine as _};
6use cookie::Key;
7use rand::TryRng;
8
9pub(crate) const NONCE_LEN: usize = 12;
10pub(crate) const TAG_LEN: usize = 16;
11pub(crate) const KEY_LEN: usize = 32;
12
13///Used to encrypt the database Values
14pub(crate) fn encrypt(name: &str, value: &str, key: &Key) -> Result<String, &'static str> {
15 let val = value.as_bytes();
16
17 let mut data = vec![0; NONCE_LEN + val.len() + TAG_LEN];
18 let (nonce, in_out) = data.split_at_mut(NONCE_LEN);
19 let (in_out, tag) = in_out.split_at_mut(val.len());
20 in_out.copy_from_slice(val);
21
22 let mut rng = rand::rng();
23 rng.try_fill_bytes(nonce)
24 .map_err(|_| "couldn't random fill nonce")?;
25
26 let nonce = Array::clone_from_slice(nonce);
27
28 // Use the UUID to preform actual cookie Sealing.
29 let aad = name.as_bytes();
30 let aead =
31 Aes256Gcm::new(&Array::try_from(key.encryption()).map_err(|_| "encryption failure!")?);
32 let aad_tag = aead
33 .encrypt_in_place_detached(&nonce, aad, in_out)
34 .map_err(|_| "encryption failure!")?;
35
36 tag.copy_from_slice(aad_tag.as_slice());
37
38 Ok(general_purpose::STANDARD.encode(&data))
39}
40
41///Used to decrypt the database Values.
42pub(crate) fn decrypt(name: &str, value: &str, key: &Key) -> Result<String, SessionError> {
43 let data = general_purpose::STANDARD.decode(value)?;
44 if data.len() <= NONCE_LEN {
45 return Err(SessionError::GenericNotSupportedError(
46 "length of decoded data is <= NONCE_LEN".to_owned(),
47 ));
48 }
49
50 let (nonce, cipher) = data.split_at(NONCE_LEN);
51 let payload = Payload {
52 msg: cipher,
53 aad: name.as_bytes(),
54 };
55
56 let aead = Aes256Gcm::new(Array::from_slice(key.encryption()));
57 Ok(String::from_utf8(
58 aead.decrypt(Array::from_slice(nonce), payload)
59 .map_err(|_| {
60 SessionError::GenericNotSupportedError(
61 "invalid key/nonce/value: bad seal".to_owned(),
62 )
63 })?,
64 )?)
65}
Added lib/axum-session/src/sec/signed.rs View lib/axum-session/src/sec/signed.rs
@@ -0,0 +1,374 @@
1use std::borrow::{Borrow, BorrowMut};
2
3use hmac::{Hmac, KeyInit, Mac};
4use sha2::Sha256;
5
6use cookie::{Cookie, CookieJar, Key};
7
8// Keep these in sync, and keep the key len synced with the `signed` docs as
9// well as the `KEYS_INFO` const in secure::Key.
10pub(crate) const BASE64_DIGEST_LEN: usize = 44;
11pub(crate) const KEY_LEN: usize = 32;
12
13use base64::{prelude::BASE64_STANDARD, DecodeError, Engine};
14use tokio::task;
15
16/// Encode `input` as the standard base64 with padding.
17pub(crate) fn encode<T: AsRef<[u8]>>(input: T) -> String {
18 BASE64_STANDARD.encode(input)
19}
20
21/// Decode `input` as the standard base64 with padding.
22pub(crate) fn decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, DecodeError> {
23 BASE64_STANDARD.decode(input)
24}
25
26pub trait CookiesAdditionJar {
27 fn message_signed<'a>(&'a self, key: &Key, message: String) -> AdditionalSignedJar<&'a Self>;
28 fn message_signed_mut<'a>(
29 &'a mut self,
30 key: &Key,
31 message: String,
32 ) -> AdditionalSignedJar<&'a mut Self>;
33}
34
35impl CookiesAdditionJar for CookieJar {
36 fn message_signed<'a>(&'a self, key: &Key, message: String) -> AdditionalSignedJar<&'a Self> {
37 AdditionalSignedJar::new(self, key, message)
38 }
39
40 fn message_signed_mut<'a>(
41 &'a mut self,
42 key: &Key,
43 message: String,
44 ) -> AdditionalSignedJar<&'a mut Self> {
45 AdditionalSignedJar::new(self, key, message)
46 }
47}
48
49/// A child cookie jar that authenticates its cookies and Adds Additional measures to ensure integrity.
50pub struct AdditionalSignedJar<J> {
51 parent: J,
52 key: [u8; KEY_LEN],
53 message: String,
54}
55
56impl<J> AdditionalSignedJar<J> {
57 /// Creates a new child `AdditionalSignedJar`
58 pub(crate) fn new(parent: J, key: &Key, message: String) -> AdditionalSignedJar<J> {
59 AdditionalSignedJar {
60 parent,
61 key: key.signing().try_into().expect("sign key len"),
62 message,
63 }
64 }
65
66 /// Signs the cookie's value and message providing integrity and authenticity.
67 async fn sign_cookie(&self, cookie: &mut Cookie<'_>) {
68 let key = self.key.to_owned();
69 let message = self.message.to_owned();
70 let value = cookie.value().to_owned();
71
72 let new_value = task::spawn_blocking(move || {
73 // Compute HMAC-SHA256 of the cookie's value.
74 let mut mac = match Hmac::<Sha256>::new_from_slice(&key) {
75 Ok(v) => v,
76 Err(err) => {
77 tracing::error!(err = %err, "key is invalid." );
78 return None;
79 }
80 };
81
82 // Add the payload to the message first.
83 let message = format!("{}{}", value, message);
84 mac.update(message.as_bytes());
85
86 // Cookie's new value is [MAC | original-value].
87 let mut new_value = encode(mac.finalize().into_bytes());
88 new_value.push_str(&value);
89 Some(new_value)
90 })
91 .await;
92
93 if let Ok(Some(value)) = new_value {
94 cookie.set_value(value);
95 }
96 }
97
98 /// Given a signed value `str` where the signature is prepended to `value`,
99 /// verifies the signed value and returns it. If there's a problem, returns
100 /// an `Err` with a string describing the issue.
101 async fn _verify(&self, cookie_value: &str) -> Result<String, &'static str> {
102 if !cookie_value.is_char_boundary(BASE64_DIGEST_LEN) {
103 return Err("missing or invalid digest");
104 }
105
106 // we clone it so we can pass them to the blocking thread for encryption verification needs.
107 let key = self.key.to_owned();
108 let message = self.message.to_owned();
109 let cookie_value = cookie_value.to_owned();
110
111 task::spawn_blocking(move || {
112 // Split [MAC | original-value] into its two parts.
113 let (digest_str, value) = cookie_value.split_at(BASE64_DIGEST_LEN);
114 let digest = decode(digest_str).map_err(|_| "bad base64 digest")?;
115
116 // Perform the verification.
117 let mut mac = Hmac::<Sha256>::new_from_slice(&key).map_err(|_| "key is invalid.")?;
118 // Add message here so we can check if it matches.
119 let message = format!("{}{}", value, message);
120 mac.update(message.as_bytes());
121 mac.verify_slice(&digest)
122 .map(|_| value.to_string())
123 .map_err(|_| "value did not verify")
124 })
125 .await
126 .map_err(|_| "thread join in _verify was unsuccessful")?
127 }
128
129 /// Verifies the authenticity and integrity of `cookie`, returning the
130 /// plaintext version if verification succeeds or `None` otherwise.
131 /// Verification _always_ succeeds if `cookie` was generated by a
132 /// `AdditionalSignedJar` with the same key and message as `self`.
133 ///
134 /// # Example
135 ///
136 /// ```rust ignore
137 /// use cookie::{CookieJar, Cookie, Key};
138 ///
139 /// let key = Key::generate();
140 /// let mut jar = CookieJar::new();
141 /// assert!(jar.message_signed(&key, "".to_owned()).get("name").is_none());
142 ///
143 /// jar.message_signed_mut(&key, "".to_owned()).add(("name", "value"));
144 /// assert_eq!(jar.message_signed(&key, "".to_owned()).get("name").unwrap().value(), "value");
145 ///
146 /// let plain = jar.get("name").cloned().unwrap();
147 /// assert_ne!(plain.value(), "value");
148 /// let verified = jar.message_signed(&key, "".to_owned()).verify(plain).unwrap();
149 /// assert_eq!(verified.value(), "value");
150 ///
151 /// let plain = Cookie::new("plaintext", "hello");
152 /// assert!(jar.message_signed(&key, "".to_owned()).verify(plain).is_none());
153 /// ```
154 pub async fn verify(&self, mut cookie: Cookie<'static>) -> Option<Cookie<'static>> {
155 match self._verify(cookie.value()).await {
156 Ok(value) => {
157 cookie.set_value(value);
158 Some(cookie)
159 }
160 Err(err) => {
161 tracing::warn!(
162 err = %err,
163 "possibly suspicious activity: Verification failed for Cookie. cookie validation string was: {}",
164 self.message
165 );
166 None
167 }
168 }
169 }
170}
171
172impl<J: Borrow<CookieJar>> AdditionalSignedJar<J> {
173 /// Returns a reference to the `Cookie` inside this jar with the name `name`
174 /// and verifies the authenticity and integrity of the cookie's value,
175 /// returning a `Cookie` with the authenticated value. If the cookie cannot
176 /// be found, or the cookie fails to verify, `None` is returned.
177 ///
178 /// # Example
179 ///
180 /// ```rust ignore
181 /// use cookie::{CookieJar, Cookie, Key};
182 ///
183 /// let key = Key::generate();
184 /// let jar = CookieJar::new();
185 /// assert!(jar.message_signed(&key, "".to_owned()).get("name").is_none());
186 ///
187 /// let mut jar = jar;
188 /// let mut signed_jar = jar.message_signed_mut(&key, "".to_owned());
189 /// signed_jar.add(Cookie::new("name", "value"));
190 /// assert_eq!(signed_jar.get("name").unwrap().value(), "value");
191 /// ```
192 pub async fn get(&self, name: &str) -> Option<Cookie<'static>> {
193 match self.parent.borrow().get(name) {
194 Some(c) => self.verify(c.clone()).await,
195 None => None,
196 }
197 }
198}
199
200impl<J: BorrowMut<CookieJar>> AdditionalSignedJar<J> {
201 /// Adds `cookie` to the parent jar. The cookie's value is signed assuring
202 /// integrity and authenticity.
203 ///
204 /// # Example
205 ///
206 /// ```rust ignore
207 /// use cookie::{CookieJar, Cookie, Key};
208 ///
209 /// let key = Key::generate();
210 /// let mut jar = CookieJar::new();
211 /// jar.message_signed_mut(&key, "".to_owned()).add(("name", "value")).await;
212 ///
213 /// assert_ne!(jar.get("name").unwrap().value(), "value");
214 /// assert!(jar.get("name").unwrap().value().contains("value"));
215 /// assert_eq!(jar.message_signed(&key, "".to_owned()).get("name").unwrap().value(), "value");
216 /// ```
217 pub async fn add<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
218 let mut cookie = cookie.into();
219 self.sign_cookie(&mut cookie).await;
220 self.parent.borrow_mut().add(cookie);
221 }
222
223 /// Adds an "original" `cookie` to this jar. The cookie's value is signed
224 /// assuring integrity and authenticity. Adding an original cookie does not
225 /// affect the [`CookieJar::delta()`] computation. This method is intended
226 /// to be used to seed the cookie jar with cookies received from a client's
227 /// HTTP message.
228 ///
229 /// For accurate `delta` computations, this method should not be called
230 /// after calling `remove`.
231 ///
232 /// # Example
233 ///
234 /// ```rust ignore
235 /// use cookie::{CookieJar, Cookie, Key};
236 ///
237 /// let key = Key::generate();
238 /// let mut jar = CookieJar::new();
239 /// jar.message_signed_mut(&key, "".to_owned()).add_original(("name", "value")).await;
240 ///
241 /// assert_eq!(jar.iter().count(), 1);
242 /// assert_eq!(jar.delta().count(), 0);
243 /// ```
244 pub async fn add_original<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
245 let mut cookie = cookie.into();
246 self.sign_cookie(&mut cookie).await;
247 self.parent.borrow_mut().add_original(cookie);
248 }
249
250 /// Removes `cookie` from the parent jar.
251 ///
252 /// For correct removal, the passed in `cookie` must contain the same `path`
253 /// and `domain` as the cookie that was initially set.
254 ///
255 /// This is identical to [`CookieJar::remove()`]. See the method's
256 /// documentation for more details.
257 ///
258 /// # Example
259 ///
260 /// ```rust ignore
261 /// use cookie::{CookieJar, Cookie, Key};
262 ///
263 /// let key = Key::generate();
264 /// let mut jar = CookieJar::new();
265 /// let mut signed_jar = jar.message_signed_mut(&key, "".to_owned());
266 ///
267 /// signed_jar.add(("name", "value"));
268 /// assert!(signed_jar.get("name").is_some());
269 ///
270 /// signed_jar.remove("name");
271 /// assert!(signed_jar.get("name").is_none());
272 /// ```
273 pub fn remove<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
274 self.parent.borrow_mut().remove(cookie.into());
275 }
276}
277
278pub(crate) async fn sign_header(
279 value: &str,
280 key: &Key,
281 message: &str,
282) -> Result<String, &'static str> {
283 let value = value.to_owned();
284 let message = message.to_owned();
285 let key = key.to_owned();
286
287 task::spawn_blocking(move || {
288 // Compute HMAC-SHA256 of the cookie's value.
289 let mut mac =
290 Hmac::<Sha256>::new_from_slice(key.signing()).map_err(|_| "Key was invalid.")?;
291 // Add the payload to the message first.
292 let message = format!("{value}{message}");
293 mac.update(message.as_bytes());
294
295 // Cookie's new value is [MAC | original-value].
296 let mut new_value = encode(mac.finalize().into_bytes());
297 new_value.push_str(&value);
298 Ok(new_value)
299 })
300 .await
301 .map_err(|_| "thread join in _verify was unsuccessful")?
302}
303
304/// Given a signed value `str` where the signature is prepended to `value`,
305/// verifies the signed value and returns it. If there's a problem, returns
306/// an `Err` with a string describing the issue.
307pub(crate) async fn verify_header(
308 header_value: &str,
309 key: &Key,
310 message: &str,
311) -> Result<String, &'static str> {
312 let header_value = header_value.to_owned();
313 let message = message.to_owned();
314 let key = key.to_owned();
315
316 task::spawn_blocking(move || {
317 if !header_value.is_char_boundary(BASE64_DIGEST_LEN) {
318 return Err("missing or invalid digest");
319 }
320
321 // Split [MAC | original-value] into its two parts.
322 let (digest_str, value) = header_value.split_at(BASE64_DIGEST_LEN);
323 let digest = decode(digest_str).map_err(|_| "bad base64 digest")?;
324
325 // Perform the verification.
326 let mut mac =
327 Hmac::<Sha256>::new_from_slice(key.signing()).map_err(|_| "Key was invalid.")?;
328 // Add message here so we can check if it matches.
329 let message = format!("{value}{message}");
330 mac.update(message.as_bytes());
331 mac.verify_slice(&digest)
332 .map(|_| value.to_string())
333 .map_err(|_| "value did not verify")
334 })
335 .await
336 .map_err(|_| "thread join in _verify was unsuccessful")?
337}
338
339#[cfg(test)]
340mod test {
341 use crate::sec::signed::CookiesAdditionJar;
342 use cookie::{Cookie, CookieJar, Key};
343
344 #[tokio::test]
345 async fn roundtrip() {
346 // Secret is SHA-256 hash of 'Super secret!' passed through HKDF-SHA256.
347 let key = Key::from(&[
348 89, 202, 200, 125, 230, 90, 197, 245, 166, 249, 34, 169, 135, 31, 20, 197, 94, 154,
349 254, 79, 60, 26, 8, 143, 254, 24, 116, 138, 92, 225, 159, 60, 157, 41, 135, 129, 31,
350 226, 196, 16, 198, 168, 134, 4, 42, 1, 196, 24, 57, 103, 241, 147, 201, 185, 233, 10,
351 180, 170, 187, 89, 252, 137, 110, 107,
352 ]);
353
354 let mut jar = CookieJar::new();
355 jar.add(Cookie::new(
356 "signed_with_ring014",
357 "3tdHXEQ2kf6fxC7dWzBGmpSLMtJenXLKrZ9cHkSsl1w=Tamper-proof",
358 ));
359 jar.add(Cookie::new(
360 "signed_with_ring016",
361 "3tdHXEQ2kf6fxC7dWzBGmpSLMtJenXLKrZ9cHkSsl1w=Tamper-proof",
362 ));
363
364 let signed = jar.message_signed(&key, "".to_owned());
365 assert_eq!(
366 signed.get("signed_with_ring014").await.unwrap().value(),
367 "Tamper-proof"
368 );
369 assert_eq!(
370 signed.get("signed_with_ring016").await.unwrap().value(),
371 "Tamper-proof"
372 );
373 }
374}
Added lib/axum-session/src/config.rs View lib/axum-session/src/config.rs
@@ -0,0 +1,921 @@
1use chrono::Duration;
2pub use cookie::{Key, SameSite};
3use std::{
4 borrow::Cow,
5 fmt::{Debug, Formatter, Result},
6 sync::Arc,
7};
8use uuid::Uuid;
9
10/// Mode at which the Session will function As.
11///
12/// # Examples
13/// ```rust
14/// use axum_session::{SessionConfig, SessionMode};
15///
16/// let config = SessionConfig::default().with_mode(SessionMode::Persistent);
17/// ```
18///
19#[derive(Debug, Clone)]
20pub enum SessionMode {
21 /// Creates a SessionID Without SessionData.The End user must run `session.create_data()`
22 /// to create the SessionData.
23 /// Functions will emit a Info warning and do nothing until Created.
24 /// Deletes SessionData and cookie if `session.store` is false.
25 /// if SessionData.store is true retains SessionData and Syncs with Database.
26 /// You can Set SessionData.store to true using 'session.set_store(true)'
27 Manual,
28 /// Always Creates a Session.
29 /// Deletes SessionData and cookie if `session.store` is false.
30 /// if SessionData.store is true retains SessionData and Syncs with Database.
31 /// You can Set SessionData.store to true using 'session.set_store(true)'
32 OptIn,
33 /// Always Creates a Session
34 /// Always retains in Memory and syncs with Database.
35 Persistent,
36}
37
38impl SessionMode {
39 /// Checks if the Mode is set to only if OptIn or Manual.
40 ///
41 pub fn is_opt_in(&self) -> bool {
42 matches!(self, SessionMode::OptIn | SessionMode::Manual)
43 }
44 /// Checks if the user needs to manually create the SessionData per user.
45 /// When created the Session will get Set to loaded.
46 pub fn is_manual(&self) -> bool {
47 matches!(self, SessionMode::Manual)
48 }
49}
50
51#[derive(Clone)]
52pub struct CookieAndHeaderConfig {
53 /// The Cookie or Header name that contains a boolean for session saving.
54 /// only used when session_mode is set to SessionMode::OptIn or Manual.
55 pub(crate) store_name: Cow<'static, str>,
56 /// Session Cookie or Header name.
57 pub(crate) session_name: Cow<'static, str>,
58 /// Session cookie domain.
59 pub(crate) cookie_domain: Option<Cow<'static, str>>,
60 /// Session cookie http only flag.
61 pub(crate) cookie_http_only: bool,
62 /// Session cookie max age None means the browser deletes cookie on close.
63 /// Please make sure the Duration is longer than max_lifespan.
64 pub(crate) cookie_max_age: Option<Duration>,
65 /// Session cookie path.
66 pub(crate) cookie_path: Cow<'static, str>,
67 /// Resticts how Cookies are sent cross-site. Default is `SameSite::Lax`.
68 pub(crate) cookie_same_site: SameSite,
69 /// Session cookie secure flag.
70 pub(crate) cookie_secure: bool,
71 /// Encyption Key used to sign cookies and header for integrity, and authenticity.
72 pub(crate) key: Option<Key>,
73 /// This is used to append __Host- to the front of all Cookie names to prevent sub domain usage.
74 /// This will not append to Headers only Cookies. It is enabled by default.
75 pub(crate) prefix_with_host: bool,
76 /// If Key is Some and this is true then the ip and user agent will be used to further sign cookies
77 /// and header values. This will help prevent others from spoofing your cookies and headers.
78 /// It is enabled by default.
79 pub(crate) with_ip_and_user_agent: bool,
80}
81
82impl Debug for CookieAndHeaderConfig {
83 fn fmt(&self, f: &mut Formatter<'_>) -> Result {
84 f.debug_struct("CookieAndHeaderConfig")
85 .field("store_name", &self.store_name)
86 .field("cookie_domain", &self.cookie_domain)
87 .field("cookie_http_only", &self.cookie_http_only)
88 .field("cookie_max_age", &self.cookie_max_age)
89 .field("session_name", &self.session_name)
90 .field("cookie_path", &self.cookie_path)
91 .field("cookie_same_site", &self.cookie_same_site)
92 .field("cookie_secure", &self.cookie_secure)
93 .field("prefix_with_host", &self.prefix_with_host)
94 .field("with_ip_and_user_agent", &self.with_ip_and_user_agent)
95 .field("key", &"key hidden")
96 .finish()
97 }
98}
99
100#[derive(Clone)]
101pub struct DatabaseConfig {
102 /// Encyption Key used to encypt Session data stored in the database for confidentiality.
103 pub(crate) database_key: Option<Key>,
104 /// Session Database table name default is sessions.
105 pub(crate) table_name: Cow<'static, str>,
106 /// This value represents the duration for how often session's data gets purged from the database per request.
107 pub(crate) purge_database_update: Duration,
108 /// Ignore's the update checks and will always save the session to the database if set to true.
109 pub(crate) always_save: bool,
110 /// Minimum time interval between database updates for the same session.
111 /// Sessions will only be updated in the database if this duration has elapsed
112 /// since the last database update. This helps reduce database write load
113 /// for frequently accessed sessions. Default is 10 seconds.
114 pub(crate) db_update_interval: Duration,
115}
116
117impl Debug for DatabaseConfig {
118 fn fmt(&self, f: &mut Formatter<'_>) -> Result {
119 f.debug_struct("DatabaseConfig")
120 .field("table_name", &self.table_name)
121 .field("purge_database_update", &self.purge_database_update)
122 .field("always_save", &self.always_save)
123 .field("db_update_interval", &self.db_update_interval)
124 .field("database_key", &"key hidden")
125 .finish()
126 }
127}
128
129#[derive(Clone)]
130pub struct MemoryConfig {
131 /// This value represents the duration for how often session's data gets purged from memory per request.
132 pub(crate) purge_update: Duration,
133 /// Session Memory lifespan, determines when to unload it from memory
134 /// this works fine since the data can stay in the database till its needed
135 /// if not yet expired.
136 pub(crate) memory_lifespan: Duration,
137 /// How many Elements could we see at one time in the Table?
138 /// So if you have 1000 unique visitors a second and each generate a UUID.
139 /// That would be 1000 * 60(secs) * 60(mins) * 24(hours) to get 1 days worth of visitors.
140 pub(crate) filter_expected_elements: u64,
141 /// The probability of how many allowable false positives you want to have based on the expected elements.
142 /// 0.01 is a good starting point.
143 pub(crate) filter_false_positive_probability: f64,
144 /// This enabled using a counting bloom filter. If this is taking to much Memory or is to slow or you just dont want
145 /// the false positives it can give you can disable it by setting it to false. This will reduce memory usage.
146 /// By default this is enabled unless the specific database cant function with it then disabled.
147 pub(crate) use_bloom_filters: bool,
148}
149
150impl Debug for MemoryConfig {
151 fn fmt(&self, f: &mut Formatter<'_>) -> Result {
152 f.debug_struct("MemoryConfig")
153 .field("memory_lifespan", &self.memory_lifespan)
154 .field("filter_expected_elements", &self.filter_expected_elements)
155 .field("use_bloom_filters", &self.use_bloom_filters)
156 .field("purge_update", &self.purge_update)
157 .field(
158 "filter_false_positive_probability",
159 &self.filter_false_positive_probability,
160 )
161 .finish()
162 }
163}
164
165#[derive(Clone)]
166pub struct IpUserAgentConfig {
167 /// Uses the Direct Socket IP addresss of the socket connection.
168 pub(crate) use_ip: bool,
169 /// (XFF) request header is a de-facto standard header for identifying the originating
170 /// IP address of a client connecting to a web server through a proxy server
171 pub(crate) use_xforward_ip: bool,
172 /// Contains information that may be added by reverse proxy servers
173 pub(crate) use_forward_ip: bool,
174 /// The last remote peers ip addresss.
175 pub(crate) use_real_ip: bool,
176 /// The Browser user agent.
177 pub(crate) use_user_agent: bool,
178}
179
180impl Debug for IpUserAgentConfig {
181 fn fmt(&self, f: &mut Formatter<'_>) -> Result {
182 f.debug_struct("IpUserAgentConfig")
183 .field("use_ip", &self.use_ip)
184 .field("use_xforward_ip", &self.use_xforward_ip)
185 .field("use_forward_ip", &self.use_forward_ip)
186 .field("use_real_ip", &self.use_real_ip)
187 .field("use_user_agent", &self.use_user_agent)
188 .finish()
189 }
190}
191
192pub trait IdGenerator: Debug + Send + Sync + 'static {
193 fn generate(&self) -> String;
194}
195
196impl IdGenerator for Uuid {
197 fn generate(&self) -> String {
198 Uuid::new_v4().to_string()
199 }
200}
201
202/// Configuration for how the Session and Cookies are used.
203///
204/// # Examples
205/// ```rust
206/// use axum_session::SessionConfig;
207///
208/// let config = SessionConfig::default();
209/// ```
210///
211#[derive(Clone)]
212pub struct SessionConfig {
213 /// Disables the need to avoid session saving.
214 pub(crate) session_mode: SessionMode,
215 pub(crate) id_generator: Arc<dyn IdGenerator>,
216 /// Minimal lifespan of database store and cookie before expiring.
217 /// This is set to the Cookie before sending and to the database before updating/inserting.
218 pub(crate) lifespan: Duration,
219 /// Maximum lifespan of database store and cookie before expiring.
220 /// This is set to the Cookie before sending and to the database before updating/inserting.
221 /// Only Set when Long Term is true.
222 pub(crate) max_lifespan: Duration,
223 /// This is to be used when your handling multiple Parallel Sessions to prevent the next one from unloaded data.
224 pub(crate) clear_check_on_load: bool,
225 /// where All Database Storage options exist.
226 pub(crate) database: DatabaseConfig,
227 /// where All In Memory Storage options exist.
228 pub(crate) memory: MemoryConfig,
229 /// where All the cookie and header Options Exist.
230 pub(crate) cookie_and_header: CookieAndHeaderConfig,
231 /// tells how we should build the string for hashing to secure the cookie.
232 pub(crate) ip_user_agent: IpUserAgentConfig,
233 pub(crate) thread_sleep_duration: Duration,
234}
235
236impl Debug for SessionConfig {
237 fn fmt(&self, f: &mut Formatter<'_>) -> Result {
238 f.debug_struct("SessionConfig")
239 .field("id_generator", &self.id_generator)
240 .field("database", &self.database)
241 .field("memory", &self.memory)
242 .field("cookie_and_header", &self.cookie_and_header)
243 .field("session_mode", &self.session_mode)
244 .field("lifespan", &self.lifespan)
245 .field("max_lifespan", &self.max_lifespan)
246 .field("clear_check_on_load", &self.clear_check_on_load)
247 .field("thread_sleep_duration", &self.thread_sleep_duration)
248 .finish()
249 }
250}
251
252impl SessionConfig {
253 /// Creates [`Default`] configuration of [`SessionConfig`].
254 /// This is equivalent to the [`SessionConfig::default()`].
255 #[inline]
256 pub fn new() -> Self {
257 Default::default()
258 }
259
260 /// Set a custom session ID generator.
261 /// By default session IDs are UUIDs, but this allows for custom
262 /// session ID formats. For example, to generate an ID that matches
263 /// those from another web framework.
264 ///
265 /// # Examples
266 /// ```rust
267 /// use axum_session::{IdGenerator, SessionConfig};
268 ///
269 /// #[derive(Debug)]
270 /// struct CustomSessionId();
271 ///
272 /// impl CustomSessionId {
273 /// pub fn new() -> Self {
274 /// CustomSessionId()
275 /// }
276 /// }
277 ///
278 /// impl IdGenerator for CustomSessionId {
279 /// fn generate(&self) -> String {
280 /// // Return a custom Session ID...
281 /// "something random".into()
282 /// }
283 /// }
284 ///
285 /// let config = SessionConfig::default().with_id_generator(CustomSessionId::new());
286 /// ```
287 #[must_use]
288 pub fn with_id_generator(mut self, id_generator: impl IdGenerator) -> Self {
289 self.id_generator = Arc::new(id_generator);
290 self
291 }
292
293 /// Set the session's store Cookie or Header name.
294 ///
295 /// # Examples
296 /// ```rust
297 /// use axum_session::SessionConfig;
298 ///
299 /// let config = SessionConfig::default().with_store_name("my_stored_cookie".to_owned());
300 /// ```
301 ///
302 #[must_use]
303 pub fn with_store_name(mut self, name: impl Into<Cow<'static, str>>) -> Self {
304 self.cookie_and_header.store_name = name.into();
305 self
306 }
307
308 /// Set's the session's cookie's domain name.
309 ///
310 /// # Examples
311 /// ```rust
312 /// use axum_session::SessionConfig;
313 ///
314 /// let config = SessionConfig::default().with_cookie_domain("www.helpme.com".to_string());
315 /// ```
316 ///
317 #[must_use]
318 pub fn with_cookie_domain(mut self, name: impl Into<Cow<'static, str>>) -> Self {
319 self.cookie_and_header.cookie_domain = Some(name.into());
320 self
321 }
322
323 /// Set's the session's Cookie or Header name.
324 ///
325 /// # Examples
326 /// ```rust
327 /// use axum_session::SessionConfig;
328 ///
329 /// let config = SessionConfig::default().with_session_name("my_cookie");
330 /// ```
331 ///
332 #[must_use]
333 pub fn with_session_name(mut self, name: impl Into<Cow<'static, str>>) -> Self {
334 self.cookie_and_header.session_name = name.into();
335 self
336 }
337
338 /// Set's the session's cookie's path.
339 ///
340 /// This is used to determine when the cookie takes effect within the website path.
341 /// Leave as default ("/") for cookie to be used site wide.
342 ///
343 /// # Examples
344 /// ```rust
345 /// use axum_session::SessionConfig;
346 ///
347 /// let config = SessionConfig::default().with_cookie_path("/");
348 /// ```
349 ///
350 #[must_use]
351 pub fn with_cookie_path(mut self, path: impl Into<Cow<'static, str>>) -> Self {
352 self.cookie_and_header.cookie_path = path.into();
353 self
354 }
355
356 /// Set's the session's cookie's Same Site Setting for Cross-Site restrictions.
357 ///
358 /// # Examples
359 /// ```rust
360 /// use axum_session::SessionConfig;
361 /// use cookie::SameSite;
362 ///
363 /// let config = SessionConfig::default().with_cookie_same_site(SameSite::Strict);
364 /// ```
365 ///
366 #[must_use]
367 pub fn with_cookie_same_site(mut self, same_site: SameSite) -> Self {
368 self.cookie_and_header.cookie_same_site = same_site;
369 self
370 }
371
372 /// Set's whether the session Persistantly stores data or on stores if storable.
373 ///
374 /// # Examples
375 /// ```rust
376 /// use axum_session::{SessionMode, SessionConfig};
377 /// use cookie::SameSite;
378 ///
379 /// let config = SessionConfig::default().with_mode(SessionMode::Persistent);
380 /// ```
381 ///
382 #[must_use]
383 pub fn with_mode(mut self, mode: SessionMode) -> Self {
384 self.session_mode = mode;
385 self
386 }
387
388 /// Set's the session's cookie's to http only.
389 ///
390 /// # Examples
391 /// ```rust
392 /// use axum_session::SessionConfig;
393 ///
394 /// let config = SessionConfig::default().with_http_only(false);
395 /// ```
396 ///
397 #[must_use]
398 pub fn with_http_only(mut self, is_set: bool) -> Self {
399 self.cookie_and_header.cookie_http_only = is_set;
400 self
401 }
402
403 /// Set's the session's lifetime (expiration time) within database storage.
404 /// This should be equal too or less than the Cookies Expiration time.
405 /// # Examples
406 /// ```rust
407 /// use axum_session::SessionConfig;
408 /// use chrono::Duration;
409 ///
410 /// let config = SessionConfig::default().with_lifetime(Duration::days(32));
411 /// ```
412 ///
413 #[must_use]
414 pub fn with_lifetime(mut self, time: Duration) -> Self {
415 self.lifespan = time;
416 self
417 }
418
419 /// Set's the session's cookies max_age (expiration time).
420 ///
421 /// If this is set to None then the Cookie will be unloaded on browser Close.
422 /// Set this to be the duration of max_lifespan or longer to prevent session drops.
423 /// This is generally in a duration of how many Days a cookie should live in the browser.
424 /// Please Ensure the Duration is greater or equal to max_lifespan for proper storage.
425 /// # Examples
426 /// ```rust
427 /// use axum_session::SessionConfig;
428 /// use chrono::Duration;
429 ///
430 /// let config = SessionConfig::default().with_max_age(Some(Duration::days(64)));
431 /// ```
432 ///
433 #[must_use]
434 pub fn with_max_age(mut self, time: Option<Duration>) -> Self {
435 self.cookie_and_header.cookie_max_age = time;
436 self
437 }
438
439 /// Set's the session's long term lifetime (expiration time) within database storage.
440 ///
441 /// # Examples
442 /// ```rust
443 /// use axum_session::SessionConfig;
444 /// use chrono::Duration;
445 ///
446 /// let config = SessionConfig::default().with_max_lifetime(Duration::days(32));
447 /// ```
448 ///
449 #[must_use]
450 pub fn with_max_lifetime(mut self, time: Duration) -> Self {
451 self.max_lifespan = time;
452 self
453 }
454
455 /// Set's the session's lifetime (expiration time) within memory storage.
456 /// This setting should be Less than lifespan and max_lifespan. This is to
457 /// Unload the data from memory and allow it to stay stored in the database.
458 ///
459 /// Set this to Duration::zero() if you dont want it to stay in memory.
460 /// Warning: This will cause it to be loaded from the database each request.
461 ///
462 /// # Examples
463 /// ```rust
464 /// use axum_session::SessionConfig;
465 /// use chrono::Duration;
466 ///
467 /// let config = SessionConfig::default().with_memory_lifetime(Duration::days(32));
468 /// ```
469 ///
470 #[must_use]
471 pub fn with_memory_lifetime(mut self, time: Duration) -> Self {
472 self.memory.memory_lifespan = time;
473 self
474 }
475
476 /// This value represents the offset duration for how often session purge for memory is ran.
477 ///
478 /// # Examples
479 /// ```rust
480 /// use axum_session::SessionConfig;
481 /// use chrono::Duration;
482 ///
483 /// let config = SessionConfig::default().with_purge_update(Duration::hours(1));
484 /// ```
485 ///
486 #[must_use]
487 pub fn with_purge_update(mut self, duration: Duration) -> Self {
488 self.memory.purge_update = duration;
489 self
490 }
491
492 /// This value represents the offset duration for how often session purge for database is ran.
493 /// If using Redis or any auto purge database this Setting will be ignored.
494 ///
495 /// # Examples
496 /// ```rust
497 /// use axum_session::SessionConfig;
498 /// use chrono::Duration;
499 ///
500 /// let config = SessionConfig::default().with_purge_database_update(Duration::hours(5));
501 /// ```
502 ///
503 #[must_use]
504 pub fn with_purge_database_update(mut self, duration: Duration) -> Self {
505 self.database.purge_database_update = duration;
506 self
507 }
508
509 /// This value represents if the database should check for updates to save or
510 /// to just save the data regardless of updates. When set to true it will disable the
511 /// update checks.
512 ///
513 /// # Examples
514 /// ```rust
515 /// use axum_session::SessionConfig;
516 /// use chrono::Duration;
517 ///
518 /// let config = SessionConfig::default().with_always_save(true);
519 /// ```
520 ///
521 #[must_use]
522 pub fn with_always_save(mut self, always_save: bool) -> Self {
523 self.database.always_save = always_save;
524 self
525 }
526
527 /// Set the database update interval for throttling session saves.
528 /// This controls how frequently a session's data is persisted to the database.
529 /// Only sessions that haven't been updated to the database within this interval
530 /// will have their data saved on the next request.
531 ///
532 /// # Examples
533 /// ```rust
534 /// use axum_session::SessionConfig;
535 /// use chrono::Duration;
536 ///
537 /// let config = SessionConfig::default()
538 /// .with_db_update_interval(Duration::try_seconds(10).unwrap_or_default());
539 /// ```
540 ///
541 #[must_use]
542 pub fn with_db_update_interval(mut self, interval: Duration) -> Self {
543 self.database.db_update_interval = interval;
544 self
545 }
546
547 /// Set's the session's secure flag for if it gets sent over https.
548 ///
549 /// # Examples
550 /// ```rust
551 /// use axum_session::SessionConfig;
552 ///
553 /// let config = SessionConfig::default().with_secure(true);
554 /// ```
555 ///
556 #[must_use]
557 pub fn with_secure(mut self, is_set: bool) -> Self {
558 self.cookie_and_header.cookie_secure = is_set;
559 self
560 }
561
562 /// Set's the session's database table name.
563 ///
564 /// # Examples
565 /// ```rust
566 /// use axum_session::SessionConfig;
567 ///
568 /// let config = SessionConfig::default().with_table_name("my_table");
569 /// ```
570 ///
571 #[must_use]
572 pub fn with_table_name(mut self, table_name: impl Into<Cow<'static, str>>) -> Self {
573 self.database.table_name = table_name.into();
574 self
575 }
576
577 /// Set's the session's cookie encyption key enabling Signed cookies.
578 ///
579 /// When Set it will enforce Signed cookies across all Sessions.
580 /// If you use Key::generate() it will make a new key each server reboot.
581 /// To prevent this make and save a key to a config file for long term usage.
582 /// For Extra Security Regenerate the key every so many months to a year.
583 /// A new key will invalidate all old Sessions so it be wise to run session_store.clear_store() on reboot.
584 ///
585 /// Must be Set to Some() in order to use Security::PerSession.
586 ///
587 /// # Examples
588 /// ```rust
589 /// use axum_session::{Key, SessionConfig};
590 ///
591 /// let config = SessionConfig::default().with_key(Key::generate());
592 /// ```
593 ///
594 #[must_use]
595 pub fn with_key(mut self, key: Key) -> Self {
596 self.cookie_and_header.key = Some(key);
597 self
598 }
599
600 /// Set's the session's database encyption key for per session key storage.
601 ///
602 /// Must be Set to Some() in order to use Security::PerSession or will panic if not.
603 ///
604 /// # Examples
605 /// ```rust
606 /// use axum_session::{Key, SessionConfig};
607 ///
608 /// let config = SessionConfig::default().with_key(Key::generate());
609 /// ```
610 ///
611 #[must_use]
612 pub fn with_database_key(mut self, key: Key) -> Self {
613 self.database.database_key = Some(key);
614 self
615 }
616
617 /// Set's the session's filters expected elements.
618 /// Please Set this by a daily value.
619 /// Example: 1000 * 60(secs) * 60(mins) * 24(hours) to get 1 days worth of visitors.
620 ///
621 /// # Examples
622 /// ```rust
623 /// use axum_session::SessionConfig;
624 ///
625 /// let config = SessionConfig::default().with_filter_expected_elements(100_000);
626 /// ```
627 ///
628 #[must_use]
629 pub fn with_filter_expected_elements(mut self, elements: u64) -> Self {
630 self.memory.filter_expected_elements = elements;
631 self
632 }
633
634 /// Set's the session's filters False Posistive probability when creating and comparing UUID.
635 ///
636 /// # Examples
637 /// ```rust
638 /// use axum_session::SessionConfig;
639 ///
640 /// let config = SessionConfig::default().with_filter_false_positive_probability(0.01);
641 /// ```
642 ///
643 #[must_use]
644 pub fn with_filter_false_positive_probability(mut self, probability: f64) -> Self {
645 self.memory.filter_false_positive_probability = probability;
646 self
647 }
648
649 /// Set's the session's bloom filters to be disabled or enabled. By default they are enabled.
650 ///
651 /// # Examples
652 /// ```rust
653 /// use axum_session::SessionConfig;
654 ///
655 /// let config = SessionConfig::default().with_bloom_filter(true);
656 /// ```
657 ///
658 #[must_use]
659 pub fn with_bloom_filter(mut self, enable: bool) -> Self {
660 self.memory.use_bloom_filters = enable;
661 self
662 }
663
664 /// Get's the session's Cookie/Header name
665 ///
666 /// # Examples
667 /// ```rust
668 /// use axum_session::SessionConfig;
669 ///
670 /// let name = SessionConfig::default().get_session_name();
671 /// ```
672 ///
673 pub fn get_session_name(&self) -> String {
674 self.cookie_and_header.session_name.to_string()
675 }
676
677 /// Get's the session's store booleans Cookie/Header name
678 ///
679 /// # Examples
680 /// ```rust
681 /// use axum_session::SessionConfig;
682 ///
683 /// let name = SessionConfig::default().get_store_name();
684 /// ```
685 ///
686 pub fn get_store_name(&self) -> String {
687 self.cookie_and_header.store_name.to_string()
688 }
689
690 /// Set's the session's loading to either true: unload data if checks fail or false: bypass.
691 ///
692 /// # Examples
693 /// ```rust
694 /// use axum_session::SessionConfig;
695 ///
696 /// let config = SessionConfig::default().with_bloom_filter(true);
697 /// ```
698 ///
699 #[must_use]
700 pub fn with_clear_check_on_load(mut self, enable: bool) -> Self {
701 self.clear_check_on_load = enable;
702 self
703 }
704
705 /// Set's the session's prefix_with_host to either true: __Host- gets prefixed to the cookie names false: __Host- does not get prepended.
706 ///
707 /// __Host- prefix: Cookies with names starting with __Host- must be set with the secure flag, must be from a secure page (HTTPS),
708 /// must not have a domain specified (and therefore, are not sent to subdomains), and the path must be /.
709 ///
710 /// # Examples
711 /// ```rust
712 /// use axum_session::SessionConfig;
713 ///
714 /// let config = SessionConfig::default().with_prefix_with_host(true);
715 /// ```
716 ///
717 #[must_use]
718 pub fn with_prefix_with_host(mut self, enable: bool) -> Self {
719 self.cookie_and_header.prefix_with_host = enable;
720 self
721 }
722
723 /// Set's the session's with_ip_and_user_agent.
724 ///
725 /// If Key is Some and this is true then the ip and user agent will be used to further sign cookies
726 /// and header values. This will help prevent others from spoofing your cookies and headers.
727 /// It is enabled by default.
728 ///
729 /// # Examples
730 /// ```rust
731 /// use axum_session::SessionConfig;
732 ///
733 /// let config = SessionConfig::default().with_ip_and_user_agent(false);
734 /// ```
735 ///
736 #[must_use]
737 pub fn with_ip_and_user_agent(mut self, enable: bool) -> Self {
738 self.cookie_and_header.with_ip_and_user_agent = enable;
739 self
740 }
741
742 /// Set's the session's to use the Socket connected IP when hashing and verifying the cookies integrity.
743 /// Only if with_ip_and_user_agent is also enabled.
744 ///
745 /// # Examples
746 /// ```rust
747 /// use axum_session::SessionConfig;
748 ///
749 /// let config = SessionConfig::default().with_hashed_ip(true);
750 /// ```
751 ///
752 #[must_use]
753 pub fn with_hashed_ip(mut self, enable: bool) -> Self {
754 self.ip_user_agent.use_ip = enable;
755 self
756 }
757
758 /// Set's the session's to use the xforward information when hashing and verifying the cookies integrity.
759 /// Only if with_ip_and_user_agent is also enabled.
760 ///
761 /// # Examples
762 /// ```rust
763 /// use axum_session::SessionConfig;
764 ///
765 /// let config = SessionConfig::default().with_hashed_xforward(true);
766 /// ```
767 ///
768 #[must_use]
769 pub fn with_hashed_xforward(mut self, enable: bool) -> Self {
770 self.ip_user_agent.use_xforward_ip = enable;
771 self
772 }
773
774 /// Set's the session's to use the forward information when hashing and verifying the cookies integrity.
775 /// Only if with_ip_and_user_agent is also enabled.
776 ///
777 /// # Examples
778 /// ```rust
779 /// use axum_session::SessionConfig;
780 ///
781 /// let config = SessionConfig::default().with_hashed_forward(true);
782 /// ```
783 ///
784 #[must_use]
785 pub fn with_hashed_forward(mut self, enable: bool) -> Self {
786 self.ip_user_agent.use_forward_ip = enable;
787 self
788 }
789
790 /// Set's the session's to use the real ip information when hashing and verifying the cookies integrity.
791 /// Only if with_ip_and_user_agent is also enabled.
792 ///
793 /// # Examples
794 /// ```rust
795 /// use axum_session::SessionConfig;
796 ///
797 /// let config = SessionConfig::default().with_hashed_real_ip(true);
798 /// ```
799 ///
800 #[must_use]
801 pub fn with_hashed_real_ip(mut self, enable: bool) -> Self {
802 self.ip_user_agent.use_real_ip = enable;
803 self
804 }
805
806 /// Set's the session's to use the browsers user agent information when hashing and verifying the cookies integrity.
807 /// Only if with_ip_and_user_agent is also enabled.
808 ///
809 /// # Examples
810 /// ```rust
811 /// use axum_session::SessionConfig;
812 ///
813 /// let config = SessionConfig::default().with_hashed_user_agent(true);
814 /// ```
815 ///
816 #[must_use]
817 pub fn with_hashed_user_agent(mut self, enable: bool) -> Self {
818 self.ip_user_agent.use_user_agent = enable;
819 self
820 }
821
822 /// Set's the session's thread Sleep Timer to reduce CPU cycle times.
823 ///
824 /// # Examples
825 /// ```rust
826 /// use axum_session::SessionConfig;
827 /// use chrono::Duration;
828 ///
829 /// let config = SessionConfig::default().with_thread_sleep_duration(Duration::try_minutes(5).unwrap_or_default());
830 /// ```
831 ///
832 #[must_use]
833 pub fn with_thread_sleep_duration(mut self, duration: Duration) -> Self {
834 self.thread_sleep_duration = duration;
835 self
836 }
837}
838
839impl Default for SessionConfig {
840 fn default() -> Self {
841 Self {
842 id_generator: Arc::new(Uuid::default()),
843 // Set to a 6 hour default in Database Session stores unloading.
844 lifespan: Duration::try_hours(6).unwrap_or_default(),
845 cookie_and_header: CookieAndHeaderConfig::default(),
846 database: DatabaseConfig::default(),
847 memory: MemoryConfig::default(),
848 // Unload long term session after 60 days if it has not been accessed.
849 max_lifespan: Duration::try_days(60).unwrap_or_default(),
850 session_mode: SessionMode::Persistent,
851 clear_check_on_load: true,
852 ip_user_agent: IpUserAgentConfig::default(),
853 thread_sleep_duration: Duration::try_minutes(5).unwrap_or_default(),
854 }
855 }
856}
857
858impl Default for MemoryConfig {
859 fn default() -> Self {
860 Self {
861 // Unload memory after 60 minutes if it has not been accessed.
862 memory_lifespan: Duration::try_minutes(60).unwrap_or_default(),
863 // Default to purge old sessions every 1 hours.
864 purge_update: Duration::try_hours(1).unwrap_or_default(),
865 // Simple is the Default mode for compatibilty with older versions of the crate.
866 filter_expected_elements: 100_000,
867 // The probability of how many allowable false positives you want to have based on the expected elements.
868 // 0.01 is a good starting point.
869 filter_false_positive_probability: 0.01,
870 // Always set to on.
871 use_bloom_filters: true,
872 }
873 }
874}
875
876impl Default for DatabaseConfig {
877 fn default() -> Self {
878 Self {
879 // Set to a 6 hour default in Database Session stores unloading.
880 table_name: "sessions".into(),
881 // Default to purge old sessions in the database every 5 hours per request.
882 purge_database_update: Duration::try_hours(5).unwrap_or_default(),
883 always_save: false,
884 // Database key is set to None it will panic if you attempt to use SecurityMode::PerSession.
885 database_key: None,
886 // Default to 10 seconds between database updates for the same session.
887 db_update_interval: Duration::try_seconds(10).unwrap_or_default(),
888 }
889 }
890}
891
892impl Default for CookieAndHeaderConfig {
893 fn default() -> Self {
894 Self {
895 session_name: "session".into(),
896 cookie_path: "/".into(),
897 cookie_max_age: Some(Duration::try_days(100).unwrap_or_default()),
898 cookie_http_only: true,
899 cookie_secure: false,
900 cookie_domain: None,
901 cookie_same_site: SameSite::Lax,
902 store_name: "store".into(),
903 // Key is set to None so Private cookies are not used by default. Please set this if you want to use private cookies.
904 key: None,
905 prefix_with_host: false,
906 with_ip_and_user_agent: true,
907 }
908 }
909}
910
911impl Default for IpUserAgentConfig {
912 fn default() -> Self {
913 Self {
914 use_ip: true,
915 use_xforward_ip: false,
916 use_forward_ip: false,
917 use_real_ip: false,
918 use_user_agent: true,
919 }
920 }
921}
Added lib/axum-session/src/databases.rs View lib/axum-session/src/databases.rs
@@ -0,0 +1,8 @@
1mod any_db;
2pub use self::any_db::*;
3
4mod null;
5pub use null::*;
6
7mod database;
8pub use database::{DatabaseError, DatabasePool};
Added lib/axum-session/src/errors.rs View lib/axum-session/src/errors.rs
@@ -0,0 +1,32 @@
1use thiserror::Error;
2
3#[derive(Error, Debug)]
4pub enum SessionError {
5 #[error(transparent)]
6 IO(#[from] std::io::Error),
7 #[error(transparent)]
8 Decode(#[from] base64::DecodeError),
9 #[error(transparent)]
10 SerdeJson(#[from] serde_json::error::Error),
11 #[error(transparent)]
12 HTTP(#[from] http::Error),
13 #[error(transparent)]
14 UUID(#[from] uuid::Error),
15 #[error(transparent)]
16 UTF8(#[from] std::string::FromUtf8Error),
17 #[error(transparent)]
18 DatabaseError(#[from] crate::DatabaseError),
19 #[error("unknown Session store error")]
20 Unknown,
21 #[error("{0}")]
22 GenericNotSupportedError(String),
23 #[error("Session was not found. Either the session was unloaded or was never created.")]
24 NoSessionError,
25 #[error(
26 "The Session Exists but is outdated, either renew it or remove it. \n
27 Session will get removed on next Session request purge update if no changes are done."
28 )]
29 OldSessionError,
30 #[error("Failed to remove expired session's from database")]
31 FailedRemoval(String),
32}
Added lib/axum-session/src/handler.rs View lib/axum-session/src/handler.rs
@@ -0,0 +1,134 @@
1use crate::{DatabasePool, SessionError, SessionStore};
2use chrono::Utc;
3#[cfg(feature = "key-store")]
4use fastbloom_rs::Deletable;
5use std::fmt::Debug;
6use tokio::time::sleep;
7
8pub async fn runner<T>(
9 session_store: SessionStore<T>,
10 kill_as_duplicate: bool,
11) -> Result<(), SessionError>
12where
13 T: DatabasePool + Clone + Debug + Sync + Send + 'static,
14{
15 if kill_as_duplicate {
16 tracing::trace!(
17 "Killed Session Manager Duplicate on thread ID: {:?}.",
18 std::thread::current().id()
19 );
20 return Ok(());
21 } else {
22 tracing::trace!("Session Manager is starting loop");
23 }
24
25 loop {
26 let (last_sweep, last_database_sweep) = {
27 let timers = session_store.timers.read().await;
28 (timers.last_expiry_sweep, timers.last_database_expiry_sweep)
29 };
30
31 let current_time = Utc::now();
32
33 if last_sweep <= current_time && !session_store.config.memory.memory_lifespan.is_zero() {
34 tracing::trace!("Session Memory Cleaning Started");
35
36 // Only unload these from filter if the Client is None as this means no database.
37 // Otherwise only unload from the filter if removed from the Database.
38 #[cfg(feature = "key-store")]
39 if session_store.is_persistent()
40 && session_store.auto_handles_expiry()
41 && session_store.config.memory.use_bloom_filters
42 {
43 let mut filter = session_store.filter.write().await;
44 session_store
45 .inner
46 .iter()
47 .filter(|r| r.autoremove < current_time)
48 .for_each(|r| filter.remove(r.key().as_bytes()));
49 }
50
51 // We do this to ensure that if Types that never got updated due to the newer timers that on memory unload
52 // we ensure they get synced before unloading them from memory but only if the database/cookie has not expired.
53 if session_store.is_persistent() {
54 for session in session_store
55 .inner
56 .iter()
57 .filter(|r| r.autoremove < current_time)
58 {
59 if !session.expired() {
60 if let Err(err) = session_store.store_session(&session).await {
61 tracing::debug!(
62 "Session Failed to save to Database with error: {}",
63 err
64 );
65 break;
66 } else {
67 tracing::debug!(
68 "Session id {}: was saved to the database.",
69 session.id
70 );
71 }
72 }
73 }
74 }
75
76 session_store
77 .inner
78 .retain(|_k, v| v.autoremove > current_time);
79
80 session_store.timers.write().await.last_expiry_sweep =
81 Utc::now() + session_store.config.memory.purge_update;
82
83 tracing::trace!("Session Memory Cleaning Finished");
84 }
85
86 // Throttle by database lifespan - e.g. sweep every 6 hours
87 if last_database_sweep <= current_time && session_store.is_persistent() {
88 tracing::trace!("Session Database Cleaning Started");
89
90 //Remove any old keys that expired and Remove them from our loaded filter.
91 #[cfg(feature = "key-store")]
92 let expired = match session_store.cleanup().await {
93 Ok(v) => v,
94 Err(err) => {
95 tracing::error!("Session Database Cleaning Failed with error: {}", err);
96 break;
97 }
98 };
99
100 #[cfg(not(feature = "key-store"))]
101 if let Err(err) = session_store.cleanup().await {
102 tracing::error!("Session Database Cleaning Failed with error: {}", err);
103 break;
104 }
105
106 #[cfg(feature = "key-store")]
107 if !session_store.auto_handles_expiry() {
108 let mut filter = session_store.filter.write().await;
109 expired.iter().for_each(|id| filter.remove(id.as_bytes()));
110 }
111
112 session_store
113 .timers
114 .write()
115 .await
116 .last_database_expiry_sweep =
117 Utc::now() + session_store.config.database.purge_database_update;
118
119 tracing::trace!("Session Database Cleaning Finished");
120 }
121
122 tracing::trace!("Session Manager is Sleeping");
123 sleep(
124 session_store
125 .config
126 .thread_sleep_duration
127 .to_std()
128 .unwrap_or_default(),
129 )
130 .await;
131 }
132
133 Ok(())
134}
Added lib/axum-session/src/headers.rs View lib/axum-session/src/headers.rs
@@ -0,0 +1,453 @@
1#[cfg(not(feature = "rest_mode"))]
2use crate::CookiesAdditionJar;
3use crate::{DatabasePool, Session, SessionConfig, SessionStore};
4#[cfg(not(feature = "rest_mode"))]
5use cookie::{Cookie, CookieJar, Key};
6use forwarded_header_value::{ForwardedHeaderValue, Identifier};
7#[cfg(not(feature = "rest_mode"))]
8use http::header::{COOKIE, SET_COOKIE};
9#[cfg(feature = "rest_mode")]
10use http::{header::HeaderName, HeaderValue};
11use http::{
12 header::{FORWARDED, USER_AGENT},
13 request::Request,
14 HeaderMap,
15};
16#[cfg(feature = "rest_mode")]
17use std::collections::HashMap;
18use std::{
19 fmt::Debug,
20 net::{IpAddr, SocketAddr},
21};
22
23const X_REAL_IP: &str = "x-real-ip";
24const X_FORWARDED_FOR: &str = "x-forwarded-for";
25
26enum NameType {
27 Store,
28 Data,
29}
30
31impl NameType {
32 #[inline]
33 pub(crate) fn get_name(&self, config: &SessionConfig) -> String {
34 let name = match self {
35 NameType::Data => config.cookie_and_header.session_name.to_string(),
36 NameType::Store => config.cookie_and_header.store_name.to_string(),
37 };
38
39 #[cfg(not(feature = "rest_mode"))]
40 if config.cookie_and_header.prefix_with_host {
41 let mut prefixed = "__Host-".to_owned();
42 prefixed.push_str(&name);
43 prefixed
44 } else {
45 name
46 }
47
48 #[allow(clippy::let_and_return)]
49 #[cfg(feature = "rest_mode")]
50 name
51 }
52}
53
54#[cfg(not(feature = "rest_mode"))]
55pub async fn get_headers_and_key<T>(
56 store: &SessionStore<T>,
57 cookies: CookieJar,
58 ip_user_agent: &str,
59) -> (Option<String>, bool)
60where
61 T: DatabasePool + Clone + Debug + Sync + Send + 'static,
62{
63 let key = store.config.cookie_and_header.key.as_ref();
64
65 let value = cookies
66 .get_cookie(
67 &store.config.cookie_and_header.session_name,
68 key,
69 ip_user_agent.to_owned(),
70 false,
71 )
72 .await
73 .map(|c| c.value().to_string());
74
75 let storable = cookies
76 .get_cookie(
77 &store.config.cookie_and_header.store_name,
78 key,
79 ip_user_agent.to_owned(),
80 true,
81 )
82 .await
83 .is_some_and(|c| c.value().parse().unwrap_or(false));
84
85 (value, storable)
86}
87
88#[cfg(feature = "rest_mode")]
89pub async fn get_headers_and_key<T>(
90 store: &SessionStore<T>,
91 headers: HashMap<String, String>,
92 ip_user_agent: &str,
93) -> (Option<String>, bool)
94where
95 T: DatabasePool + Clone + Debug + Sync + Send + 'static,
96{
97 use crate::sec::verify_header;
98 let key = store.config.cookie_and_header.key.as_ref();
99
100 let name = store.config.cookie_and_header.session_name.to_string();
101 let value = match headers.get(&name) {
102 Some(c) => {
103 if let Some(key) = key {
104 verify_header(c, key, ip_user_agent).await.ok()
105 } else {
106 Some(c.to_owned())
107 }
108 }
109 None => None,
110 };
111
112 let name = store.config.cookie_and_header.store_name.to_string();
113 let storable = match headers.get(&name) {
114 Some(c) => {
115 if let Some(key) = key {
116 verify_header(c, key, ip_user_agent).await.ok()
117 } else {
118 Some(c.to_owned())
119 }
120 }
121 None => None,
122 }
123 .map(|c| c.parse().unwrap_or(false));
124
125 (value, storable.unwrap_or(false))
126}
127
128#[cfg(not(feature = "rest_mode"))]
129pub(crate) trait CookiesExt {
130 async fn get_cookie(
131 &self,
132 name: &str,
133 key: Option<&Key>,
134 message: String,
135 bypass: bool,
136 ) -> Option<Cookie<'static>>;
137 async fn add_cookie(
138 &mut self,
139 cookie: Cookie<'static>,
140 key: &Option<Key>,
141 message: String,
142 bypass: bool,
143 );
144}
145
146#[cfg(not(feature = "rest_mode"))]
147impl CookiesExt for CookieJar {
148 async fn get_cookie(
149 &self,
150 name: &str,
151 key: Option<&Key>,
152 message: String,
153 bypass: bool,
154 ) -> Option<Cookie<'static>> {
155 if !bypass {
156 if let Some(key) = key {
157 return self.message_signed(key, message).get(name).await;
158 }
159 }
160
161 self.get(name).cloned()
162 }
163
164 async fn add_cookie(
165 &mut self,
166 cookie: Cookie<'static>,
167 key: &Option<Key>,
168 message: String,
169 bypass: bool,
170 ) {
171 if !bypass {
172 if let Some(key) = key {
173 self.message_signed_mut(key, message).add(cookie).await;
174 return;
175 }
176 }
177
178 self.add(cookie);
179 }
180}
181
182#[cfg(not(feature = "rest_mode"))]
183fn create_cookie<'a>(config: &SessionConfig, value: String, cookie_type: NameType) -> Cookie<'a> {
184 let mut cookie_builder = Cookie::build((cookie_type.get_name(config), value))
185 .path(config.cookie_and_header.cookie_path.clone())
186 .secure(config.cookie_and_header.cookie_secure)
187 .http_only(config.cookie_and_header.cookie_http_only)
188 .same_site(config.cookie_and_header.cookie_same_site);
189
190 if let Some(domain) = &config.cookie_and_header.cookie_domain {
191 cookie_builder = cookie_builder.domain(domain.clone());
192 }
193
194 if let Some(max_age) = config.cookie_and_header.cookie_max_age {
195 let time_duration = max_age.to_std().expect("Max Age out of bounds");
196 cookie_builder =
197 cookie_builder.expires(Some((std::time::SystemTime::now() + time_duration).into()));
198 }
199
200 cookie_builder.build()
201}
202
203#[cfg(not(feature = "rest_mode"))]
204fn remove_cookie<'a>(config: &SessionConfig, cookie_type: NameType) -> Cookie<'a> {
205 let mut cookie_builder = Cookie::build((cookie_type.get_name(config), ""))
206 .path(config.cookie_and_header.cookie_path.clone())
207 .secure(config.cookie_and_header.cookie_secure)
208 .http_only(config.cookie_and_header.cookie_http_only)
209 .same_site(config.cookie_and_header.cookie_same_site);
210
211 if let Some(domain) = &config.cookie_and_header.cookie_domain {
212 cookie_builder = cookie_builder.domain(domain.clone());
213 }
214
215 let mut cookie = cookie_builder.build();
216 cookie.make_removal();
217 cookie
218}
219
220#[cfg(not(feature = "rest_mode"))]
221/// This will get a CookieJar from the Headers.
222pub(crate) fn get_cookies(headers: &HeaderMap) -> CookieJar {
223 let mut jar = CookieJar::new();
224
225 let cookie_iter = headers
226 .get_all(COOKIE)
227 .into_iter()
228 .filter_map(|value| value.to_str().ok())
229 .flat_map(|value| value.split(';'))
230 .filter_map(|cookie| Cookie::parse_encoded(cookie.to_owned()).ok());
231
232 for cookie in cookie_iter {
233 jar.add_original(cookie);
234 }
235
236 jar
237}
238
239#[cfg(feature = "rest_mode")]
240/// This will get a Hashmap of all the headers that Exist.
241pub(crate) fn get_headers<T>(
242 store: &SessionStore<T>,
243 headers: &HeaderMap,
244) -> HashMap<String, String>
245where
246 T: DatabasePool + Clone + Debug + Sync + Send + 'static,
247{
248 let mut map = HashMap::new();
249
250 for name in [
251 store.config.cookie_and_header.session_name.to_string(),
252 store.config.cookie_and_header.store_name.to_string(),
253 ] {
254 if let Some(value) = headers.get(&name) {
255 if let Ok(val) = value.to_str() {
256 map.insert(name, val.to_owned());
257 }
258 }
259 }
260
261 map
262}
263
264#[cfg(not(feature = "rest_mode"))]
265fn set_cookies(jar: CookieJar, headers: &mut HeaderMap) {
266 for cookie in jar.delta() {
267 if let Ok(header_value) = cookie.encoded().to_string().parse() {
268 headers.append(SET_COOKIE, header_value);
269 }
270 }
271}
272
273/// Used to Set either the Header Values or the Cookie Values.
274pub(crate) async fn set_headers<T>(
275 session: &Session<T>,
276 headers: &mut HeaderMap,
277 ip_user_agent: &str,
278 destroy: bool,
279 storable: bool,
280) where
281 T: DatabasePool + Clone + Debug + Sync + Send + 'static,
282{
283 // Lets make a new jar as we only want to add our cookies to the Response cookie header.\
284 #[cfg(not(feature = "rest_mode"))]
285 {
286 let mut cookies = CookieJar::new();
287
288 // Add SessionID
289 if (storable || !session.store.config.session_mode.is_opt_in()) && !destroy {
290 cookies
291 .add_cookie(
292 create_cookie(&session.store.config, session.id.clone(), NameType::Data),
293 &session.store.config.cookie_and_header.key,
294 ip_user_agent.to_owned(),
295 false,
296 )
297 .await;
298 } else {
299 cookies
300 .add_cookie(
301 remove_cookie(&session.store.config, NameType::Data),
302 &session.store.config.cookie_and_header.key,
303 ip_user_agent.to_owned(),
304 false,
305 )
306 .await;
307 }
308
309 // Add Session Store Boolean
310 if session.store.config.session_mode.is_opt_in() && storable && !destroy {
311 cookies
312 .add_cookie(
313 create_cookie(&session.store.config, storable.to_string(), NameType::Store),
314 &session.store.config.cookie_and_header.key,
315 ip_user_agent.to_owned(),
316 true,
317 )
318 .await;
319 } else {
320 cookies
321 .add_cookie(
322 remove_cookie(&session.store.config, NameType::Store),
323 &session.store.config.cookie_and_header.key,
324 ip_user_agent.to_owned(),
325 true,
326 )
327 .await;
328 }
329
330 set_cookies(cookies, headers);
331 }
332 #[cfg(feature = "rest_mode")]
333 {
334 use crate::sec::sign_header;
335 // Add SessionID
336 if (storable || !session.store.config.session_mode.is_opt_in()) && !destroy {
337 let name = NameType::Data.get_name(&session.store.config);
338 let value = if let Some(key) = session.store.config.cookie_and_header.key.as_ref() {
339 match sign_header(&session.id, key, ip_user_agent).await {
340 Ok(v) => v,
341 Err(err) => {
342 tracing::error!(err = %err, "Failed to sign Session ID so blank will be used.");
343 String::new()
344 }
345 }
346 } else {
347 session.id.clone()
348 };
349
350 if let Ok(name) = HeaderName::from_bytes(name.as_bytes()) {
351 if let Ok(value) = HeaderValue::from_str(&value) {
352 headers.insert(name, value);
353 }
354 }
355 }
356
357 // Add Session Store Boolean
358 if session.store.config.session_mode.is_opt_in() && storable && !destroy {
359 let name = NameType::Store.get_name(&session.store.config);
360 //storable doesn't need signing or encryption.
361 let value = storable.to_string();
362
363 if let Ok(name) = HeaderName::from_bytes(name.as_bytes()) {
364 if let Ok(value) = HeaderValue::from_str(&value) {
365 headers.insert(name, value);
366 }
367 }
368 }
369 }
370}
371
372///Rather than getting a single IP from the x_real, X forwarded and socket ip
373///It is better to use all 3 to ensure none of them have changed. Setting the default
374/// to be a empty String if it is not present. we will combine these together in a single Message String.
375pub(crate) fn get_ips_hash<T, D>(req: &Request<T>, store: &SessionStore<D>) -> String
376where
377 D: DatabasePool + Clone + Debug + Sync + Send + 'static,
378{
379 if store.config.cookie_and_header.key.is_some()
380 && store.config.cookie_and_header.with_ip_and_user_agent
381 {
382 let headers = req.headers();
383
384 let ip = if store.config.ip_user_agent.use_ip {
385 req.extensions()
386 .get::<axum::extract::ConnectInfo<SocketAddr>>()
387 .map(|addr| addr.ip().to_string())
388 .unwrap_or_default()
389 } else {
390 "".to_owned()
391 };
392
393 let x_forward_for_ip = if store.config.ip_user_agent.use_xforward_ip {
394 headers
395 .get(X_FORWARDED_FOR)
396 .and_then(|hv| hv.to_str().ok())
397 .and_then(|s| s.split(',').find_map(|s| s.trim().parse::<IpAddr>().ok()))
398 .map(|ip| ip.to_string())
399 .unwrap_or_default()
400 } else {
401 "".to_owned()
402 };
403
404 let forwarded_ip = if store.config.ip_user_agent.use_forward_ip {
405 headers
406 .get_all(FORWARDED)
407 .iter()
408 .find_map(|hv| {
409 hv.to_str()
410 .ok()
411 .and_then(|s| ForwardedHeaderValue::from_forwarded(s).ok())
412 .and_then(|f| {
413 f.iter()
414 .filter_map(|fs| fs.forwarded_for.as_ref())
415 .find_map(|ff| match ff {
416 Identifier::SocketAddr(a) => Some(a.ip()),
417 Identifier::IpAddr(ip) => Some(*ip),
418 _ => None,
419 })
420 })
421 })
422 .map(|ip| ip.to_string())
423 .unwrap_or_default()
424 } else {
425 "".to_owned()
426 };
427
428 let real_ip = if store.config.ip_user_agent.use_real_ip {
429 headers
430 .get(X_REAL_IP)
431 .and_then(|hv| hv.to_str().ok())
432 .and_then(|s| s.parse::<IpAddr>().ok())
433 .map(|ip| ip.to_string())
434 .unwrap_or_default()
435 } else {
436 "".to_owned()
437 };
438
439 let user_agent = if store.config.ip_user_agent.use_user_agent {
440 headers
441 .get(USER_AGENT)
442 .and_then(|hv| hv.to_str().ok())
443 .map(|useragent| useragent.to_string())
444 .unwrap_or_default()
445 } else {
446 "".to_owned()
447 };
448
449 format!("{ip};{x_forward_for_ip};{forwarded_ip};{real_ip};{user_agent}",)
450 } else {
451 String::new()
452 }
453}
Added lib/axum-session/src/layer.rs View lib/axum-session/src/layer.rs
@@ -0,0 +1,72 @@
1use std::{fmt, sync::Arc};
2
3use crate::{runner, DatabasePool, SessionService, SessionStore};
4use std::sync::atomic::{AtomicBool, Ordering};
5use tower_layer::Layer;
6
7static KILL_AS_DUP: AtomicBool = AtomicBool::new(false);
8
9/// Sessions Layer used with Axum to activate the Service.
10///
11/// # Examples
12/// ```rust ignore
13/// use axum_session::{SessionNullPool, SessionConfig, SessionStore, SessionLayer};
14/// use uuid::Uuid;
15///
16/// let config = SessionConfig::default();
17/// let session_store = SessionStore::<SessionNullPool>::new(None, config).await.unwrap();
18/// let layer = SessionLayer::new(session_store);
19/// ```
20///
21#[derive(Clone)]
22pub struct SessionLayer<T>
23where
24 T: DatabasePool + Clone + fmt::Debug + std::marker::Sync + std::marker::Send + 'static,
25{
26 session_store: SessionStore<T>,
27}
28
29impl<T> SessionLayer<T>
30where
31 T: DatabasePool + Clone + fmt::Debug + std::marker::Sync + std::marker::Send + 'static,
32{
33 /// Constructs a SessionLayer used with Axum to activate the Service.
34 ///
35 /// # Examples
36 /// ```rust ignore
37 /// use axum_session::{SessionNullPool, SessionConfig, SessionStore, SessionLayer};
38 /// use uuid::Uuid;
39 ///
40 /// let config = SessionConfig::default();
41 /// let session_store = SessionStore::<SessionNullPool>::new(None, config).await.unwrap();
42 /// let layer = SessionLayer::new(session_store);
43 /// ```
44 ///
45 #[inline]
46 pub fn new(session_store: SessionStore<T>) -> Self {
47 SessionLayer { session_store }
48 }
49}
50
51impl<S, T> Layer<S> for SessionLayer<T>
52where
53 T: DatabasePool + Clone + fmt::Debug + std::marker::Sync + std::marker::Send + 'static,
54{
55 type Service = SessionService<S, T>;
56
57 fn layer(&self, inner: S) -> Self::Service {
58 let kill_as_duplicate = KILL_AS_DUP.load(Ordering::Relaxed);
59
60 let service = SessionService {
61 session_store: self.session_store.clone(),
62 handle: Arc::new(tokio::spawn(runner(
63 self.session_store.clone(),
64 kill_as_duplicate,
65 ))),
66 inner,
67 };
68
69 KILL_AS_DUP.store(true, Ordering::Relaxed);
70 service
71 }
72}
Added lib/axum-session/src/lib.rs View lib/axum-session/src/lib.rs
@@ -0,0 +1,29 @@
1#![doc = include_str!("../README.md")]
2#![allow(dead_code)]
3#![warn(clippy::all, nonstandard_style, future_incompatible)]
4#![forbid(unsafe_code)]
5#![cfg_attr(docsrs, feature(doc_cfg))]
6
7mod config;
8pub mod databases;
9mod errors;
10mod handler;
11pub(crate) mod headers;
12mod layer;
13mod sec;
14mod service;
15mod session;
16mod session_data;
17mod session_store;
18
19pub use config::{IdGenerator, Key, SameSite, SessionConfig, SessionMode};
20pub use databases::*;
21pub use errors::SessionError;
22pub use layer::SessionLayer;
23pub use sec::*;
24pub use session::{ReadOnlySession, Session};
25pub use session_store::SessionStore;
26
27pub(crate) use handler::*;
28pub(crate) use service::SessionService;
29pub(crate) use session_data::{SessionData, SessionTimers};
Added lib/axum-session/src/sec.rs View lib/axum-session/src/sec.rs
@@ -0,0 +1,4 @@
1pub(crate) mod encrypt;
2mod signed;
3
4pub use signed::*;
Added lib/axum-session/src/service.rs View lib/axum-session/src/service.rs
@@ -0,0 +1,348 @@
1use crate::{headers::*, DatabasePool, Session, SessionData, SessionError, SessionStore};
2use axum::{response::Response, BoxError};
3use bytes::Bytes;
4use chrono::{Duration, Utc};
5#[cfg(feature = "key-store")]
6use fastbloom_rs::Deletable;
7use futures::future::BoxFuture;
8use http::Request;
9use http_body::Body as HttpBody;
10use std::{
11 convert::Infallible,
12 fmt::{self, Debug, Formatter},
13 sync::Arc,
14 task::{Context, Poll},
15};
16use tokio::task::JoinHandle;
17use tower_service::Service;
18
19#[derive(Clone)]
20pub struct SessionService<S, T>
21where
22 T: DatabasePool + Clone + Debug + Sync + Send + 'static,
23{
24 pub(crate) session_store: SessionStore<T>,
25 pub(crate) handle: Arc<JoinHandle<Result<(), SessionError>>>,
26 pub(crate) inner: S,
27}
28
29pub(crate) fn trace_error<ResBody>(
30 err: SessionError,
31 msg: &str,
32) -> Result<Response<ResBody>, Infallible>
33where
34 ResBody: HttpBody<Data = Bytes> + Default + Send + 'static,
35 ResBody::Error: Into<BoxError>,
36{
37 tracing::error!(err = %err, msg);
38 let mut res = Response::default();
39 *res.status_mut() = http::StatusCode::INTERNAL_SERVER_ERROR;
40 Ok(res)
41}
42
43impl<S, T, ReqBody, ResBody> Service<Request<ReqBody>> for SessionService<S, T>
44where
45 S: Service<Request<ReqBody>, Response = Response<ResBody>, Error = Infallible>
46 + Clone
47 + Send
48 + 'static,
49 S::Future: Send + 'static,
50 ReqBody: Send + 'static,
51 Infallible: From<<S as Service<Request<ReqBody>>>::Error>,
52 ResBody: HttpBody<Data = Bytes> + Default + Send + 'static,
53 ResBody::Error: Into<BoxError>,
54 T: DatabasePool + Clone + Debug + Sync + Send + 'static,
55{
56 type Response = Response<ResBody>;
57 type Error = Infallible;
58 type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
59
60 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
61 self.inner.poll_ready(cx)
62 }
63
64 fn call(&mut self, mut req: Request<ReqBody>) -> Self::Future {
65 let store = self.session_store.clone();
66 let not_ready_inner = self.inner.clone();
67 let mut ready_inner = std::mem::replace(&mut self.inner, not_ready_inner);
68
69 Box::pin(async move {
70 let ip_user_agent = get_ips_hash(&req, &store);
71
72 #[cfg(not(feature = "rest_mode"))]
73 let cookies = get_cookies(req.headers());
74
75 #[cfg(not(feature = "rest_mode"))]
76 let (session_id, storable) = get_headers_and_key(&store, cookies, &ip_user_agent).await;
77
78 #[cfg(feature = "rest_mode")]
79 let headers = get_headers(&store, req.headers());
80
81 #[cfg(feature = "rest_mode")]
82 let (session_id, storable) = get_headers_and_key(&store, headers, &ip_user_agent).await;
83
84 let (mut session, is_new) = match Session::new(store, session_id).await {
85 Ok(v) => v,
86 Err(err) => {
87 return trace_error(err, "failed to generate Session ID");
88 }
89 };
90
91 // Check if the session id exists if not lets check if it exists in the database or generate a new session.
92 // If manual mode is enabled then do not check for a Session unless the ID is not new.
93 if is_new && !session.store.config.session_mode.is_manual() {
94 let sess = SessionData::new(session.id.clone(), storable, &session.store.config);
95 session.store.inner.insert(session.id.clone(), sess);
96 } else if (!is_new || !session.store.config.session_mode.is_manual())
97 && !session.store.service_session_data(&session)
98 {
99 let mut fresh_session = session
100 .store
101 .load_session(session.id.clone())
102 .await
103 .ok()
104 .flatten()
105 .unwrap_or_else(|| {
106 tracing::debug!(
107 "Session {} did not exist in Database. So it was Recreated.",
108 session.id.clone()
109 );
110 SessionData::new(session.id.clone(), storable, &session.store.config)
111 });
112
113 fresh_session.autoremove = Utc::now() + session.store.config.memory.memory_lifespan;
114 // `expires` is what drives the "session is dead -> wipe its
115 // data" check. Loading a row whose `expires` predates this
116 // process (or was never persisted) must not mark a live
117 // session as expired, or `service_clear` clears its data.
118 if fresh_session.expires <= Utc::now() {
119 fresh_session.expires = if fresh_session.longterm {
120 Utc::now() + session.store.config.max_lifespan
121 } else {
122 Utc::now() + session.store.config.lifespan
123 };
124 }
125 fresh_session.store = storable;
126 fresh_session.update = true;
127 fresh_session.requests = 1;
128 session
129 .store
130 .inner
131 .insert(session.id.clone(), fresh_session);
132 };
133
134 // Sets a clone of the Store in the Extensions for Direct usage and sets the Session for Direct usage
135 //req.extensions_mut().insert(store.clone());
136 req.extensions_mut().insert(session.clone());
137
138 let mut response = ready_inner.call(req).await?;
139
140 // Lets get the last sweep times so we can use them to deturmine if its time to just save sessions.
141 let (last_sweep, last_database_sweep) = {
142 let timers = session.store.timers.read().await;
143 (timers.last_expiry_sweep, timers.last_database_expiry_sweep)
144 };
145
146 let (renew, storable, destroy, loaded) =
147 if let Some(session_data) = session.store.inner.get(&session.id) {
148 (
149 session_data.renew,
150 session_data.store,
151 session_data.destroy,
152 true,
153 )
154 } else {
155 // The in-memory entry can already be gone here (a parallel
156 // request unloaded it, or `memory_lifespan = 0` removed it).
157 // Reporting `storable = false` would hit the opt-in branch
158 // below, which deletes the database session and expires the
159 // cookies of a perfectly valid session. Fall back to the
160 // flag parsed from this request instead.
161 (false, storable, false, false)
162 };
163
164 tracing::trace!(
165 renew = renew,
166 storable = storable,
167 destroy = destroy,
168 loaded = loaded,
169 "Session id: {}",
170 session.id
171 );
172
173 if !destroy && (!session.store.config.session_mode.is_manual() || loaded) && renew {
174 // Lets change the Session ID and destory the old Session from the database.
175 let session_id = match Session::generate_id(&session.store).await {
176 Ok(v) => v,
177 Err(err) => {
178 return trace_error(err, "failed to Generate Session ID");
179 }
180 };
181
182 // Lets remove it from the database first.
183 if session.store.is_persistent() {
184 if let Err(err) = session
185 .store
186 .database_remove_session(session.id.clone())
187 .await
188 {
189 return trace_error(err, "failed to remove session from database");
190 };
191 }
192
193 //lets remove it from the filter. if the bottom fails just means it did not exist or was already unloaded.
194 #[cfg(feature = "key-store")]
195 if session.store.config.memory.use_bloom_filters {
196 let mut filter = session.store.filter.write().await;
197 filter.remove(session.id.as_bytes());
198 }
199
200 // Lets remove update and reinsert.
201 if let Some((_, mut session_data)) = session.store.inner.remove(&session.id) {
202 session_data.id = session_id.clone();
203 session_data.renew = false;
204 session.id = session_id.clone();
205 session.store.inner.insert(session.id.clone(), session_data);
206 }
207 }
208
209 // Add the Session ID so it can link back to a Session if one exists.
210 if (!session.store.config.session_mode.is_opt_in() || storable)
211 && session.store.is_persistent()
212 && !destroy
213 {
214 let clone_session = if let Some(mut sess) =
215 session.store.inner.get_mut(&session.id.clone())
216 {
217 // Calculate intervals for DB update throttling; durations are clamped to zero.
218 let now = Utc::now();
219
220 let time_since_last_db_update =
221 (now - sess.last_db_update).max(Duration::zero());
222 let db_update_threshold = session.store.config.database.db_update_interval;
223
224 // Time remaining until the next scheduled database cleanup.
225 let time_remaining_next_database_sweep =
226 (last_database_sweep - now).max(Duration::zero());
227
228 // Time remaining until the next scheduled memory cleanup.
229 let time_remaining_next_memory_sweep = (last_sweep - now).max(Duration::zero());
230
231 // Time remaining within the current update threshold.
232 let time_until_next_update =
233 (db_update_threshold - time_since_last_db_update).max(Duration::zero());
234
235 // Check if session should be updated (memory and/or database)
236 let should_update =
237 session.store.config.database.always_save || sess.update || !sess.expired();
238
239 if should_update {
240 // Update session expiry in memory
241 if sess.longterm {
242 sess.expires = Utc::now() + session.store.config.max_lifespan;
243 } else {
244 sess.expires = Utc::now() + session.store.config.lifespan;
245 };
246
247 // The database is updated if any of the following conditions are met:
248 // 1. 'always_save' is enabled in config.
249 // 2. The session's manual update flag (sess.update) is set.
250 // 3. The threshold interval has passed since the last update (and session is not expired).
251 // 4. The next memory sweep is approaching (within grace period).
252 // 5. The next database sweep is approaching (within grace period).
253 let should_update_db = session.store.config.database.always_save
254 || sess.update
255 || (!sess.expired()
256 && time_since_last_db_update >= db_update_threshold)
257 || (!sess.expired()
258 && time_remaining_next_memory_sweep <= time_until_next_update)
259 || (!sess.expired()
260 && time_remaining_next_database_sweep <= time_until_next_update);
261
262 if should_update_db {
263 sess.last_db_update = Utc::now();
264 sess.update = false;
265 Some(sess.clone())
266 } else {
267 None
268 }
269 } else {
270 None
271 }
272 } else {
273 None
274 };
275
276 if let Some(sess) = clone_session {
277 if let Err(err) = session.store.store_session(&sess).await {
278 return trace_error(err, "failed to save session to database");
279 } else {
280 tracing::debug!("Session id {}: was saved to the database.", session.id);
281 }
282 }
283 }
284
285 //lets tell the system we can unload this request now.
286 //If there are still more left the bottom wont unload anything.
287 session.remove_request();
288
289 if ((session.store.config.session_mode.is_opt_in() && !storable) || destroy)
290 && !session.is_parallel()
291 {
292 #[cfg(feature = "key-store")]
293 if session.store.config.memory.use_bloom_filters {
294 let mut filter = session.store.filter.write().await;
295 filter.remove(session.id.as_bytes());
296 }
297
298 let _ = session.store.inner.remove(&session.id);
299
300 if session.store.is_persistent() {
301 if let Err(err) = session
302 .store
303 .database_remove_session(session.id.clone())
304 .await
305 {
306 return trace_error(err, "failed to remove session from database");
307 }
308 }
309 }
310
311 // We will Deleted the data in memory as it should be stored in the database instead.
312 // if user is using this without a database then it will only work as a per request data store.
313 if session.store.config.memory.memory_lifespan.is_zero() && !session.is_parallel() {
314 #[cfg(feature = "key-store")]
315 if !session.store.is_persistent() && session.store.config.memory.use_bloom_filters {
316 let mut filter = session.store.filter.write().await;
317 filter.remove(session.id.as_bytes());
318 }
319
320 session.store.inner.remove(&session.id);
321 }
322
323 set_headers(
324 &session,
325 response.headers_mut(),
326 &ip_user_agent,
327 destroy,
328 storable,
329 )
330 .await;
331
332 Ok(response)
333 })
334 }
335}
336
337impl<S, T> Debug for SessionService<S, T>
338where
339 S: Debug,
340 T: DatabasePool + Clone + Debug + Sync + Send + 'static,
341{
342 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
343 f.debug_struct("SessionService")
344 .field("session_store", &self.session_store)
345 .field("inner", &self.inner)
346 .finish()
347 }
348}
Added lib/axum-session/src/session.rs View lib/axum-session/src/session.rs
@@ -0,0 +1,580 @@
1use crate::{DatabasePool, SessionData, SessionError, SessionStore};
2use axum::extract::FromRequestParts;
3
4#[cfg(feature = "key-store")]
5use fastbloom_rs::Membership;
6use http::{request::Parts, StatusCode};
7use serde::Serialize;
8use std::fmt::Debug;
9
10/// A Session Store.
11///
12/// Provides a Storage Handler to SessionStore and contains the ID of the current session.
13///
14/// This is Auto generated by the Session Layer Upon Service Execution.
15#[derive(Debug, Clone)]
16pub struct Session<T>
17where
18 T: DatabasePool + Clone + Debug + Sync + Send + 'static,
19{
20 /// The SessionStore that holds all the Sessions.
21 pub(crate) store: SessionStore<T>,
22 /// The Sessions current ID for looking up its store.
23 pub(crate) id: String,
24}
25
26/// Adds `FromRequestParts<B>` for Session
27///
28/// Returns the Session from Axum's request extensions state
29impl<T, S> FromRequestParts<S> for Session<T>
30where
31 T: DatabasePool + Clone + Debug + Sync + Send + 'static,
32 S: Send + Sync,
33{
34 type Rejection = (http::StatusCode, &'static str);
35
36 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
37 parts.extensions.get::<Session<T>>().cloned().ok_or((
38 StatusCode::INTERNAL_SERVER_ERROR,
39 "Can't extract Axum `Session`. Is `SessionLayer` enabled?",
40 ))
41 }
42}
43
44impl<S> Session<S>
45where
46 S: DatabasePool + Clone + Debug + Sync + Send + 'static,
47{
48 #[allow(clippy::needless_pass_by_ref_mut)]
49 pub(crate) async fn new(
50 store: SessionStore<S>,
51 value: Option<String>,
52 ) -> Result<(Self, bool), SessionError> {
53 let (id, is_new) = match value {
54 Some(v) => (v, false),
55 None => (Self::generate_id(&store).await?, true),
56 };
57
58 #[cfg(feature = "key-store")]
59 if store.config.memory.use_bloom_filters {
60 let contained = {
61 let filter = store.filter.read().await;
62 filter.contains(id.as_bytes())
63 };
64
65 if !contained {
66 let mut filter = store.filter.write().await;
67 filter.add(id.as_bytes());
68 }
69 }
70
71 Ok((Self { id, store }, is_new))
72 }
73
74 #[cfg(feature = "key-store")]
75 pub(crate) async fn generate_id(store: &SessionStore<S>) -> Result<String, SessionError> {
76 loop {
77 let token = store.config.id_generator.generate();
78
79 if (!store.config.memory.use_bloom_filters || store.auto_handles_expiry())
80 && !store.inner.contains_key(&token)
81 {
82 //This fixes an already used but in database issue.
83 if let Some(client) = &store.client {
84 // Unwrap should be safe to use as we would want it to crash if there was a major database error.
85 // This would mean the database no longer is online or the table missing etc.
86 if !client
87 .exists(&token.to_string(), &store.config.database.table_name)
88 .await?
89 {
90 return Ok(token);
91 }
92 } else {
93 return Ok(token);
94 }
95 } else {
96 let filter = store.filter.read().await;
97
98 if !filter.contains(token.to_string().as_bytes()) {
99 return Ok(token);
100 }
101 }
102 }
103 }
104
105 #[cfg(not(feature = "key-store"))]
106 pub(crate) async fn generate_id(store: &SessionStore<S>) -> Result<String, SessionError> {
107 loop {
108 let token = store.config.id_generator.generate();
109
110 if !store.inner.contains_key(&token) {
111 //This fixes an already used but in database issue.
112 if let Some(client) = &store.client {
113 // Unwrap should be safe to use as we would want it to crash if there was a major database error.
114 // This would mean the database no longer is online or the table missing etc.
115 if !client
116 .exists(&token.to_string(), &store.config.database.table_name)
117 .await?
118 {
119 return Ok(token);
120 }
121 } else {
122 return Ok(token);
123 }
124 }
125 }
126 }
127 /// Sets the Session to create the SessionData based on the current Session ID.
128 /// You can only use this if SessionMode::Manual is set or it will Panic.
129 /// This will also set the store to true similar to session.set_store(true);
130 ///
131 /// # Examples
132 /// ```rust ignore
133 /// session.create_data();
134 /// ```
135 ///
136 #[inline]
137 pub fn create_data(&self) {
138 if !self.store.config.session_mode.is_manual() {
139 panic!(
140 "Session must be set to SessionMode::Manual in order to use create_data,
141 as the Session data is created already."
142 );
143 }
144 let session_data = SessionData::new(self.id.clone(), true, &self.store.config);
145 self.store.inner.insert(self.id.clone(), session_data);
146 }
147
148 /// Checks if the SessionData was created or not.
149 ///
150 /// # Examples
151 /// ```rust ignore
152 /// if session.data_exists() {
153 /// println!("data Exists");
154 /// }
155 /// ```
156 ///
157 #[inline]
158 pub fn data_exists(&self) -> bool {
159 self.store.inner.contains_key(&self.id)
160 }
161
162 /// Set session flags to renew/regenerate the ID.
163 /// This deletes data from the database keyed with the old ID.
164 /// This helps to enhance security when logging into secure
165 /// areas on a website. The current session's data will be
166 /// stored with the new ID.
167 ///
168 /// # Examples
169 /// ```rust ignore
170 /// session.renew();
171 /// ```
172 ///
173 #[inline]
174 pub fn renew(&self) {
175 self.store.renew(self.id.clone());
176 }
177
178 /// Sets the Session to force update the database.
179 /// This will increase the Timer on the sessions store
180 /// making the session live longer in the persistent database.
181 ///
182 /// # Examples
183 /// ```rust ignore
184 /// session.renew();
185 /// ```
186 ///
187 #[inline]
188 pub fn update(&self) {
189 self.store.update(self.id.clone());
190 }
191
192 /// Sets the Current Session to be Destroyed.
193 /// This will Deleted the Session and Cookies upon Response Phase.
194 ///
195 /// # Examples
196 /// ```rust ignore
197 /// session.destroy();
198 /// ```
199 ///
200 #[inline]
201 pub fn destroy(&self) {
202 self.store.destroy(self.id.clone());
203 }
204
205 /// Sets the Current Session to a long term expiration. Useful for Remember Me setups.
206 /// This will also update the database on Response Phase.
207 ///
208 /// # Examples
209 /// ```rust ignore
210 /// session.set_longterm(true);
211 /// ```
212 ///
213 #[inline]
214 pub fn set_longterm(&self, longterm: bool) {
215 self.store.set_longterm(self.id.clone(), longterm);
216 }
217
218 /// Allows the Current Session to store.
219 /// This will also update the database on Response Phase.
220 ///
221 /// This is only used when `SessionMode` is Manual or OptIn.
222 /// This will allow the Session to be stored if true.
223 /// This will delete and not allow a session to be stored if false.
224 ///
225 /// # Examples
226 /// ```rust ignore
227 /// session.set_store(true);
228 /// ```
229 ///
230 #[inline]
231 pub fn set_store(&self, can_store: bool) {
232 self.store.set_store(self.id.clone(), can_store);
233 }
234
235 /// Gets data from the Session's HashMap
236 ///
237 /// Provides an `Option<T>` that returns the requested data from the Sessions store.
238 /// Returns None if Key does not exist or if serde_json failed to deserialize.
239 ///
240 /// # Examples
241 /// ```rust ignore
242 /// let id = session.get("user-id").unwrap_or(0);
243 /// ```
244 ///
245 ///Used to get data stored within SessionData's hashmap from a key value.
246 ///
247 #[inline]
248 pub fn get<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
249 self.store.get(self.id.clone(), key)
250 }
251
252 /// Removes a Key from the Current Session's HashMap returning it.
253 ///
254 /// Provides an `Option<T> `that returns the requested data from the Sessions store.
255 /// Returns None if Key does not exist or if serde_json failed to deserialize.
256 ///
257 /// # Examples
258 /// ```rust ignore
259 /// let id = session.get_remove("user-id").unwrap_or(0);
260 /// ```
261 ///
262 /// Used to get data stored within SessionData's hashmap from a key value.
263 ///
264 #[inline]
265 pub fn get_remove<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
266 self.store.get_remove(self.id.clone(), key)
267 }
268
269 /// Sets data to the Current Session's HashMap.
270 /// This will also update the database on Response Phase.
271 ///
272 /// # Examples
273 /// ```rust ignore
274 /// session.set("user-id", 1);
275 /// ```
276 ///
277 #[inline]
278 pub fn set(&self, key: &str, value: impl Serialize) {
279 self.store.set(self.id.clone(), key, value);
280 }
281
282 /// Removes a Key from the Current Session's HashMap.
283 /// Does not process the String into a Type, Just removes it.
284 /// This will also update the database on Response Phase.
285 ///
286 /// # Examples
287 /// ```rust ignore
288 /// let _ = session.remove("user-id");
289 /// ```
290 ///
291 #[inline]
292 pub fn remove(&self, key: &str) {
293 self.store.remove(self.id.clone(), key);
294 }
295
296 /// Clears all data from the Current Session's HashMap instantly.
297 /// This will also update the database on Response Phase.
298 ///
299 /// # Examples
300 /// ```rust ignore
301 /// session.clear();
302 /// ```
303 ///
304 #[inline]
305 pub fn clear(&self) {
306 self.store.clear_session_data(self.id.clone());
307 }
308
309 /// Returns a i64 count of how many Sessions exist.
310 ///
311 /// If the Session is persistent it will return all sessions within the database.
312 /// If the Session is not persistent it will return a count within SessionStore.
313 ///
314 /// # Examples
315 /// ```rust ignore
316 /// let count = session.count().await;
317 /// ```
318 ///
319 #[inline]
320 pub async fn count(&self) -> i64 {
321 self.store.count_sessions().await
322 }
323
324 /// Returns the SessionID for this Session.
325 ///
326 /// # Examples
327 /// ```rust ignore
328 /// let session_id = session.get_session_id();
329 /// ```
330 ///
331 #[inline]
332 pub fn get_session_id(&self) -> String {
333 self.id.clone()
334 }
335
336 /// Returns the store for this Session.
337 ///
338 /// The store contains everything that all sessions need.
339 ///
340 /// # Examples
341 /// ```rust ignore
342 /// let store = session.get_store();
343 /// ```
344 ///
345 #[inline]
346 pub fn get_store(&self) -> &SessionStore<S> {
347 &self.store
348 }
349
350 /// Returns a mutable store for this Session.
351 ///
352 /// The store contains everything that all sessions need.
353 ///
354 /// # Examples
355 /// ```rust ignore
356 /// let store = session.get_store_mut();
357 /// ```
358 ///
359 #[inline]
360 pub fn get_mut_store(&mut self) -> &mut SessionStore<S> {
361 &mut self.store
362 }
363
364 /// Removes a Request from the request counter
365 /// used to determine if parallel requests exist.
366 /// prevents data deletion until requests == 0.
367 ///
368 /// # Examples
369 /// ```rust ignore
370 /// session.remove_request();
371 /// ```
372 ///
373 #[inline]
374 pub(crate) fn remove_request(&self) {
375 self.store.remove_session_request(self.id.clone());
376 }
377
378 /// Removes a Request from the request counter
379 /// used to determine if parallel requests exist.
380 /// prevents data deletion until requests == 0.
381 ///
382 /// # Examples
383 /// ```rust ignore
384 /// session.set_request();
385 /// ```
386 ///
387 #[inline]
388 pub(crate) fn set_request(&self) {
389 self.store.set_session_request(self.id.clone());
390 }
391
392 /// checks if a session has more than one request.
393 ///
394 /// # Examples
395 /// ```rust ignore
396 /// session.is_parallel();
397 /// ```
398 ///
399 #[inline]
400 pub(crate) fn is_parallel(&self) -> bool {
401 self.store.is_session_parallel(self.id.clone())
402 }
403
404 /// checks if a session exists and if it is outdated.
405 ///
406 /// # Examples
407 /// ```rust ignore
408 /// session.verify();
409 /// ```
410 ///
411 #[cfg(feature = "advanced")]
412 #[cfg_attr(docsrs, doc(cfg(feature = "advanced")))]
413 #[inline]
414 pub fn verify(&self) -> Result<(), SessionError> {
415 self.store.verify(self.id.clone())
416 }
417
418 /// Updates the sessions stored database expire time.
419 /// Use this before forcing a update to the database store.
420 /// will update the database expires based on
421 /// if the session is longterm then configs max_lifespan.
422 /// if not then configs lifespan.
423 ///
424 /// THIS WILL NOT UPDATE THE DATABASE SIDE.
425 ///
426 /// # Examples
427 /// ```rust ignore
428 /// session.update_database_expires();
429 /// ```
430 ///
431 #[cfg(feature = "advanced")]
432 #[cfg_attr(docsrs, doc(cfg(feature = "advanced")))]
433 #[inline]
434 pub fn update_database_expires(&self) -> Result<(), SessionError> {
435 self.store.update_database_expires(self.id.clone())
436 }
437
438 /// Updates the Sessions In memory auto remove timer.
439 /// Will prevent it from being removed for the configs set memory_lifespan.
440 ///
441 /// # Examples
442 /// ```rust ignore
443 /// session.update_memory_expires();
444 /// ```
445 ///
446 #[cfg(feature = "advanced")]
447 #[cfg_attr(docsrs, doc(cfg(feature = "advanced")))]
448 #[inline]
449 pub fn update_memory_expires(&self) -> Result<(), SessionError> {
450 self.store.update_memory_expires(self.id.clone())
451 }
452
453 /// forces a update to the databases stored data for the session.
454 /// Make sure to update the databases expire time before running this or
455 /// the data could be unloaded by a request checking for outdated sessions.
456 ///
457 /// # Examples
458 /// ```rust ignore
459 /// session.force_database_update().await;
460 /// ```
461 ///
462 #[cfg(feature = "advanced")]
463 #[cfg_attr(docsrs, doc(cfg(feature = "advanced")))]
464 #[inline]
465 pub async fn force_database_update(&self) -> Result<(), SessionError> {
466 self.store.force_database_update(self.id.clone()).await
467 }
468
469 /// Removes the session from the memory store if it is not parallel.
470 /// If it is parallel then each parallel session will need to call this once.
471 /// when all parallel sessions are dead this gets unloaded.
472 ///
473 /// THIS DOES NOT CLEAR THE KEY STORE.
474 ///
475 /// # Examples
476 /// ```rust ignore
477 /// session.memory_remove_session();
478 /// ```
479 ///
480 #[cfg(feature = "advanced")]
481 #[cfg_attr(docsrs, doc(cfg(feature = "advanced")))]
482 #[inline]
483 pub fn memory_remove_session(&self) -> Result<(), SessionError> {
484 self.store.memory_remove_session(self.id.clone())
485 }
486
487 /// Removes the session from the Database store.
488 ///
489 /// THIS DOES NOT REMOVE THE KEY STORE.
490 ///
491 /// # Examples
492 /// ```rust ignore
493 /// session.database_remove_session().await;
494 /// ```
495 ///
496 #[cfg(feature = "advanced")]
497 #[cfg_attr(docsrs, doc(cfg(feature = "advanced")))]
498 #[inline]
499 pub async fn database_remove_session(&self) -> Result<(), SessionError> {
500 self.store.database_remove_session(self.id.clone()).await
501 }
502}
503
504#[derive(Debug, Clone)]
505pub struct ReadOnlySession<T>
506where
507 T: DatabasePool + Clone + Debug + Sync + Send + 'static,
508{
509 pub(crate) store: SessionStore<T>,
510 pub(crate) id: String,
511}
512
513impl<T> From<Session<T>> for ReadOnlySession<T>
514where
515 T: DatabasePool + Clone + Debug + Sync + Send + 'static,
516{
517 fn from(session: Session<T>) -> Self {
518 ReadOnlySession {
519 store: session.store,
520 id: session.id,
521 }
522 }
523}
524
525/// Adds `FromRequestParts<B>` for Session
526///
527/// Returns the Session from Axum's request extensions state.
528impl<T, S> FromRequestParts<S> for ReadOnlySession<T>
529where
530 T: DatabasePool + Clone + Debug + Sync + Send + 'static,
531 S: Send + Sync,
532{
533 type Rejection = (http::StatusCode, &'static str);
534
535 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
536 let session = parts.extensions.get::<Session<T>>().cloned().ok_or((
537 StatusCode::INTERNAL_SERVER_ERROR,
538 "Can't extract Axum `Session`. Is `SessionLayer` enabled?",
539 ))?;
540
541 Ok(session.into())
542 }
543}
544
545impl<S> ReadOnlySession<S>
546where
547 S: DatabasePool + Clone + Debug + Sync + Send + 'static,
548{
549 /// Gets data from the Session's HashMap
550 ///
551 /// Provides an `Option<T>` that returns the requested data from the Sessions store.
552 /// Returns None if Key does not exist or if serde_json failed to deserialize.
553 ///
554 /// # Examples
555 /// ```rust ignore
556 /// let id = session.get("user-id").unwrap_or(0);
557 /// ```
558 ///
559 ///Used to get data stored within SessionData's hashmap from a key value.
560 ///
561 #[inline]
562 pub fn get<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
563 self.store.get(self.id.clone(), key)
564 }
565
566 /// Returns a i64 count of how many Sessions exist.
567 ///
568 /// If the Session is persistent it will return all sessions within the database.
569 /// If the Session is not persistent it will return a count within SessionStore.
570 ///
571 /// # Examples
572 /// ```rust ignore
573 /// let count = session.count().await;
574 /// ```
575 ///
576 #[inline]
577 pub async fn count(&self) -> i64 {
578 self.store.count_sessions().await
579 }
580}
Added lib/axum-session/src/session_data.rs View lib/axum-session/src/session_data.rs
@@ -0,0 +1,338 @@
1use crate::SessionConfig;
2use chrono::{DateTime, Duration, Utc};
3use serde::{Deserialize, Serialize};
4use std::{collections::HashMap, fmt::Debug};
5
6/// The Store and Configured Data for a Session.
7///
8/// # Examples
9/// ```rust ignore
10/// use axum_session::{SessionConfig, SessionData};
11/// use uuid::Uuid;
12///
13/// let config = SessionConfig::default();
14/// let token = Uuid::new_v4();
15/// let session_data = SessionData::new(token.to_string(), true, &config);
16/// ```
17///
18#[derive(Debug, Serialize, Deserialize, Clone)]
19pub struct SessionData {
20 #[serde(skip)]
21 pub(crate) id: String,
22 pub(crate) data: HashMap<String, String>,
23 #[serde(default = "serde_expires_default")]
24 pub(crate) expires: DateTime<Utc>,
25 #[serde(default = "serde_autoremove_default")]
26 pub(crate) autoremove: DateTime<Utc>,
27 #[serde(skip)]
28 pub(crate) destroy: bool,
29 #[serde(skip)]
30 pub(crate) renew: bool,
31 pub(crate) longterm: bool,
32 #[serde(skip)]
33 pub(crate) store: bool,
34 #[serde(skip)]
35 pub(crate) update: bool,
36 #[serde(skip)]
37 pub(crate) requests: usize,
38 #[serde(skip)]
39 pub(crate) last_db_update: DateTime<Utc>,
40}
41
42/// Fallback for sessions persisted before `expires` was serialized (or written
43/// by an older release): treat them as still valid for the configured lifespan
44/// instead of instantly expiring, which would wipe the stored session data.
45fn serde_expires_default() -> DateTime<Utc> {
46 Utc::now() + Duration::days(7)
47}
48
49/// Same reasoning as [`serde_expires_default`]; an unloaded session should not
50/// be treated as immediately eligible for memory eviction.
51fn serde_autoremove_default() -> DateTime<Utc> {
52 Utc::now() + Duration::minutes(60)
53}
54
55impl SessionData {
56 /// Constructs a new SessionData.
57 ///
58 /// # Examples
59 /// ```rust ignore
60 /// use axum_session::{SessionConfig, SessionData};
61 /// use uuid::Uuid;
62 ///
63 /// let config = SessionConfig::default();
64 /// let token = Uuid::new_v4();
65 /// let session_data = SessionData::new(token.to_string(), true, &config);
66 /// ```
67 ///
68 #[inline]
69 pub(crate) fn new(id: String, storable: bool, config: &SessionConfig) -> Self {
70 Self {
71 id,
72 data: HashMap::new(),
73 expires: Utc::now() + config.lifespan,
74 destroy: false,
75 renew: false,
76 autoremove: Utc::now() + config.memory.memory_lifespan,
77 longterm: false,
78 store: storable,
79 update: true,
80 requests: 1,
81 last_db_update: Utc::now(),
82 }
83 }
84
85 /// Determines whether the session has expired.
86 ///
87 /// # Examples
88 /// ```rust ignore
89 /// use axum_session::{SessionConfig, SessionData};
90 /// use uuid::Uuid;
91 ///
92 /// let config = SessionConfig::default();
93 /// let token = Uuid::new_v4();
94 /// let session_data = SessionData::new(token.to_string(), true, &config);
95 /// let expired = session_data.expired();
96 /// ```
97 ///
98 #[inline]
99 pub(crate) fn expired(&self) -> bool {
100 self.expires < Utc::now()
101 }
102
103 /// Validates and checks if the Session is to be destroyed.
104 /// If so the Sessions Data is Cleared.
105 /// autoremove is then updated for the session regardless.
106 ///
107 /// # Examples
108 /// ```rust ignore
109 /// use axum_session::{SessionConfig, SessionData};
110 /// use uuid::Uuid;
111 ///
112 /// let config = SessionConfig::default();
113 /// let token = Uuid::new_v4();
114 /// let mut session_data = SessionData::new(token.to_string(), true, &config);
115 /// let expired = session_data.service_clear(Duration::days(5));
116 /// ```
117 ///
118 #[inline]
119 pub(crate) fn service_clear(&mut self, memory_lifespan: Duration, clear_check: bool) {
120 if clear_check && self.autoremove < Utc::now() {
121 self.update = true;
122
123 if self.expired() {
124 self.data.clear();
125 }
126 }
127
128 self.autoremove = Utc::now() + memory_lifespan;
129 }
130
131 /// Set session flags to renew/regenerate the ID.
132 /// This deletes data from the database keyed with the old ID.
133 /// This helps to enhance security when logging into secure
134 /// areas on a website. The current session's data will be
135 /// stored with the new ID.
136 ///
137 /// # Examples
138 /// ```rust ignore
139 /// session.renew();
140 /// ```
141 ///
142 #[inline]
143 pub fn renew(&mut self) {
144 self.renew = true;
145 self.update = true;
146 }
147
148 /// Sets the Session to force update the database.
149 /// This will increase the Timer on the sessions store
150 /// making the session live longer in the persistent database.
151 ///
152 /// # Examples
153 /// ```rust ignore
154 /// session.update();
155 /// ```
156 ///
157 #[inline]
158 pub fn update(&mut self) {
159 self.update = true;
160 }
161
162 /// Sets the Current Session to be Destroyed.
163 /// This will Deleted the Session and Cookies upon Response Phase.
164 ///
165 /// # Examples
166 /// ```rust ignore
167 /// session.destroy();
168 /// ```
169 ///
170 #[inline]
171 pub fn destroy(&mut self) {
172 self.destroy = true;
173 }
174
175 /// Sets the Current Session to a long term expiration. Useful for Remember Me setups.
176 /// This will also update the database on Response Phase.
177 ///
178 /// # Examples
179 /// ```rust ignore
180 /// session.set_longterm(true);
181 /// ```
182 ///
183 #[inline]
184 pub fn set_longterm(&mut self, longterm: bool) {
185 self.longterm = longterm;
186 self.update = true;
187 }
188
189 /// Sets the Current Session to be storable.
190 /// This will also update the database on Response Phase.
191 ///
192 /// This is only used when `SessionMode` is Manual or Storable.
193 /// This will allow the Session to be stored if true.
194 /// This will delete and not allow a session to be stored if false.
195 ///
196 /// # Examples
197 /// ```rust ignore
198 /// session.set_store(true);
199 /// ```
200 ///
201 #[inline]
202 pub fn set_store(&mut self, can_store: bool) {
203 self.store = can_store;
204 self.update = true;
205 }
206
207 /// Gets data from the Session's HashMap
208 ///
209 /// Provides an Option<T> that returns the requested data from the Sessions store.
210 /// Returns None if Key does not exist or if serde_json failed to deserialize.
211 ///
212 /// # Examples
213 /// ```rust ignore
214 /// let id = session.get("user-id").unwrap_or(0);
215 /// ```
216 ///
217 ///Used to get data stored within SessionDatas hashmap from a key value.
218 ///
219 #[inline]
220 pub fn get<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
221 let string = self.data.get(key)?;
222 serde_json::from_str(string).ok()
223 }
224
225 /// Removes a Key from the Current Session's HashMap returning it.
226 /// This will also update the database on Response Phase.
227 ///
228 /// Provides an Option<T> that returns the requested data from the Sessions store.
229 /// Returns None if Key does not exist or if serde_json failed to deserialize.
230 ///
231 /// # Examples
232 /// ```rust ignore
233 /// let id = session.get_remove("user-id").unwrap_or(0);
234 /// ```
235 ///
236 /// Used to get data stored within SessionDatas hashmap from a key value.
237 ///
238 #[inline]
239 pub fn get_remove<T: serde::de::DeserializeOwned>(&mut self, key: &str) -> Option<T> {
240 let string = self.data.remove(key)?;
241 self.update = true;
242 serde_json::from_str(&string).ok()
243 }
244
245 /// Sets data to the Current Session's HashMap.
246 /// This will also update the database on Response Phase.
247 ///
248 /// # Examples
249 /// ```rust ignore
250 /// session.set("user-id", 1);
251 /// ```
252 ///
253 #[inline]
254 pub fn set(&mut self, key: &str, value: impl Serialize) {
255 let value = serde_json::to_string(&value).unwrap_or_else(|_| "".to_string());
256 let _ = self.data.insert(key.to_string(), value);
257 self.update = true;
258 }
259
260 /// Removes a Key from the Current Session's HashMap.
261 /// Does not process the String into a Type, Just removes it.
262 /// This will also update the database on Response Phase.
263 ///
264 /// # Examples
265 /// ```rust ignore
266 /// let _ = session.remove("user-id");
267 /// ```
268 ///
269 #[inline]
270 pub fn remove(&mut self, key: &str) {
271 let _ = self.data.remove(key);
272 self.update = true;
273 }
274
275 /// Clears all data from the Current Session's HashMap.
276 /// This will also update the database on Response Phase.
277 ///
278 /// # Examples
279 /// ```rust ignore
280 /// session.clear();
281 /// ```
282 ///
283 #[inline]
284 pub fn clear(&mut self) {
285 self.data.clear();
286 self.update = true;
287 }
288
289 /// Removes a Request from the request counter
290 /// used to determine if parallel requests exist.
291 /// prevents data deletion until requests == 0.
292 ///
293 /// # Examples
294 /// ```rust ignore
295 /// session.remove_request();
296 /// ```
297 ///
298 #[inline]
299 pub(crate) fn remove_request(&mut self) {
300 self.requests = self.requests.saturating_sub(1);
301 }
302
303 /// Removes a Request from the request counter
304 /// used to determine if parallel requests exist.
305 /// prevents data deletion until requests == 0.
306 ///
307 /// # Examples
308 /// ```rust ignore
309 /// session.set_request();
310 /// ```
311 ///
312 #[inline]
313 pub(crate) fn set_request(&mut self) {
314 self.requests = self.requests.saturating_add(1);
315 }
316
317 /// checks if a session has a request still.
318 ///
319 /// # Examples
320 /// ```rust ignore
321 /// session.is_parallel();
322 /// ```
323 ///
324 #[inline]
325 pub(crate) fn is_parallel(&self) -> bool {
326 self.requests >= 1
327 }
328}
329
330/// Internal Timers
331///
332/// used to keep track of the last ran expiration check for both database and memory session data.
333///
334#[derive(Debug)]
335pub(crate) struct SessionTimers {
336 pub(crate) last_expiry_sweep: DateTime<Utc>,
337 pub(crate) last_database_expiry_sweep: DateTime<Utc>,
338}
Added lib/axum-session/src/session_store.rs View lib/axum-session/src/session_store.rs
@@ -0,0 +1,622 @@
1use crate::{
2 sec::encrypt, DatabasePool, Session, SessionConfig, SessionData, SessionError, SessionTimers,
3};
4use axum::extract::FromRequestParts;
5use chrono::{Duration, Utc};
6use dashmap::DashMap;
7#[cfg(feature = "key-store")]
8use fastbloom_rs::Deletable;
9#[cfg(feature = "key-store")]
10use fastbloom_rs::{CountingBloomFilter, FilterBuilder, Membership};
11use http::{request::Parts, StatusCode};
12use serde::Serialize;
13use std::{fmt::Debug, sync::Arc};
14use tokio::sync::RwLock;
15
16/// Contains the main Services storage for all session's and database access for persistent Sessions.
17///
18/// # Examples
19/// ```rust ignore
20/// use axum_session::{SessionNullPool, SessionConfig, SessionStore};
21///
22/// let config = SessionConfig::default();
23/// let session_store = SessionStore::<SessionNullPool>::new(None, config).await.unwrap();
24/// ```
25///
26#[derive(Clone, Debug)]
27pub struct SessionStore<T>
28where
29 T: DatabasePool + Clone + Debug + Sync + Send + 'static,
30{
31 /// Client for the database.
32 pub client: Option<T>,
33 /// locked Hashmap containing UserID and their session data.
34 pub(crate) inner: Arc<DashMap<String, SessionData>>,
35 /// Session Configuration.
36 pub config: SessionConfig,
37 /// Session Timers used for Clearing Memory and Database.
38 pub(crate) timers: Arc<RwLock<SessionTimers>>,
39 #[cfg(feature = "key-store")]
40 /// Filter used to keep track of what session IDs exist.
41 pub(crate) filter: Arc<RwLock<CountingBloomFilter>>,
42}
43
44impl<T, S> FromRequestParts<S> for SessionStore<T>
45where
46 T: DatabasePool + Clone + Debug + Sync + Send + 'static,
47 S: Send + Sync,
48{
49 type Rejection = (http::StatusCode, &'static str);
50
51 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
52 let session = parts.extensions.get::<Session<T>>().ok_or((
53 StatusCode::INTERNAL_SERVER_ERROR,
54 "Can't extract Axum `Session`. Is `SessionLayer` enabled?",
55 ))?;
56
57 Ok(session.store.clone())
58 }
59}
60
61impl<T> SessionStore<T>
62where
63 T: DatabasePool + Clone + Debug + Sync + Send + 'static,
64{
65 /// Constructs a New `SessionStore` and Creates the Database Table
66 /// needed for the Session if it does not exist if client is not `None`.
67 ///
68 /// # Examples
69 /// ```rust ignore
70 /// use axum_session::{SessionNullPool, SessionConfig, SessionStore};
71 ///
72 /// let config = SessionConfig::default();
73 /// let session_store = SessionStore::<SessionNullPool>::new(None, config).await.unwrap();
74 /// ```
75 ///
76 #[inline]
77 pub async fn new(client: Option<T>, config: SessionConfig) -> Result<Self, SessionError> {
78 if let Some(client) = &client {
79 client.initiate(&config.database.table_name).await?
80 }
81
82 // If we have a database client then lets also get any SessionId's that Exist within the database
83 // that are not yet expired.
84 #[cfg(feature = "key-store")]
85 let filter = Self::create_filter(&client, &config).await?;
86
87 Ok(Self {
88 client,
89 inner: Default::default(),
90 config,
91 timers: Arc::new(RwLock::new(SessionTimers {
92 // the first expiry sweep is scheduled one lifetime from start-up
93 last_expiry_sweep: Utc::now() + Duration::try_hours(1).unwrap_or_default(),
94 // the first expiry sweep is scheduled one lifetime from start-up
95 last_database_expiry_sweep: Utc::now() + Duration::try_hours(6).unwrap_or_default(),
96 })),
97 #[cfg(feature = "key-store")]
98 filter: Arc::new(RwLock::new(filter)),
99 })
100 }
101
102 /// Used to create and Fill the Filter.
103 #[cfg(feature = "key-store")]
104 pub(crate) async fn create_filter(
105 client: &Option<T>,
106 config: &SessionConfig,
107 ) -> Result<CountingBloomFilter, SessionError> {
108 let mut filter = FilterBuilder::new(
109 config.memory.filter_expected_elements,
110 config.memory.filter_false_positive_probability,
111 )
112 .build_counting_bloom_filter();
113
114 if config.memory.use_bloom_filters {
115 // If client exist then lets preload the id's within the database so the filter is accurate.
116 if let Some(client) = &client {
117 let ids = client.get_ids(&config.database.table_name).await?;
118
119 ids.iter().for_each(|id| filter.add(id.as_bytes()));
120 }
121 }
122
123 Ok(filter)
124 }
125
126 /// Checks if the database is in persistent mode.
127 ///
128 /// Returns true if client is Some().
129 ///
130 /// # Examples
131 /// ```rust ignore
132 /// use axum_session::{SessionNullPool, SessionConfig, SessionStore};
133 ///
134 /// let config = SessionConfig::default();
135 /// let session_store = SessionStore::<SessionNullPool>::new(None, config).await.unwrap();
136 /// let is_persistent = session_store.is_persistent();
137 /// ```
138 ///
139 #[inline]
140 pub fn is_persistent(&self) -> bool {
141 self.client.is_some()
142 }
143
144 /// Cleans Expired sessions from the Database based on Utc::now().
145 ///
146 /// If client is None it will return Ok(()).
147 ///
148 /// # Errors
149 /// - ['SessionError::Sqlx'] is returned if database connection has failed or user does not have permissions.
150 ///
151 /// # Examples
152 /// ```rust ignore
153 /// use axum_session::{SessionNullPool, SessionConfig, SessionStore};
154 ///
155 /// let config = SessionConfig::default();
156 /// let session_store = SessionStore::<SessionNullPool>::new(None, config).await.unwrap();
157 /// async {
158 /// let _ = session_store.cleanup().await.unwrap();
159 /// };
160 /// ```
161 ///
162 #[inline]
163 pub async fn cleanup(&self) -> Result<Vec<String>, SessionError> {
164 if let Some(client) = &self.client {
165 Ok(client
166 .delete_by_expiry(&self.config.database.table_name)
167 .await?)
168 } else {
169 Ok(Vec::new())
170 }
171 }
172
173 /// Returns count of existing sessions within database.
174 ///
175 /// If client is None it will return Ok(0).
176 ///
177 /// # Errors
178 /// - ['SessionError::Sqlx'] is returned if database connection has failed or user does not have permissions.
179 ///
180 /// # Examples
181 /// ```rust ignore
182 /// use axum_session::{SessionNullPool, SessionConfig, SessionStore};
183 ///
184 /// let config = SessionConfig::default();
185 /// let session_store = SessionStore::<SessionNullPool>::new(None, config).await.unwrap();
186 /// async {
187 /// let count = session_store.count().await.unwrap();
188 /// };
189 /// ```
190 ///
191 #[inline]
192 pub async fn count(&self) -> Result<i64, SessionError> {
193 if let Some(client) = &self.client {
194 let count = client.count(&self.config.database.table_name).await?;
195 return Ok(count);
196 }
197
198 Ok(0)
199 }
200
201 /// private internal function that loads a session's data from the database using an ID string.
202 ///
203 /// If client is None it will return Ok(None).
204 ///
205 /// # Errors
206 /// - ['SessionError::Sqlx'] is returned if database connection has failed or user does not have permissions.
207 /// - ['SessionError::SerdeJson'] is returned if it failed to deserialize the sessions data.
208 ///
209 /// # Examples
210 /// ```rust ignore
211 /// use axum_session::{SessionNullPool, SessionConfig, SessionStore};
212 /// use uuid::Uuid;
213 ///
214 /// let config = SessionConfig::default();
215 /// let session_store = SessionStore::<SessionNullPool>::new(None, config).await.unwrap();
216 /// let token = Uuid::new_v4();
217 /// async {
218 /// let session_data = session_store.load_session(token.to_string()).await.unwrap();
219 /// };
220 /// ```
221 ///
222 pub(crate) async fn load_session(
223 &self,
224 cookie_value: String,
225 ) -> Result<Option<SessionData>, SessionError> {
226 if let Some(client) = &self.client {
227 let result: Option<String> = client
228 .load(&cookie_value, &self.config.database.table_name)
229 .await?;
230
231 if let Some(mut session) = result
232 .map(|session| {
233 if let Some(key) = self.config.database.database_key.as_ref() {
234 serde_json::from_str::<SessionData>(
235 &match encrypt::decrypt(&cookie_value, &session, key) {
236 Ok(v) => v,
237 Err(err) => {
238 tracing::error!(err = %err, "Failed to decrypt Session data from database.");
239 String::new()
240 }
241 }
242 )
243 } else {
244 serde_json::from_str::<SessionData>(&session)
245 }
246 })
247 .transpose()?
248 {
249 session.id = cookie_value;
250 return Ok(Some(session));
251 }
252 }
253
254 Ok(None)
255 }
256
257 /// private internal function that stores a session's data to the database.
258 ///
259 /// If client is None it will return Ok(()).
260 ///
261 /// # Errors
262 /// - ['SessionError::Sqlx'] is returned if database connection has failed or user does not have permissions.
263 /// - ['SessionError::SerdeJson'] is returned if it failed to serialize the sessions data.
264 ///
265 /// # Examples
266 /// ```rust ignore
267 /// use axum_session::{SessionNullPool, SessionConfig, SessionStore, SessionData};
268 /// use uuid::Uuid;
269 ///
270 /// let config = SessionConfig::default();
271 /// let session_store = SessionStore::<SessionNullPool>::new(None, config.clone()).await.unwrap();
272 /// let token = Uuid::new_v4();
273 /// let session_data = SessionData::new(token, true, &config);
274 ///
275 /// async {
276 /// let _ = session_store.store_session(&session_data).await.unwrap();
277 /// };
278 /// ```
279 ///
280 pub(crate) async fn store_session(&self, session: &SessionData) -> Result<(), SessionError> {
281 if let Some(client) = &self.client {
282 client
283 .store(
284 &session.id,
285 &if let Some(key) = self.config.database.database_key.as_ref() {
286 encrypt::encrypt(&session.id, &serde_json::to_string(session)?, key)
287 .map_err(|e| {
288 SessionError::GenericNotSupportedError(format!(
289 "Error: {e} Occurred when encrypting a Session.",
290 ))
291 })?
292 } else {
293 serde_json::to_string(session)?
294 },
295 session.expires.timestamp(),
296 &self.config.database.table_name,
297 )
298 .await?;
299 }
300
301 Ok(())
302 }
303
304 /// Deletes all sessions in the database.
305 ///
306 /// If client is None it will return Ok(()).
307 ///
308 /// # Errors
309 /// - ['SessionError::Sqlx'] is returned if database connection has failed or user does not have permissions.
310 ///
311 /// # Examples
312 /// ```rust ignore
313 /// use axum_session::{SessionNullPool, SessionConfig, SessionStore};
314 ///
315 /// let config = SessionConfig::default();
316 /// let session_store = SessionStore::<SessionNullPool>::new(None, config.clone()).await.unwrap();
317 ///
318 /// async {
319 /// let _ = session_store.clear_store().await.unwrap();
320 /// };
321 /// ```
322 ///
323 #[inline]
324 pub async fn clear_store(&self) -> Result<(), SessionError> {
325 if let Some(client) = &self.client {
326 client.delete_all(&self.config.database.table_name).await?;
327 }
328
329 Ok(())
330 }
331
332 /// Deletes all sessions in Memory.
333 /// This will also Clear those keys from the filter cache if a persistent database does not exist.
334 ///
335 /// # Examples
336 /// ```rust ignore
337 /// use axum_session::{SessionNullPool, SessionConfig, SessionStore};
338 ///
339 /// let config = SessionConfig::default();
340 /// let session_store = SessionStore::<SessionNullPool>::new(None, config.clone()).await.unwrap();
341 ///
342 /// async {
343 /// let _ = session_store.clear().await.unwrap();
344 /// };
345 /// ```
346 ///
347 #[inline]
348 pub async fn clear(&mut self) {
349 #[cfg(feature = "key-store")]
350 if self.client.is_none() {
351 let mut filter = self.filter.write().await;
352 self.inner
353 .iter()
354 .for_each(|value| filter.remove(value.key().as_bytes()));
355 }
356
357 self.inner.clear();
358 }
359
360 /// Attempts to load check and clear Data.
361 ///
362 /// If no session is found returns false.
363 pub(crate) fn service_session_data(&self, session: &Session<T>) -> bool {
364 if let Some(mut inner) = self.inner.get_mut(&session.id) {
365 inner.service_clear(
366 self.config.memory.memory_lifespan,
367 self.config.clear_check_on_load,
368 );
369 inner.set_request();
370 return true;
371 }
372
373 false
374 }
375
376 #[inline]
377 pub(crate) fn renew(&self, id: String) {
378 if let Some(mut instance) = self.inner.get_mut(&id) {
379 instance.renew();
380 } else {
381 tracing::warn!("Session data unexpectedly missing");
382 }
383 }
384
385 #[inline]
386 pub(crate) fn destroy(&self, id: String) {
387 if let Some(mut instance) = self.inner.get_mut(&id) {
388 instance.destroy();
389 } else {
390 tracing::warn!("Session data unexpectedly missing");
391 }
392 }
393
394 #[inline]
395 pub(crate) fn set_longterm(&self, id: String, longterm: bool) {
396 if let Some(mut instance) = self.inner.get_mut(&id) {
397 instance.set_longterm(longterm);
398 } else {
399 tracing::warn!("Session data unexpectedly missing");
400 }
401 }
402
403 #[inline]
404 pub(crate) fn set_store(&self, id: String, storable: bool) {
405 if let Some(mut instance) = self.inner.get_mut(&id) {
406 instance.set_store(storable);
407 } else {
408 tracing::warn!("Session data unexpectedly missing");
409 }
410 }
411
412 #[inline]
413 pub(crate) fn update(&self, id: String) {
414 if let Some(mut instance) = self.inner.get_mut(&id) {
415 instance.update();
416 } else {
417 tracing::warn!("Session data unexpectedly missing");
418 }
419 }
420
421 #[inline]
422 pub(crate) fn get<N: serde::de::DeserializeOwned>(&self, id: String, key: &str) -> Option<N> {
423 if let Some(instance) = self.inner.get(&id) {
424 instance.get(key)
425 } else {
426 tracing::warn!("Session data unexpectedly missing");
427 None
428 }
429 }
430
431 #[inline]
432 pub(crate) fn get_remove<N: serde::de::DeserializeOwned>(
433 &self,
434 id: String,
435 key: &str,
436 ) -> Option<N> {
437 if let Some(mut instance) = self.inner.get_mut(&id) {
438 instance.get_remove(key)
439 } else {
440 tracing::warn!("Session data unexpectedly missing");
441 None
442 }
443 }
444
445 #[inline]
446 pub(crate) fn set(&self, id: String, key: &str, value: impl Serialize) {
447 if let Some(mut instance) = self.inner.get_mut(&id) {
448 instance.set(key, value);
449 } else {
450 tracing::warn!("Session data unexpectedly missing");
451 }
452 }
453
454 #[inline]
455 pub(crate) fn remove(&self, id: String, key: &str) {
456 if let Some(mut instance) = self.inner.get_mut(&id) {
457 instance.remove(key);
458 } else {
459 tracing::warn!("Session data unexpectedly missing");
460 }
461 }
462
463 #[inline]
464 pub(crate) fn clear_session_data(&self, id: String) {
465 if let Some(mut instance) = self.inner.get_mut(&id) {
466 instance.clear();
467 } else {
468 tracing::warn!("Session data unexpectedly missing");
469 }
470 }
471
472 #[inline]
473 pub(crate) fn set_session_request(&self, id: String) {
474 if let Some(mut instance) = self.inner.get_mut(&id) {
475 instance.set_request();
476 } else {
477 tracing::warn!("Session data unexpectedly missing");
478 }
479 }
480
481 #[inline]
482 pub(crate) fn remove_session_request(&self, id: String) {
483 if let Some(mut instance) = self.inner.get_mut(&id) {
484 instance.remove_request();
485 } else {
486 tracing::warn!("Session data unexpectedly missing");
487 }
488 }
489
490 #[inline]
491 pub(crate) fn is_session_parallel(&self, id: String) -> bool {
492 if let Some(instance) = self.inner.get(&id) {
493 instance.is_parallel()
494 } else {
495 tracing::warn!("Session data unexpectedly missing");
496 false
497 }
498 }
499
500 #[inline]
501 pub(crate) async fn count_sessions(&self) -> i64 {
502 if self.is_persistent() {
503 self.count().await.unwrap_or(0i64)
504 } else {
505 self.inner.len() as i64
506 }
507 }
508
509 #[inline]
510 pub(crate) fn auto_handles_expiry(&self) -> bool {
511 if let Some(client) = &self.client {
512 client.auto_handles_expiry()
513 } else {
514 false
515 }
516 }
517
518 #[cfg(feature = "advanced")]
519 #[cfg_attr(docsrs, doc(cfg(feature = "advanced")))]
520 #[inline]
521 pub fn verify(&self, id: String) -> Result<(), SessionError> {
522 if let Some(instance) = self.inner.get(&id) {
523 if instance.expires < Utc::now() {
524 Err(SessionError::OldSessionError)
525 } else {
526 Ok(())
527 }
528 } else {
529 Err(SessionError::NoSessionError)
530 }
531 }
532
533 #[cfg(feature = "advanced")]
534 #[cfg_attr(docsrs, doc(cfg(feature = "advanced")))]
535 #[inline]
536 pub fn update_database_expires(&self, id: String) -> Result<(), SessionError> {
537 if let Some(mut instance) = self.inner.get_mut(&id) {
538 if instance.longterm {
539 instance.expires = Utc::now() + self.config.max_lifespan;
540 } else {
541 instance.expires = Utc::now() + self.config.lifespan;
542 }
543
544 Ok(())
545 } else {
546 Err(SessionError::NoSessionError)
547 }
548 }
549
550 #[cfg(feature = "advanced")]
551 #[cfg_attr(docsrs, doc(cfg(feature = "advanced")))]
552 #[inline]
553 pub fn update_memory_expires(&self, id: String) -> Result<(), SessionError> {
554 if let Some(mut instance) = self.inner.get_mut(&id) {
555 instance.autoremove = Utc::now() + self.config.memory.memory_lifespan;
556
557 Ok(())
558 } else {
559 Err(SessionError::NoSessionError)
560 }
561 }
562
563 #[cfg(feature = "advanced")]
564 #[cfg_attr(docsrs, doc(cfg(feature = "advanced")))]
565 #[inline]
566 pub async fn force_database_update(&self, id: String) -> Result<(), SessionError> {
567 let session = if let Some(instance) = self.inner.get(&id) {
568 instance.clone()
569 } else {
570 return Err(SessionError::NoSessionError);
571 };
572
573 self.store_session(&session).await
574 }
575
576 #[cfg(feature = "advanced")]
577 #[cfg_attr(docsrs, doc(cfg(feature = "advanced")))]
578 #[inline]
579 pub fn memory_remove_session(&self, id: String) -> Result<(), SessionError> {
580 let is_parallel = if let Some(mut instance) = self.inner.get_mut(&id) {
581 instance.remove_request();
582 instance.is_parallel()
583 } else {
584 return Err(SessionError::NoSessionError);
585 };
586
587 if is_parallel {
588 let _ = self.inner.remove(&id);
589 }
590
591 Ok(())
592 }
593
594 #[cfg(feature = "advanced")]
595 #[cfg_attr(docsrs, doc(cfg(feature = "advanced")))]
596 #[inline]
597 pub fn force_memory_remove_session(&self, id: String) -> Result<(), SessionError> {
598 if self.inner.remove(&id).is_some() {
599 Ok(())
600 } else {
601 Err(SessionError::NoSessionError)
602 }
603 }
604
605 #[inline]
606 pub(crate) async fn database_remove_session(&self, id: String) -> Result<(), SessionError> {
607 if let Some(client) = &self.client {
608 client
609 .delete_one_by_id(&id, &self.config.database.table_name)
610 .await?;
611 }
612
613 Ok(())
614 }
615
616 #[cfg(feature = "advanced")]
617 #[cfg_attr(docsrs, doc(cfg(feature = "advanced")))]
618 #[inline]
619 pub async fn remove_stored_database_session(&self, id: String) -> Result<(), SessionError> {
620 self.database_remove_session(id).await
621 }
622}
Added lib/axum-session/Cargo.toml View lib/axum-session/Cargo.toml
@@ -0,0 +1,143 @@
1# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
2#
3# When uploading crates to the registry Cargo will automatically
4# "normalize" Cargo.toml files for maximal compatibility
5# with all versions of Cargo and also rewrite `path` dependencies
6# to registry (e.g., crates.io) dependencies.
7#
8# If you are reading this file be aware that the original Cargo.toml
9# will likely look very different (and much more reasonable).
10# See Cargo.toml.orig for the original contents.
11
12[package]
13edition = "2021"
14rust-version = "1.78"
15name = "axum_session"
16version = "0.21.0"
17publish = false
18authors = ["Andrew Wheeler <genusistimelord@gmail.com>"]
19build = false
20autolib = false
21autobins = false
22autoexamples = false
23autotests = false
24autobenches = false
25description = "📝 Session management layer for axum that supports HTTP and Rest."
26documentation = "https://docs.rs/axum_session"
27readme = "README.md"
28keywords = [
29 "Axum",
30 "Tower",
31 "Session",
32 "Store",
33]
34license = "MIT OR Apache-2.0"
35repository = "https://github.com/AscendingCreations/AxumSession"
36
37[package.metadata.docs.rs]
38features = [
39 "key-store",
40 "advanced",
41]
42rustdoc-args = [
43 "--document-private-items",
44 "--cfg",
45 "docsrs",
46]
47
48[features]
49advanced = []
50key-store = ["fastbloom-rs"]
51rest_mode = []
52
53[lib]
54name = "axum_session"
55path = "src/lib.rs"
56
57[dependencies.aes-gcm]
58version = "0.11.0"
59
60[dependencies.async-trait]
61version = "0.1.92"
62
63[dependencies.axum]
64version = "0.8.9"
65
66[dependencies.base64]
67version = "0.23.1"
68
69[dependencies.bytes]
70version = "1.12.1"
71
72[dependencies.chrono]
73version = "0.4.45"
74features = [
75 "clock",
76 "serde",
77]
78default-features = false
79
80[dependencies.cookie]
81version = "0.18.2"
82features = [
83 "percent-encode",
84 "private",
85]
86
87[dependencies.dashmap]
88version = "6.2.1"
89
90[dependencies.fastbloom-rs]
91version = "0.5.10"
92optional = true
93
94[dependencies.forwarded-header-value]
95version = "0.1.1"
96
97[dependencies.futures]
98version = "0.3.33"
99
100[dependencies.hmac]
101version = "0.13.0"
102
103[dependencies.http]
104version = "1.5.0"
105
106[dependencies.http-body]
107version = "1.1.0"
108
109[dependencies.rand]
110version = "0.10.2"
111
112[dependencies.serde]
113version = "1.0.229"
114features = ["derive"]
115
116[dependencies.serde_json]
117version = "1.0.151"
118
119[dependencies.sha2]
120version = "0.11.0"
121
122[dependencies.thiserror]
123version = "2.0.20"
124
125[dependencies.tokio]
126version = "1.53.1"
127features = ["full"]
128
129[dependencies.tower-layer]
130version = "0.3.3"
131
132[dependencies.tower-service]
133version = "0.3.3"
134
135[dependencies.tracing]
136version = "0.1.44"
137
138[dependencies.uuid]
139version = "1.24.0"
140features = [
141 "serde",
142 "v4",
143]
Added lib/axum-session/LICENSE View lib/axum-session/LICENSE
@@ -0,0 +1,21 @@
1MIT License
2
3Copyright (c) 2023 Ascending Creations
4
5Permission is hereby granted, free of charge, to any person obtaining a copy
6of this software and associated documentation files (the "Software"), to deal
7in the Software without restriction, including without limitation the rights
8to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9copies of the Software, and to permit persons to whom the Software is
10furnished to do so, subject to the following conditions:
11
12The above copyright notice and this permission notice shall be included in all
13copies or substantial portions of the Software.
14
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21SOFTWARE.
Added lib/axum-session/README.md View lib/axum-session/README.md
@@ -0,0 +1,137 @@
1<h1 align="center">
2Axum Session
3</h1>
4
5[![https://crates.io/crates/axum_session](https://img.shields.io/crates/v/axum_session?style=plastic)](https://crates.io/crates/axum_session)
6[![Docs](https://docs.rs/axum_session/badge.svg)](https://docs.rs/axum_session)
7[![Discord Server](https://img.shields.io/discord/81844480201728000?label=&labelColor=6A7EC2&logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/gVXNDwpS3Z)
8
9## 📑 Overview
10
11<p align="center">
12`axum_session` provide's a Session management middleware that stores all session data within a MemoryStore internally.
13Optionally it can save data to a persistent database for long term storage.
14Uses Cookie or Header stored UUID's to sync back to the session store.
15</p>
16
17- Cookies or Header Store of Generated Session UUID and a Store Boolean.
18- Uses a DatabasePool Trait so you can implement your own Sub Storage Layer.
19- Convenient API for `Session` no need to mark as Read or Write making Usage Easier.
20- Uses `dashmap` for internal memory lookup and storage to achieve high throughput.
21- Uses Serde for Data Serialization so it can store any Serde supported types into the Sessions data.
22- Supports Redis, SurrealDB, MongoDB and SQLx optional Databases out of the Box.
23- Supports Memory Only usage. No need to use a persistent database.
24- Supports Cookie and Header Signing for integrity, and authenticity.
25- Supports Database Session Data Encryption for confidentiality, integrity.
26- Supports SessionID renewal for enhanced Security.
27- Optional Fastbloom key storage for reduced Database lookups during new UUID generation. Boosting Bandwidth.
28- Optional Rest Mode that Disables Cookies and uses the Header values instead.
29- uses `#![forbid(unsafe_code)]` to ensure everything is implemented as safe rust.
30- has an `advanced` API to allow further control of a session.
31- uses IP address's and user agent to deter spoofing of signed cookies and headers.
32
33## 🚨 Help
34
35If you need help with this library or have suggestions please go to our [Discord Group](https://discord.gg/gVXNDwpS3Z)
36
37## 📦 Install
38
39Axum Session uses [`tokio`].
40to your cargo include for Axum Session.
41
42[`tokio`]: https://github.com/tokio-rs/tokio
43
44```toml
45# Cargo.toml
46[dependencies]
47# Postgres + rustls
48axum_session = { version = "0.19.0" }
49```
50
51## 📱 Cargo Feature Flags
52
53| Features | Description |
54| ----------------------------- | -------------------------------------------------------------------------------------------------- |
55| `advanced` | Enables functions that provide more control over sessions. |
56| `rest_mode` | Disables cookie handling and instead only uses a header. For rest API requests and responses. |
57| `key-store` | Enables optional in-process key storage. This increases RAM usage depending on Fastbloom settings. |
58
59
60| Database Crate | Persistent | Description |
61| ----------------------------------------------------------------------------------- | ---------- | ----------------------------------------------------------- |
62| [`axum_session_sqlx`](https://crates.io/crates/axum_session_sqlx) | Yes | Sqlx session store |
63| [`axum_session_surreal`](https://crates.io/crates/axum_session_surreal) | Yes | Surreal session store |
64| [`axum_session_mongo`](https://crates.io/crates/axum_session_mongo) | Yes | Mongo session store |
65| [`axum_session_redispool`](https://crates.io/crates/axum_session_redispool) | Yes | RedisPool session store |
66
67## 🔎 Example Default Setup
68
69You can find examples within the [`Repository`](https://github.com/AscendingCreations/AxumSession/tree/main/examples)
70
71```rust ignore
72use sqlx::{ConnectOptions, postgres::{PgPoolOptions, PgConnectOptions}};
73use std::net::SocketAddr;
74use axum_session::{Session, SessionPgPool, SessionConfig, SessionStore, SessionLayer};
75use axum::{
76 Router,
77 routing::get,
78};
79use tokio::net::TcpListener;
80
81#[tokio::main]
82async fn main() {
83
84 let pool = connect_to_database().await.unwrap();
85
86 //This Defaults as normal Cookies.
87 //To enable signed cookies for integrity, and authenticity please check the enable_signed_cookies_headers Example.
88 let session_config = SessionConfig::default()
89 .with_table_name("sessions_table");
90
91 // create SessionStore and initiate the database tables
92 let session_store = SessionStore::<SessionPgPool>::new(Some(pool.clone().into()), session_config).await.unwrap();
93
94 // build our application with some routes
95 let app = Router::new()
96 .route("/greet", get(greet))
97 .layer(SessionLayer::new(session_store));
98
99 // run it
100 let addr = SocketAddr::from(([0, 0, 0, 0], 8000));
101
102 println!("listening on {}", addr);
103 let listener = TcpListener::bind(addr).await.unwrap();
104 axum::serve(listener, app).await.unwrap();
105}
106
107async fn greet(session: Session<SessionPgPool>) -> String {
108 let mut count: usize = session.get("count").unwrap_or(0);
109
110 count += 1;
111 session.set("count", count);
112
113 count.to_string()
114}
115
116async fn connect_to_database() -> anyhow::Result<sqlx::Pool<sqlx::Postgres>> {
117 // ...
118 unimplemented!()
119}
120```
121
122## 🔑 Key Store Details
123
124To enable and use fastbloom key storage for less database lookups.
125Add the feature `"key-store"` to the crate’s features. This feature will increase the ram usage server side.
126but will heavily improve the bandwidth limitations and reduce latency of returns from the server.
127This is based on how much the `filter_expected_elements` and `filter_false_positive_probability` are set too.
128The higher they are the more ram is used. You will also need to Enable the bloom filter in the config for it to be used. By default,
129the `use_bloom_filters` is enabled and these config options exist whither or not the feature is enabled.
130Please refer to `with_filter_expected_elements` and `with_filter_false_positive_probability` within the documents to set the options.
131Otherwise stick with the default settings which should work in most situations. Just do note these options provide on how many False positives
132could possibly occur when comparing a UUID to what currently exists, which means it will keep trying till it finds none that match.
133Higher values decrease the chance of a false positive but increase ram usage.
134
135## 😎 Session Login and Authentication via `axum_session_auth`
136
137For user login, login caching and authentication please see [`axum_session_auth`](https://github.com/AscendingCreations/AxumSessionAuth).
Modified Cargo.lock View Cargo.lock
@@ -825,8 +825,6 @@
825825[[package]]
826826name = "axum_session"
827827version = "0.21.0"
828source = "registry+https://github.com/rust-lang/crates.io-index"
829checksum = "337130e61b1166b57510a481c76f585f08d9b78449c754c4d1d4155f56823ecc"
830828dependencies = [
831829 "aes-gcm 0.11.0",
832830 "async-trait",
Modified Cargo.toml View Cargo.toml
@@ -48,7 +48,7 @@
4848serde = "1.0.229"
4949serde_json = "1.0.151"
5050axum = { version = "0.8.9", features = ["ws"] }
51axum_session = { version = "0.21.0", features = ["key-store"] }
51axum_session = { path = "lib/axum-session", features = ["key-store"] }
5252tower = "0.5.3"
5353tower-http = { version = "0.7.0", features = ["trace", "limit", "cors"] }
5454axum-otel-metrics = "0.14.1"