#![doc = include_str!("../README.md")]
#![warn(missing_docs)]
#[cfg(test)]
mod test;
pub mod memory;
#[cfg(feature = "redis")]
pub mod redis;
use std::{
sync::Arc,
time::Duration,
};
use rand::{rngs::OsRng, Rng, TryRngCore};
use rocket::{
fairing::{
Fairing,
Info,
Kind,
},
http::{
private::cookie::CookieBuilder,
Status,
},
request::{
FromRequest,
Outcome,
},
response::Responder,
tokio::sync::Mutex,
Build, Request, Response, Rocket, State,
};
use thiserror::Error;
fn new_id(length: usize) -> SessionID {
SessionID(
OsRng
.unwrap_err()
.sample_iter(&rand::distr::Alphanumeric)
.take(length)
.map(char::from)
.collect(),
)
}
const ID_LENGTH: usize = 24;
#[rocket::async_trait]
pub trait Store: Send + Sync {
type Value;
async fn get(&self, id: &str) -> SessionResult<Option<Self::Value>>;
async fn set(&self, id: &str, value: Self::Value, duration: Duration) -> SessionResult<()>;
async fn touch(&self, id: &str, duration: Duration) -> SessionResult<()>;
async fn remove(&self, id: &str) -> SessionResult<()>;
}
#[derive(Debug, Clone)]
struct SessionID(String);
impl AsRef<str> for SessionID {
fn as_ref(&self) -> &str {
&self.0
}
}
pub struct Session<'s, T: Send + Sync + Clone + 'static> {
store: &'s State<SessionStore<T>>,
token: SessionID,
new_token: Arc<Mutex<Option<SessionID>>>,
}
impl<'s, T: Send + Sync + Clone + 'static> Session<'s, T> {
pub async fn get(&self) -> SessionResult<Option<T>> {
self.store.store.get(self.token.as_ref()).await
}
pub async fn set(&self, value: T) -> SessionResult<()> {
self.store
.store
.set(self.token.as_ref(), value, self.store.duration)
.await
}
pub async fn touch(&self) -> SessionResult<()> {
self.store
.store
.touch(self.token.as_ref(), self.store.duration)
.await
}
pub async fn remove(&self) -> SessionResult<()> {
self.store.store.remove(self.token.as_ref()).await
}
pub async fn regenerate_token<'r>(&mut self) -> SessionResult<()> {
let mut new_token_opt = self.new_token.lock().await;
if new_token_opt.is_some() {
return Ok(());
}
let session_opt = self.get().await?;
self.remove().await?;
self.token = new_id(ID_LENGTH);
*new_token_opt = Some(self.token.clone());
if let Some(session) = session_opt {
self.set(session).await?;
}
Ok(())
}
}
#[rocket::async_trait]
impl<T, 'r, 's> FromRequest<'r> for Session<'s, T>
where
T: Send + Sync + 'static + Clone,
'r: 's,
{
type Error = ();
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let store: &State<SessionStore<T>> = request
.guard()
.await
.expect("Session store must be set in fairing");
let (token, new_token) = request
.local_cache_async(async {
let cookies = request.cookies();
cookies.get(store.name.as_str()).map_or_else(
|| {
let token = new_id(ID_LENGTH);
(token.clone(), Arc::new(Mutex::new(Some(token))))
},
|c| {
(
SessionID(String::from(c.value())),
Arc::new(Mutex::new(None)),
)
},
)
})
.await
.clone();
let session = Session {
store,
token,
new_token,
};
Outcome::Success(session)
}
}
pub struct SessionStore<T> {
pub store: Box<dyn Store<Value = T>>,
pub name: String,
pub duration: Duration,
pub cookie_builder: CookieBuilder<'static>,
}
impl<T> SessionStore<T> {
pub fn fairing(self) -> SessionStoreFairing<T> {
SessionStoreFairing {
store: Mutex::new(Some(self)),
}
}
}
pub struct SessionStoreFairing<T> {
store: Mutex<Option<SessionStore<T>>>,
}
#[rocket::async_trait]
impl<T: Send + Sync + Clone + 'static> Fairing for SessionStoreFairing<T> {
fn info(&self) -> rocket::fairing::Info {
Info {
name: "Session Store",
kind: Kind::Ignite | Kind::Response | Kind::Singleton,
}
}
async fn on_ignite(&self, rocket: Rocket<Build>) -> Result<Rocket<Build>, Rocket<Build>> {
let mut lock = self.store.lock().await;
let store = lock.take().expect("Expected store");
let rocket = rocket.manage(store);
Ok(rocket)
}
async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
match Session::<T>::from_request(request).await {
Outcome::Success(session) => {
if let Some(new_token) = &*session.new_token.lock().await {
let mut cookie = session.store.cookie_builder.clone().build();
cookie.set_name(&session.store.name);
cookie.set_value(&new_token.0);
response.adjoin_header(cookie);
}
}
_ => (),
}
}
}
pub type SessionResult<T> = Result<T, SessionError>;
#[derive(Error, Debug)]
#[error("could not access the session store")]
pub struct SessionError;
impl<'r, 'o: 'r> Responder<'r, 'o> for SessionError {
fn respond_to(self, _request: &'r Request<'_>) -> rocket::response::Result<'o> {
Err(Status::InternalServerError)
}
}