diff --git a/src/routes/login.rs b/src/routes/login.rs index 390190f..1d4bc8d 100644 --- a/src/routes/login.rs +++ b/src/routes/login.rs @@ -5,13 +5,12 @@ use axum::{ RequestExt, }; use axum_typed_multipart::{TryFromMultipart, TypedMultipart}; -use snafu::prelude::*; -use tower_cookies::{Cookies, Cookie}; +use tower_cookies::Cookies; use yunohost_api::{Username, Password}; use crate::{ error::*, - state::{COOKIE_NAME, RoutableAppState, sessions::LoggedInUser}, + state::{COOKIE_NAME, RoutableAppState, LoggedInUser}, }; #[derive(Debug, TryFromMultipart, Deserialize)] @@ -74,7 +73,7 @@ pub async fn route(user: Option, cookies: Cookies, state: State, cookies: Cookies, state: State) -> String { +pub async fn route(user: Option, cookies: Cookies) -> String { if let Some(user) = user { let cookie = user.cookie(); cookies.remove(Cookie::new(Cow::Owned(cookie.name().to_string()), Cow::Owned(cookie.value().to_string()))); diff --git a/src/state/login.rs b/src/state/login.rs new file mode 100644 index 0000000..a443b83 --- /dev/null +++ b/src/state/login.rs @@ -0,0 +1,83 @@ +use axum::{ + extract::FromRequestParts, + http::request::Parts, + RequestPartsExt, +}; +use snafu::prelude::*; +use tower_cookies::Cookies; +use yunohost_api::Username; + +use crate::{ + error::*, + state::{COOKIE_NAME, RoutableAppState, sessions::*}, +}; + +#[derive(Clone, Debug)] +pub struct LoggedInUser { + username: Username, + timestamp: i64, +} + +impl LoggedInUser { + pub fn new(username: Username, timestamp: i64) -> LoggedInUser { + LoggedInUser { + username, + timestamp, + } + } + + pub fn username(&self) -> &Username { + &self.username + } + + pub fn timestamp(&self) -> i64 { + self.timestamp + } + + +} + +impl From for LoggedInUser { + fn from(s: Session) -> LoggedInUser { + LoggedInUser::new(s.username, s.timestamp) + } +} + +impl std::fmt::Display for LoggedInUser { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.username.fmt(fmt) + } +} + +#[async_trait] +impl FromRequestParts for LoggedInUser { + type Rejection = Error; + + async fn from_request_parts(parts: &mut Parts, state: &RoutableAppState) -> Result { + let cookies: Cookies = parts.extract().await.map_err(|_| Error::Cookie)?; + trace!("[SESSION:{}/login] Checking if the user has a valid session", COOKIE_NAME); + let session_cookie = if let Some(session_cookie) = cookies.get(COOKIE_NAME) { + trace!("[SESSION:{}/login] User claims to have a session with cookie: {}", COOKIE_NAME, &session_cookie); + session_cookie + } else { + trace!("[SESSION:{}/login] No current session cookie", COOKIE_NAME); + return Err(Error::Session { + source: SessionError::NoSession { + cookie_name: COOKIE_NAME.to_string(), + } + }); + }; + + if let Some(session) = state.sessions.verify_cookie_session(session_cookie.value()).await.context(SessionSnafu)? { + debug!("[SESSION:{}/login] User {} resumed session.", COOKIE_NAME, &session.username); + return Ok(session.into()); + } else { + trace!("[SESSION:{}/login] User session is invalid or has expired.", COOKIE_NAME); + return Err(Error::Session { + source: SessionError::InvalidOrExpired { + cookie_name: COOKIE_NAME.to_string(), + } + }); + } + } +} diff --git a/src/state/logout.rs b/src/state/logout.rs new file mode 100644 index 0000000..ed0fde2 --- /dev/null +++ b/src/state/logout.rs @@ -0,0 +1,85 @@ +use axum::{ + extract::FromRequestParts, + http::request::Parts, + RequestPartsExt, +}; +use snafu::prelude::*; +use std::borrow::Cow; +use tower_cookies::{Cookies, Cookie}; +use yunohost_api::Username; + +use crate::{ + error::*, + state::{COOKIE_NAME, RoutableAppState, sessions::*}, +}; + +pub struct LoggedOutUser(Session); + +impl LoggedOutUser { + pub fn new(session: Session) -> Self { + Self(session) + } + + pub fn username(&self) -> &Username { + self.0.username() + } + + pub fn cookie(&self) -> Cookie { + Cookie::build( + Cow::Owned(self.0.cookie_name.to_string()), + Cow::Owned(self.0.content.to_string()), + ).path( + Cow::Owned("/".to_string()) + ).finish() + } +} + +impl From for LoggedOutUser { + fn from(s: Session) -> LoggedOutUser { + LoggedOutUser::new(s) + } +} + +#[async_trait] +impl FromRequestParts for LoggedOutUser { + type Rejection = Error; + + async fn from_request_parts(parts: &mut Parts, state: &RoutableAppState) -> Result { + let cookies: Cookies = parts.extract().await.map_err(|_| Error::Cookie)?; + trace!("[SESSION:{}/logout] Checking if the user has a valid session", COOKIE_NAME); + trace!("{:#?}", parts.headers); + let session_cookie = if let Some(session_cookie) = cookies.get(COOKIE_NAME) { + trace!("[SESSION:{}/logout] User claims to have a session with cookie: {}", COOKIE_NAME, &session_cookie); + session_cookie + } else { + trace!("[SESSION:{}/logout] No current session cookie", COOKIE_NAME); + return Err(Error::Session { + source: SessionError::NoSession { + cookie_name: COOKIE_NAME.to_string(), + } + }); + }; + + if let Some(session) = state.sessions.verify_cookie_session(session_cookie.value()).await.context(SessionSnafu)? { + debug!("[SESSION:{}/logout] User {} resumed session, requesting to log out.", COOKIE_NAME, &session.username); + if state.sessions.invalidate_session(session.timestamp, &session.content).await { + return Ok(session.into()); + } else { + warn!("[SESSION:{}/logout] User {} requested logout but was already logged out. Here's the session:\n{:#?}", COOKIE_NAME, &session.username, &session); + warn!("This is probably a race condition but is completely harmless."); + return Err(Error::Session { + source: SessionError::InvalidOrExpired { + cookie_name: COOKIE_NAME.to_string(), + } + }); + } + } else { + trace!("[SESSION:{}/logout] User session is invalid or has expired.", COOKIE_NAME); + return Err(Error::Session { + source: SessionError::InvalidOrExpired { + cookie_name: COOKIE_NAME.to_string(), + } + }); + } + } +} diff --git a/src/state/mod.rs b/src/state/mod.rs index 345f99c..623213b 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -5,8 +5,12 @@ use std::sync::Arc; use crate::error::*; +pub mod login; +pub use login::LoggedInUser; +pub mod logout; +pub use logout::LoggedOutUser; pub mod sessions; -use sessions::SessionManager; +pub use sessions::{Session, SessionManager}; pub type RoutableAppState = Arc; diff --git a/src/state/sessions.rs b/src/state/sessions.rs index 5e08971..926264e 100644 --- a/src/state/sessions.rs +++ b/src/state/sessions.rs @@ -1,20 +1,11 @@ -use axum::{ - extract::FromRequestParts, - http::request::Parts, - RequestPartsExt, -}; use ring::{hmac,rand}; use snafu::prelude::*; use std::borrow::Cow; use tokio::sync::RwLock; -use tower_cookies::{Cookies, Cookie}; +use tower_cookies::Cookie; use yunohost_api::Username; -use crate::{ - error::*, - state::{COOKIE_NAME, RoutableAppState}, - utils::time::now, -}; +use crate::utils::time::now; /// An error related to session management #[derive(Debug, Snafu)] @@ -217,151 +208,3 @@ impl Session { ).finish() } } - -#[derive(Clone, Debug)] -pub struct LoggedInUser { - username: Username, - timestamp: i64, -} - -impl LoggedInUser { - pub fn new(username: Username, timestamp: i64) -> LoggedInUser { - LoggedInUser { - username, - timestamp, - } - } - - pub fn as_str(&self) -> &str { - self.username.as_str() - } - - pub fn username(&self) -> &Username { - &self.username - } - - pub fn timestamp(&self) -> i64 { - self.timestamp - } - - -} - -impl From for LoggedInUser { - fn from(s: Session) -> LoggedInUser { - LoggedInUser { - username: s.username, - timestamp: s.timestamp, - } - } -} - -impl std::fmt::Display for LoggedInUser { - fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.username.fmt(fmt) - } -} - -#[async_trait] -impl FromRequestParts for LoggedInUser { - type Rejection = Error; - - async fn from_request_parts(parts: &mut Parts, state: &RoutableAppState) -> Result { - let cookies: Cookies = parts.extract().await.map_err(|_| Error::Cookie)?; - trace!("[SESSION:{}/login] Checking if the user has a valid session", COOKIE_NAME); - let session_cookie = if let Some(session_cookie) = cookies.get(COOKIE_NAME) { - trace!("[SESSION:{}/login] User claims to have a session with cookie: {}", COOKIE_NAME, &session_cookie); - session_cookie - } else { - trace!("[SESSION:{}/login] No current session cookie", COOKIE_NAME); - return Err(Error::Session { - source: SessionError::NoSession { - cookie_name: COOKIE_NAME.to_string(), - } - }); - }; - - if let Some(session) = state.sessions.verify_cookie_session(session_cookie.value()).await.context(SessionSnafu)? { - debug!("[SESSION:{}/login] User {} resumed session.", COOKIE_NAME, &session.username); - return Ok(session.into()); - } else { - trace!("[SESSION:{}/login] User session is invalid or has expired.", COOKIE_NAME); - return Err(Error::Session { - source: SessionError::InvalidOrExpired { - cookie_name: COOKIE_NAME.to_string(), - } - }); - } - } -} - -pub struct LoggedOutUser(Session); - -impl LoggedOutUser { - pub fn new(session: Session) -> Self { - Self(session) - } - - pub fn username(&self) -> &Username { - self.0.username() - } - - pub fn cookie(&self) -> Cookie { - Cookie::build( - Cow::Owned(self.0.cookie_name.to_string()), - Cow::Owned(self.0.content.to_string()), - ).path( - Cow::Owned("/".to_string()) - ).finish() - } -} - -impl From for LoggedOutUser { - fn from(s: Session) -> LoggedOutUser { - LoggedOutUser(s) - } -} - -#[async_trait] -impl FromRequestParts for LoggedOutUser { - type Rejection = Error; - - async fn from_request_parts(parts: &mut Parts, state: &RoutableAppState) -> Result { - let cookies: Cookies = parts.extract().await.map_err(|_| Error::Cookie)?; - trace!("[SESSION:{}/logout] Checking if the user has a valid session", COOKIE_NAME); - trace!("{:#?}", parts.headers); - let session_cookie = if let Some(session_cookie) = cookies.get(COOKIE_NAME) { - trace!("[SESSION:{}/logout] User claims to have a session with cookie: {}", COOKIE_NAME, &session_cookie); - session_cookie - } else { - trace!("[SESSION:{}/logout] No current session cookie", COOKIE_NAME); - return Err(Error::Session { - source: SessionError::NoSession { - cookie_name: COOKIE_NAME.to_string(), - } - }); - }; - - if let Some(session) = state.sessions.verify_cookie_session(session_cookie.value()).await.context(SessionSnafu)? { - debug!("[SESSION:{}/logout] User {} resumed session, requesting to log out.", COOKIE_NAME, &session.username); - if state.sessions.invalidate_session(session.timestamp, &session.content).await { - return Ok(session.into()); - } else { - warn!("[SESSION:{}/logout] User {} requested logout but was already logged out. Here's the session:\n{:#?}", COOKIE_NAME, &session.username, &session); - warn!("This is probably a race condition but is completely harmless."); - return Err(Error::Session { - source: SessionError::InvalidOrExpired { - cookie_name: COOKIE_NAME.to_string(), - } - }); - } - } else { - trace!("[SESSION:{}/logout] User session is invalid or has expired.", COOKIE_NAME); - return Err(Error::Session { - source: SessionError::InvalidOrExpired { - cookie_name: COOKIE_NAME.to_string(), - } - }); - } - } -}