about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/frontend/login.rs1
-rw-r--r--src/frontend/mod.rs461
-rw-r--r--src/frontend/templates/mod.rs38
3 files changed, 208 insertions, 292 deletions
diff --git a/src/frontend/login.rs b/src/frontend/login.rs
index 1c7c662..35ce3db 100644
--- a/src/frontend/login.rs
+++ b/src/frontend/login.rs
@@ -5,7 +5,6 @@ use serde::{Deserialize, Serialize};
 use sha2::{Digest, Sha256};
 use std::convert::TryInto;
 use std::str::FromStr;
-use tide::{Request, Response, Result};
 
 use crate::frontend::templates::Template;
 use crate::frontend::{FrontendError, IndiewebEndpoints};
diff --git a/src/frontend/mod.rs b/src/frontend/mod.rs
index c0452f3..ffeb9de 100644
--- a/src/frontend/mod.rs
+++ b/src/frontend/mod.rs
@@ -1,24 +1,24 @@
+#![warn(clippy::todo)]
 use std::convert::TryInto;
-
 use crate::database::Storage;
-use crate::ApplicationState;
-use log::{error, info};
 use serde::{Deserialize, Serialize};
-use tide::{Next, Request, Response, Result, StatusCode};
+use futures_util::TryFutureExt;
+use warp::{http::StatusCode, Filter, host::Authority, path::FullPath};
 
 static POSTS_PER_PAGE: usize = 20;
 
-pub mod login;
+//pub mod login;
 
 mod templates;
+#[allow(unused_imports)]
 use templates::{ErrorPage, MainPage, OnboardingPage, Template};
 
 #[derive(Clone, Serialize, Deserialize)]
 pub struct IndiewebEndpoints {
-    authorization_endpoint: String,
-    token_endpoint: String,
-    webmention: Option<String>,
-    microsub: Option<String>,
+    pub authorization_endpoint: String,
+    pub token_endpoint: String,
+    pub webmention: Option<String>,
+    pub microsub: Option<String>,
 }
 
 #[derive(Deserialize)]
@@ -32,6 +32,7 @@ struct FrontendError {
     source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
     code: StatusCode,
 }
+
 impl FrontendError {
     pub fn with_code<C>(code: C, msg: &str) -> Self
     where
@@ -40,7 +41,7 @@ impl FrontendError {
         Self {
             msg: msg.to_string(),
             source: None,
-            code: code.try_into().unwrap_or(StatusCode::InternalServerError),
+            code: code.try_into().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
         }
     }
     pub fn msg(&self) -> &str {
@@ -50,15 +51,17 @@ impl FrontendError {
         self.code
     }
 }
+
 impl From<crate::database::StorageError> for FrontendError {
     fn from(err: crate::database::StorageError) -> Self {
         Self {
             msg: "Database error".to_string(),
             source: Some(Box::new(err)),
-            code: StatusCode::InternalServerError,
+            code: StatusCode::INTERNAL_SERVER_ERROR,
         }
     }
 }
+
 impl std::error::Error for FrontendError {
     fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
         self.source
@@ -66,12 +69,15 @@ impl std::error::Error for FrontendError {
             .map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
     }
 }
+
 impl std::fmt::Display for FrontendError {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         write!(f, "{}", self.msg)
     }
 }
 
+impl warp::reject::Reject for FrontendError {}
+
 async fn get_post_from_database<S: Storage>(
     db: &S,
     url: &str,
@@ -85,7 +91,7 @@ async fn get_post_from_database<S: Storage>(
         Ok(result) => match result {
             Some(post) => Ok(post),
             None => Err(FrontendError::with_code(
-                StatusCode::NotFound,
+                StatusCode::NOT_FOUND,
                 "Post not found in the database",
             )),
         },
@@ -94,12 +100,12 @@ async fn get_post_from_database<S: Storage>(
                 // TODO: Authentication
                 if user.is_some() {
                     Err(FrontendError::with_code(
-                        StatusCode::Forbidden,
+                        StatusCode::FORBIDDEN,
                         "User authenticated AND forbidden to access this resource",
                     ))
                 } else {
                     Err(FrontendError::with_code(
-                        StatusCode::Unauthorized,
+                        StatusCode::UNAUTHORIZED,
                         "User needs to authenticate themselves",
                     ))
                 }
@@ -109,12 +115,14 @@ async fn get_post_from_database<S: Storage>(
     }
 }
 
+#[allow(dead_code)]
 #[derive(Deserialize)]
 struct OnboardingFeed {
     slug: String,
     name: String,
 }
 
+#[allow(dead_code)]
 #[derive(Deserialize)]
 struct OnboardingData {
     user: serde_json::Value,
@@ -123,7 +131,7 @@ struct OnboardingData {
     feeds: Vec<OnboardingFeed>,
 }
 
-pub async fn onboarding_receiver<S: Storage>(mut req: Request<ApplicationState<S>>) -> Result {
+/*pub async fn onboarding_receiver<S: Storage>(mut req: Request<ApplicationState<S>>) -> Result {
     use serde_json::json;
 
     log::debug!("Entering onboarding receiver...");
@@ -213,279 +221,186 @@ pub async fn onboarding_receiver<S: Storage>(mut req: Request<ApplicationState<S
 
     Ok(Response::builder(201).header("Location", "/").build())
 }
+*/
 
-pub async fn coffee<S: Storage>(_: Request<ApplicationState<S>>) -> Result {
-    Err(FrontendError::with_code(
-        StatusCode::ImATeapot,
-        "Someone asked this website to brew them some coffee...",
-    )
-    .into())
+fn request_uri() -> impl Filter<Extract = (String,), Error = warp::Rejection> + Copy {
+    crate::util::require_host()
+        .and(warp::path::full())
+        .map(|host: Authority, path: FullPath| "https://".to_owned() + host.as_str() + path.as_str())
 }
 
-pub async fn mainpage<S: Storage>(mut req: Request<ApplicationState<S>>) -> Result {
-    // This cannot error out as the URL must be valid. Or there is something horribly wrong
-    // and we shouldn't serve this request anyway.
-    <dyn AsMut<tide::http::Request>>::as_mut(&mut req)
-        .url_mut()
-        .set_scheme("https")
-        .unwrap();
-    let backend = &req.state().storage;
-    let query = req.query::<QueryParams>()?;
-    let authorization_endpoint = req.state().authorization_endpoint.to_string();
-    let token_endpoint = req.state().token_endpoint.to_string();
-    let user: Option<String> = req.session().get("user");
-
-    #[cfg(any(not(debug_assertions), test))]
-    let url = req.url();
-    #[cfg(all(debug_assertions, not(test)))]
-    let url = url::Url::parse("https://localhost:8080/").unwrap();
-
-    let hcard_url = url.as_str();
-    let feed_url = url.join("feeds/main").unwrap().to_string();
-
-    let card = get_post_from_database(backend, hcard_url, None, &user).await;
-    let feed = get_post_from_database(backend, &feed_url, query.after, &user).await;
-
-    if card.is_err() && feed.is_err() {
-        // Uh-oh! No main feed and no h-card? Need to do onboarding.
-        // We can do it from inside the app without ever requesting an auth token.
-        let card_err = card.unwrap_err();
-        let feed_err = feed.unwrap_err();
-        if card_err.code == 404 {
-            // Yes, we definitely need some onboarding here.
-            Ok(Response::builder(200)
-                .content_type("text/html; charset=utf-8")
-                .body(
-                    Template {
-                        title: "Kittybox - Onboarding",
-                        blog_name: "Kitty Box!",
-                        endpoints: IndiewebEndpoints {
-                            authorization_endpoint,
-                            token_endpoint,
-                            webmention: None,
-                            microsub: None,
-                        },
-                        feeds: Vec::default(),
-                        user: None,
-                        content: OnboardingPage {}.to_string(),
-                    }
-                    .to_string(),
-                )
-                .build())
-        } else {
-            Err(feed_err.into())
-        }
-    } else {
-        Ok(Response::builder(200)
-            .content_type("text/html; charset=utf-8")
-            .body(
-                Template {
-                    title: &format!("{} - Main page", url.host().unwrap().to_string()),
-                    blog_name: &backend
-                        .get_setting("site_name", hcard_url)
-                        .await
-                        .unwrap_or_else(|_| "Kitty Box!".to_string()),
-                    endpoints: IndiewebEndpoints {
-                        authorization_endpoint,
-                        token_endpoint,
-                        webmention: None,
-                        microsub: None,
-                    },
-                    feeds: backend
-                        .get_channels(hcard_url)
-                        .await
-                        .unwrap_or_else(|_| Vec::default()),
-                    user,
-                    content: MainPage {
-                        feed: &feed?,
-                        card: &card?,
+#[forbid(clippy::unwrap_used)]
+pub fn homepage<D: Storage>(db: D, endpoints: IndiewebEndpoints) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
+    let inject_db = move || db.clone();
+    warp::any()
+        .map(inject_db.clone())
+        .and(crate::util::require_host())
+        .and(warp::query())
+        .and_then(|db: D, host: Authority, q: QueryParams| async move {
+            let path = format!("https://{}/", host.to_string());
+            let feed_path = format!("https://{}/feeds/main", host.to_string());
+
+            match tokio::try_join!(
+                get_post_from_database(&db, &path, None, &None),
+                get_post_from_database(&db, &feed_path, q.after, &None)
+            ) {
+                Ok((hcard, hfeed)) => Ok((
+                    Some(hcard),
+                    Some(hfeed),
+                    StatusCode::OK
+                )),
+                Err(err) => {
+                    if err.code == StatusCode::NOT_FOUND {
+                        // signal for onboarding flow
+                        Ok((None, None, err.code))
+                    } else {
+                        Err(warp::reject::custom(err))
                     }
-                    .to_string(),
                 }
-                .to_string(),
-            )
-            .build())
-    }
-}
-
-pub async fn render_post<S: Storage>(mut req: Request<ApplicationState<S>>) -> Result {
-    let query = req.query::<QueryParams>()?;
-    let authorization_endpoint = req.state().authorization_endpoint.to_string();
-    let token_endpoint = req.state().token_endpoint.to_string();
-    let user: Option<String> = req.session().get("user");
-
-    // This cannot error out as the URL must be valid. Or there is something horribly wrong
-    // and we shouldn't serve this request anyway.
-    <dyn AsMut<tide::http::Request>>::as_mut(&mut req)
-        .url_mut()
-        .set_scheme("https")
-        .unwrap();
-    #[cfg(any(not(debug_assertions), test))]
-    let url = req.url();
-    #[cfg(all(debug_assertions, not(test)))]
-    let url = url::Url::parse("https://localhost:8080/")
-        .unwrap()
-        .join(req.url().path())
-        .unwrap();
-
-    let mut entry_url = req.url().clone();
-    entry_url.set_query(None);
-
-    let post = get_post_from_database(&req.state().storage, entry_url.as_str(), query.after, &user)
-        .await?;
-
-    #[cfg(debug_assertions)]
-    if let Some(value) = req.header("Accept") {
-        log::debug!("{:?}", value);
-
-        if value == "application/json" {
-            return Ok(Response::builder(200)
-                .content_type("application/json; charset=utf-8")
-                .body(post.to_string())
-                .build());
-        }
-    }
-
-    let template: String = match post["type"][0]
-        .as_str()
-        .expect("Empty type array or invalid type")
-    {
-        "h-entry" => templates::Entry { post: &post }.to_string(),
-        "h-card" => templates::VCard { card: &post }.to_string(),
-        "h-feed" => templates::Feed { feed: &post }.to_string(),
-        _ => {
-            return Err(FrontendError::with_code(
-                StatusCode::InternalServerError,
-                "Couldn't render an unknown type",
-            )
-            .into())
-        }
-    };
-    let origin = url.origin();
-    let owner = origin.ascii_serialization() + "/";
-
-    Ok(Response::builder(200)
-        .content_type("text/html; charset=utf-8")
-        .body(
-            Template {
-                title: post["properties"]["name"][0]
-                    .as_str()
-                    .unwrap_or(&format!("Note at {}", url.host().unwrap().to_string())),
-                blog_name: &req
-                    .state()
-                    .storage
-                    .get_setting("site_name", &owner) // XXX I'm pretty sure this is bound to cause issues with IDN-style domains
-                    .await
-                    .unwrap_or_else(|_| "Kitty Box!".to_string()),
-                endpoints: IndiewebEndpoints {
-                    authorization_endpoint,
-                    token_endpoint,
-                    webmention: None,
-                    microsub: None,
+            }
+        })
+        .and(warp::any().map(move || endpoints.clone()))
+        .and(crate::util::require_host())
+        .and(warp::any().map(inject_db))
+        .then(|content: (Option<serde_json::Value>, Option<serde_json::Value>, StatusCode), endpoints: IndiewebEndpoints, host: Authority, db: D| async move {
+            let owner = format!("https://{}/", host.as_str());
+            let blog_name = db.get_setting("site_name", &owner).await
+                .unwrap_or_else(|_| "Kitty Box!".to_string());
+            let feeds = db.get_channels(&owner).await.unwrap_or_default();
+            match content {
+                (Some(card), Some(feed), StatusCode::OK) => {
+                    warp::reply::html(Template {
+                        title: &blog_name,
+                        blog_name: &blog_name,
+                        endpoints,
+                        feeds,
+                        user: None, // TODO
+                        content: MainPage { feed: &feed, card: &card }.to_string()
+                    }.to_string())
                 },
-                feeds: req
-                    .state()
-                    .storage
-                    .get_channels(&owner)
-                    .await
-                    .unwrap_or_else(|_| Vec::default()),
-                user,
-                content: template,
+                _ => {
+                    // TODO Onboarding
+                    todo!("Onboarding flow")
+                }
             }
-            .to_string(),
-        )
-        .build())
+        })
 }
 
-pub struct ErrorHandlerMiddleware {}
-
-#[async_trait::async_trait]
-impl<S> tide::Middleware<ApplicationState<S>> for ErrorHandlerMiddleware
-where
-    S: crate::database::Storage,
-{
-    async fn handle(
-        &self,
-        request: Request<ApplicationState<S>>,
-        next: Next<'_, ApplicationState<S>>,
-    ) -> Result {
-        let authorization_endpoint = request.state().authorization_endpoint.to_string();
-        let token_endpoint = request.state().token_endpoint.to_string();
-        let owner = request.url().origin().ascii_serialization() + "/";
-        let site_name = &request
-            .state()
-            .storage
-            .get_setting("site_name", &owner)
-            .await
-            .unwrap_or_else(|_| "Kitty Box!".to_string());
-        let feeds = request
-            .state()
-            .storage
-            .get_channels(&owner)
-            .await
-            .unwrap_or_else(|_| Vec::default());
-        let user: Option<String> = request.session().get("user");
-        let mut res = next.run(request).await;
-        let mut code: Option<StatusCode> = None;
-        let mut msg: Option<String> = None;
-        if let Some(err) = res.downcast_error::<FrontendError>() {
-            code = Some(err.code());
-            error!("Error caught while processing request: {}", err.msg());
-            if err.code() == 400 {
-                msg = Some(err.msg().to_string());
+#[forbid(clippy::unwrap_used)]
+pub fn catchall<D: Storage>(db: D, endpoints: IndiewebEndpoints) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
+    let inject_db = move || db.clone();
+    warp::any()
+        .map(inject_db.clone())
+        .and(request_uri())
+        .and(warp::query())
+        .and_then(|db: D, path: String, query: QueryParams| async move {
+            get_post_from_database(&db, &path, query.after, &None).map_err(warp::reject::custom).await
+        })
+        // Rendering pipeline
+        .and_then(|post: serde_json::Value| async move {
+            let post_name = &post["properties"]["name"][0].as_str().to_owned();
+            match post["type"][0]
+                .as_str()
+            {
+                Some("h-entry") => Ok((
+                    post_name.unwrap_or("Note").to_string(),
+                    templates::Entry { post: &post }.to_string(),
+                    StatusCode::OK
+                )),
+                Some("h-card") => Ok((
+                    post_name.unwrap_or("Contact card").to_string(),
+                    templates::VCard { card: &post }.to_string(),
+                    StatusCode::OK
+                )),
+                Some("h-feed") => Ok((
+                    post_name.unwrap_or("Feed").to_string(),
+                    templates::Feed { feed: &post }.to_string(),
+                    StatusCode::OK
+                )),
+                _ => Err(warp::reject::custom(FrontendError::with_code(
+                    StatusCode::INTERNAL_SERVER_ERROR,
+                    &format!("Couldn't render an unknown type: {}", post["type"][0]),
+                )))
             }
-            let mut err: &dyn std::error::Error = err;
-            while let Some(e) = err.source() {
-                error!("Caused by: {}", e);
-                err = e;
+        })
+        .recover(|err: warp::Rejection| {
+            use warp::Rejection;
+            use futures_util::future;
+            if let Some(err) = err.find::<FrontendError>() {
+                return future::ok::<(String, String, StatusCode), Rejection>((
+                    format!("Error: HTTP {}", err.code().as_u16()),
+                    ErrorPage { code: err.code(), msg: Some(err.msg().to_string()) }.to_string(),
+                    err.code()
+                ));
             }
-        }
-        if let Some(code) = code {
-            res.set_status(code);
-            res.set_content_type("text/html; charset=utf-8");
-            res.set_body(
-                Template {
-                    title: "Error",
-                    blog_name: site_name,
-                    endpoints: IndiewebEndpoints {
-                        authorization_endpoint,
-                        token_endpoint,
-                        webmention: None,
-                        microsub: None,
-                    },
-                    feeds,
-                    user,
-                    content: ErrorPage { code, msg }.to_string(),
-                }
-                .to_string(),
-            );
-        }
-        Ok(res)
-    }
+            future::err::<(String, String, StatusCode), Rejection>(err)
+        })
+        .unify()
+        .and(warp::any().map(move || endpoints.clone()))
+        .and(crate::util::require_host())
+        .and(warp::any().map(inject_db))
+        .then(|content: (String, String, StatusCode), endpoints: IndiewebEndpoints, host: Authority, db: D| async move {
+            let owner = format!("https://{}/", host.as_str());
+            let blog_name = db.get_setting("site_name", &owner).await
+                .unwrap_or_else(|_| "Kitty Box!".to_string());
+            let feeds = db.get_channels(&owner).await.unwrap_or_default();
+            let (title, content, code) = content;
+            warp::reply::with_status(warp::reply::html(Template {
+                title: &title,
+                blog_name: &blog_name,
+                endpoints,
+                feeds,
+                user: None, // TODO
+                content,
+            }.to_string()), code)
+        })
+
 }
 
 static STYLE_CSS: &[u8] = include_bytes!("./style.css");
 static ONBOARDING_JS: &[u8] = include_bytes!("./onboarding.js");
 static ONBOARDING_CSS: &[u8] = include_bytes!("./onboarding.css");
 
-pub async fn handle_static<S: Storage>(req: Request<ApplicationState<S>>) -> Result {
-    Ok(match req.param("path") {
-        Ok("style.css") => Ok(Response::builder(200)
-            .content_type("text/css; charset=utf-8")
-            .body(STYLE_CSS)
-            .build()),
-        Ok("onboarding.js") => Ok(Response::builder(200)
-            .content_type("text/javascript; charset=utf-8")
-            .body(ONBOARDING_JS)
-            .build()),
-        Ok("onboarding.css") => Ok(Response::builder(200)
-            .content_type("text/css; charset=utf-8")
-            .body(ONBOARDING_CSS)
-            .build()),
-        Ok(_) => Err(FrontendError::with_code(
-            StatusCode::NotFound,
-            "Static file not found",
-        )),
-        Err(_) => panic!("Invalid usage of the frontend::handle_static() function"),
-    }?)
+static MIME_JS: &str = "application/javascript";
+static MIME_CSS: &str = "text/css";
+
+fn _dispatch_static(name: &str) -> Option<(&'static [u8], &'static str)> {
+    match name {
+        "style.css" => Some((STYLE_CSS, MIME_CSS)),
+        "onboarding.js" => Some((ONBOARDING_JS, MIME_JS)),
+        "onboarding.css" => Some((ONBOARDING_CSS, MIME_CSS)),
+        _ => None
+    }
+}
+
+pub fn static_files() -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Copy {
+    use futures_util::future;
+
+    warp::get()
+        .and(warp::path::param()
+             .and_then(|filename: String| {
+                 match _dispatch_static(&filename) {
+                     Some((buf, content_type)) => future::ok(
+                         warp::reply::with_header(
+                             buf, "Content-Type", content_type
+                         )
+                     ),
+                     None => future::err(warp::reject())
+                 }
+             }))
+        .or(warp::head()
+            .and(warp::path::param()
+                 .and_then(|filename: String| {
+                     match _dispatch_static(&filename) {
+                         Some((buf, content_type)) => future::ok(
+                             warp::reply::with_header(
+                                 warp::reply::with_header(
+                                     warp::reply(), "Content-Type", content_type
+                                 ),
+                                 "Content-Length", buf.len()
+                             )
+                         ),
+                         None => future::err(warp::reject())
+                     }
+                 })))
 }
diff --git a/src/frontend/templates/mod.rs b/src/frontend/templates/mod.rs
index c21d30b..364a716 100644
--- a/src/frontend/templates/mod.rs
+++ b/src/frontend/templates/mod.rs
@@ -1,7 +1,7 @@
 use crate::database::MicropubChannel;
 use crate::frontend::IndiewebEndpoints;
 use ellipse::Ellipse;
-use http_types::StatusCode;
+use warp::http::StatusCode;
 use log::error;
 
 /// Return a pretty location specifier from a geo: URI.
@@ -397,12 +397,12 @@ markup::define! {
         @Feed { feed }
     }
     ErrorPage(code: StatusCode, msg: Option<String>) {
-        h1 { @format!("HTTP {} {}", code, code.canonical_reason()) }
-        @match code {
-            StatusCode::Unauthorized => {
+        h1 { @format!("HTTP {}", code) }
+        @match *code {
+            StatusCode::UNAUTHORIZED => {
                 p { "Looks like you need to authenticate yourself before seeing this page. Try logging in with IndieAuth using the Login button above!" }
             }
-            StatusCode::Forbidden => {
+            StatusCode::FORBIDDEN => {
                 p { "Looks like you're forbidden from viewing this page." }
                 p {
                     "This might've been caused by being banned from viewing my website"
@@ -410,16 +410,16 @@ markup::define! {
                     "like a private post that's not intended for you. It's ok, it happens."
                 }
             }
-            StatusCode::Gone => {
+            StatusCode::GONE => {
                 p { "Looks like the page you're trying to find is gone and is never coming back." }
             }
-            StatusCode::UnavailableForLegalReasons => {
+            StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS => {
             p { "The page is there, but I can't legally provide it to you because the censorship said so." }
             }
-            StatusCode::NotFound => {
+            StatusCode::NOT_FOUND => {
                 p { "Looks like there's no such page. Maybe you or someone else mistyped a URL or my database experienced data loss." }
             }
-            StatusCode::ImATeapot => {
+            StatusCode::IM_A_TEAPOT => {
                 p { "Wait, do you seriously expect my website to brew you coffee? It's not a coffee machine!" }
 
                 p {
@@ -433,16 +433,18 @@ markup::define! {
                     }
                 }
             }
-            StatusCode::BadRequest => {
-                @if msg.is_none() {
-                    p {
-                        "There was an undescribed error in your request. "
-                        "Please try again later or with a different request."
+            StatusCode::BAD_REQUEST => {
+                @match msg {
+                    None => {
+                        p {
+                            "There was an undescribed error in your request. "
+                            "Please try again later or with a different request."
+                        }
                     }
-                } else {
-                    p {
-                        "There was a following error in your request: "
-                        @msg.as_ref().unwrap()
+                    Some(msg) => {
+                        p {
+                            "There was a following error in your request: " @msg
+                        }
                     }
                 }
             }