otter/src/server/gpodder/mod.rs

121 lines
3.6 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_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::<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),
}
}
}