121 lines
3.6 KiB
Rust
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),
|
|
}
|
|
}
|
|
}
|