From f00d842badb932307824c45b4017397daa5e89cc Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 15 Mar 2025 20:59:00 +0100 Subject: [PATCH 1/3] feat: implement session last_seen update --- src/db/repository/auth.rs | 16 ++++++++++++++++ src/gpodder/mod.rs | 3 +++ src/gpodder/repository.rs | 8 +++++++- src/server/gpodder/mod.rs | 13 +++++++++---- 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/db/repository/auth.rs b/src/db/repository/auth.rs index e65f046..138fbcb 100644 --- a/src/db/repository/auth.rs +++ b/src/db/repository/auth.rs @@ -181,4 +181,20 @@ impl gpodder::AuthStore for SqliteRepository { .execute(&mut self.pool.get()?) .map(|_| ())?) } + + fn refresh_session( + &self, + session: &gpodder::Session, + timestamp: DateTime, + ) -> Result<(), AuthErr> { + if diesel::update(sessions::table.filter(sessions::id.eq(session.id))) + .set(sessions::last_seen.eq(timestamp.timestamp())) + .execute(&mut self.pool.get()?)? + == 0 + { + Err(AuthErr::UnknownSession) + } else { + Ok(()) + } + } } diff --git a/src/gpodder/mod.rs b/src/gpodder/mod.rs index 8336de1..53abd1f 100644 --- a/src/gpodder/mod.rs +++ b/src/gpodder/mod.rs @@ -52,6 +52,9 @@ pub trait AuthStore { /// Remove the session with the given session ID fn remove_session(&self, session_id: i64) -> Result<(), AuthErr>; + + /// Update the session's timestamp + fn refresh_session(&self, session: &Session, timestamp: DateTime) -> Result<(), AuthErr>; } pub trait DeviceRepository { diff --git a/src/gpodder/repository.rs b/src/gpodder/repository.rs index 411f9cf..cf76904 100644 --- a/src/gpodder/repository.rs +++ b/src/gpodder/repository.rs @@ -20,7 +20,7 @@ impl GpodderRepository { } } - pub fn validate_session(&self, session_id: i64) -> Result { + pub fn get_session(&self, session_id: i64) -> Result { let session = self .store .get_session(session_id)? @@ -65,6 +65,12 @@ impl GpodderRepository { Ok(session) } + pub fn refresh_session(&self, session: &models::Session) -> Result<(), AuthErr> { + let now = Utc::now(); + + self.store.refresh_session(session, now) + } + pub fn remove_session(&self, session_id: i64) -> Result<(), AuthErr> { self.store.remove_session(session_id) } diff --git a/src/server/gpodder/mod.rs b/src/server/gpodder/mod.rs index 776100e..4746323 100644 --- a/src/server/gpodder/mod.rs +++ b/src/server/gpodder/mod.rs @@ -47,10 +47,15 @@ pub async fn auth_middleware(State(ctx): State, mut req: Request, next: .get(SESSION_ID_COOKIE) .and_then(|c| c.value().parse::().ok()) { - let ctx_clone = ctx.clone(); - match tokio::task::spawn_blocking(move || ctx_clone.store.validate_session(session_id)) - .await - .unwrap() + let ctx = ctx.clone(); + match tokio::task::spawn_blocking(move || { + let session = ctx.store.get_session(session_id)?; + ctx.store.refresh_session(&session)?; + + Ok(session) + }) + .await + .unwrap() { Ok(session) => { auth_user = Some(session.user); From bc805154747ac5543e5fbe8fd8f69c0c441d5d42 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 15 Mar 2025 21:21:35 +0100 Subject: [PATCH 2/3] feat: implement background old session cleanup task --- src/cli/serve.rs | 25 ++++++++++++++++++++++++- src/config.rs | 7 +++++++ src/db/repository/auth.rs | 9 +++++++++ src/gpodder/mod.rs | 19 +++++++++++++++++++ src/gpodder/repository.rs | 6 ++++++ 5 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/cli/serve.rs b/src/cli/serve.rs index 664a1e0..67cf6e7 100644 --- a/src/cli/serve.rs +++ b/src/cli/serve.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use crate::{db, server}; pub fn serve(config: &crate::config::Config) -> u8 { @@ -11,7 +13,7 @@ pub fn serve(config: &crate::config::Config) -> u8 { let ctx = server::Context { store: crate::gpodder::GpodderRepository::new(repo), }; - let app = server::app(ctx); + let app = server::app(ctx.clone()); let rt = tokio::runtime::Builder::new_multi_thread() .enable_all() @@ -21,7 +23,28 @@ pub fn serve(config: &crate::config::Config) -> u8 { let address = format!("{}:{}", config.domain, config.port); tracing::info!("Starting server on {address}"); + let session_removal_duration = Duration::from_secs(config.session_cleanup_interval); + rt.block_on(async { + tokio::task::spawn(async move { + let mut interval = tokio::time::interval(session_removal_duration); + + loop { + interval.tick().await; + + tracing::info!("Performing session cleanup"); + + match ctx.store.remove_old_sessions() { + Ok(n) => { + tracing::info!("Removed {} old sessions", n); + } + Err(err) => { + tracing::error!("Error occured during session cleanup: {}", err); + } + } + } + }); + let listener = tokio::net::TcpListener::bind(address).await.unwrap(); axum::serve(listener, app.into_make_service()) .await diff --git a/src/config.rs b/src/config.rs index 0f7df52..67bc59a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,6 +10,8 @@ pub struct Config { pub domain: String, #[serde(default = "default_port")] pub port: u16, + #[serde(default = "default_session_cleanup_interval")] + pub session_cleanup_interval: u64, } fn default_data_dir() -> PathBuf { @@ -23,3 +25,8 @@ fn default_domain() -> String { fn default_port() -> u16 { 8080 } + +fn default_session_cleanup_interval() -> u64 { + // Default is once a day + 60 * 60 * 24 +} diff --git a/src/db/repository/auth.rs b/src/db/repository/auth.rs index 138fbcb..8bd7905 100644 --- a/src/db/repository/auth.rs +++ b/src/db/repository/auth.rs @@ -197,4 +197,13 @@ impl gpodder::AuthStore for SqliteRepository { Ok(()) } } + + fn remove_old_sessions(&self, min_last_seen: DateTime) -> Result { + let min_last_seen = min_last_seen.timestamp(); + + Ok( + diesel::delete(sessions::table.filter(sessions::last_seen.lt(min_last_seen))) + .execute(&mut self.pool.get()?)?, + ) + } } diff --git a/src/gpodder/mod.rs b/src/gpodder/mod.rs index 53abd1f..d8ff790 100644 --- a/src/gpodder/mod.rs +++ b/src/gpodder/mod.rs @@ -1,10 +1,13 @@ pub mod models; mod repository; +use std::fmt::Display; + use chrono::{DateTime, Utc}; pub use models::*; pub use repository::GpodderRepository; +#[derive(Debug)] pub enum AuthErr { UnknownSession, UnknownUser, @@ -12,6 +15,19 @@ pub enum AuthErr { Other(Box), } +impl Display for AuthErr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::UnknownUser => write!(f, "unknown user"), + Self::UnknownSession => write!(f, "unknown session"), + Self::InvalidPassword => write!(f, "invalid password"), + Self::Other(err) => err.fmt(f), + } + } +} + +impl std::error::Error for AuthErr {} + pub trait Store: AuthStore + DeviceRepository + SubscriptionRepository + EpisodeActionRepository { @@ -55,6 +71,9 @@ pub trait AuthStore { /// Update the session's timestamp fn refresh_session(&self, session: &Session, timestamp: DateTime) -> Result<(), AuthErr>; + + /// Remove any sessions whose last_seen timestamp is before the given minimum value + fn remove_old_sessions(&self, min_last_seen: DateTime) -> Result; } pub trait DeviceRepository { diff --git a/src/gpodder/repository.rs b/src/gpodder/repository.rs index cf76904..e55d010 100644 --- a/src/gpodder/repository.rs +++ b/src/gpodder/repository.rs @@ -75,6 +75,12 @@ impl GpodderRepository { self.store.remove_session(session_id) } + pub fn remove_old_sessions(&self) -> Result { + let min_last_seen = Utc::now() - TimeDelta::seconds(MAX_SESSION_AGE); + + self.store.remove_old_sessions(min_last_seen) + } + pub fn devices_for_user(&self, user: &models::User) -> Result, AuthErr> { self.store.devices_for_user(user) } From 65e83ecb1fac44975c4ab8a27ebbbc161dd6561c Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 15 Mar 2025 21:31:39 +0100 Subject: [PATCH 3/3] fix: return persistent cookies instead of session cookies --- src/server/gpodder/advanced/auth.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/server/gpodder/advanced/auth.rs b/src/server/gpodder/advanced/auth.rs index ac5cbe6..242082a 100644 --- a/src/server/gpodder/advanced/auth.rs +++ b/src/server/gpodder/advanced/auth.rs @@ -4,13 +4,11 @@ use axum::{ Router, }; use axum_extra::{ - extract::{ - cookie::{Cookie, Expiration}, - CookieJar, - }, + extract::{cookie::Cookie, CookieJar}, headers::{authorization::Basic, Authorization}, TypedHeader, }; +use cookie::time::Duration; use crate::server::{ error::{AppError, AppResult}, @@ -45,7 +43,7 @@ async fn post_login( .unwrap()?; Ok(jar.add( - Cookie::build((SESSION_ID_COOKIE, session.id.to_string())).expires(Expiration::Session), + Cookie::build((SESSION_ID_COOKIE, session.id.to_string())).max_age(Duration::days(365)), )) }