-
Notifications
You must be signed in to change notification settings - Fork 322
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
630 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
#[async_std::main] | ||
async fn main() -> Result<(), std::io::Error> { | ||
tide::log::start(); | ||
let mut app = tide::new(); | ||
|
||
app.middleware(tide::sessions::SessionMiddleware::new( | ||
tide::sessions::MemoryStore::new(), | ||
std::env::var("TIDE_SECRET") | ||
.expect( | ||
"Please provide a TIDE_SECRET value of at \ | ||
least 32 bytes in order to run this example", | ||
) | ||
.as_bytes(), | ||
)); | ||
|
||
app.middleware(tide::utils::Before( | ||
|mut request: tide::Request<()>| async move { | ||
let session = request.session_mut(); | ||
let visits: usize = session.get("visits").unwrap_or_default(); | ||
session.insert("visits", visits + 1).unwrap(); | ||
request | ||
}, | ||
)); | ||
|
||
app.at("/").get(|req: tide::Request<()>| async move { | ||
let visits: usize = req.session().get("visits").unwrap(); | ||
Ok(format!("you have visited this website {} times", visits)) | ||
}); | ||
|
||
app.at("/reset") | ||
.get(|mut req: tide::Request<()>| async move { | ||
req.session_mut().destroy(); | ||
Ok(tide::Redirect::new("/")) | ||
}); | ||
|
||
app.listen("127.0.0.1:8080").await?; | ||
|
||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,259 @@ | ||
use super::{Session, SessionStore}; | ||
use crate::http::{ | ||
cookies::{Cookie, Key, SameSite}, | ||
format_err, | ||
}; | ||
use crate::{utils::async_trait, Middleware, Next, Request}; | ||
use std::time::Duration; | ||
|
||
use async_session::{ | ||
base64, | ||
hmac::{Hmac, Mac, NewMac}, | ||
sha2::Sha256, | ||
}; | ||
|
||
const BASE64_DIGEST_LEN: usize = 44; | ||
|
||
/// Middleware to enable sessions. | ||
/// ## example: | ||
/// ```rust | ||
/// # async_std::task::block_on(async { | ||
/// let mut app = tide::new(); | ||
/// | ||
/// app.middleware(tide::sessions::SessionMiddleware::new( | ||
/// tide::sessions::MemoryStore::new(), | ||
/// b"use std::env::var(\"TIDE_SECRET\").unwrap().as_bytes() instead of a fixed value" | ||
/// )); | ||
/// | ||
/// app.middleware(tide::utils::Before(|mut request: tide::Request<()>| async move { | ||
/// let session = request.session_mut(); | ||
/// let visits: usize = session.get("visits").unwrap_or_default(); | ||
/// session.insert("visits", visits + 1).unwrap(); | ||
/// request | ||
/// })); | ||
/// | ||
/// app.at("/").get(|req: tide::Request<()>| async move { | ||
/// let visits: usize = req.session().get("visits").unwrap(); | ||
/// Ok(format!("you have visited this website {} times", visits)) | ||
/// }); | ||
/// | ||
/// app.at("/reset") | ||
/// .get(|mut req: tide::Request<()>| async move { | ||
/// req.session_mut().destroy(); | ||
/// Ok(tide::Redirect::new("/")) | ||
/// }); | ||
/// # }) | ||
/// ``` | ||
pub struct SessionMiddleware<Store> { | ||
store: Store, | ||
cookie_path: String, | ||
cookie_name: String, | ||
session_ttl: Option<Duration>, | ||
save_unchanged: bool, | ||
same_site_policy: SameSite, | ||
key: Key, | ||
} | ||
|
||
impl<Store: SessionStore> std::fmt::Debug for SessionMiddleware<Store> { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
f.debug_struct("SessionMiddleware") | ||
.field("store", &self.store) | ||
.field("cookie_path", &self.cookie_path) | ||
.field("cookie_name", &self.cookie_name) | ||
.field("session_ttl", &self.session_ttl) | ||
.field("same_site_policy", &self.same_site_policy) | ||
.field("key", &"..") | ||
.field("save_unchanged", &self.save_unchanged) | ||
.finish() | ||
} | ||
} | ||
|
||
#[async_trait] | ||
impl<Store, State> Middleware<State> for SessionMiddleware<Store> | ||
where | ||
Store: SessionStore, | ||
State: Clone + Send + Sync + 'static, | ||
{ | ||
async fn handle(&self, mut request: Request<State>, next: Next<'_, State>) -> crate::Result { | ||
let cookie = request.cookie(&self.cookie_name); | ||
let cookie_value = cookie | ||
.clone() | ||
.and_then(|cookie| self.verify_signature(cookie.value()).ok()); | ||
|
||
let mut session = self.load_or_create(cookie_value).await; | ||
|
||
if let Some(ttl) = self.session_ttl { | ||
session.expire_in(ttl); | ||
} | ||
|
||
let secure_cookie = request.url().scheme() == "https"; | ||
request.set_ext(session.clone()); | ||
|
||
let mut response = next.run(request).await; | ||
|
||
if session.is_destroyed() { | ||
if let Err(e) = self.store.destroy_session(session).await { | ||
crate::log::error!("unable to destroy session", { error: e.to_string() }); | ||
} | ||
|
||
if let Some(mut cookie) = cookie { | ||
cookie.set_path("/"); | ||
response.remove_cookie(cookie); | ||
} | ||
} else if self.save_unchanged || session.data_changed() { | ||
if let Some(cookie_value) = self | ||
.store | ||
.store_session(session) | ||
.await | ||
.map_err(|e| format_err!("{}", e.to_string()))? | ||
{ | ||
let cookie = self.build_cookie(secure_cookie, cookie_value); | ||
response.insert_cookie(cookie); | ||
} | ||
} | ||
|
||
Ok(response) | ||
} | ||
} | ||
|
||
impl<Store: SessionStore> SessionMiddleware<Store> { | ||
/// Creates a new SessionMiddleware with a mandatory cookie | ||
/// signing secret. The `secret` MUST be at least 32 bytes long, | ||
/// and should be cryptographically random. It is recommended to | ||
/// retrieve this at runtime from the environment instead of | ||
/// compiling it into your application. SessionMiddleware::new | ||
/// will panic if the secret is fewer than 32 bytes. | ||
/// | ||
/// The defaults for SessionMiddleware are: | ||
/// * cookie path: "/" | ||
/// * cookie name: "tide.sid" | ||
/// * session ttl: one day | ||
/// * same site: strict | ||
/// * save unchanged: enabled | ||
pub fn new(store: Store, secret: &[u8]) -> Self { | ||
Self { | ||
store, | ||
save_unchanged: true, | ||
cookie_path: "/".into(), | ||
cookie_name: "tide.sid".into(), | ||
same_site_policy: SameSite::Strict, | ||
session_ttl: Some(Duration::from_secs(24 * 60 * 60)), | ||
key: Key::derive_from(secret), | ||
} | ||
} | ||
|
||
/// Sets a cookie path for this session middleware. | ||
/// The default for this value is "/" | ||
pub fn with_cookie_path(mut self, cookie_path: impl AsRef<str>) -> Self { | ||
self.cookie_path = cookie_path.as_ref().to_owned(); | ||
self | ||
} | ||
|
||
/// Sets a session ttl. This will be used both for the cookie | ||
/// expiry and also for the session-internal expiry. | ||
/// | ||
/// The default for this value is one day. Set this to None to not | ||
/// set a cookie or session expiry. This is not recommended. | ||
pub fn with_session_ttl(mut self, session_ttl: Option<Duration>) -> Self { | ||
self.session_ttl = session_ttl; | ||
self | ||
} | ||
|
||
/// Sets the name of the cookie that the session is stored with or in. | ||
/// | ||
/// If you are running multiple tide applications on the same | ||
/// domain, you will need different values for each | ||
/// application. The default value is "tide.sid" | ||
pub fn with_cookie_name(mut self, cookie_name: impl AsRef<str>) -> Self { | ||
self.cookie_name = cookie_name.as_ref().to_owned(); | ||
self | ||
} | ||
|
||
/// Disables the `save_unchanged` setting. When `save_unchanged` | ||
/// is enabled, a session will cookie will always be set. With | ||
/// `save_unchanged` disabled, the session data must be modified | ||
/// from the `Default` value in order for it to save. If a session | ||
/// already exists and its data unmodified in the course of a | ||
/// request, the session will only be persisted if | ||
/// `save_unchanged` is enabled. | ||
pub fn without_save_unchanged(mut self) -> Self { | ||
self.save_unchanged = false; | ||
self | ||
} | ||
|
||
/// Sets the same site policy for the session cookie. Defaults to | ||
/// SameSite::Strict. See [incrementally better | ||
/// cookies](https://tools.ietf.org/html/draft-west-cookie-incrementalism-01) | ||
/// for more information about this setting | ||
pub fn with_same_site_policy(mut self, policy: SameSite) -> Self { | ||
self.same_site_policy = policy; | ||
self | ||
} | ||
|
||
//--- methods below here are private --- | ||
|
||
async fn load_or_create(&self, cookie_value: Option<String>) -> Session { | ||
let session = match cookie_value { | ||
Some(cookie_value) => self.store.load_session(cookie_value).await.ok().flatten(), | ||
None => None, | ||
}; | ||
|
||
session | ||
.and_then(|session| session.validate()) | ||
.unwrap_or_default() | ||
} | ||
|
||
fn build_cookie(&self, secure: bool, cookie_value: String) -> Cookie<'static> { | ||
let mut cookie = Cookie::build(self.cookie_name.clone(), cookie_value) | ||
.http_only(true) | ||
.same_site(self.same_site_policy) | ||
.secure(secure) | ||
.path(self.cookie_path.clone()) | ||
.finish(); | ||
|
||
if let Some(ttl) = self.session_ttl { | ||
cookie.set_expires(Some((std::time::SystemTime::now() + ttl).into())); | ||
} | ||
|
||
self.sign_cookie(&mut cookie); | ||
|
||
cookie | ||
} | ||
|
||
// the following is reused verbatim from | ||
// https://github.com/SergioBenitez/cookie-rs/blob/master/src/secure/signed.rs#L33-L43 | ||
/// Signs the cookie's value providing integrity and authenticity. | ||
fn sign_cookie(&self, cookie: &mut Cookie<'_>) { | ||
// Compute HMAC-SHA256 of the cookie's value. | ||
let mut mac = Hmac::<Sha256>::new_varkey(&self.key.signing()).expect("good key"); | ||
mac.update(cookie.value().as_bytes()); | ||
|
||
// Cookie's new value is [MAC | original-value]. | ||
let mut new_value = base64::encode(&mac.finalize().into_bytes()); | ||
new_value.push_str(cookie.value()); | ||
cookie.set_value(new_value); | ||
} | ||
|
||
// the following is reused verbatim from | ||
// https://github.com/SergioBenitez/cookie-rs/blob/master/src/secure/signed.rs#L45-L63 | ||
/// Given a signed value `str` where the signature is prepended to `value`, | ||
/// verifies the signed value and returns it. If there's a problem, returns | ||
/// an `Err` with a string describing the issue. | ||
fn verify_signature(&self, cookie_value: &str) -> Result<String, &'static str> { | ||
if cookie_value.len() < BASE64_DIGEST_LEN { | ||
return Err("length of value is <= BASE64_DIGEST_LEN"); | ||
} | ||
|
||
// Split [MAC | original-value] into its two parts. | ||
let (digest_str, value) = cookie_value.split_at(BASE64_DIGEST_LEN); | ||
let digest = base64::decode(digest_str).map_err(|_| "bad base64 digest")?; | ||
|
||
// Perform the verification. | ||
let mut mac = Hmac::<Sha256>::new_varkey(&self.key.signing()).expect("good key"); | ||
mac.update(value.as_bytes()); | ||
mac.verify(&digest) | ||
.map(|_| value.to_string()) | ||
.map_err(|_| "value did not verify") | ||
} | ||
} |
Oops, something went wrong.