Skip to content

Commit 7b0a320

Browse files
committed
Tide sessions
1 parent 724afc9 commit 7b0a320

File tree

8 files changed

+632
-2
lines changed

8 files changed

+632
-2
lines changed

Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,18 @@ features = ["docs"]
2424
rustdoc-args = ["--cfg", "feature=\"docs\""]
2525

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

3536
[dependencies]
3637
async-h1 = { version = "2.0.1", optional = true }
38+
async-session = { version = "2.0.0", optional = true }
3739
async-sse = "4.0.0"
3840
async-std = { version = "1.6.0", features = ["unstable"] }
3941
async-trait = "0.1.36"
@@ -66,3 +68,5 @@ required-features = ["unstable"]
6668
name = "router"
6769
harness = false
6870

71+
[patch.crates-io]
72+
async-session = { git = "https://github.com/http-rs/async-session", branch = "master" }

examples/sessions.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
std::env::var("TIDE_SECRET")
9+
.expect(
10+
"Please provide a TIDE_SECRET value of at \
11+
least 32 bytes in order to run this example",
12+
)
13+
.as_bytes(),
14+
));
15+
16+
app.middleware(tide::utils::Before(
17+
|mut request: tide::Request<()>| async move {
18+
let session = request.session_mut();
19+
let visits: usize = session.get("visits").unwrap_or_default();
20+
session.insert("visits", visits + 1).unwrap();
21+
request
22+
},
23+
));
24+
25+
app.at("/").get(|req: tide::Request<()>| async move {
26+
let visits: usize = req.session().get("visits").unwrap();
27+
Ok(format!("you have visited this website {} times", visits))
28+
});
29+
30+
app.at("/reset")
31+
.get(|mut req: tide::Request<()>| async move {
32+
req.session_mut().destroy();
33+
Ok(tide::Redirect::new("/"))
34+
});
35+
36+
app.listen("127.0.0.1:8080").await?;
37+
38+
Ok(())
39+
}

src/lib.rs

Lines changed: 3 additions & 0 deletions
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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,12 @@ impl<State> Request<State> {
260260
self.req.ext().get()
261261
}
262262

263+
/// Get a mutable reference to value stored in request extensions.
264+
#[must_use]
265+
pub fn ext_mut<T: Send + Sync + 'static>(&mut self) -> Option<&mut T> {
266+
self.req.ext_mut().get_mut()
267+
}
268+
263269
/// Set a request extension value.
264270
pub fn set_ext<T: Send + Sync + 'static>(&mut self, val: T) -> Option<T> {
265271
self.req.ext_mut().insert(val)
@@ -506,6 +512,32 @@ impl<State> Request<State> {
506512
.and_then(|cookie_data| cookie_data.content.read().unwrap().get(name).cloned())
507513
}
508514

515+
/// Retrieves a reference to the current session.
516+
///
517+
/// # Panics
518+
///
519+
/// This method will panic if a tide::sessions:SessionMiddleware has not
520+
/// been run.
521+
#[cfg(feature = "sessions")]
522+
pub fn session(&self) -> &crate::sessions::Session {
523+
self.ext::<crate::sessions::Session>().expect(
524+
"request session not initialized, did you enable tide::sessions::SessionMiddleware?",
525+
)
526+
}
527+
528+
/// Retrieves a mutable reference to the current session.
529+
///
530+
/// # Panics
531+
///
532+
/// This method will panic if a tide::sessions:SessionMiddleware has not
533+
/// been run.
534+
#[cfg(feature = "sessions")]
535+
pub fn session_mut(&mut self) -> &mut crate::sessions::Session {
536+
self.ext_mut().expect(
537+
"request session not initialized, did you enable tide::sessions::SessionMiddleware?",
538+
)
539+
}
540+
509541
/// Get the length of the body stream, if it has been set.
510542
///
511543
/// This value is set when passing a fixed-size object into as the body. E.g. a string, or a

src/sessions/middleware.rs

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

0 commit comments

Comments
 (0)