use std::{
borrow::Cow,
future::Future,
marker::PhantomData,
pin::Pin,
sync::Arc,
task::{ready, Context, Poll},
};
use cookie::{Cookie, CookieJar};
use http::{Request, Response};
use pin_project_lite::pin_project;
use tower::{Layer, Service};
use tower_sesh_core::SessionStore;
use crate::{
config::{CookieSecurity, PlainCookie, PrivateCookie, SameSite, SignedCookie},
session::{self, Session},
util::CookieJarExt,
};
#[derive(Debug)]
pub struct SessionLayer<T, Store: SessionStore<T>, C: CookieSecurity = PrivateCookie> {
store: Arc<Store>,
config: Config,
cookie_controller: C,
_marker: PhantomData<fn() -> T>,
}
#[derive(Debug)]
pub struct SessionManager<S, T, Store: SessionStore<T>, C: CookieSecurity> {
inner: S,
layer: SessionLayer<T, Store, C>,
}
#[derive(Clone, Debug)]
pub(crate) struct Config {
pub(crate) cookie_name: Cow<'static, str>,
pub(crate) domain: Option<Cow<'static, str>>,
pub(crate) http_only: bool,
pub(crate) path: Cow<'static, str>,
pub(crate) same_site: SameSite,
pub(crate) secure: bool,
pub(crate) session_config: SessionConfig,
}
#[derive(Clone, Debug)]
pub(crate) struct SessionConfig {
pub(crate) ignore_invalid_session: bool,
}
const DEFAULT_COOKIE_NAME: &str = "id";
impl Default for Config {
fn default() -> Self {
Config {
cookie_name: Cow::Borrowed(DEFAULT_COOKIE_NAME),
domain: None,
http_only: true,
path: Cow::Borrowed("/"),
same_site: SameSite::Strict,
secure: true,
session_config: SessionConfig::default(),
}
}
}
impl Default for SessionConfig {
fn default() -> Self {
SessionConfig {
ignore_invalid_session: true,
}
}
}
impl<T, Store: SessionStore<T>> SessionLayer<T, Store> {
#[track_caller]
pub fn new(store: Arc<Store>, key: &[u8]) -> SessionLayer<T, Store> {
let key = match cookie::Key::try_from(key) {
Ok(key) => key,
Err(_) => panic!("key must be 64 bytes in length"),
};
Self {
store,
config: Config::default(),
cookie_controller: PrivateCookie::new(key),
_marker: PhantomData,
}
}
}
impl<T, Store: SessionStore<T>, C: CookieSecurity> SessionLayer<T, Store, C> {
#[track_caller]
pub fn signed(self) -> SessionLayer<T, Store, SignedCookie> {
let key = self.cookie_controller.into_key();
SessionLayer {
store: self.store,
config: self.config,
cookie_controller: SignedCookie::new(key),
_marker: PhantomData,
}
}
#[track_caller]
pub fn private(self) -> SessionLayer<T, Store, PrivateCookie> {
let key = self.cookie_controller.into_key();
SessionLayer {
store: self.store,
config: self.config,
cookie_controller: PrivateCookie::new(key),
_marker: PhantomData,
}
}
pub fn cookie_name(mut self, name: impl Into<Cow<'static, str>>) -> Self {
self.config.cookie_name = name.into();
self
}
pub fn domain(mut self, domain: impl Into<Cow<'static, str>>) -> Self {
self.config.domain = Some(domain.into());
self
}
pub fn http_only(mut self, enable: bool) -> Self {
self.config.http_only = enable;
self
}
pub fn path(mut self, path: impl Into<Cow<'static, str>>) -> Self {
self.config.path = path.into();
self
}
pub fn same_site(mut self, same_site: SameSite) -> Self {
self.config.same_site = same_site;
self
}
pub fn secure(mut self, enable: bool) -> Self {
self.config.secure = enable;
self
}
pub fn ignore_invalid_session(mut self, enable: bool) -> Self {
self.config.session_config.ignore_invalid_session = enable;
self
}
}
impl<T, Store: SessionStore<T>> SessionLayer<T, Store, PlainCookie> {
pub fn plain(store: Arc<Store>) -> SessionLayer<T, Store, PlainCookie> {
SessionLayer {
store,
config: Config::default(),
cookie_controller: PlainCookie,
_marker: PhantomData,
}
}
}
impl<T, Store: SessionStore<T>, C: CookieSecurity> Clone for SessionLayer<T, Store, C> {
fn clone(&self) -> Self {
Self {
store: Arc::clone(&self.store),
config: self.config.clone(),
cookie_controller: self.cookie_controller.clone(),
_marker: PhantomData,
}
}
}
impl<S, T, Store: SessionStore<T>, C: CookieSecurity> Layer<S> for SessionLayer<T, Store, C> {
type Service = SessionManager<S, T, Store, C>;
fn layer(&self, inner: S) -> Self::Service {
SessionManager {
inner,
layer: self.clone(),
}
}
}
impl<S, T, Store: SessionStore<T>, C: CookieSecurity> Clone for SessionManager<S, T, Store, C>
where
S: Clone,
{
fn clone(&self) -> Self {
SessionManager {
inner: self.inner.clone(),
layer: self.layer.clone(),
}
}
}
impl<S, T, Store: SessionStore<T>, C: CookieSecurity> SessionManager<S, T, Store, C> {
fn session_cookie<'c>(&self, jar: &'c CookieJar) -> Option<Cookie<'c>> {
self.layer
.cookie_controller
.get(jar, &self.layer.config.cookie_name)
}
}
impl<ReqBody, ResBody, S, T, Store: SessionStore<T>, C: CookieSecurity> Service<Request<ReqBody>>
for SessionManager<S, T, Store, C>
where
S: Service<Request<ReqBody>, Response = Response<ResBody>>,
T: 'static + Send + Sync,
{
type Response = S::Response;
type Error = S::Error;
type Future = ResponseFuture<S::Future, T, C>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, mut req: Request<ReqBody>) -> Self::Future {
let jar = CookieJar::from_headers(req.headers());
let cookie = self.session_cookie(&jar).map(Cookie::into_owned);
session::lazy::insert(
cookie,
&self.layer.store,
req.extensions_mut(),
self.layer.config.session_config.clone(),
);
let session: Option<Session<T>> =
session::lazy::take(req.extensions_mut()).expect("this panic should be removed");
todo!()
}
}
pin_project! {
pub struct ResponseFuture<F, T, C: CookieSecurity> {
state: State<T, C>,
#[pin]
future: F,
}
}
enum State<T, C> {
Session {
session: Session<T>,
cookie_controller: C,
},
Fallback,
}
impl<F, B, E, T, C: CookieSecurity> Future for ResponseFuture<F, T, C>
where
F: Future<Output = Result<Response<B>, E>>,
{
type Output = Result<Response<B>, E>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let mut res = ready!(this.future.poll(cx)?);
if let State::Session {
session,
cookie_controller,
} = this.state
{
todo!("sync changes in session state to store and set the `Set-Cookie` header");
}
Poll::Ready(Ok(res))
}
}