Skip to content

Commit f1cbfa8

Browse files
committed
Tide sessions
1 parent 07a472e commit f1cbfa8

File tree

8 files changed

+614
-2
lines changed

8 files changed

+614
-2
lines changed

Cargo.toml

+5-1
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,17 @@ features = ["docs"]
2424
rustdoc-args = ["--cfg", "feature=\"docs\""]
2525

2626
[features]
27-
default = ["h1-server"]
27+
default = ["h1-server", "sessions"]
2828
h1-server = ["async-h1"]
2929
docs = ["unstable"]
30+
sessions = ["async-session"]
3031
unstable = []
3132
# DO NOT USE. Only exists to expose internals so they can be benchmarked.
3233
__internal__bench = []
3334

3435
[dependencies]
3536
async-h1 = { version = "2.0.1", optional = true }
37+
async-session = { version = "1.0.2", optional = true }
3638
async-sse = "4.0.0"
3739
async-std = { version = "1.6.0", features = ["unstable"] }
3840
async-trait = "0.1.36"
@@ -65,3 +67,5 @@ required-features = ["unstable"]
6567
name = "router"
6668
harness = false
6769

70+
[patch.crates-io]
71+
async-session = { git = "https://github.com/jbr/async-session", branch = "tide" }

examples/sessions.rs

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#[async_std::main]
2+
async fn main() -> Result<(), std::io::Error> {
3+
tide::log::start();
4+
let mut app = tide::new();
5+
6+
app.middleware(tide::sessions::SessionMiddleware::new(
7+
tide::sessions::MemoryStore::new(),
8+
b"use std::env::var(\"TIDE_SECRET\").unwrap().as_bytes() instead of a fixed value",
9+
));
10+
11+
app.middleware(tide::utils::Before(
12+
|mut request: tide::Request<()>| async move {
13+
let visits: usize = request.session_get("visits").unwrap_or_default();
14+
request.session_insert("visits", visits + 1).unwrap();
15+
request
16+
},
17+
));
18+
19+
app.at("/").get(|req: tide::Request<()>| async move {
20+
let visits: usize = req.session_get("visits").unwrap();
21+
Ok(format!("you have visited this website {} times", visits))
22+
});
23+
24+
app.at("/reset")
25+
.get(|mut req: tide::Request<()>| async move {
26+
req.session_mut().destroy();
27+
Ok(tide::Redirect::new("/"))
28+
});
29+
30+
app.listen("127.0.0.1:8080").await?;
31+
32+
Ok(())
33+
}

src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,9 @@ pub mod security;
215215
pub mod sse;
216216
pub mod utils;
217217

218+
#[cfg(feature = "sessions")]
219+
pub mod sessions;
220+
218221
pub use endpoint::Endpoint;
219222
pub use middleware::{Middleware, Next};
220223
pub use redirect::Redirect;

src/request.rs

+29
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,11 @@ impl<State> Request<State> {
260260
self.req.ext().get()
261261
}
262262

263+
#[must_use]
264+
pub fn ext_mut<T: Send + Sync + 'static>(&mut self) -> Option<&mut T> {
265+
self.req.ext_mut().get_mut()
266+
}
267+
263268
/// Set a request extension value.
264269
pub fn set_ext<T: Send + Sync + 'static>(&mut self, val: T) -> Option<T> {
265270
self.req.ext_mut().insert(val)
@@ -506,6 +511,30 @@ impl<State> Request<State> {
506511
.and_then(|cookie_data| cookie_data.content.read().unwrap().get(name).cloned())
507512
}
508513

514+
#[cfg(feature = "sessions")]
515+
pub fn session(&self) -> &crate::sessions::Session {
516+
self.ext::<crate::sessions::Session>().expect(
517+
"request session not initialized, did you enable tide::sessions::SessionMiddleware?",
518+
)
519+
}
520+
521+
#[cfg(feature = "sessions")]
522+
pub fn session_mut(&mut self) -> &mut crate::sessions::Session {
523+
self.ext_mut().expect(
524+
"request session not initialized, did you enable tide::sessions::SessionMiddleware?",
525+
)
526+
}
527+
528+
#[cfg(feature = "sessions")]
529+
pub fn session_get<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
530+
self.session().get(key)
531+
}
532+
533+
#[cfg(feature = "sessions")]
534+
pub fn session_insert(&mut self, key: &str, value: impl serde::Serialize) -> crate::Result<()> {
535+
Ok(self.session_mut().insert(key, value)?)
536+
}
537+
509538
/// Get the length of the body stream, if it has been set.
510539
///
511540
/// This value is set when passing a fixed-size object into as the body. E.g. a string, or a

src/sessions/middleware.rs

