use std::sync::Arc;
use actix_web::{
body::EitherBody,
dev::{ServiceRequest, ServiceResponse, Transform, Service},
http::header::SET_COOKIE,
HttpMessage,
};
use std::future::Ready;
use crate::session::{SessionStore, SessionConfig, Session};
pub struct SessionCookie {
store: Arc<dyn SessionStore>,
config: SessionConfig,
}
impl SessionCookie {
pub fn new(store: Arc<dyn SessionStore>, config: SessionConfig) -> Self {
Self { store, config }
}
}
impl<S, B> Transform<S, ServiceRequest> for SessionCookie
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + Send + 'static,
B: 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = actix_web::Error;
type Transform = SessionCookieMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
let middleware = SessionCookieMiddleware {
service: Arc::new(std::sync::Mutex::new(Some(service))),
store: self.store.clone(),
config: self.config.clone(),
};
std::future::ready(Ok(middleware))
}
}
pub struct SessionCookieMiddleware<S> {
service: Arc<std::sync::Mutex<Option<S>>>,
store: Arc<dyn SessionStore>,
config: SessionConfig,
}
impl<S, B> Service<ServiceRequest> for SessionCookieMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + Send + 'static,
B: 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = actix_web::Error;
type Future = std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + 'static>>;
fn poll_ready(&self, _ctx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
std::task::Poll::Ready(Ok(()))
}
fn call(&self, req: ServiceRequest) -> Self::Future {
let service = self.service.lock().unwrap().take().expect("service already taken");
let store = self.store.clone();
let config = self.config.clone();
let cookie_name = config.name.clone();
let session_id_opt = req.cookie(&cookie_name).map(|c| c.value().to_string());
Box::pin(async move {
let session = load_or_create_session(&store, &config, session_id_opt.as_deref()).await?;
let session = Arc::new(tokio::sync::Mutex::new(session));
req.extensions_mut().insert(session.clone());
let response = service.call(req).await?.map_into_left_body();
let current = session.lock().await.clone();
if let Err(e) = store.save(¤t).await {
tracing::warn!("Failed to persist session: {}", e);
}
let same_site_str = match config.same_site {
actix_web::cookie::SameSite::Strict => "Strict",
actix_web::cookie::SameSite::Lax => "Lax",
actix_web::cookie::SameSite::None => "None",
};
let cookie_str = format!(
"{}={}; Path={}; HttpOnly; SameSite={}",
config.name,
current.id,
config.path,
same_site_str
);
let mut response = response;
response.headers_mut().append(SET_COOKIE, cookie_str.parse().unwrap());
Ok(response)
})
}
}
async fn load_or_create_session(
store: &Arc<dyn SessionStore>,
config: &SessionConfig,
cookie_value: Option<&str>,
) -> Result<Session, actix_web::Error> {
match cookie_value {
Some(id) => {
match store.load(id).await {
Ok(Some(s)) => Ok(s),
Ok(None) => Ok(Session::new(config.ttl)),
Err(e) => Err(actix_web::error::ErrorInternalServerError(format!(
"session error: {}", e
))),
}
}
None => Ok(Session::new(config.ttl)),
}
}
pub trait SessionExt {
fn session(&self) -> Option<Arc<tokio::sync::Mutex<Session>>>;
}
impl SessionExt for actix_web::HttpRequest {
fn session(&self) -> Option<Arc<tokio::sync::Mutex<Session>>> {
self.extensions().get::<Arc<tokio::sync::Mutex<Session>>>().cloned()
}
}