Skip to main content

salvo_session/
lib.rs

1#![cfg_attr(test, allow(clippy::unwrap_used))]
2//! # Salvo Session Support
3//!
4//! Salvo's session middleware is built on top of
5//! [`saysion`](https://github.com/salvo-rs/saysion).
6//!
7//! See a complete example: [`session-login`](https://github.com/salvo-rs/salvo/tree/main/examples/session-login)
8//!
9//! Sessions allow Salvo applications to securely attach data to browser sessions,
10//! enabling retrieval and modification of this data on subsequent visits.
11//! Session data is typically retained only for the duration of a browser session.
12//!
13//! ## Stores
14//!
15//! It is highly recommended to use an external-datastore-backed session storage
16//! for production Salvo applications. For a list of currently available session
17//! stores, see [the documentation for saysion](https://github.com/salvo-rs/saysion).
18//!
19//! ## Security
20//!
21//! While each session store may have different security implications,
22//! Salvo's session system works as follows:
23//!
24//! On each request, Salvo checks for the cookie specified by `cookie_name`
25//! in the handler configuration.
26//!
27//! ### When no cookie is found:
28//!
29//! 1. A cryptographically random cookie value is generated
30//! 2. A cookie is set on the outbound response and signed with an HKDF key derived from the
31//!    `secret` provided when creating the SessionHandler
32//! 3. The session store uses a SHA256 digest of the cookie value to store the session along with an
33//!    optional expiry time
34//!
35//! ### When a cookie is found:
36//!
37//! 1. The HKDF-derived signing key verifies the cookie value's signature
38//! 2. If verification succeeds, the value is passed to the session store to retrieve the associated
39//!    Session
40//! 3. For most session stores, this involves taking a SHA256 digest of the cookie value and
41//!    retrieving a serialized Session from an external datastore
42//!
43//! ### Expiry Handling
44//!
45//! Sessions include expiry information in both the cookie and the serialization format.
46//! Even if an adversary tampers with a cookie's expiry, Salvo validates
47//! the expiry on the contained session before using it.
48//!
49//! ### Error Handling
50//!
51//! If any failures occur during session retrieval, a new empty session
52//! is generated for the request, which proceeds through the application normally.
53//!
54//! ## Stale/Expired Session Cleanup
55//!
56//! Any session store (except the cookie store) will accumulate stale sessions over time.
57//! Although Salvo ensures expired sessions won't be used, it remains the
58//! application's responsibility to periodically call cleanup on the session
59//! store if required.
60//!
61//! Read more: <https://salvo.rs>
62#![doc(html_favicon_url = "https://salvo.rs/favicon-32x32.png")]
63#![doc(html_logo_url = "https://salvo.rs/images/logo.svg")]
64#![cfg_attr(docsrs, feature(doc_cfg))]
65
66use std::fmt::{self, Formatter};
67use std::time::Duration;
68
69use cookie::{Cookie, Key, KeyError, SameSite};
70use salvo_core::http::SecureCookiePolicy;
71use salvo_core::{Depot, Error, FlowCtrl, Handler, Request, Response, async_trait};
72use saysion::base64::Engine as _;
73use saysion::base64::engine::general_purpose;
74use saysion::hmac::{Hmac, Mac};
75use saysion::sha2::Sha256;
76pub use saysion::{CookieStore, MemoryStore, Session, SessionStore};
77
78/// Key for store data in depot.
79pub const SESSION_KEY: &str = "::salvo::session";
80const BASE64_DIGEST_LEN: usize = 44;
81
82/// Trait for `Depot` to get and set session.
83pub trait SessionDepotExt {
84    /// Sets session
85    fn set_session(&mut self, session: Session) -> &mut Self;
86    /// Take session
87    fn take_session(&mut self) -> Option<Session>;
88    /// Get session reference
89    fn session(&self) -> Option<&Session>;
90    /// Get session mutable reference
91    fn session_mut(&mut self) -> Option<&mut Session>;
92}
93
94impl SessionDepotExt for Depot {
95    #[inline]
96    fn set_session(&mut self, session: Session) -> &mut Self {
97        self.insert(SESSION_KEY, session);
98        self
99    }
100    #[inline]
101    fn take_session(&mut self) -> Option<Session> {
102        self.remove(SESSION_KEY)
103            .and_then(|v| v.downcast::<Session>().ok())
104            .map(|v| *v)
105    }
106    #[inline]
107    fn session(&self) -> Option<&Session> {
108        self.get(SESSION_KEY).ok()
109    }
110    #[inline]
111    fn session_mut(&mut self) -> Option<&mut Session> {
112        self.get_mut(SESSION_KEY).ok()
113    }
114}
115
116/// `HandlerBuilder` is a builder for [`SessionHandler`].
117pub struct HandlerBuilder<S> {
118    store: S,
119    cookie_path: String,
120    cookie_name: String,
121    cookie_domain: Option<String>,
122    session_ttl: Option<Duration>,
123    save_unchanged: bool,
124    same_site_policy: SameSite,
125    secure_cookie_policy: SecureCookiePolicy,
126    key: Key,
127    fallback_keys: Vec<Key>,
128}
129impl<S> fmt::Debug for HandlerBuilder<S>
130where
131    S: SessionStore + fmt::Debug,
132{
133    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
134        f.debug_struct("HandlerBuilder")
135            .field("store", &self.store)
136            .field("cookie_path", &self.cookie_path)
137            .field("cookie_name", &self.cookie_name)
138            .field("cookie_domain", &self.cookie_domain)
139            .field("session_ttl", &self.session_ttl)
140            .field("same_site_policy", &self.same_site_policy)
141            .field("secure_cookie_policy", &self.secure_cookie_policy)
142            .field("key", &"..")
143            .field("fallback_keys", &"..")
144            .field("save_unchanged", &self.save_unchanged)
145            .finish()
146    }
147}
148
149impl<S> HandlerBuilder<S>
150where
151    S: SessionStore,
152{
153    /// Creates a new `HandlerBuilder`.
154    ///
155    /// # Security Note
156    ///
157    /// The `secret` must be at least 64 bytes (512 bits) for adequate security. Will panic on keys
158    /// shorter than 64 bytes. For non-panicking version, use [`HandlerBuilder::try_new`].
159    ///
160    /// **Example of generating a secure key:**
161    /// ```ignore
162    /// use rand::RngCore;
163    /// let mut key = [0u8; 64];
164    /// rand::rngs::OsRng.fill_bytes(&mut key);
165    /// ```
166    #[inline]
167    #[must_use]
168    pub fn new(store: S, secret: &[u8]) -> Self {
169        Self::try_new(store, secret).expect("secret key must be at least 64 bytes")
170    }
171
172    /// Try creating new `HandlerBuilder`
173    ///
174    /// # Security Note
175    ///
176    /// The `secret` must be at least 64 bytes (512 bits) for adequate security.
177    ///
178    /// **Example of generating a secure key:**
179    /// ```ignore
180    /// use rand::RngCore;
181    /// let mut key = [0u8; 64];
182    /// rand::rngs::OsRng.fill_bytes(&mut key);
183    /// ```
184    #[inline]
185    pub fn try_new(store: S, secret: &[u8]) -> Result<Self, KeyError> {
186        let key = Key::try_from(secret)?;
187        Ok(Self {
188            store,
189            save_unchanged: true,
190            cookie_path: "/".into(),
191            cookie_name: "salvo.session.id".into(),
192            cookie_domain: None,
193            same_site_policy: SameSite::Lax,
194            secure_cookie_policy: SecureCookiePolicy::AutoFromScheme,
195            session_ttl: Some(Duration::from_secs(24 * 60 * 60)),
196            key,
197            fallback_keys: vec![],
198        })
199    }
200
201    /// Sets a cookie path for this session middleware.
202    ///
203    /// The default for this value is "/".
204    #[inline]
205    #[must_use]
206    pub fn cookie_path(mut self, cookie_path: impl Into<String>) -> Self {
207        self.cookie_path = cookie_path.into();
208        self
209    }
210
211    /// Sets a session ttl. This will be used both for the cookie
212    /// expiry and also for the session-internal expiry.
213    ///
214    /// The default for this value is one day. Set this to None to not
215    /// set a cookie or session expiry. This is not recommended.
216    #[inline]
217    #[must_use]
218    pub fn session_ttl(mut self, session_ttl: Option<Duration>) -> Self {
219        self.session_ttl = session_ttl;
220        self
221    }
222
223    /// Sets the name of the cookie that the session is stored with or in.
224    ///
225    /// If you are running multiple Salvo applications on the same
226    /// domain, you will need different values for each
227    /// application. The default value is `"salvo.session.id"`.
228    #[inline]
229    #[must_use]
230    pub fn cookie_name(mut self, cookie_name: impl Into<String>) -> Self {
231        self.cookie_name = cookie_name.into();
232        self
233    }
234
235    /// Sets the `save_unchanged` value.
236    ///
237    /// When `save_unchanged` is enabled, a session cookie will always be set.
238    ///
239    /// With `save_unchanged` disabled, the session data must be modified
240    /// from the `Default` value in order for it to save. If a session
241    /// already exists and its data unmodified in the course of a
242    /// request, the session will only be persisted if
243    /// `save_unchanged` is enabled.
244    #[inline]
245    #[must_use]
246    pub fn save_unchanged(mut self, value: bool) -> Self {
247        self.save_unchanged = value;
248        self
249    }
250
251    /// Sets the same site policy for the session cookie. Defaults to
252    /// SameSite::Lax. See [incrementally better
253    /// cookies](https://tools.ietf.org/html/draft-west-cookie-incrementalism-01)
254    /// for more information about this setting.
255    #[inline]
256    #[must_use]
257    pub fn same_site_policy(mut self, policy: SameSite) -> Self {
258        self.same_site_policy = policy;
259        self
260    }
261
262    /// Forces the `Secure` attribute for session cookies.
263    ///
264    /// By default this is detected from the request URI scheme. Use this option in
265    /// production when TLS is terminated before the Salvo application.
266    #[inline]
267    #[must_use]
268    pub fn secure_cookie(mut self, secure: bool) -> Self {
269        self.secure_cookie_policy = SecureCookiePolicy::from_bool(secure);
270        self
271    }
272
273    /// Sets the policy used to decide whether session cookies include `Secure`.
274    ///
275    /// The default policy is [`SecureCookiePolicy::AutoFromScheme`]. `SameSite::None`
276    /// session cookies are always sent with `Secure`.
277    #[inline]
278    #[must_use]
279    pub fn secure_cookie_policy(mut self, policy: SecureCookiePolicy) -> Self {
280        self.secure_cookie_policy = policy;
281        self
282    }
283
284    /// Sets the domain of the cookie.
285    #[inline]
286    #[must_use]
287    pub fn cookie_domain(mut self, cookie_domain: impl AsRef<str>) -> Self {
288        self.cookie_domain = Some(cookie_domain.as_ref().to_owned());
289        self
290    }
291    /// Sets fallbacks.
292    #[inline]
293    #[must_use]
294    pub fn fallback_keys(mut self, keys: Vec<impl Into<Key>>) -> Self {
295        self.fallback_keys = keys.into_iter().map(|s| s.into()).collect();
296        self
297    }
298
299    /// Add fallback secret.
300    #[inline]
301    #[must_use]
302    pub fn add_fallback_key(mut self, key: impl Into<Key>) -> Self {
303        self.fallback_keys.push(key.into());
304        self
305    }
306
307    /// Build `SessionHandler`
308    pub fn build(self) -> Result<SessionHandler<S>, Error> {
309        let Self {
310            store,
311            save_unchanged,
312            cookie_path,
313            cookie_name,
314            cookie_domain,
315            session_ttl,
316            same_site_policy,
317            secure_cookie_policy,
318            key,
319            fallback_keys,
320        } = self;
321        let hmac = Hmac::<Sha256>::new_from_slice(key.signing())
322            .map_err(|_| Error::Other("invalid key length".into()))?;
323        let fallback_hmacs = fallback_keys
324            .iter()
325            .map(|key| Hmac::<Sha256>::new_from_slice(key.signing()))
326            .collect::<Result<Vec<_>, _>>()
327            .map_err(|_| Error::Other("invalid key length".into()))?;
328        Ok(SessionHandler {
329            store,
330            save_unchanged,
331            cookie_path,
332            cookie_name,
333            cookie_domain,
334            session_ttl,
335            same_site_policy,
336            secure_cookie_policy,
337            hmac,
338            fallback_hmacs,
339        })
340    }
341}
342
343/// `SessionHandler` is a middleware for session.
344pub struct SessionHandler<S> {
345    store: S,
346    cookie_path: String,
347    cookie_name: String,
348    cookie_domain: Option<String>,
349    session_ttl: Option<Duration>,
350    save_unchanged: bool,
351    same_site_policy: SameSite,
352    secure_cookie_policy: SecureCookiePolicy,
353    hmac: Hmac<Sha256>,
354    fallback_hmacs: Vec<Hmac<Sha256>>,
355}
356impl<S> fmt::Debug for SessionHandler<S>
357where
358    S: SessionStore + fmt::Debug,
359{
360    #[inline]
361    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
362        f.debug_struct("SessionHandler")
363            .field("store", &self.store)
364            .field("cookie_path", &self.cookie_path)
365            .field("cookie_name", &self.cookie_name)
366            .field("cookie_domain", &self.cookie_domain)
367            .field("session_ttl", &self.session_ttl)
368            .field("same_site_policy", &self.same_site_policy)
369            .field("secure_cookie_policy", &self.secure_cookie_policy)
370            .field("key", &"..")
371            .field("fallback_keys", &"..")
372            .field("save_unchanged", &self.save_unchanged)
373            .finish()
374    }
375}
376#[async_trait]
377impl<S> Handler for SessionHandler<S>
378where
379    S: SessionStore + Send + Sync + 'static,
380{
381    async fn handle(
382        &self,
383        req: &mut Request,
384        depot: &mut Depot,
385        res: &mut Response,
386        ctrl: &mut FlowCtrl,
387    ) {
388        let cookie = req.cookies().get(&self.cookie_name);
389        let cookie_value = cookie.and_then(|cookie| self.verify_signature(cookie.value()).ok());
390
391        let mut session = self.load_or_create(cookie_value).await;
392
393        if let Some(ttl) = self.session_ttl {
394            session.expire_in(ttl);
395        }
396
397        depot.set_session(session);
398
399        ctrl.call_next(req, depot, res).await;
400        if ctrl.is_ceased() {
401            return;
402        }
403
404        let session = depot.take_session().expect("session should exist in depot");
405        if session.is_destroyed() {
406            if let Err(e) = self.store.destroy_session(session).await {
407                tracing::error!(error = ?e, "unable to destroy session");
408            }
409            let secure_cookie =
410                self.same_site_policy == SameSite::None || self.secure_cookie_policy.is_secure(req);
411            res.remove_cookie_with(self.build_removal_cookie(secure_cookie));
412        } else if self.save_unchanged || session.data_changed() {
413            match self.store.store_session(session).await {
414                Ok(cookie_value) => {
415                    if let Some(cookie_value) = cookie_value {
416                        let secure_cookie = self.same_site_policy == SameSite::None
417                            || self.secure_cookie_policy.is_secure(req);
418                        let cookie = self.build_cookie(secure_cookie, cookie_value);
419                        res.add_cookie(cookie);
420                    }
421                }
422                Err(e) => {
423                    tracing::error!(error = ?e, "store session error");
424                }
425            }
426        }
427    }
428}
429
430impl<S> SessionHandler<S>
431where
432    S: SessionStore + Send + Sync + 'static,
433{
434    /// Creates a new `HandlerBuilder`.
435    pub fn builder(store: S, secret: &[u8]) -> HandlerBuilder<S> {
436        HandlerBuilder::new(store, secret)
437    }
438    #[inline]
439    async fn load_or_create(&self, cookie_value: Option<String>) -> Session {
440        let session = match cookie_value {
441            Some(cookie_value) => self.store.load_session(cookie_value).await.ok().flatten(),
442            None => None,
443        };
444
445        session
446            .and_then(|session| session.validate())
447            .unwrap_or_default()
448    }
449    // the following is reused verbatim from
450    // https://github.com/SergioBenitez/cookie-rs/blob/master/src/secure/signed.rs#L51-L66
451    /// Given a signed value `str` where the signature is prepended to `value`,
452    /// verifies the signed value and returns it. If there's a problem, returns
453    /// an `Err` with a string describing the issue.
454    fn verify_signature(&self, cookie_value: &str) -> Result<String, Error> {
455        // Split [MAC | original-value] into its two parts.
456        //
457        // The digest prefix is always ASCII base64; if the cookie value's
458        // 44th byte falls inside a multi-byte UTF-8 codepoint, the value is
459        // malformed and we reject it instead of panicking via `split_at`.
460        let Some((digest_str, value)) = cookie_value.split_at_checked(BASE64_DIGEST_LEN) else {
461            return Err(Error::Other(
462                "length of value is <= BASE64_DIGEST_LEN".into(),
463            ));
464        };
465        let digest = general_purpose::STANDARD
466            .decode(digest_str)
467            .map_err(|_| Error::Other("bad base64 digest".into()))?;
468
469        // Perform the verification.
470        let mut hmac = self.hmac.clone();
471        hmac.update(value.as_bytes());
472        if hmac.verify_slice(&digest).is_ok() {
473            return Ok(value.to_owned());
474        }
475        for hmac in &self.fallback_hmacs {
476            let mut hmac = hmac.clone();
477            hmac.update(value.as_bytes());
478            if hmac.verify_slice(&digest).is_ok() {
479                return Ok(value.to_owned());
480            }
481        }
482        Err(Error::Other("value did not verify".into()))
483    }
484    fn build_cookie(&self, secure: bool, cookie_value: String) -> Cookie<'static> {
485        let mut cookie = Cookie::build((self.cookie_name.clone(), cookie_value))
486            .http_only(true)
487            .same_site(self.same_site_policy)
488            .secure(secure)
489            .path(self.cookie_path.clone())
490            .build();
491
492        if let Some(ttl) = self.session_ttl {
493            cookie.set_expires(Some((std::time::SystemTime::now() + ttl).into()));
494        }
495
496        if let Some(cookie_domain) = self.cookie_domain.clone() {
497            cookie.set_domain(cookie_domain)
498        }
499
500        self.sign_cookie(&mut cookie);
501
502        cookie
503    }
504    fn build_removal_cookie(&self, secure: bool) -> Cookie<'static> {
505        let mut cookie = Cookie::build((self.cookie_name.clone(), ""))
506            .http_only(true)
507            .same_site(self.same_site_policy)
508            .secure(secure)
509            .path(self.cookie_path.clone())
510            .build();
511
512        if let Some(cookie_domain) = self.cookie_domain.clone() {
513            cookie.set_domain(cookie_domain)
514        }
515
516        cookie
517    }
518    // The following is reused verbatim from
519    // https://github.com/SergioBenitez/cookie-rs/blob/master/src/secure/signed.rs#L37-46
520    /// signs the cookie's value providing integrity and authenticity.
521    fn sign_cookie(&self, cookie: &mut Cookie<'_>) {
522        // Compute HMAC-SHA256 of the cookie's value.
523        let mut mac = self.hmac.clone();
524        mac.update(cookie.value().as_bytes());
525
526        // Cookie's new value is [MAC | original-value].
527        let mut new_value = general_purpose::STANDARD.encode(mac.finalize().into_bytes());
528        new_value.push_str(cookie.value());
529        cookie.set_value(new_value);
530    }
531}
532
533#[cfg(test)]
534mod tests {
535    use salvo_core::http::Method;
536    use salvo_core::http::header::*;
537    use salvo_core::prelude::*;
538    use salvo_core::test::{ResponseExt, TestClient};
539
540    use super::*;
541
542    #[test]
543    fn test_session_data() {
544        let builder = SessionHandler::builder(
545            saysion::CookieStore,
546            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
547        )
548        .cookie_domain("test.domain")
549        .cookie_name("test_cookie")
550        .cookie_path("/abc")
551        .same_site_policy(SameSite::Strict)
552        .session_ttl(Some(Duration::from_secs(30)));
553        assert!(format!("{builder:?}").contains("test_cookie"));
554
555        let handler = builder.build().unwrap();
556        assert!(format!("{handler:?}").contains("test_cookie"));
557        assert_eq!(handler.cookie_domain, Some("test.domain".into()));
558        assert_eq!(handler.cookie_name, "test_cookie");
559        assert_eq!(handler.cookie_path, "/abc");
560        assert_eq!(handler.same_site_policy, SameSite::Strict);
561        assert_eq!(handler.session_ttl, Some(Duration::from_secs(30)));
562    }
563
564    #[tokio::test]
565    async fn test_session_login() {
566        #[handler]
567        pub async fn login(req: &mut Request, depot: &mut Depot, res: &mut Response) {
568            if req.method() == Method::POST {
569                let mut session = Session::new();
570                session
571                    .insert("username", req.form::<String>("username").await.unwrap())
572                    .unwrap();
573                depot.set_session(session);
574                res.render(Redirect::other("/"));
575            } else {
576                res.render(Text::Html("login page"));
577            }
578        }
579
580        #[handler]
581        pub async fn logout(depot: &mut Depot, res: &mut Response) {
582            if let Some(session) = depot.session_mut() {
583                session.remove("username");
584            }
585            res.render(Redirect::other("/"));
586        }
587
588        #[handler]
589        pub async fn home(depot: &mut Depot, res: &mut Response) {
590            let mut content = r#"home"#.into();
591            if let Some(session) = depot.session_mut()
592                && let Some(username) = session.get::<String>("username")
593            {
594                content = username;
595            }
596            res.render(Text::Html(content));
597        }
598
599        let session_handler = SessionHandler::builder(
600            MemoryStore::new(),
601            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
602        )
603        .build()
604        .unwrap();
605        let router = Router::new()
606            .hoop(session_handler)
607            .get(home)
608            .push(Router::with_path("login").get(login).post(login))
609            .push(Router::with_path("logout").get(logout));
610        let service = Service::new(router);
611
612        let response = TestClient::post("http://127.0.0.1:8698/login")
613            .raw_form("username=salvo")
614            .send(&service)
615            .await;
616        assert_eq!(response.status_code, Some(StatusCode::SEE_OTHER));
617        let cookie = response.headers().get(SET_COOKIE).unwrap();
618
619        let mut response = TestClient::get("http://127.0.0.1:8698/")
620            .add_header(COOKIE, cookie, true)
621            .send(&service)
622            .await;
623        assert_eq!(response.take_string().await.unwrap(), "salvo");
624
625        let response = TestClient::get("http://127.0.0.1:8698/logout")
626            .send(&service)
627            .await;
628        assert_eq!(response.status_code, Some(StatusCode::SEE_OTHER));
629
630        let mut response = TestClient::get("http://127.0.0.1:8698/")
631            .send(&service)
632            .await;
633        assert_eq!(response.take_string().await.unwrap(), "home");
634    }
635
636    #[test]
637    fn test_verify_signature_rejects_non_ascii_at_digest_boundary() {
638        // 43 ASCII bytes + a 2-byte UTF-8 codepoint puts byte 44 (the digest
639        // length) inside the multi-byte char. `split_at` would panic; the
640        // checked variant must reject the cookie cleanly.
641        let handler = SessionHandler::builder(
642            MemoryStore::new(),
643            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
644        )
645        .build()
646        .unwrap();
647
648        let malformed = "A".repeat(43) + "Ä" + "rest";
649        assert_eq!(malformed.len(), 43 + 2 + 4);
650        assert!(handler.verify_signature(&malformed).is_err());
651    }
652
653    #[test]
654    fn test_verify_signature_rejects_short_value() {
655        let handler = SessionHandler::builder(
656            MemoryStore::new(),
657            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
658        )
659        .build()
660        .unwrap();
661
662        assert!(handler.verify_signature("too-short").is_err());
663        assert!(handler.verify_signature("").is_err());
664    }
665
666    // Tests for HandlerBuilder
667    #[test]
668    fn test_handler_builder_new() {
669        let builder = HandlerBuilder::new(
670            MemoryStore::new(),
671            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
672        );
673        assert_eq!(builder.cookie_path, "/");
674        assert_eq!(builder.cookie_name, "salvo.session.id");
675        assert!(builder.cookie_domain.is_none());
676        assert!(builder.save_unchanged);
677        assert_eq!(builder.same_site_policy, SameSite::Lax);
678        assert_eq!(
679            builder.secure_cookie_policy,
680            SecureCookiePolicy::AutoFromScheme
681        );
682        assert_eq!(builder.session_ttl, Some(Duration::from_secs(24 * 60 * 60)));
683    }
684
685    #[test]
686    fn test_handler_builder_cookie_path() {
687        let builder = HandlerBuilder::new(
688            MemoryStore::new(),
689            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
690        )
691        .cookie_path("/custom");
692        assert_eq!(builder.cookie_path, "/custom");
693    }
694
695    #[test]
696    fn test_handler_builder_session_ttl() {
697        let builder = HandlerBuilder::new(
698            MemoryStore::new(),
699            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
700        )
701        .session_ttl(Some(Duration::from_secs(3600)));
702        assert_eq!(builder.session_ttl, Some(Duration::from_secs(3600)));
703    }
704
705    #[test]
706    fn test_handler_builder_session_ttl_none() {
707        let builder = HandlerBuilder::new(
708            MemoryStore::new(),
709            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
710        )
711        .session_ttl(None);
712        assert!(builder.session_ttl.is_none());
713    }
714
715    #[test]
716    fn test_handler_builder_cookie_name() {
717        let builder = HandlerBuilder::new(
718            MemoryStore::new(),
719            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
720        )
721        .cookie_name("my_session");
722        assert_eq!(builder.cookie_name, "my_session");
723    }
724
725    #[test]
726    fn test_handler_builder_save_unchanged() {
727        let builder = HandlerBuilder::new(
728            MemoryStore::new(),
729            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
730        )
731        .save_unchanged(false);
732        assert!(!builder.save_unchanged);
733    }
734
735    #[test]
736    fn test_handler_builder_same_site_policy() {
737        let builder = HandlerBuilder::new(
738            MemoryStore::new(),
739            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
740        )
741        .same_site_policy(SameSite::None);
742        assert_eq!(builder.same_site_policy, SameSite::None);
743    }
744
745    #[test]
746    fn test_handler_builder_cookie_domain() {
747        let builder = HandlerBuilder::new(
748            MemoryStore::new(),
749            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
750        )
751        .cookie_domain("example.com");
752        assert_eq!(builder.cookie_domain, Some("example.com".to_owned()));
753    }
754
755    #[test]
756    fn test_handler_builder_fallback_keys() {
757        let builder = HandlerBuilder::new(
758            MemoryStore::new(),
759            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
760        )
761        .fallback_keys(vec![Key::from(
762            b"fallbackfallbackfallbackfallbackfallbackfallbackfallbackfallback" as &[u8],
763        )]);
764        assert_eq!(builder.fallback_keys.len(), 1);
765    }
766
767    #[test]
768    fn test_handler_builder_add_fallback_key() {
769        let builder = HandlerBuilder::new(
770            MemoryStore::new(),
771            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
772        )
773        .add_fallback_key(Key::from(
774            b"fallbackfallbackfallbackfallbackfallbackfallbackfallbackfallback" as &[u8],
775        ))
776        .add_fallback_key(Key::from(
777            b"anotherkeyanotherkeyanotherkeyanotherkeyanotherkeyanotherkeyanot" as &[u8],
778        ));
779        assert_eq!(builder.fallback_keys.len(), 2);
780    }
781
782    #[test]
783    fn test_handler_builder_build() {
784        let handler = HandlerBuilder::new(
785            MemoryStore::new(),
786            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
787        )
788        .build()
789        .unwrap();
790        assert_eq!(handler.cookie_path, "/");
791        assert_eq!(handler.cookie_name, "salvo.session.id");
792    }
793
794    #[test]
795    fn test_handler_builder_debug() {
796        let builder = HandlerBuilder::new(
797            MemoryStore::new(),
798            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
799        );
800        let debug_str = format!("{builder:?}");
801        assert!(debug_str.contains("HandlerBuilder"));
802        assert!(debug_str.contains("cookie_path"));
803        assert!(debug_str.contains("cookie_name"));
804    }
805
806    #[test]
807    fn test_handler_builder_chain() {
808        let handler = HandlerBuilder::new(
809            MemoryStore::new(),
810            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
811        )
812        .cookie_path("/app")
813        .cookie_name("app_session")
814        .cookie_domain("app.example.com")
815        .session_ttl(Some(Duration::from_secs(7200)))
816        .save_unchanged(false)
817        .same_site_policy(SameSite::Strict)
818        .secure_cookie(true)
819        .build()
820        .unwrap();
821
822        assert_eq!(handler.cookie_path, "/app");
823        assert_eq!(handler.cookie_name, "app_session");
824        assert_eq!(handler.cookie_domain, Some("app.example.com".to_owned()));
825        assert_eq!(handler.session_ttl, Some(Duration::from_secs(7200)));
826        assert!(!handler.save_unchanged);
827        assert_eq!(handler.same_site_policy, SameSite::Strict);
828        assert_eq!(handler.secure_cookie_policy, SecureCookiePolicy::Always);
829    }
830
831    #[test]
832    fn test_handler_builder_secure_cookie_policy() {
833        let builder = HandlerBuilder::new(
834            MemoryStore::new(),
835            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
836        )
837        .secure_cookie_policy(SecureCookiePolicy::Never);
838
839        assert_eq!(builder.secure_cookie_policy, SecureCookiePolicy::Never);
840    }
841
842    // Tests for SessionHandler
843    #[test]
844    fn test_session_handler_builder() {
845        let handler = SessionHandler::builder(
846            MemoryStore::new(),
847            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
848        )
849        .build()
850        .unwrap();
851        assert_eq!(handler.cookie_name, "salvo.session.id");
852    }
853
854    #[test]
855    fn test_session_handler_debug() {
856        let handler = SessionHandler::builder(
857            MemoryStore::new(),
858            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
859        )
860        .build()
861        .unwrap();
862        let debug_str = format!("{handler:?}");
863        assert!(debug_str.contains("SessionHandler"));
864        assert!(debug_str.contains("cookie_path"));
865    }
866
867    // Tests for SessionDepotExt
868    #[test]
869    fn test_depot_set_session() {
870        let mut depot = Depot::new();
871        let session = Session::new();
872        depot.set_session(session);
873        assert!(depot.session().is_some());
874    }
875
876    #[test]
877    fn test_depot_take_session() {
878        let mut depot = Depot::new();
879        let session = Session::new();
880        depot.set_session(session);
881        let taken = depot.take_session();
882        assert!(taken.is_some());
883        assert!(depot.session().is_none());
884    }
885
886    #[test]
887    fn test_depot_session() {
888        let mut depot = Depot::new();
889        assert!(depot.session().is_none());
890
891        depot.set_session(Session::new());
892        assert!(depot.session().is_some());
893    }
894
895    #[test]
896    fn test_depot_session_mut() {
897        let mut depot = Depot::new();
898        depot.set_session(Session::new());
899
900        if let Some(session) = depot.session_mut() {
901            session.insert("key", "value").unwrap();
902        }
903
904        if let Some(session) = depot.session() {
905            assert_eq!(session.get::<String>("key"), Some("value".to_owned()));
906        }
907    }
908
909    // Tests for session with destroyed state
910    #[tokio::test]
911    async fn test_session_destroy() {
912        #[handler]
913        pub async fn destroy_session(depot: &mut Depot, res: &mut Response) {
914            if let Some(session) = depot.session_mut() {
915                session.destroy();
916            }
917            res.render("destroyed");
918        }
919
920        let session_handler = SessionHandler::builder(
921            MemoryStore::new(),
922            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
923        )
924        .build()
925        .unwrap();
926
927        let router = Router::new()
928            .hoop(session_handler)
929            .push(Router::with_path("destroy").get(destroy_session));
930        let service = Service::new(router);
931
932        let response = TestClient::get("http://127.0.0.1:8698/destroy")
933            .send(&service)
934            .await;
935        assert_eq!(response.status_code, Some(StatusCode::OK));
936    }
937
938    #[tokio::test]
939    async fn test_session_destroy_preserves_cookie_path_and_domain() {
940        #[handler]
941        pub async fn destroy_session(depot: &mut Depot) {
942            if let Some(session) = depot.session_mut() {
943                session.destroy();
944            }
945        }
946
947        let session_handler = SessionHandler::builder(
948            MemoryStore::new(),
949            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
950        )
951        .cookie_domain("example.com")
952        .cookie_path("/app")
953        .build()
954        .unwrap();
955
956        let router = Router::new()
957            .hoop(session_handler)
958            .push(Router::with_path("destroy").get(destroy_session));
959        let service = Service::new(router);
960
961        let response = TestClient::get("http://127.0.0.1:8698/destroy")
962            .add_header(COOKIE, "salvo.session.id=stale", true)
963            .send(&service)
964            .await;
965
966        let cookie = response
967            .headers()
968            .get(SET_COOKIE)
969            .expect("set-cookie header")
970            .to_str()
971            .expect("set-cookie should be valid");
972        assert!(cookie.starts_with("salvo.session.id="));
973        assert!(cookie.contains("Max-Age=0"));
974        assert!(cookie.contains("Path=/app"));
975        assert!(cookie.contains("Domain=example.com"));
976    }
977
978    #[tokio::test]
979    async fn test_session_can_force_secure_cookie_behind_tls_terminator() {
980        #[handler]
981        pub async fn index() -> &'static str {
982            "ok"
983        }
984
985        let session_handler = SessionHandler::builder(
986            MemoryStore::new(),
987            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
988        )
989        .secure_cookie(true)
990        .build()
991        .unwrap();
992
993        let router = Router::new().hoop(session_handler).get(index);
994        let service = Service::new(router);
995
996        let response = TestClient::get("http://127.0.0.1:8698/")
997            .send(&service)
998            .await;
999        let cookie = response
1000            .headers()
1001            .get(SET_COOKIE)
1002            .unwrap()
1003            .to_str()
1004            .unwrap();
1005        assert!(cookie.contains("Secure"));
1006    }
1007
1008    #[tokio::test]
1009    async fn test_session_same_site_none_forces_secure_cookie() {
1010        #[handler]
1011        pub async fn index() -> &'static str {
1012            "ok"
1013        }
1014
1015        let session_handler = SessionHandler::builder(
1016            MemoryStore::new(),
1017            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
1018        )
1019        .same_site_policy(SameSite::None)
1020        .secure_cookie(false)
1021        .build()
1022        .unwrap();
1023
1024        let router = Router::new().hoop(session_handler).get(index);
1025        let service = Service::new(router);
1026
1027        let response = TestClient::get("http://127.0.0.1:8698/")
1028            .send(&service)
1029            .await;
1030        let cookie = response
1031            .headers()
1032            .get(SET_COOKIE)
1033            .unwrap()
1034            .to_str()
1035            .unwrap();
1036        assert!(cookie.contains("SameSite=None"));
1037        assert!(cookie.contains("Secure"));
1038    }
1039
1040    // Tests for session with save_unchanged = false
1041    #[tokio::test]
1042    async fn test_session_save_unchanged_false() {
1043        #[handler]
1044        pub async fn no_change(depot: &mut Depot, res: &mut Response) {
1045            // Access session but don't modify it
1046            let _ = depot.session();
1047            res.render("no change");
1048        }
1049
1050        let session_handler = SessionHandler::builder(
1051            MemoryStore::new(),
1052            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
1053        )
1054        .save_unchanged(false)
1055        .build()
1056        .unwrap();
1057
1058        let router = Router::new()
1059            .hoop(session_handler)
1060            .push(Router::with_path("nochange").get(no_change));
1061        let service = Service::new(router);
1062
1063        let response = TestClient::get("http://127.0.0.1:8698/nochange")
1064            .send(&service)
1065            .await;
1066        assert_eq!(response.status_code, Some(StatusCode::OK));
1067        // When save_unchanged is false and no data is modified, no cookie should be set
1068        // for a new session (unless there's existing session data)
1069    }
1070
1071    // Tests for session data persistence
1072    #[tokio::test]
1073    async fn test_session_data_persistence() {
1074        #[handler]
1075        pub async fn set_data(depot: &mut Depot, res: &mut Response) {
1076            if let Some(session) = depot.session_mut() {
1077                session.insert("counter", 1).unwrap();
1078            }
1079            res.render("set");
1080        }
1081
1082        #[handler]
1083        pub async fn get_data(depot: &mut Depot, res: &mut Response) {
1084            let counter = if let Some(session) = depot.session() {
1085                session.get::<i32>("counter").unwrap_or(0)
1086            } else {
1087                0
1088            };
1089            res.render(format!("{counter}"));
1090        }
1091
1092        let session_handler = SessionHandler::builder(
1093            MemoryStore::new(),
1094            b"secretabsecretabsecretabsecretabsecretabsecretabsecretabsecretab",
1095        )
1096        .build()
1097        .unwrap();
1098
1099        let router = Router::new()
1100            .hoop(session_handler)
1101            .push(Router::with_path("set").get(set_data))
1102            .push(Router::with_path("get").get(get_data));
1103        let service = Service::new(router);
1104
1105        // Set data
1106        let response = TestClient::get("http://127.0.0.1:8698/set")
1107            .send(&service)
1108            .await;
1109        let cookie = response.headers().get(SET_COOKIE).unwrap();
1110
1111        // Get data with same session
1112        let mut response = TestClient::get("http://127.0.0.1:8698/get")
1113            .add_header(COOKIE, cookie, true)
1114            .send(&service)
1115            .await;
1116        assert_eq!(response.take_string().await.unwrap(), "1");
1117    }
1118
1119    // Test for SESSION_KEY constant
1120    #[test]
1121    fn test_session_key_constant() {
1122        assert_eq!(SESSION_KEY, "::salvo::session");
1123    }
1124
1125    // Test for BASE64_DIGEST_LEN constant
1126    #[test]
1127    fn test_base64_digest_len() {
1128        assert_eq!(BASE64_DIGEST_LEN, 44);
1129    }
1130}