use axum::extract::{FromRequestParts, OptionalFromRequestParts}; use axum::response::Redirect; use axum_extra::extract::cookie::{Cookie, CookieJar}; use http::request::Parts; use http::uri::Uri; use uuid::Uuid; use std::sync::{Arc, RwLock}; use crate::db::User; use crate::http::{HttpState, InternalRedirect}; pub const COOKIE_NAME: &str = "lldap_session"; #[derive(Clone, Debug)] pub struct HttpSessionManager { inner: Arc>>, } impl HttpSessionManager { pub fn new() -> Self { Self { inner: Arc::new(RwLock::new(vec![])), } } pub fn add_session(&self, user: User, cookies: CookieJar) -> CookieJar { let uuid = Uuid::new_v4(); let mut cookie = Cookie::new(COOKIE_NAME, uuid.to_string()); cookie.set_path("/"); let session = HttpSession { user, uuid }; { self.inner.write().unwrap().push(session); } cookies.add(cookie) } pub fn get_session(&self, cookies: &CookieJar) -> Option { let cookie = cookies.get(COOKIE_NAME)?; let previous_uuid = Uuid::parse_str(cookie.value()).ok()?; self.inner .read() .unwrap() .iter() .find(|session| session.uuid == previous_uuid) .cloned() } pub fn remove_session(&self, session: &HttpSession, cookies: CookieJar) -> CookieJar { let mut cookie = Cookie::new(COOKIE_NAME, String::new()); cookie.set_path("/"); let idx = { let Some(idx) = self.inner.read().unwrap().iter().position(|s| s == session) else { return cookies; }; idx }; { self.inner.write().unwrap().remove(idx); } cookies.remove(cookie) } } #[derive(Clone, Debug)] pub struct HttpSession { // TODO: by storing the session here, it means // it needs to be updated every time we change the user. pub user: User, pub uuid: Uuid, } impl PartialEq for HttpSession { fn eq(&self, other: &Self) -> bool { self.uuid == other.uuid } } impl FromRequestParts for HttpSession { type Rejection = Redirect; async fn from_request_parts( parts: &mut Parts, state: &HttpState, ) -> Result { let cookies = CookieJar::from_request_parts(parts, state).await.unwrap(); if let Some(session) = state.sessions.get_session(&cookies) { return Ok(session); } // Extract the requested URL, turn it into base64, to let the login page // know where to redirect us. // But first, remove the scheme/host/port from URL. let s = Uri::builder() .path_and_query(parts.uri.path_and_query().unwrap().clone()) .build() .unwrap(); let r = InternalRedirect::from_string(s.to_string()); Err(Redirect::to(&format!( "/login?redirect={}", r.to_base64url() ))) } } pub struct OptionalHttpSession(pub HttpSession); impl std::ops::Deref for OptionalHttpSession { type Target = HttpSession; fn deref(&self) -> &Self::Target { &self.0 } } impl OptionalFromRequestParts for OptionalHttpSession { type Rejection = Redirect; async fn from_request_parts( parts: &mut Parts, state: &HttpState, ) -> Result, Self::Rejection> { let maybe_session = HttpSession::from_request_parts(parts, state) .await .ok() .map(Self); Ok(maybe_session) } }