#![deny(missing_docs)]
#![deny(rustdoc::broken_intra_doc_links)]
pub mod cache;
pub mod cookie;
pub mod database;
pub mod driver;
pub mod manager;
pub mod middleware;
pub mod session;
pub mod store;
pub use cache::CacheSessionStore;
pub use cookie::CookieSessionStore;
pub use database::{DatabaseSessionStore, SessionRow};
pub use driver::SessionDriver;
pub use manager::{SessionConfig, SessionManager};
pub use middleware::{SessionRequestExt, StartSession};
pub use session::{generate_session_id, Session, SessionData, TOKEN_KEY};
pub use store::{is_well_formed, MemorySessionStore, SessionStore};
#[cfg(test)]
mod tests {
use super::*;
use rainier_http::{Request, Response};
use rainier_middleware::{Middleware, Pipeline};
use std::sync::Arc;
#[tokio::test]
async fn logging_in_rotates_the_session_and_keeps_the_user() {
let store = Arc::new(MemorySessionStore::default());
let start: Arc<dyn Middleware> =
Arc::new(StartSession::new(Arc::clone(&store) as Arc<dyn SessionStore>));
let run = |cookie: Option<String>, action: &'static str| {
let start = Arc::clone(&start);
async move {
let mut builder = Request::builder();
if let Some(cookie) = cookie {
builder = builder.header("cookie", &cookie);
}
Pipeline::new()
.through_arc(start)
.then(move |request: Request| async move {
let session = request.session().expect("a session");
match action {
"visit" => {
session.put("cart", vec![1u64, 2]).unwrap();
}
"login" => {
session.regenerate();
session.put("user_id", 7u64).unwrap();
}
_ => {}
}
Response::text("ok")
})
.run(builder.build())
.await
}
};
let cookie_of = |response: &Response| {
response
.header("set-cookie")
.and_then(|h| h.split(';').next())
.and_then(|c| c.strip_prefix("rainier_session="))
.map(str::to_string)
};
let first = run(None, "visit").await;
let anonymous = cookie_of(&first).expect("a cookie");
let second = run(Some(format!("rainier_session={anonymous}")), "login").await;
let authenticated = cookie_of(&second).expect("a new cookie");
assert_ne!(authenticated, anonymous, "the id must change on login");
assert!(
store.read(&anonymous).await.unwrap().is_none(),
"the pre-login row is what a fixation attack holds"
);
let data = store.read(&authenticated).await.unwrap().expect("the new session");
assert_eq!(data.values["user_id"], 7);
assert_eq!(data.values["cart"], serde_json::json!([1, 2]), "the cart survives login");
}
}
#[cfg(test)]
mod driver_tests {
use super::*;
use rainier_cache::MemoryCache;
use rainier_crypt::{Encryption, Key, KeyRing};
use rainier_http::{Request, Response};
use rainier_middleware::{Middleware, Pipeline};
use std::sync::Arc;
async fn counts_across_requests(store: Arc<dyn SessionStore>) -> (u64, u64) {
let start: Arc<dyn Middleware> = Arc::new(StartSession::new(store));
let visit = |cookie: Option<String>| {
let start = Arc::clone(&start);
async move {
let mut builder = Request::builder();
if let Some(cookie) = cookie {
builder = builder.header("cookie", &cookie);
}
let response = Pipeline::new()
.through_arc(start)
.then(|request: Request| async move {
let session = request.session().expect("a session");
let seen: u64 = session.get("seen").unwrap_or(0);
session.put("seen", seen + 1).unwrap();
Response::text(seen.to_string())
})
.run(builder.build())
.await;
let cookie = response
.header("set-cookie")
.and_then(|h| h.split(';').next())
.and_then(|c| c.strip_prefix("rainier_session="))
.map(str::to_string);
let body = response.into_http().into_body().collect().await.unwrap();
(String::from_utf8(body.to_vec()).unwrap().parse::<u64>().unwrap(), cookie)
}
};
let (first, cookie) = visit(None).await;
let cookie = cookie.expect("the session should be persisted");
let (second, _) = visit(Some(format!("rainier_session={cookie}"))).await;
(first, second)
}
fn crypt() -> Encryption {
Encryption::from_keys(KeyRing::new(Key::generate()))
}
#[tokio::test]
async fn every_driver_persists_a_session_across_requests() {
let drivers: Vec<(&str, Arc<dyn SessionStore>)> = vec![
("memory", Arc::new(MemorySessionStore::default())),
("cache", Arc::new(CacheSessionStore::new(Arc::new(MemoryCache::new())))),
("cookie", Arc::new(CookieSessionStore::new(crypt()))),
];
for (name, store) in drivers {
assert_eq!(counts_across_requests(store).await, (0, 1), "{name}");
}
}
#[tokio::test]
async fn the_cookie_driver_carries_the_session_and_not_an_id() {
let store: Arc<dyn SessionStore> = Arc::new(CookieSessionStore::new(crypt()));
let start: Arc<dyn Middleware> = Arc::new(StartSession::new(Arc::clone(&store)));
let response = Pipeline::new()
.through_arc(start)
.then(|request: Request| async move {
request.session().unwrap().put("user_id", 42u64).unwrap();
Response::text("ok")
})
.run(Request::builder().build())
.await;
let value = response
.header("set-cookie")
.and_then(|h| h.split(';').next())
.and_then(|c| c.strip_prefix("rainier_session="))
.expect("a cookie");
assert!(value.len() > 64, "a cookie session is longer than an id: {}", value.len());
assert!(!is_well_formed(value), "it is not an id at all");
assert!(!value.contains("user_id"), "the client cannot read it: {value}");
}
#[tokio::test]
async fn a_cookie_session_from_another_applications_key_is_discarded() {
let store: Arc<dyn SessionStore> = Arc::new(CookieSessionStore::new(crypt()));
let theirs = CookieSessionStore::new(crypt());
let forged = theirs.encode(&generate_session_id(), &SessionData::default()).unwrap();
let start: Arc<dyn Middleware> = Arc::new(StartSession::new(store));
let response = Pipeline::new()
.through_arc(start)
.then(|request: Request| async move {
let session = request.session().expect("a fresh session");
Response::text(session.is_empty().to_string())
})
.run(Request::builder().header("cookie", &format!("rainier_session={forged}")).build())
.await;
let body = response.into_http().into_body().collect().await.unwrap();
assert_eq!(body, "true", "a session we did not write must not be trusted");
}
}