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 { 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, 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::().ok()) { let ctx_clone = ctx.clone(); match tokio::task::spawn_blocking(move || ctx_clone.store.validate_session(session_id)) .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::>>() .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 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), } } }