use std::{borrow::Cow, fmt, future::Future, pin::Pin, rc::Rc, sync::Arc};
use actix_utils::future::{ready, Ready};
use actix_web::{
body::MessageBody,
cookie::{time::Duration, Cookie, CookieJar, Key},
dev::{forward_ready, ResponseHead, Service, ServiceRequest, ServiceResponse, Transform},
http::header::{HeaderValue, SET_COOKIE},
HttpResponse,
};
use serde_json::{Map, Value};
use super::{
config::{
self, Configuration, CookieConfiguration, CookieContentSecurity, SessionMiddlewareBuilder,
TtlExtensionPolicy,
},
storage::{SessionKey, SessionStore},
Session, SessionStatus,
};
use crate::{memorydb::MemoryDB, Result};
#[derive(Clone)]
pub struct SessionMiddleware {
storage_backend: Rc<SessionStore>,
configuration: Rc<Configuration>,
}
impl SessionMiddleware {
pub fn new(client: Arc<dyn MemoryDB>, key: Key) -> Self {
Self::builder(client, key).build()
}
pub fn builder(client: Arc<dyn MemoryDB>, key: Key) -> SessionMiddlewareBuilder {
SessionMiddlewareBuilder::new(client, config::default_configuration(key))
}
pub(crate) fn from_parts(store: SessionStore, configuration: Configuration) -> Self {
Self {
storage_backend: Rc::new(store),
configuration: Rc::new(configuration),
}
}
}
impl<S, B> Transform<S, ServiceRequest> for SessionMiddleware
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
S::Future: 'static,
B: MessageBody + 'static,
{
type Response = ServiceResponse<B>;
type Error = actix_web::Error;
type Transform = InnerSessionMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(InnerSessionMiddleware {
service: Rc::new(service),
configuration: Rc::clone(&self.configuration),
storage_backend: Rc::clone(&self.storage_backend),
}))
}
}
fn e500<E: fmt::Debug + fmt::Display + 'static>(err: E) -> actix_web::Error {
actix_web::error::InternalError::from_response(
err,
HttpResponse::InternalServerError().finish(),
)
.into()
}
#[doc(hidden)]
#[non_exhaustive]
pub struct InnerSessionMiddleware<S> {
service: Rc<S>,
configuration: Rc<Configuration>,
storage_backend: Rc<SessionStore>,
}
impl<S, B> Service<ServiceRequest> for InnerSessionMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
S::Future: 'static,
{
type Response = ServiceResponse<B>;
type Error = actix_web::Error;
#[allow(clippy::type_complexity)]
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
forward_ready!(service);
fn call(&self, mut req: ServiceRequest) -> Self::Future {
let service = Rc::clone(&self.service);
let storage_backend = Rc::clone(&self.storage_backend);
let configuration = Rc::clone(&self.configuration);
Box::pin(async move {
let session_key = extract_session_key(&req, &configuration.cookie);
let (session_key, session_state) =
load_session_state(session_key, storage_backend.as_ref()).await?;
Session::set_session(&mut req, session_state);
let mut res = service.call(req).await?;
let (status, session_state) = Session::get_changes(&mut res);
let mut ttl = configuration.session.state_ttl;
let mut cookie = Cow::Borrowed(&configuration.cookie);
if let Some(x) = session_state.get("_ttl") {
if let Some(x) = x.as_i64() {
ttl = Duration::seconds(x);
let mut tmp = cookie.into_owned();
tmp.max_age = Some(ttl);
cookie = Cow::Owned(tmp);
}
}
let id = session_state
.get("_id")
.map(|x| x.to_string().trim_matches('"').to_owned());
match session_key {
None => {
if !session_state.is_empty() {
let session_key = storage_backend
.save(session_state, &id, &ttl)
.await
.map_err(e500)?;
set_session_cookie(res.response_mut().head_mut(), session_key, &cookie)
.map_err(e500)?;
}
}
Some(session_key) => {
match status {
SessionStatus::Changed => {
let session_key = storage_backend
.update(session_key, session_state, &id, &ttl)
.await
.map_err(e500)?;
set_session_cookie(res.response_mut().head_mut(), session_key, &cookie)
.map_err(e500)?;
}
SessionStatus::Purged => {
storage_backend
.delete(&session_key, &id)
.await
.map_err(e500)?;
delete_session_cookie(res.response_mut().head_mut(), &cookie)
.map_err(e500)?;
}
SessionStatus::Renewed => {
storage_backend
.delete(&session_key, &id)
.await
.map_err(e500)?;
let session_key = storage_backend
.save(session_state, &id, &ttl)
.await
.map_err(e500)?;
set_session_cookie(res.response_mut().head_mut(), session_key, &cookie)
.map_err(e500)?;
}
SessionStatus::Unchanged => {
if matches!(
configuration.ttl_extension_policy,
TtlExtensionPolicy::OnEveryRequest
) {
storage_backend
.update_ttl(&session_key, &id, &ttl)
.await
.map_err(e500)?;
if configuration.cookie.max_age.is_some() {
set_session_cookie(
res.response_mut().head_mut(),
session_key,
&cookie,
)
.map_err(e500)?;
}
}
}
};
}
}
Ok(res)
})
}
}
fn extract_session_key(req: &ServiceRequest, config: &CookieConfiguration) -> Option<SessionKey> {
let cookies = req.cookies().ok()?;
let session_cookie = cookies
.iter()
.find(|&cookie| cookie.name() == config.name)?;
let mut jar = CookieJar::new();
jar.add_original(session_cookie.clone());
let verification_result = match config.content_security {
CookieContentSecurity::Signed => jar.signed(&config.key).get(&config.name),
CookieContentSecurity::Private => jar.private(&config.key).get(&config.name),
};
verification_result?.value().to_owned().try_into().ok()
}
async fn load_session_state(
session_key: Option<SessionKey>,
storage_backend: &SessionStore,
) -> Result<(Option<SessionKey>, Map<String, Value>), actix_web::Error> {
if let Some(session_key) = session_key {
match storage_backend.load(&session_key).await {
Ok(state) => {
if let Some(state) = state {
Ok((Some(session_key), state))
} else {
Ok((None, Map::new()))
}
}
Err(err) => Err(e500(err)),
}
} else {
Ok((None, Map::new()))
}
}
fn set_session_cookie(
response: &mut ResponseHead,
session_key: SessionKey,
config: &CookieConfiguration,
) -> Result<()> {
let value: String = session_key.into();
let mut cookie = Cookie::new(config.name.clone(), value);
cookie.set_secure(config.secure);
cookie.set_http_only(config.http_only);
cookie.set_same_site(config.same_site);
cookie.set_path(config.path.clone());
if let Some(max_age) = config.max_age {
cookie.set_max_age(max_age);
}
if let Some(ref domain) = config.domain {
cookie.set_domain(domain.clone());
}
let mut jar = CookieJar::new();
match config.content_security {
CookieContentSecurity::Signed => jar.signed_mut(&config.key).add(cookie),
CookieContentSecurity::Private => jar.private_mut(&config.key).add(cookie),
}
let cookie = jar.delta().next().unwrap();
let val = HeaderValue::from_str(&cookie.encoded().to_string())?;
response.headers_mut().append(SET_COOKIE, val);
Ok(())
}
fn delete_session_cookie(response: &mut ResponseHead, config: &CookieConfiguration) -> Result<()> {
let removal_cookie = Cookie::build(config.name.clone(), "")
.path(config.path.clone())
.secure(config.secure)
.http_only(config.http_only)
.same_site(config.same_site);
let mut removal_cookie = if let Some(ref domain) = config.domain {
removal_cookie.domain(domain)
} else {
removal_cookie
}
.finish();
removal_cookie.make_removal();
let val = HeaderValue::from_str(&removal_cookie.to_string())?;
response.headers_mut().append(SET_COOKIE, val);
Ok(())
}