Skip to content

Commit 92fe62e

Browse files
committed
get async-session working with tide
1 parent 5af77ce commit 92fe62e

File tree

6 files changed

+983
-170
lines changed

6 files changed

+983
-170
lines changed

Cargo.toml

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,19 @@ authors = [
1717

1818
[dependencies]
1919
async-trait = "0.1.24"
20-
async-std = "1.5.0"
21-
cookie = "0.13.3"
22-
uuid = {version = "0.8.1", features = ["v4"] }
20+
async-std = "1.6.0"
21+
serde = { version = "1.0.114", features = ["rc", "derive"] }
22+
rand = "0.7.3"
23+
base64 = "0.12.3"
24+
sha2 = "0.9.1"
25+
hmac = "0.8.1"
26+
serde_json = "1.0.56"
27+
kv-log-macro = "1.0.7"
28+
bincode = "1.3.1"
29+
chrono = { version = "0.4.13", features = ["serde"] }
30+
anyhow = "1.0.31"
31+
blake3 = "0.3.5"
32+
2333

2434
[dev-dependencies]
25-
tide = "0.6.0"
35+
async-std = { version = "1.6.2", features = ["attributes"] }

src/cookie_store.rs

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
use crate::{async_trait, Result, Session, SessionStore};
2+
3+
/// A session store that serializes the entire session into a Cookie.
4+
///
5+
/// # ***This is not recommended for most production deployments.***
6+
///
7+
/// This implementation uses [`bincode`](::bincode) to serialize the
8+
/// Session to decrease the size of the cookie. Note: There is a
9+
/// maximum of 4093 cookie bytes allowed _per domain_, so the cookie
10+
/// store is limited in capacity.
11+
///
12+
/// **Note:** Currently, the data in the cookie is only signed, but *not
13+
/// encrypted*. If the contained session data is sensitive and
14+
/// should not be read by a user, the cookie store is not an
15+
/// appropriate choice.
16+
///
17+
/// Expiry: `SessionStore::destroy_session` and
18+
/// `SessionStore::clear_store` are not meaningful for the
19+
/// CookieStore, and noop. Destroying a session must be done at the
20+
/// cookie setting level, which is outside of the scope of this crate.
21+
22+
#[derive(Debug, Clone, Copy)]
23+
pub struct CookieStore;
24+
25+
impl CookieStore {
26+
/// constructs a new CookieStore
27+
pub fn new() -> Self {
28+
Self
29+
}
30+
}
31+
32+
#[async_trait]
33+
impl SessionStore for CookieStore {
34+
async fn load_session(&self, cookie_value: String) -> Result<Option<Session>> {
35+
let serialized = base64::decode(&cookie_value)?;
36+
let session: Session = bincode::deserialize(&serialized)?;
37+
Ok(session.validate())
38+
}
39+
40+
async fn store_session(&self, session: Session) -> Result<Option<String>> {
41+
let serialized = bincode::serialize(&session)?;
42+
Ok(Some(base64::encode(serialized)))
43+
}
44+
45+
async fn destroy_session(&self, _session: Session) -> Result {
46+
Ok(())
47+
}
48+
49+
async fn clear_store(&self) -> Result {
50+
Ok(())
51+
}
52+
}
53+
54+
#[cfg(test)]
55+
mod tests {
56+
use super::*;
57+
use async_std::task;
58+
use std::time::Duration;
59+
#[async_std::test]
60+
async fn creating_a_new_session_with_no_expiry() -> Result {
61+
let store = CookieStore::new();
62+
let mut session = Session::new();
63+
session.insert("key", "Hello")?;
64+
let cloned = session.clone();
65+
let cookie_value = store.store_session(session).await?.unwrap();
66+
let loaded_session = store.load_session(cookie_value).await?.unwrap();
67+
assert_eq!(cloned.id(), loaded_session.id());
68+
assert_eq!("Hello", &loaded_session.get::<String>("key").unwrap());
69+
assert!(!loaded_session.is_expired());
70+
assert!(loaded_session.validate().is_some());
71+
Ok(())
72+
}
73+
74+
#[async_std::test]
75+
async fn updating_a_session() -> Result {
76+
let store = CookieStore::new();
77+
let mut session = Session::new();
78+
79+
session.insert("key", "value")?;
80+
let cookie_value = store.store_session(session).await?.unwrap();
81+
82+
let mut session = store.load_session(cookie_value.clone()).await?.unwrap();
83+
session.insert("key", "other value")?;
84+
85+
let new_cookie_value = store.store_session(session).await?.unwrap();
86+
let session = store.load_session(new_cookie_value).await?.unwrap();
87+
assert_eq!(&session.get::<String>("key").unwrap(), "other value");
88+
89+
Ok(())
90+
}
91+
92+
#[async_std::test]
93+
async fn updating_a_session_extending_expiry() -> Result {
94+
let store = CookieStore::new();
95+
let mut session = Session::new();
96+
session.expire_in(Duration::from_secs(1));
97+
let original_expires = session.expiry().unwrap().clone();
98+
let cookie_value = store.store_session(session).await?.unwrap();
99+
100+
let mut session = store.load_session(cookie_value.clone()).await?.unwrap();
101+
102+
assert_eq!(session.expiry().unwrap(), &original_expires);
103+
session.expire_in(Duration::from_secs(3));
104+
let new_expires = session.expiry().unwrap().clone();
105+
let cookie_value = store.store_session(session).await?.unwrap();
106+
107+
let session = store.load_session(cookie_value.clone()).await?.unwrap();
108+
assert_eq!(session.expiry().unwrap(), &new_expires);
109+
110+
task::sleep(Duration::from_secs(3)).await;
111+
assert_eq!(None, store.load_session(cookie_value).await?);
112+
113+
Ok(())
114+
}
115+
116+
#[async_std::test]
117+
async fn creating_a_new_session_with_expiry() -> Result {
118+
let store = CookieStore::new();
119+
let mut session = Session::new();
120+
session.expire_in(Duration::from_secs(3));
121+
session.insert("key", "value")?;
122+
let cloned = session.clone();
123+
124+
let cookie_value = store.store_session(session).await?.unwrap();
125+
126+
let loaded_session = store.load_session(cookie_value.clone()).await?.unwrap();
127+
assert_eq!(cloned.id(), loaded_session.id());
128+
assert_eq!("value", &*loaded_session.get::<String>("key").unwrap());
129+
130+
assert!(!loaded_session.is_expired());
131+
132+
task::sleep(Duration::from_secs(3)).await;
133+
assert_eq!(None, store.load_session(cookie_value).await?);
134+
135+
Ok(())
136+
}
137+
}

src/lib.rs

Lines changed: 51 additions & 166 deletions
Original file line numberDiff line numberDiff line change
@@ -1,185 +1,70 @@
11
//! Async HTTP sessions.
22
//!
3-
//! This crate provides a generic interface between cookies and storage
4-
//! backends to create a concept of sessions. It provides an interface that
5-
//! can be used to encode and store sessions, and decode and load sessions
6-
//! generating cookies in the process.
3+
//! This crate provides a generic interface between cookie values and
4+
//! storage backends to create a concept of sessions. It provides an
5+
//! interface that can be used to encode and store sessions, and
6+
//! decode and load sessions generating cookies in the process.
77
//!
8-
//! # Security
9-
//!
10-
//! This module has not been vetted for security purposes, and in particular
11-
//! the in-memory storage backend is wildly insecure. Please thoroughly
12-
//! validate whether this crate is a match for your intended use case before
13-
//! relying on it in any sensitive context.
14-
//!
15-
//! # Examples
8+
//! # Example
169
//!
1710
//! ```
18-
//! use async_session::mem::MemoryStore;
19-
//! use async_session::{Session, SessionStore};
20-
//! use cookie::CookieJar;
11+
//! use async_session::{Session, SessionStore, MemoryStore};
2112
//!
22-
//! # fn main() -> std::io::Result<()> {
13+
//! # fn main() -> async_session::Result {
2314
//! # async_std::task::block_on(async {
2415
//! #
2516
//! // Init a new session store we can persist sessions to.
2617
//! let mut store = MemoryStore::new();
2718
//!
2819
//! // Create a new session.
29-
//! let sess = store.create_session();
20+
//! let mut session = Session::new();
21+
//! session.insert("user_id", 1)?;
22+
//! assert!(session.data_changed());
3023
//!
31-
//! // Persist the session to our backend, and store a cookie
32-
//! // to later access the session.
33-
//! let mut jar = CookieJar::new();
34-
//! let sess = store.store_session(sess, &mut jar).await?;
24+
//! // retrieve the cookie value to store in a session cookie
25+
//! let cookie_value = store.store_session(session).await?.unwrap();
3526
//!
3627
//! // Retrieve the session using the cookie.
37-
//! let sess = store.load_session(&jar).await?;
38-
//! println!("session: {:?}", sess);
28+
//! let session = store.load_session(cookie_value).await?.unwrap();
29+
//! assert_eq!(session.get::<usize>("user_id").unwrap(), 1);
30+
//! assert!(!session.data_changed());
3931
//! #
4032
//! # Ok(()) }) }
4133
//! ```
4234
43-
#![forbid(unsafe_code, future_incompatible, rust_2018_idioms)]
44-
#![deny(missing_debug_implementations, nonstandard_style)]
45-
#![warn(missing_docs, missing_doc_code_examples, unreachable_pub)]
46-
47-
use async_trait::async_trait;
48-
use std::collections::HashMap;
49-
50-
/// An async session backend.
51-
#[async_trait]
52-
pub trait SessionStore: Send + Sync + 'static + Clone {
53-
/// The type of error that can occur when storing and loading errors.
54-
type Error;
55-
56-
/// Get a session from the storage backend.
57-
///
58-
/// The input should usually be the content of a cookie. This will then be
59-
/// parsed by the session middleware into a valid session.
60-
async fn load_session(&self, jar: &cookie::CookieJar) -> Result<Session, Self::Error>;
61-
62-
/// Store a session on the storage backend.
63-
///
64-
/// This method should return a stringified representation of the session so
65-
/// that it can be sent back to the client through a cookie.
66-
async fn store_session(
67-
&mut self,
68-
session: Session,
69-
jar: &mut cookie::CookieJar,
70-
) -> Result<(), Self::Error>;
71-
}
72-
73-
/// The main session type.
74-
#[derive(Clone, Debug)]
75-
pub struct Session {
76-
inner: HashMap<String, String>,
77-
}
78-
79-
impl Session {
80-
/// Create a new session.
81-
pub fn new() -> Self {
82-
Self {
83-
inner: HashMap::new(),
84-
}
85-
}
86-
87-
/// Insert a new value into the Session.
88-
pub fn insert(&mut self, k: String, v: String) -> Option<String> {
89-
self.inner.insert(k, v)
90-
}
91-
92-
/// Get a value from the session.
93-
pub fn get(&self, k: &str) -> Option<&String> {
94-
self.inner.get(k)
95-
}
96-
}
97-
98-
/// In-memory session store.
99-
pub mod mem {
100-
use async_std::io::{Error, ErrorKind};
101-
use async_std::sync::{Arc, RwLock};
102-
use cookie::Cookie;
103-
use std::collections::HashMap;
104-
105-
use async_trait::async_trait;
106-
use uuid::Uuid;
107-
108-
use crate::{Session, SessionStore};
109-
110-
/// An in-memory session store.
111-
///
112-
/// # Security
113-
///
114-
/// This store *does not* generate secure sessions, and should under no
115-
/// circumstance be used in production. It's meant only to quickly create
116-
/// sessions.
117-
#[derive(Debug)]
118-
pub struct MemoryStore {
119-
inner: Arc<RwLock<HashMap<String, Session>>>,
120-
}
121-
122-
impl MemoryStore {
123-
/// Create a new instance of MemoryStore.
124-
pub fn new() -> Self {
125-
Self {
126-
inner: Arc::new(RwLock::new(HashMap::new())),
127-
}
128-
}
129-
130-
/// Generates a new session by generating a new uuid.
131-
///
132-
/// This is *not* a secure way of generating sessions, and is intended for debug purposes only.
133-
pub fn create_session(&self) -> Session {
134-
let mut sess = Session::new();
135-
sess.insert("id".to_string(), uuid::Uuid::new_v4().to_string());
136-
sess
137-
}
138-
}
139-
140-
impl Clone for MemoryStore {
141-
fn clone(&self) -> Self {
142-
Self {
143-
inner: self.inner.clone(),
144-
}
145-
}
146-
}
147-
148-
#[async_trait]
149-
impl SessionStore for MemoryStore {
150-
/// The type of error that can occur when storing and loading errors.
151-
type Error = std::io::Error;
152-
153-
/// Get a session from the storage backend.
154-
async fn load_session(&self, jar: &cookie::CookieJar) -> Result<Session, Self::Error> {
155-
let id = match jar.get("session") {
156-
Some(cookie) => Uuid::parse_str(cookie.value()),
157-
None => return Err(Error::new(ErrorKind::Other, "No session cookie found")),
158-
};
159-
160-
let id = id
161-
.map_err(|_| Error::new(ErrorKind::Other, "Cookie content was not a valid uuid"))?
162-
.to_string();
163-
164-
let inner = self.inner.read().await;
165-
let sess = inner.get(&id).ok_or(Error::from(ErrorKind::Other))?;
166-
Ok(sess.clone())
167-
}
168-
169-
/// Store a session on the storage backend.
170-
///
171-
/// The data inside the session will be url-encoded so it can be stored
172-
/// inside a cookie.
173-
async fn store_session(
174-
&mut self,
175-
sess: Session,
176-
jar: &mut cookie::CookieJar,
177-
) -> Result<(), Self::Error> {
178-
let mut inner = self.inner.write().await;
179-
let id = sess.get("id").unwrap().to_string();
180-
inner.insert(id.clone(), sess);
181-
jar.add(Cookie::new("session", id));
182-
Ok(())
183-
}
184-
}
185-
}
35+
// #![forbid(unsafe_code, future_incompatible)]
36+
// #![deny(missing_debug_implementations, nonstandard_style)]
37+
// #![warn(missing_docs, missing_doc_code_examples, unreachable_pub)]
38+
#![forbid(unsafe_code, future_incompatible)]
39+
#![deny(
40+
missing_debug_implementations,
41+
nonstandard_style,
42+
missing_docs,
43+
unreachable_pub,
44+
missing_copy_implementations,
45+
unused_qualifications
46+
)]
47+
48+
pub use anyhow::Error;
49+
/// An anyhow::Result with default return type of ()
50+
pub type Result<T = ()> = std::result::Result<T, Error>;
51+
52+
mod cookie_store;
53+
mod memory_store;
54+
mod session;
55+
mod session_store;
56+
57+
pub use cookie_store::CookieStore;
58+
pub use memory_store::MemoryStore;
59+
pub use session::Session;
60+
pub use session_store::SessionStore;
61+
62+
pub use async_trait::async_trait;
63+
pub use base64;
64+
pub use blake3;
65+
pub use chrono;
66+
pub use hmac;
67+
pub use kv_log_macro as log;
68+
pub use serde;
69+
pub use serde_json;
70+
pub use sha2;

0 commit comments

Comments
 (0)