From 797257cce721a8c14e3b630ac4dddc8b9d41a989 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Thu, 24 Mar 2022 09:01:26 +0100 Subject: [PATCH] Start migrating to Axum Now with the homepage and the static files --- Cargo.lock | 75 ++++++++++++++++++- crates/cli/src/commands/server.rs | 10 +-- crates/handlers/Cargo.toml | 4 +- crates/handlers/src/lib.rs | 25 +++++-- crates/handlers/src/views/index.rs | 28 ++++++- crates/handlers/src/views/mod.rs | 26 +++---- crates/static-files/Cargo.toml | 5 +- crates/static-files/src/lib.rs | 115 ++++++++++------------------- 8 files changed, 182 insertions(+), 106 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ad67745..cc73ddc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -460,6 +460,50 @@ dependencies = [ "zeroize", ] +[[package]] +name = "axum" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9f346c92c1e9a71d14fe4aaf7c2a5d9932cc4e5e48d8fb6641524416eb79ddd" +dependencies = [ + "async-trait", + "axum-core", + "bitflags", + "bytes 1.1.0", + "futures-util", + "http", + "http-body", + "hyper", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbcda393bef9c87572779cb8ef916f12d77750b27535dd6819fa86591627a51" +dependencies = [ + "async-trait", + "bytes 1.1.0", + "futures-util", + "http", + "http-body", + "mime", +] + [[package]] name = "backtrace" version = "0.3.64" @@ -1820,6 +1864,18 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" +[[package]] +name = "mas-axum-utils" +version = "0.1.0" +dependencies = [ + "async-trait", + "axum", + "futures-util", + "headers", + "mas-templates", + "sqlx", +] + [[package]] name = "mas-cli" version = "0.1.0" @@ -1928,6 +1984,7 @@ version = "0.1.0" dependencies = [ "anyhow", "argon2", + "axum", "chrono", "crc", "data-encoding", @@ -1936,6 +1993,7 @@ dependencies = [ "hyper", "indoc", "lettre", + "mas-axum-utils", "mas-config", "mas-data-model", "mas-email", @@ -2052,10 +2110,13 @@ dependencies = [ name = "mas-static-files" version = "0.1.0" dependencies = [ + "axum", "headers", + "http", "mime_guess", "rust-embed", - "warp", + "tokio", + "tower", ] [[package]] @@ -2173,6 +2234,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "matchit" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9376a4f0340565ad675d11fc1419227faf5f60cd7ac9cb2e7185a471f30af833" + [[package]] name = "md-5" version = "0.9.1" @@ -3788,6 +3855,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "sync_wrapper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" + [[package]] name = "synstructure" version = "0.12.6" diff --git a/crates/cli/src/commands/server.rs b/crates/cli/src/commands/server.rs index 902f99d8..27555c7d 100644 --- a/crates/cli/src/commands/server.rs +++ b/crates/cli/src/commands/server.rs @@ -203,20 +203,16 @@ impl Options { .context("could not watch for templates changes")?; } - // Start the server - let root = mas_handlers::root(&pool, &templates, &key_store, &encrypter, &mailer, &config); + let router = + mas_handlers::router(&pool, &templates, &key_store, &encrypter, &mailer, &config); // Explicitely the config to properly zeroize secret keys drop(config); - let warp_service = warp::service(root); - - let service = mas_http::ServerLayer::default().layer(warp_service); - info!("Listening on http://{}", listener.local_addr().unwrap()); Server::from_tcp(listener)? - .serve(Shared::new(service)) + .serve(router.into_make_service()) .with_graceful_shutdown(shutdown_signal()) .await?; diff --git a/crates/handlers/Cargo.toml b/crates/handlers/Cargo.toml index 416d5c1d..9204cb27 100644 --- a/crates/handlers/Cargo.toml +++ b/crates/handlers/Cargo.toml @@ -22,6 +22,8 @@ anyhow = "1.0.56" # Web server warp = "0.3.2" hyper = { version = "0.14.17", features = ["full"] } +tower = "0.4.12" +axum = "0.4.8" # Emails lettre = { version = "0.10.0-rc.4", default-features = false, features = ["builder"] } @@ -63,7 +65,7 @@ mas-static-files = { path = "../static-files" } mas-storage = { path = "../storage" } mas-templates = { path = "../templates" } mas-warp-utils = { path = "../warp-utils" } -tower = "0.4.12" +mas-axum-utils = { path = "../axum-utils" } [dev-dependencies] indoc = "1.0.4" diff --git a/crates/handlers/src/lib.rs b/crates/handlers/src/lib.rs index 974d7daf..404f583f 100644 --- a/crates/handlers/src/lib.rs +++ b/crates/handlers/src/lib.rs @@ -21,12 +21,11 @@ use std::sync::Arc; +use axum::{extract::Extension, routing::get, Router}; use mas_config::{Encrypter, RootConfig}; use mas_email::Mailer; use mas_jose::StaticKeystore; -use mas_static_files::filter as static_files; use mas_templates::Templates; -use mas_warp_utils::filters; use sqlx::PgPool; use warp::{filters::BoxedFilter, Filter, Reply}; @@ -55,10 +54,26 @@ pub fn root( &config.http, &config.csrf, ); - let static_files = - static_files(config.http.web_root.clone()).and(filters::trace::name("GET static file")); - let filter = health.or(views).unify().or(static_files).unify().or(oauth2); + let filter = health.or(views).unify().or(oauth2); filter.with(warp::log(module_path!())).boxed() } + +pub fn router( + pool: &PgPool, + templates: &Templates, + key_store: &Arc, + encrypter: &Encrypter, + mailer: &Mailer, + config: &RootConfig, +) -> Router { + Router::new() + .route("/", get(self::views::index::get)) + .fallback(mas_static_files::Assets) + .layer(Extension(pool.clone())) + .layer(Extension(templates.clone())) + .layer(Extension(key_store.clone())) + .layer(Extension(encrypter.clone())) + .layer(Extension(mailer.clone())) +} diff --git a/crates/handlers/src/views/index.rs b/crates/handlers/src/views/index.rs index ae88565b..d584d003 100644 --- a/crates/handlers/src/views/index.rs +++ b/crates/handlers/src/views/index.rs @@ -1,4 +1,4 @@ -// Copyright 2021 The Matrix.org Foundation C.I.C. +// Copyright 2021, 2022 The Matrix.org Foundation C.I.C. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,6 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::str::FromStr; + +use axum::{ + extract::Extension, + response::{Html, IntoResponse}, +}; +use mas_axum_utils::{fancy_error, FancyError}; use mas_config::{CsrfConfig, Encrypter, HttpConfig}; use mas_data_model::BrowserSession; use mas_storage::PostgresqlBackend; @@ -25,8 +32,10 @@ use mas_warp_utils::filters::{ with_templates, CsrfToken, }; use sqlx::PgPool; +use url::Url; use warp::{filters::BoxedFilter, reply::html, Filter, Rejection, Reply}; +/* pub(super) fn filter( pool: &PgPool, templates: &Templates, @@ -62,3 +71,20 @@ async fn get( let reply = cookie_saver.save_encrypted(&csrf_token, reply)?; Ok(Box::new(reply)) } +*/ + +pub async fn get( + Extension(templates): Extension, +) -> Result { + let ctx = IndexContext::new( + Url::from_str("https://example.com/.well-known/openid-discovery").unwrap(), + ) + .maybe_with_session::(None) + .with_csrf("csrf_token".to_string()); + + let content = templates + .render_index(&ctx) + .await + .map_err(fancy_error(templates))?; + Ok(Html(content)) +} diff --git a/crates/handlers/src/views/mod.rs b/crates/handlers/src/views/mod.rs index ebd5643f..937a0c32 100644 --- a/crates/handlers/src/views/mod.rs +++ b/crates/handlers/src/views/mod.rs @@ -18,19 +18,18 @@ use mas_templates::Templates; use sqlx::PgPool; use warp::{filters::BoxedFilter, Filter, Reply}; -mod account; -mod index; -mod login; -mod logout; -mod reauth; -mod register; -mod shared; -mod verify; +pub mod account; +pub mod index; +pub mod login; +pub mod logout; +pub mod reauth; +pub mod register; +pub mod shared; +pub mod verify; use self::{ - account::filter as account, index::filter as index, login::filter as login, - logout::filter as logout, reauth::filter as reauth, register::filter as register, - verify::filter as verify, + account::filter as account, login::filter as login, logout::filter as logout, + reauth::filter as reauth, register::filter as register, verify::filter as verify, }; pub(crate) use self::{ login::LoginRequest, reauth::ReauthRequest, register::RegisterRequest, shared::PostAuthAction, @@ -44,7 +43,6 @@ pub(super) fn filter( http_config: &HttpConfig, csrf_config: &CsrfConfig, ) -> BoxedFilter<(Box,)> { - let index = index(pool, templates, encrypter, http_config, csrf_config); let account = account(pool, templates, mailer, encrypter, http_config, csrf_config); let login = login(pool, templates, encrypter, csrf_config); let register = register(pool, templates, encrypter, csrf_config); @@ -52,9 +50,7 @@ pub(super) fn filter( let reauth = reauth(pool, templates, encrypter, csrf_config); let verify = verify(pool, templates, encrypter, csrf_config); - index - .or(account) - .unify() + account .or(login) .unify() .or(register) diff --git a/crates/static-files/Cargo.toml b/crates/static-files/Cargo.toml index e0de6f78..3f8c2469 100644 --- a/crates/static-files/Cargo.toml +++ b/crates/static-files/Cargo.toml @@ -9,7 +9,10 @@ license = "Apache-2.0" dev = [] [dependencies] +axum = "0.4.8" headers = "0.3.7" +http = "0.2.6" mime_guess = "2.0.4" rust-embed = "6.3.0" -warp = "0.3.2" +tokio = { version = "1.17.0", features = ["fs"] } +tower = "0.4.12" diff --git a/crates/static-files/src/lib.rs b/crates/static-files/src/lib.rs index 0d270615..d62414d2 100644 --- a/crates/static-files/src/lib.rs +++ b/crates/static-files/src/lib.rs @@ -18,95 +18,60 @@ #![deny(clippy::all, missing_docs, rustdoc::broken_intra_doc_links)] #![warn(clippy::pedantic)] -use std::path::PathBuf; +use std::{ + convert::Infallible, + future::{ready, Ready}, +}; -use warp::{filters::BoxedFilter, Filter, Reply}; +use axum::{ + body::{boxed, Full}, + response::{IntoResponse, Response}, +}; +use headers::{ContentLength, ContentType, HeaderMapExt}; +use http::{Request, StatusCode}; +use rust_embed::RustEmbed; +use tower::Service; -#[cfg(not(feature = "dev"))] -mod builtin { - use std::{fmt::Write, str::FromStr}; +// TODO: read the assets live from the filesystem - use headers::{ContentLength, ContentType, ETag, HeaderMapExt}; - use rust_embed::RustEmbed; - use warp::{ - filters::BoxedFilter, hyper::StatusCode, path::Tail, reply::Response, Filter, Rejection, - Reply, - }; +/// Embedded public assets +#[derive(RustEmbed, Clone)] +#[folder = "public/"] +pub struct Assets; - #[derive(RustEmbed)] - #[folder = "public/"] - struct Asset; - - #[allow(clippy::unused_async)] - async fn serve_embed( - path: Tail, - if_none_match: Option, - ) -> Result, Rejection> { - let path = path.as_str(); - let asset = Asset::get(path).ok_or_else(warp::reject::not_found)?; - - // TODO: this etag calculation is ugly - let etag = { - let mut s = String::with_capacity(32 * 2 + 2); - write!(s, "\"").unwrap(); - for b in asset.metadata.sha256_hash() { - write!(s, "{:02x}", b).unwrap(); - } - write!(s, "\"").unwrap(); - s - }; - - if Some(&etag) == if_none_match.as_ref() { - return Ok(Box::new(StatusCode::NOT_MODIFIED)); - }; +impl Assets { + fn get_response(path: &str) -> Option { + let asset = Self::get(path)?; let len = asset.data.len().try_into().unwrap(); let mime = mime_guess::from_path(path).first_or_octet_stream(); - let mut res = Response::new(asset.data.into()); + let mut res = Response::new(boxed(Full::from(asset.data))); res.headers_mut().typed_insert(ContentType::from(mime)); res.headers_mut().typed_insert(ContentLength(len)); - res.headers_mut() - .typed_insert(ETag::from_str(&etag).unwrap()); - Ok(Box::new(res)) - } - - pub(crate) fn filter() -> BoxedFilter<(impl Reply,)> { - warp::path::tail() - .and(warp::filters::header::optional("If-None-Match")) - .and_then(serve_embed) - .boxed() + Some(res) } } -#[cfg(feature = "dev")] -mod builtin { - use std::path::PathBuf; +impl Service> for Assets { + type Response = Response; + type Error = Infallible; + type Future = Ready>; - use warp::{filters::BoxedFilter, Reply}; + fn poll_ready( + &mut self, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + std::task::Poll::Ready(Ok(())) + } - pub(crate) fn filter() -> BoxedFilter<(impl Reply,)> { - let path = PathBuf::from(format!("{}/public", env!("CARGO_MANIFEST_DIR"))); - super::filter_for_path(path) - } -} - -fn box_reply(reply: impl Reply + 'static) -> Box { - Box::new(reply) -} - -fn filter_for_path(path: PathBuf) -> BoxedFilter<(impl Reply,)> { - warp::fs::dir(path).boxed() -} - -/// [`warp`] filter that serves static files -#[must_use] -pub fn filter(path: Option) -> BoxedFilter<(Box,)> { - let f = self::builtin::filter(); - - if let Some(path) = path { - f.or(filter_for_path(path)).map(box_reply).boxed() - } else { - f.map(box_reply).boxed() + fn call(&mut self, req: Request) -> Self::Future { + let path = req.uri().path().trim_start_matches('/'); + // TODO: support HEAD requests + // TODO: support ETag + // TODO: support range requests + let response = + Self::get_response(path).unwrap_or_else(|| StatusCode::NOT_FOUND.into_response()); + ready(Ok(response)) } }