+250
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
use super::{Session, SessionStore};
2+
use crate::http::cookies::{Cookie, Key, SameSite};
3+
use crate::{utils::async_trait, Middleware, Next, Request};
4+
use std::time::Duration;
5+
6+
use async_session::{
7+
base64,
8+
hmac::{Hmac, Mac, NewMac},
9+
sha2::Sha256,
10+
};
11+
12+
const BASE64_DIGEST_LEN: usize = 44;
13+
14+
/// Middleware to enable sessions.
15+
/// ## example:
16+
/// ```rust
17+
/// # async_std::task::block_on(async {
18+
/// let mut app = tide::new();
19+
///
20+
/// app.middleware(tide::sessions::SessionMiddleware::new(
21+
/// tide::sessions::MemoryStore::new(),
22+
/// b"use std::env::var(\"TIDE_SECRET\").unwrap().as_bytes() instead of a fixed value"
23+
/// ));
24+
///
25+
/// app.middleware(tide::utils::Before(|mut request: tide::Request<()>| async move {
26+
/// let visits: usize = request.session_get("visits").unwrap_or_default();
27+
/// request.session_insert("visits", visits + 1).unwrap();
28+
/// request
29+
/// }));
30+
///
31+
/// app.at("/").get(|req: tide::Request<()>| async move {
32+
/// let visits: usize = req.session_get("visits").unwrap();
33+
/// Ok(format!("you have visited this website {} times", visits))
34+
/// });
35+
///
36+
/// app.at("/reset")
37+
/// .get(|mut req: tide::Request<()>| async move {
38+
/// req.session_mut().destroy();
39+
/// Ok(tide::Redirect::new("/"))
40+
/// });
41+
/// # })
42+
/// ```
43+
44+
pub struct SessionMiddleware<Store> {
45+
store: Store,
46+
cookie_path: String,
47+
cookie_name: String,
48+
session_ttl: Option<Duration>,
49+
save_unchanged: bool,
50+
same_site_policy: SameSite,
51+
key: Key,
52+
}
53+
54+
impl<Store: SessionStore> std::fmt::Debug for SessionMiddleware<Store> {
55+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56+
f.debug_struct("SessionMiddleware")
57+
.field("store", &self.store)
58+
.field("cookie_path", &self.cookie_path)
59+
.field("cookie_name", &self.cookie_name)
60+
.field("session_ttl", &self.session_ttl)
61+
.field("same_site_policy", &self.same_site_policy)
62+
.field("key", &"..")
63+
.field("save_unchanged", &self.save_unchanged)
64+
.finish()
65+
}
66+
}
67+
68+
#[async_trait]
69+
impl<Store, State> Middleware<State> for SessionMiddleware<Store>
70+
where
71+
Store: SessionStore,
72+
State: Clone + Send + Sync + 'static,
73+
{
74+
async fn handle(&self, mut request: Request<State>, next: Next<'_, State>) -> crate::Result {
75+
let cookie = request.cookie(&self.cookie_name);
76+
let cookie_value = cookie
77+
.clone()
78+
.and_then(|cookie| self.verify_signature(cookie.value()).ok());
79+
80+
let mut session = self.load_or_create(cookie_value).await;
81+
82+
if let Some(ttl) = self.session_ttl {
83+
session.expire_in(ttl);
84+
}
85+
86+
let secure_cookie = request.url().scheme() == "https";
87+
request.set_ext(session.clone());
88+
89+
let mut response = next.run(request).await;
90+
91+
if session.is_destroyed() {
92+
if let Err(e) = self.store.destroy_session(session).await {
93+
crate::log::error!("unable to destroy session", { error: e.to_string() });
94+
}
95+
96+
if let Some(mut c) = cookie {
97+
c.set_path("/");
98+
response.remove_cookie(c);
99+
}
100+
} else if self.save_unchanged || session.data_changed() {
101+
if let Some(cookie_value) = self.store.store_session(session).await {
102+
let cookie = self.build_cookie(secure_cookie, cookie_value);
103+
response.insert_cookie(cookie);
104+
}
105+
}
106+
107+
Ok(response)
108+
}
109+
}
110+
111+
impl<Store: SessionStore> SessionMiddleware<Store> {
112+
/// Creates a new SessionMiddleware with a mandatory cookie
113+
/// signing secret. The `secret` MUST be at least 32 bytes long,
114+
/// and should be cryptographically random. It is recommended to
115+
/// retrieve this at runtime from the environment instead of
116+
/// compiling it into your application. SessionMiddleware::new
117+
/// will panic if the secret is fewer than 32 bytes.
118+
///
119+
/// The defaults for SessionMiddleware are:
120+
/// * cookie path: "/"
121+
/// * cookie name: "tide.sid"
122+
/// * session ttl: one day
123+
/// * same site: strict
124+
/// * save unchanged: enabled
125+
pub fn new(store: Store, secret: &[u8]) -> Self {
126+
Self {
127+
store,
128+
save_unchanged: true,
129+
cookie_path: "/".into(),
130+
cookie_name: "tide.sid".into(),
131+
same_site_policy: SameSite::Strict,
132+
session_ttl: Some(Duration::from_secs(24 * 60 * 60)),
133+
key: Key::derive_from(secret),
134+
}
135+
}
136+
137+
/// Sets a cookie path for this session middleware.
138+
/// The default for this value is "/"
139+
pub fn with_cookie_path(mut self, cookie_path: impl AsRef<str>) -> Self {
140+
self.cookie_path = cookie_path.as_ref().to_owned();
141+
self
142+
}
143+
144+
/// Sets a session ttl. This will be used both for the cookie
145+
/// expiry and also for the session-internal expiry.
146+
///
147+
/// The default for this value is one day. Set this to None to not
148+
/// set a cookie or session expiry. This is not recommended.
149+
pub fn with_session_ttl(mut self, session_ttl: Option<Duration>) -> Self {
150+
self.session_ttl = session_ttl;
151+
self
152+
}
153+
154+
/// Sets the name of the cookie that the session is stored with or in.
155+
///
156+
/// If you are running multiple tide applications on the same
157+
/// domain, you will need different values for each
158+
/// application. The default value is "tide.sid"
159+
pub fn with_cookie_name(mut self, cookie_name: impl AsRef<str>) -> Self {
160+
self.cookie_name = cookie_name.as_ref().to_owned();
161+
self
162+
}
163+
164+
/// Disables the `save_unchanged` setting. When `save_unchanged`
165+
/// is enabled, a session will cookie will always be set. With
166+
/// `save_unchanged` disabled, the session data must be modified
167+
/// from the `Default` value in order for it to save. If a session
168+
/// already exists and its data unmodified in the course of a
169+
/// request, the session will only be persisted if
170+
/// `save_unchanged` is enabled.
171+
pub fn without_save_unchanged(mut self) -> Self {
172+
self.save_unchanged = false;
173+
self
174+
}
175+
176+
/// Sets the same site policy for the session cookie. Defaults to
177+
/// SameSite::Strict. See [incrementally better
178+
/// cookies](https://tools.ietf.org/html/draft-west-cookie-incrementalism-01)
179+
/// for more information about this setting
180+
pub fn with_same_site_policy(mut self, policy: SameSite) -> Self {
181+
self.same_site_policy = policy;
182+
self
183+
}
184+
185+
//--- methods below here are private ---
186+
187+
async fn load_or_create(&self, cookie_value: Option<String>) -> Session {
188+
let session = match cookie_value {
189+
Some(cookie_value) => self.store.load_session(cookie_value).await,
190+
None => None,
191+
};
192+
193+
session
194+
.and_then(|session| session.validate())
195+
.unwrap_or_default()
196+
}
197+
198+
fn build_cookie(&self, secure: bool, cookie_value: String) -> Cookie<'static> {
199+
let mut cookie = Cookie::build(self.cookie_name.clone(), cookie_value)
200+
.http_only(true)
201+
.same_site(self.same_site_policy)
202+
.secure(secure)
203+
.path(self.cookie_path.clone())
204+
.finish();
205+
206+
if let Some(ttl) = self.session_ttl {
207+
cookie.set_expires(Some((std::time::SystemTime::now() + ttl).into()));
208+
}
209+
210+
self.sign_cookie(&mut cookie);
211+
212+
cookie
213+
}
214+
215+
// the following is reused verbatim from
216+
// https://github.com/SergioBenitez/cookie-rs/blob/master/src/secure/signed.rs#L33-L43
217+
/// Signs the cookie's value providing integrity and authenticity.
218+
fn sign_cookie(&self, cookie: &mut Cookie<'_>) {
219+
// Compute HMAC-SHA256 of the cookie's value.
220+
let mut mac = Hmac::<Sha256>::new_varkey(&self.key.signing()).expect("good key");
221+
mac.update(cookie.value().as_bytes());
222+
223+
// Cookie's new value is [MAC | original-value].
224+
let mut new_value = base64::encode(&mac.finalize().into_bytes());
225+
new_value.push_str(cookie.value());
226+
cookie.set_value(new_value);
227+
}
228+
229+
// the following is reused verbatim from
230+
// https://github.com/SergioBenitez/cookie-rs/blob/master/src/secure/signed.rs#L45-L63
231+
/// Given a signed value `str` where the signature is prepended to `value`,
232+
/// verifies the signed value and returns it. If there's a problem, returns
233+
/// an `Err` with a string describing the issue.
234+
fn verify_signature(&self, cookie_value: &str) -> Result<String, &'static str> {
235+
if cookie_value.len() < BASE64_DIGEST_LEN {
236+
return Err("length of value is <= BASE64_DIGEST_LEN");
237+
}
238+
239+
// Split [MAC | original-value] into its two parts.
240+
let (digest_str, value) = cookie_value.split_at(BASE64_DIGEST_LEN);
241+
let digest = base64::decode(digest_str).map_err(|_| "bad base64 digest")?;
242+
243+
// Perform the verification.
244+
let mut mac = Hmac::<Sha256>::new_varkey(&self.key.signing()).expect("good key");
245+
mac.update(value.as_bytes());
246+
mac.verify(&digest)
247+
.map(|_| value.to_string())
248+
.map_err(|_| "value did not verify")
249+
}
250+
}

0 commit comments

Comments
 (0)