126 lines
		
	
	
		
			3.7 KiB
		
	
	
	
		
			Rust
		
	
	
			
		
		
	
	
			126 lines
		
	
	
		
			3.7 KiB
		
	
	
	
		
			Rust
		
	
	
mod advanced;
 | 
						|
mod format;
 | 
						|
mod models;
 | 
						|
mod simple;
 | 
						|
 | 
						|
use axum::{
 | 
						|
    extract::{Request, State},
 | 
						|
    http::{header::WWW_AUTHENTICATE, HeaderName, HeaderValue, StatusCode},
 | 
						|
    middleware::Next,
 | 
						|
    response::{IntoResponse, Response},
 | 
						|
    RequestExt, Router,
 | 
						|
};
 | 
						|
use axum_extra::{
 | 
						|
    extract::{cookie::Cookie, CookieJar},
 | 
						|
    headers::{authorization::Basic, Authorization},
 | 
						|
    TypedHeader,
 | 
						|
};
 | 
						|
use tower_http::set_header::SetResponseHeaderLayer;
 | 
						|
 | 
						|
use crate::{gpodder, server::error::AppError};
 | 
						|
 | 
						|
use super::Context;
 | 
						|
 | 
						|
const SESSION_ID_COOKIE: &str = "sessionid";
 | 
						|
 | 
						|
pub fn router(ctx: Context) -> Router<Context> {
 | 
						|
    Router::new()
 | 
						|
        .merge(simple::router(ctx.clone()))
 | 
						|
        .nest("/api/2", advanced::router(ctx))
 | 
						|
        // https://gpoddernet.readthedocs.io/en/latest/api/reference/general.html#cors
 | 
						|
        // All endpoints should send this CORS header value so the endpoints can be used from web
 | 
						|
        // applications
 | 
						|
        .layer(SetResponseHeaderLayer::overriding(
 | 
						|
            HeaderName::from_static("access-control-allow-origin"),
 | 
						|
            HeaderValue::from_static("*"),
 | 
						|
        ))
 | 
						|
}
 | 
						|
 | 
						|
/// This middleware accepts
 | 
						|
pub async fn auth_middleware(State(ctx): State<Context>, mut req: Request, next: Next) -> Response {
 | 
						|
    // SAFETY: this extractor's error type is Infallible
 | 
						|
    let mut jar: CookieJar = req.extract_parts().await.unwrap();
 | 
						|
    let mut auth_user = None;
 | 
						|
 | 
						|
    // First try to validate the session
 | 
						|
    if let Some(session_id) = jar
 | 
						|
        .get(SESSION_ID_COOKIE)
 | 
						|
        .and_then(|c| c.value().parse::<i64>().ok())
 | 
						|
    {
 | 
						|
        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);
 | 
						|
            }
 | 
						|
            Err(gpodder::AuthErr::UnknownSession) => {
 | 
						|
                jar = jar.add(
 | 
						|
                    Cookie::build((SESSION_ID_COOKIE, String::new()))
 | 
						|
                        .max_age(cookie::time::Duration::ZERO),
 | 
						|
                );
 | 
						|
            }
 | 
						|
            Err(err) => {
 | 
						|
                return AppError::from(err).into_response();
 | 
						|
            }
 | 
						|
        };
 | 
						|
    }
 | 
						|
 | 
						|
    // Only if the sessionid wasn't present or valid do we check the credentials.
 | 
						|
    if auth_user.is_none() {
 | 
						|
        if let Ok(auth) = req
 | 
						|
            .extract_parts::<TypedHeader<Authorization<Basic>>>()
 | 
						|
            .await
 | 
						|
        {
 | 
						|
            match tokio::task::spawn_blocking(move || {
 | 
						|
                ctx.store
 | 
						|
                    .validate_credentials(auth.username(), auth.password())
 | 
						|
            })
 | 
						|
            .await
 | 
						|
            .unwrap()
 | 
						|
            .map_err(AppError::from)
 | 
						|
            {
 | 
						|
                Ok(user) => {
 | 
						|
                    auth_user = Some(user);
 | 
						|
                }
 | 
						|
                Err(err) => {
 | 
						|
                    return err.into_response();
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    if let Some(user) = auth_user {
 | 
						|
        req.extensions_mut().insert(user);
 | 
						|
 | 
						|
        (jar, next.run(req).await).into_response()
 | 
						|
    } else {
 | 
						|
        let mut res = (jar, StatusCode::UNAUTHORIZED).into_response();
 | 
						|
 | 
						|
        // This is what the gpodder.net service returns, and some clients seem to depend on it
 | 
						|
        res.headers_mut().insert(
 | 
						|
            WWW_AUTHENTICATE,
 | 
						|
            HeaderValue::from_static("Basic realm=\"\""),
 | 
						|
        );
 | 
						|
 | 
						|
        res
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
impl From<gpodder::AuthErr> for AppError {
 | 
						|
    fn from(value: gpodder::AuthErr) -> Self {
 | 
						|
        match value {
 | 
						|
            gpodder::AuthErr::UnknownUser
 | 
						|
            | gpodder::AuthErr::UnknownSession
 | 
						|
            | gpodder::AuthErr::InvalidPassword => Self::Unauthorized,
 | 
						|
            gpodder::AuthErr::Other(err) => Self::Other(err),
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |