diff options
Diffstat (limited to 'src/lib.rs')
-rw-r--r-- | src/lib.rs | 211 |
1 files changed, 136 insertions, 75 deletions
diff --git a/src/lib.rs b/src/lib.rs index 4aeaca5..a52db4c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,22 +4,28 @@ use std::sync::Arc; use axum::extract::{FromRef, FromRequestParts, OptionalFromRequestParts}; -use axum_extra::extract::{cookie::{Cookie, Key}, SignedCookieJar}; +use axum_extra::extract::{ + cookie::{Cookie, Key}, + SignedCookieJar, +}; use database::{FileStorage, PostgresStorage, Storage}; use indieauth::backend::{AuthBackend, FileBackend as FileAuthBackend}; use kittybox_util::queue::JobQueue; -use media::storage::{MediaStore, file::FileStore as FileMediaStore}; -use tokio::{sync::{Mutex, RwLock}, task::JoinSet}; +use media::storage::{file::FileStore as FileMediaStore, MediaStore}; +use tokio::{ + sync::{Mutex, RwLock}, + task::JoinSet, +}; use webmentions::queue::PostgresJobQueue; /// Database abstraction layer for Kittybox, allowing the CMS to work with any kind of database. pub mod database; pub mod frontend; +pub mod indieauth; +pub mod login; pub mod media; pub mod micropub; -pub mod indieauth; pub mod webmentions; -pub mod login; //pub mod admin; const OAUTH2_SOFTWARE_ID: &str = "6f2eee84-c22c-4c9e-b900-10d4e97273c8"; @@ -27,10 +33,10 @@ const OAUTH2_SOFTWARE_ID: &str = "6f2eee84-c22c-4c9e-b900-10d4e97273c8"; #[derive(Clone)] pub struct AppState<A, S, M, Q> where -A: AuthBackend + Sized + 'static, -S: Storage + Sized + 'static, -M: MediaStore + Sized + 'static, -Q: JobQueue<webmentions::Webmention> + Sized + A: AuthBackend + Sized + 'static, + S: Storage + Sized + 'static, + M: MediaStore + Sized + 'static, + Q: JobQueue<webmentions::Webmention> + Sized, { pub auth_backend: A, pub storage: S, @@ -39,7 +45,7 @@ Q: JobQueue<webmentions::Webmention> + Sized pub http: reqwest_middleware::ClientWithMiddleware, pub background_jobs: Arc<Mutex<JoinSet<()>>>, pub cookie_key: Key, - pub session_store: SessionStore + pub session_store: SessionStore, } pub type SessionStore = Arc<RwLock<std::collections::HashMap<uuid::Uuid, Session>>>; @@ -60,7 +66,11 @@ pub struct NoSessionError; impl axum::response::IntoResponse for NoSessionError { fn into_response(self) -> axum::response::Response { // TODO: prettier error message - (axum::http::StatusCode::UNAUTHORIZED, "You are not logged in, but this page requires a session.").into_response() + ( + axum::http::StatusCode::UNAUTHORIZED, + "You are not logged in, but this page requires a session.", + ) + .into_response() } } @@ -72,11 +82,17 @@ where { type Rejection = std::convert::Infallible; - async fn from_request_parts(parts: &mut axum::http::request::Parts, state: &S) -> Result<Option<Self>, Self::Rejection> { - let jar = SignedCookieJar::<Key>::from_request_parts(parts, state).await.unwrap(); + async fn from_request_parts( + parts: &mut axum::http::request::Parts, + state: &S, + ) -> Result<Option<Self>, Self::Rejection> { + let jar = SignedCookieJar::<Key>::from_request_parts(parts, state) + .await + .unwrap(); let session_store = SessionStore::from_ref(state).read_owned().await; - Ok(jar.get("session_id") + Ok(jar + .get("session_id") .as_ref() .map(Cookie::value_trimmed) .and_then(|id| uuid::Uuid::parse_str(id).ok()) @@ -103,7 +119,10 @@ where // have to repeat this magic invocation. impl<S, M, Q> FromRef<AppState<Self, S, M, Q>> for FileAuthBackend -where S: Storage, M: MediaStore, Q: JobQueue<webmentions::Webmention> +where + S: Storage, + M: MediaStore, + Q: JobQueue<webmentions::Webmention>, { fn from_ref(input: &AppState<Self, S, M, Q>) -> Self { input.auth_backend.clone() @@ -111,7 +130,10 @@ where S: Storage, M: MediaStore, Q: JobQueue<webmentions::Webmention> } impl<A, M, Q> FromRef<AppState<A, Self, M, Q>> for PostgresStorage -where A: AuthBackend, M: MediaStore, Q: JobQueue<webmentions::Webmention> +where + A: AuthBackend, + M: MediaStore, + Q: JobQueue<webmentions::Webmention>, { fn from_ref(input: &AppState<A, Self, M, Q>) -> Self { input.storage.clone() @@ -119,7 +141,10 @@ where A: AuthBackend, M: MediaStore, Q: JobQueue<webmentions::Webmention> } impl<A, M, Q> FromRef<AppState<A, Self, M, Q>> for FileStorage -where A: AuthBackend, M: MediaStore, Q: JobQueue<webmentions::Webmention> +where + A: AuthBackend, + M: MediaStore, + Q: JobQueue<webmentions::Webmention>, { fn from_ref(input: &AppState<A, Self, M, Q>) -> Self { input.storage.clone() @@ -128,7 +153,10 @@ where A: AuthBackend, M: MediaStore, Q: JobQueue<webmentions::Webmention> impl<A, S, Q> FromRef<AppState<A, S, Self, Q>> for FileMediaStore // where A: AuthBackend, S: Storage -where A: AuthBackend, S: Storage, Q: JobQueue<webmentions::Webmention> +where + A: AuthBackend, + S: Storage, + Q: JobQueue<webmentions::Webmention>, { fn from_ref(input: &AppState<A, S, Self, Q>) -> Self { input.media_store.clone() @@ -136,7 +164,11 @@ where A: AuthBackend, S: Storage, Q: JobQueue<webmentions::Webmention> } impl<A, S, M, Q> FromRef<AppState<A, S, M, Q>> for Key -where A: AuthBackend, S: Storage, M: MediaStore, Q: JobQueue<webmentions::Webmention> +where + A: AuthBackend, + S: Storage, + M: MediaStore, + Q: JobQueue<webmentions::Webmention>, { fn from_ref(input: &AppState<A, S, M, Q>) -> Self { input.cookie_key.clone() @@ -144,7 +176,11 @@ where A: AuthBackend, S: Storage, M: MediaStore, Q: JobQueue<webmentions::Webmen } impl<A, S, M, Q> FromRef<AppState<A, S, M, Q>> for reqwest_middleware::ClientWithMiddleware -where A: AuthBackend, S: Storage, M: MediaStore, Q: JobQueue<webmentions::Webmention> +where + A: AuthBackend, + S: Storage, + M: MediaStore, + Q: JobQueue<webmentions::Webmention>, { fn from_ref(input: &AppState<A, S, M, Q>) -> Self { input.http.clone() @@ -152,7 +188,11 @@ where A: AuthBackend, S: Storage, M: MediaStore, Q: JobQueue<webmentions::Webmen } impl<A, S, M, Q> FromRef<AppState<A, S, M, Q>> for Arc<Mutex<JoinSet<()>>> -where A: AuthBackend, S: Storage, M: MediaStore, Q: JobQueue<webmentions::Webmention> +where + A: AuthBackend, + S: Storage, + M: MediaStore, + Q: JobQueue<webmentions::Webmention>, { fn from_ref(input: &AppState<A, S, M, Q>) -> Self { input.background_jobs.clone() @@ -161,7 +201,10 @@ where A: AuthBackend, S: Storage, M: MediaStore, Q: JobQueue<webmentions::Webmen #[cfg(feature = "sqlx")] impl<A, S, M> FromRef<AppState<A, S, M, Self>> for PostgresJobQueue<webmentions::Webmention> -where A: AuthBackend, S: Storage, M: MediaStore +where + A: AuthBackend, + S: Storage, + M: MediaStore, { fn from_ref(input: &AppState<A, S, M, Self>) -> Self { input.job_queue.clone() @@ -169,7 +212,11 @@ where A: AuthBackend, S: Storage, M: MediaStore } impl<A, S, M, Q> FromRef<AppState<A, S, M, Q>> for SessionStore -where A: AuthBackend, S: Storage, M: MediaStore, Q: JobQueue<webmentions::Webmention> +where + A: AuthBackend, + S: Storage, + M: MediaStore, + Q: JobQueue<webmentions::Webmention>, { fn from_ref(input: &AppState<A, S, M, Q>) -> Self { input.session_store.clone() @@ -177,23 +224,26 @@ where A: AuthBackend, S: Storage, M: MediaStore, Q: JobQueue<webmentions::Webmen } pub mod companion { - use std::{collections::HashMap, sync::Arc}; use axum::{ extract::{Extension, Path}, - response::{IntoResponse, Response} + response::{IntoResponse, Response}, }; + use std::{collections::HashMap, sync::Arc}; #[derive(Debug, Clone, Copy)] struct Resource { data: &'static [u8], - mime: &'static str + mime: &'static str, } impl IntoResponse for &Resource { fn into_response(self) -> Response { - (axum::http::StatusCode::OK, - [("Content-Type", self.mime)], - self.data).into_response() + ( + axum::http::StatusCode::OK, + [("Content-Type", self.mime)], + self.data, + ) + .into_response() } } @@ -203,17 +253,21 @@ pub mod companion { #[tracing::instrument] async fn map_to_static( Path(name): Path<String>, - Extension(resources): Extension<ResourceTable> + Extension(resources): Extension<ResourceTable>, ) -> Response { tracing::debug!("Searching for {} in the resource table...", name); match resources.get(name.as_str()) { Some(res) => res.into_response(), None => { - #[cfg(debug_assertions)] tracing::error!("Not found"); + #[cfg(debug_assertions)] + tracing::error!("Not found"); - (axum::http::StatusCode::NOT_FOUND, - [("Content-Type", "text/plain")], - "Not found. Sorry.".as_bytes()).into_response() + ( + axum::http::StatusCode::NOT_FOUND, + [("Content-Type", "text/plain")], + "Not found. Sorry.".as_bytes(), + ) + .into_response() } } } @@ -249,47 +303,52 @@ pub mod companion { Arc::new(map) }; - axum::Router::new() - .route( - "/{filename}", - axum::routing::get(map_to_static) - .layer(Extension(resources)) - ) + axum::Router::new().route( + "/{filename}", + axum::routing::get(map_to_static).layer(Extension(resources)), + ) } } async fn teapot_route() -> impl axum::response::IntoResponse { use axum::http::{header, StatusCode}; - (StatusCode::IM_A_TEAPOT, [(header::CONTENT_TYPE, "text/plain")], "Sorry, can't brew coffee yet!") + ( + StatusCode::IM_A_TEAPOT, + [(header::CONTENT_TYPE, "text/plain")], + "Sorry, can't brew coffee yet!", + ) } async fn health_check<D>( axum::extract::State(data): axum::extract::State<D>, ) -> impl axum::response::IntoResponse where - D: crate::database::Storage + D: crate::database::Storage, { (axum::http::StatusCode::OK, std::borrow::Cow::Borrowed("OK")) } pub async fn compose_kittybox<St, A, S, M, Q>() -> axum::Router<St> where -A: AuthBackend + 'static + FromRef<St>, -S: Storage + 'static + FromRef<St>, -M: MediaStore + 'static + FromRef<St>, -Q: kittybox_util::queue::JobQueue<crate::webmentions::Webmention> + FromRef<St>, -reqwest_middleware::ClientWithMiddleware: FromRef<St>, -Arc<Mutex<JoinSet<()>>>: FromRef<St>, -crate::SessionStore: FromRef<St>, -axum_extra::extract::cookie::Key: FromRef<St>, -St: Clone + Send + Sync + 'static + A: AuthBackend + 'static + FromRef<St>, + S: Storage + 'static + FromRef<St>, + M: MediaStore + 'static + FromRef<St>, + Q: kittybox_util::queue::JobQueue<crate::webmentions::Webmention> + FromRef<St>, + reqwest_middleware::ClientWithMiddleware: FromRef<St>, + Arc<Mutex<JoinSet<()>>>: FromRef<St>, + crate::SessionStore: FromRef<St>, + axum_extra::extract::cookie::Key: FromRef<St>, + St: Clone + Send + Sync + 'static, { use axum::routing::get; axum::Router::new() .route("/", get(crate::frontend::homepage::<S>)) .fallback(get(crate::frontend::catchall::<S>)) .route("/.kittybox/micropub", crate::micropub::router::<A, S, St>()) - .route("/.kittybox/onboarding", crate::frontend::onboarding::router::<St, S>()) + .route( + "/.kittybox/onboarding", + crate::frontend::onboarding::router::<St, S>(), + ) .nest("/.kittybox/media", crate::media::router::<St, A, M>()) .merge(crate::indieauth::router::<St, A, S>()) .merge(crate::webmentions::router::<St, Q>()) @@ -297,34 +356,36 @@ St: Clone + Send + Sync + 'static .nest("/.kittybox/login", crate::login::router::<St, S>()) .route( "/.kittybox/static/{*path}", - axum::routing::get(crate::frontend::statics) + axum::routing::get(crate::frontend::statics), ) .route("/.kittybox/coffee", get(teapot_route)) - .nest("/.kittybox/micropub/client", crate::companion::router::<St>()) + .nest( + "/.kittybox/micropub/client", + crate::companion::router::<St>(), + ) .layer(tower_http::trace::TraceLayer::new_for_http()) .layer(tower_http::catch_panic::CatchPanicLayer::new()) - .layer(tower_http::sensitive_headers::SetSensitiveHeadersLayer::new([ - axum::http::header::AUTHORIZATION, - axum::http::header::COOKIE, - axum::http::header::SET_COOKIE, - ])) + .layer( + tower_http::sensitive_headers::SetSensitiveHeadersLayer::new([ + axum::http::header::AUTHORIZATION, + axum::http::header::COOKIE, + axum::http::header::SET_COOKIE, + ]), + ) .layer(tower_http::set_header::SetResponseHeaderLayer::appending( axum::http::header::CONTENT_SECURITY_POLICY, - axum::http::HeaderValue::from_static( - concat!( - "default-src 'none';", // Do not allow unknown things we didn't foresee. - "img-src https:;", // Allow hotlinking images from anywhere. - "form-action 'self';", // Only allow sending forms back to us. - "media-src 'self';", // Only allow embedding media from us. - "script-src 'self';", // Only run scripts we serve. - "style-src 'self';", // Only use styles we serve. - "base-uri 'none';", // Do not allow to change the base URI. - "object-src 'none';", // Do not allow to embed objects (Flash/ActiveX). - - // Allow embedding the Bandcamp player for jam posts. - // TODO: perhaps make this policy customizable?… - "frame-src 'self' https://bandcamp.com/EmbeddedPlayer/;" - ) - ) + axum::http::HeaderValue::from_static(concat!( + "default-src 'none';", // Do not allow unknown things we didn't foresee. + "img-src https:;", // Allow hotlinking images from anywhere. + "form-action 'self';", // Only allow sending forms back to us. + "media-src 'self';", // Only allow embedding media from us. + "script-src 'self';", // Only run scripts we serve. + "style-src 'self';", // Only use styles we serve. + "base-uri 'none';", // Do not allow to change the base URI. + "object-src 'none';", // Do not allow to embed objects (Flash/ActiveX). + // Allow embedding the Bandcamp player for jam posts. + // TODO: perhaps make this policy customizable?… + "frame-src 'self' https://bandcamp.com/EmbeddedPlayer/;" + )), )) } |