Compare commits
	
		
			3 Commits 
		
	
	
		
			330877c8c5
			...
			65e83ecb1f
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								
									
								
								 | 
						65e83ecb1f | |
| 
							
							
								
									
								
								 | 
						bc80515474 | |
| 
							
							
								
									
								
								 | 
						f00d842bad | 
| 
						 | 
					@ -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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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()?)?,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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 {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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)),
 | 
				
			||||||
    ))
 | 
					    ))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -47,8 +47,13 @@ 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 || {
 | 
				
			||||||
 | 
					            let session = ctx.store.get_session(session_id)?;
 | 
				
			||||||
 | 
					            ctx.store.refresh_session(&session)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Ok(session)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
        .await
 | 
					        .await
 | 
				
			||||||
        .unwrap()
 | 
					        .unwrap()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue