Skip to content

Commit

Permalink
define properties, fix cantook auth
Browse files Browse the repository at this point in the history
Essential work started towards fixing #346
  • Loading branch information
aaronleopold committed Jun 7, 2024
1 parent c121f53 commit 9871c63
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 31 deletions.
79 changes: 77 additions & 2 deletions apps/server/src/middleware/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use axum::{
extract::{FromRef, FromRequestParts, OriginalUri},
http::{header, request::Parts, Method, StatusCode},
response::{IntoResponse, Redirect, Response},
Json,
};
use base64::{engine::general_purpose::STANDARD, Engine};
use prisma_client_rust::{
Expand All @@ -12,6 +13,10 @@ use prisma_client_rust::{
};
use stump_core::{
db::entity::{User, UserPermission},
opds::v2_0::authentication::{
OPDSAuthenticationDocumentBuilder, OPDSSupportedAuthFlow,
OPDS_AUTHENTICATION_DOCUMENT_REL, OPDS_AUTHENTICATION_DOCUMENT_TYPE,
},
prisma::{user, PrismaClient},
};
use tower_sessions::Session;
Expand Down Expand Up @@ -87,8 +92,12 @@ where

let Some(auth_header) = auth_header else {
if is_opds {
// Prompt for basic auth on OPDS routes
return Err(BasicAuth.into_response());
let opds_version = request_uri
.split('/')
.nth(2)
.map(|v| v.replace("v", ""))
.unwrap_or("1.2".to_string());
return Err(OPDSBasicAuth::new(opds_version).into_response());
} else if is_swagger {
// Sign in via React app and then redirect to server-side swagger-ui
return Err(Redirect::to("/auth?redirect=%2Fswagger-ui/").into_response());
Expand Down Expand Up @@ -237,6 +246,72 @@ where
}
}

pub struct OPDSBasicAuth {
version: String,
}

impl OPDSBasicAuth {
pub fn new(version: String) -> Self {
Self { version }
}
}

impl IntoResponse for OPDSBasicAuth {
fn into_response(self) -> Response {
if self.version == "2.0" {
// TODO: investigate whether there is a better way to do this than
// piggybacking off of the IntoResponse impl of Json...
let json_repsonse = Json(
OPDSAuthenticationDocumentBuilder::default()
.description(OPDSSupportedAuthFlow::Basic.description().to_string())
.build()
.unwrap(),
)
.into_response();
let body = json_repsonse.into_body();
let body = BoxBody::from(body);

// TODO: determine if relative paths work...
Response::builder()
.status(StatusCode::UNAUTHORIZED)
.header("Authorization", "Basic")
.header(
"WWW-Authenticate",
format!("Basic realm=\"stump OPDS v{}\"", self.version),
)
.header(
"Content-Type",
format!("{OPDS_AUTHENTICATION_DOCUMENT_TYPE}; charset=utf-8"),
)
.header(
"Link",
format!(
"<{}>; rel=\"{OPDS_AUTHENTICATION_DOCUMENT_REL}\"; type=\"{OPDS_AUTHENTICATION_DOCUMENT_TYPE}\"",
"/opds/v2.0/auth"
),
)
.body(body)
.unwrap_or_else(|e| {
tracing::error!(error = ?e, "Failed to build response");
StatusCode::INTERNAL_SERVER_ERROR.into_response()
})
} else {
Response::builder()
.status(StatusCode::UNAUTHORIZED)
.header("Authorization", "Basic")
.header(
"WWW-Authenticate",
format!("Basic realm=\"stump OPDS v{}\"", self.version),
)
.body(BoxBody::default())
.unwrap_or_else(|e| {
tracing::error!(error = ?e, "Failed to build response");
StatusCode::INTERNAL_SERVER_ERROR.into_response()
})
}
}
}

pub struct BasicAuth;

impl IntoResponse for BasicAuth {
Expand Down
19 changes: 18 additions & 1 deletion apps/server/src/routers/opds/v2_0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ use axum::{
extract::State, middleware::from_extractor_with_state, routing::get, Json, Router,
};
use stump_core::opds::v2_0::{
authentication::{
OPDSAuthenticationDocument, OPDSAuthenticationDocumentBuilder,
OPDSSupportedAuthFlow,
},
feed::{OPDSFeed, OPDSFeedBuilder},
group::OPDSFeedGroupBuilder,
link::{
Expand Down Expand Up @@ -47,10 +51,23 @@ const DEFAULT_LIMIT: i64 = 10;

pub(crate) fn mount(app_state: AppState) -> Router<AppState> {
Router::new()
.nest("/v2.0", Router::new().route("/catalog", get(catalog)))
.nest(
"/v2.0",
Router::new()
.route("/auth", get(auth))
.route("/catalog", get(catalog)),
)
.layer(from_extractor_with_state::<Auth, AppState>(app_state))
}

async fn auth() -> APIResult<Json<OPDSAuthenticationDocument>> {
Ok(Json(
OPDSAuthenticationDocumentBuilder::default()
.description(OPDSSupportedAuthFlow::Basic.description().to_string())
.build()?,
))
}

async fn catalog(
State(ctx): State<AppState>,
session: Session,
Expand Down
80 changes: 53 additions & 27 deletions core/src/opds/v2_0/authentication.rs
Original file line number Diff line number Diff line change
@@ -1,68 +1,94 @@
use derive_builder::Builder;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;

use super::link::OPDSLinkType;
use super::link::OPDSLink;

pub const OPDS_AUTHENTICATION_DOCUMENT_REL: &str = "http://opds-spec.org/auth/document";
pub const OPDS_AUTHENTICATION_DOCUMENT_TYPE: &str =
"application/opds-authentication+json";

/// A struct for representing an authentication document
///
/// See https://drafts.opds.io/authentication-for-opds-1.0.html#23-syntax
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Builder, Serialize, Deserialize)]
#[builder(build_fn(error = "crate::CoreError"), default, setter(into))]
#[skip_serializing_none]
pub struct AuthenticationDocument {
pub struct OPDSAuthenticationDocument {
/// Unique identifier for the Catalog provider and canonical location for the Authentication Document.
/// This is actually a URL
id: String,
/// A list of supported Authentication Flows as defined in section [3. Authentication Flows](https://drafts.opds.io/authentication-for-opds-1.0.html#3-authentication-flows).
authentication: Vec<SupportedAuthFlow>,
authentication: Vec<OPDSAuthenticationFlow>,
/// Title of the Catalog being accessed.
title: String,
/// A description of the service being displayed to the user.
description: Option<String>,
/// A list of links using the same syntax as defined in 2.3.2. Links.
links: Option<Vec<OPDSLink>>,
}

impl Default for OPDSAuthenticationDocument {
fn default() -> Self {
Self {
id: String::from("/opds/v2.0/auth"),
authentication: vec![OPDSAuthenticationFlow::default()],
title: String::from("Stump OPDS V2 Auth"),
description: None,
links: Some(vec![OPDSLink::help(), OPDSLink::logo()]),
}
}
}

/// A struct for representing a supported authentication flow
///
/// See https://drafts.opds.io/authentication-for-opds-1.0.html#3-authentication-flows
#[derive(Debug, Serialize, Deserialize)]
pub struct AuthenticationFlow {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OPDSAuthenticationFlow {
/// A URI that identifies the nature of an Authentication Flow.
#[serde(rename = "type")]
_type: SupportedAuthFlow,
/// The URI of the authentication flow
uri: String,
_type: OPDSSupportedAuthFlow,
// /// The URI of the authentication flow
// uri: String,
/// A list of labels that can be used to provide alternate labels for fields that the client will display to the user.
labels: Option<OPDSAuthenticationLabels>,
}

impl Default for OPDSAuthenticationFlow {
fn default() -> Self {
Self {
_type: OPDSSupportedAuthFlow::Basic,
labels: Some(OPDSAuthenticationLabels {
login: Some(String::from("Username")),
password: Some(String::from("Password")),
}),
}
}
}

/// A struct for representing authentication labels, meant to provide alternate labels
/// for fields that the client will display to the user.
///
/// See https://drafts.opds.io/authentication-for-opds-1.0.html#311-labels
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[skip_serializing_none]
pub struct AuthenticationLabels {
pub struct OPDSAuthenticationLabels {
login: Option<String>,
password: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub enum SupportedAuthFlow {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum OPDSSupportedAuthFlow {
#[serde(rename = "http://opds-spec.org/auth/basic")]
Basic,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct AuthenticateLink {
/// The URI of the authentication document
href: String,
/// The type of the link
#[serde(rename = "type")]
_type: OPDSLinkType,
}

impl AuthenticateLink {
pub fn new(href: String) -> Self {
Self {
href,
_type: OPDSLinkType::OpdsAuth,
impl OPDSSupportedAuthFlow {
pub fn description(&self) -> &str {
match self {
OPDSSupportedAuthFlow::Basic => {
"Enter your username and password to authenticate."
},
}
}
}
23 changes: 22 additions & 1 deletion core/src/opds/v2_0/link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ pub enum OPDSLinkRel {
Previous,
First,
Last,
Help,
Logo,
}

impl OPDSLinkRel {
Expand All @@ -47,7 +49,7 @@ impl OPDSLinkRel {
pub enum OPDSLinkType {
#[serde(rename = "application/opds+json")]
OpdsJson,
#[serde(rename = "application/opds-publication+json")]
#[serde(rename = "http://opds-spec.org/auth/document")]
OpdsAuth,
#[serde(rename = "image/jpeg")]
ImageJpeg,
Expand Down Expand Up @@ -138,6 +140,25 @@ pub enum OPDSLink {
Image(OPDSImageLink),
}

impl OPDSLink {
pub fn help() -> Self {
Self::Link(OPDSBaseLink {
href: String::from("https://stumpapp.dev"),
rel: OPDSLinkRel::Help.item(),
..Default::default()
})
}

pub fn logo() -> Self {
// TODO: determine if relative paths work...
Self::Link(OPDSBaseLink {
href: String::from("/favicon.ico"),
rel: OPDSLinkRel::Logo.item(),
..Default::default()
})
}
}

impl From<library::Data> for OPDSNavigationLink {
fn from(library: library::Data) -> Self {
OPDSNavigationLinkBuilder::default()
Expand Down
1 change: 1 addition & 0 deletions core/src/opds/v2_0/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod feed;
pub mod group;
pub mod link;
pub mod metadata;
pub mod properties;
mod utils;

pub use utils::ArrayOrItem;
Expand Down
44 changes: 44 additions & 0 deletions core/src/opds/v2_0/properties.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use derive_builder::Builder;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;

use super::link::OPDSLinkType;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OPDSDynamicProperties(serde_json::Value);

#[derive(Debug, Default, Builder, Clone, Serialize, Deserialize)]
#[builder(build_fn(error = "crate::CoreError"), default, setter(into))]
#[skip_serializing_none]
pub struct OPDSProperties {
authenticate: Option<OPDSAuthenticateProperties>,
#[serde(flatten)]
dynamic_properties: Option<OPDSDynamicProperties>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OPDSAuthenticateProperties {
/// The URI of the authentication document
href: String,
/// The type of the link
#[serde(rename = "type")]
_type: OPDSLinkType,
}

impl Default for OPDSAuthenticateProperties {
fn default() -> Self {
Self {
href: String::from("/opds/v2.0/auth"),
_type: OPDSLinkType::OpdsAuth,
}
}
}

impl OPDSAuthenticateProperties {
pub fn new(href: String) -> Self {
Self {
href,
_type: OPDSLinkType::OpdsAuth,
}
}
}

0 comments on commit 9871c63

Please sign in to comment.