about summary refs log tree commit diff
path: root/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib.rs')
-rw-r--r--src/lib.rs211
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/;"
+            )),
         ))
 }