Compare commits

...

3 Commits

7 changed files with 103 additions and 11 deletions

View File

@ -1,3 +1,5 @@
use std::time::Duration;
use crate::{db, server}; use crate::{db, server};
pub fn serve(config: &crate::config::Config) -> u8 { pub fn serve(config: &crate::config::Config) -> u8 {
@ -11,7 +13,7 @@ pub fn serve(config: &crate::config::Config) -> u8 {
let ctx = server::Context { let ctx = server::Context {
store: crate::gpodder::GpodderRepository::new(repo), 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() let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all() .enable_all()
@ -21,7 +23,28 @@ pub fn serve(config: &crate::config::Config) -> u8 {
let address = format!("{}:{}", config.domain, config.port); let address = format!("{}:{}", config.domain, config.port);
tracing::info!("Starting server on {address}"); tracing::info!("Starting server on {address}");
let session_removal_duration = Duration::from_secs(config.session_cleanup_interval);
rt.block_on(async { 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(); let listener = tokio::net::TcpListener::bind(address).await.unwrap();
axum::serve(listener, app.into_make_service()) axum::serve(listener, app.into_make_service())
.await .await

View File

@ -10,6 +10,8 @@ pub struct Config {
pub domain: String, pub domain: String,
#[serde(default = "default_port")] #[serde(default = "default_port")]
pub port: u16, pub port: u16,
#[serde(default = "default_session_cleanup_interval")]
pub session_cleanup_interval: u64,
} }
fn default_data_dir() -> PathBuf { fn default_data_dir() -> PathBuf {
@ -23,3 +25,8 @@ fn default_domain() -> String {
fn default_port() -> u16 { fn default_port() -> u16 {
8080 8080
} }
fn default_session_cleanup_interval() -> u64 {
// Default is once a day
60 * 60 * 24
}

View File

@ -181,4 +181,29 @@ impl gpodder::AuthStore for SqliteRepository {
.execute(&mut self.pool.get()?) .execute(&mut self.pool.get()?)
.map(|_| ())?) .map(|_| ())?)
} }
fn refresh_session(
&self,
session: &gpodder::Session,
timestamp: DateTime<chrono::Utc>,
) -> 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(())
}
}
fn remove_old_sessions(&self, min_last_seen: DateTime<chrono::Utc>) -> Result<usize, AuthErr> {
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()?)?,
)
}
} }

View File

@ -1,10 +1,13 @@
pub mod models; pub mod models;
mod repository; mod repository;
use std::fmt::Display;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
pub use models::*; pub use models::*;
pub use repository::GpodderRepository; pub use repository::GpodderRepository;
#[derive(Debug)]
pub enum AuthErr { pub enum AuthErr {
UnknownSession, UnknownSession,
UnknownUser, UnknownUser,
@ -12,6 +15,19 @@ pub enum AuthErr {
Other(Box<dyn std::error::Error + Sync + Send>), Other(Box<dyn std::error::Error + Sync + Send>),
} }
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: pub trait Store:
AuthStore + DeviceRepository + SubscriptionRepository + EpisodeActionRepository AuthStore + DeviceRepository + SubscriptionRepository + EpisodeActionRepository
{ {
@ -52,6 +68,12 @@ pub trait AuthStore {
/// Remove the session with the given session ID /// Remove the session with the given session ID
fn remove_session(&self, session_id: i64) -> Result<(), AuthErr>; fn remove_session(&self, session_id: i64) -> Result<(), AuthErr>;
/// Update the session's timestamp
fn refresh_session(&self, session: &Session, timestamp: DateTime<Utc>) -> Result<(), AuthErr>;
/// Remove any sessions whose last_seen timestamp is before the given minimum value
fn remove_old_sessions(&self, min_last_seen: DateTime<Utc>) -> Result<usize, AuthErr>;
} }
pub trait DeviceRepository { pub trait DeviceRepository {

View File

@ -20,7 +20,7 @@ impl GpodderRepository {
} }
} }
pub fn validate_session(&self, session_id: i64) -> Result<models::Session, AuthErr> { pub fn get_session(&self, session_id: i64) -> Result<models::Session, AuthErr> {
let session = self let session = self
.store .store
.get_session(session_id)? .get_session(session_id)?
@ -65,10 +65,22 @@ impl GpodderRepository {
Ok(session) 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> { pub fn remove_session(&self, session_id: i64) -> Result<(), AuthErr> {
self.store.remove_session(session_id) self.store.remove_session(session_id)
} }
pub fn remove_old_sessions(&self) -> Result<usize, AuthErr> {
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<Vec<models::Device>, AuthErr> { pub fn devices_for_user(&self, user: &models::User) -> Result<Vec<models::Device>, AuthErr> {
self.store.devices_for_user(user) self.store.devices_for_user(user)
} }

View File

@ -4,13 +4,11 @@ use axum::{
Router, Router,
}; };
use axum_extra::{ use axum_extra::{
extract::{ extract::{cookie::Cookie, CookieJar},
cookie::{Cookie, Expiration},
CookieJar,
},
headers::{authorization::Basic, Authorization}, headers::{authorization::Basic, Authorization},
TypedHeader, TypedHeader,
}; };
use cookie::time::Duration;
use crate::server::{ use crate::server::{
error::{AppError, AppResult}, error::{AppError, AppResult},
@ -45,7 +43,7 @@ async fn post_login(
.unwrap()?; .unwrap()?;
Ok(jar.add( 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)),
)) ))
} }

View File

@ -47,10 +47,15 @@ pub async fn auth_middleware(State(ctx): State<Context>, mut req: Request, next:
.get(SESSION_ID_COOKIE) .get(SESSION_ID_COOKIE)
.and_then(|c| c.value().parse::<i64>().ok()) .and_then(|c| c.value().parse::<i64>().ok())
{ {
let ctx_clone = ctx.clone(); let ctx = ctx.clone();
match tokio::task::spawn_blocking(move || ctx_clone.store.validate_session(session_id)) match tokio::task::spawn_blocking(move || {
.await let session = ctx.store.get_session(session_id)?;
.unwrap() ctx.store.refresh_session(&session)?;
Ok(session)
})
.await
.unwrap()
{ {
Ok(session) => { Ok(session) => {
auth_user = Some(session.user); auth_user = Some(session.user);