From 8b693d44ed9562799e25a4f23d14b1956f935586 Mon Sep 17 00:00:00 2001 From: Brent Schroeter Date: Tue, 11 Mar 2025 22:22:30 -0700 Subject: [PATCH] fix "no nesting at root" bug with empty base_url --- Cargo.lock | 41 ++---------------------- Cargo.toml | 2 +- src/main.rs | 30 ++++++++++++++++- src/router.rs | 89 +++++++++++++++++++++++++-------------------------- 4 files changed, 75 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5599982..27c103e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,21 +38,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - [[package]] name = "allocator-api2" version = "0.2.18" @@ -205,11 +190,10 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.18" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df895a515f70646414f4b45c0b79082783b80552b373a68283012928df56f522" +checksum = "310c9bcae737a48ef5cdee3174184e6d548b292739ede61a1f955ef76a738861" dependencies = [ - "brotli", "flate2", "futures-core", "memchr", @@ -481,27 +465,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "brotli" -version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "4.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - [[package]] name = "bumpalo" version = "3.16.0" diff --git a/Cargo.toml b/Cargo.toml index cca7d2d..9b7abf5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ futures = "0.3.31" uuid = { version = "1.11.0", features = ["js", "serde", "v4", "v7"] } rand = "0.8.5" tracing-subscriber = { version = "0.3.19", features = ["chrono", "env-filter"] } -tower-http = { version = "0.6.2", features = ["compression-br", "compression-gzip", "fs", "trace", "tracing"] } +tower-http = { version = "0.6.2", features = ["compression-gzip", "fs", "normalize-path", "trace"] } tokio = { version = "1.42.0", features = ["macros", "rt-multi-thread", "tracing"] } deadpool-diesel = { version = "0.6.1", features = ["postgres", "serde"] } axum = { version = "0.8.1", features = ["macros"] } diff --git a/src/main.rs b/src/main.rs index 2817998..eca0731 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,11 +23,16 @@ mod worker; use std::process::exit; +use axum::{extract::Request, middleware::map_request, ServiceExt}; use chrono::{TimeDelta, Utc}; use clap::{Parser, Subcommand}; use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; use email::SmtpOptions; use tokio::time::sleep; +use tower::ServiceBuilder; +use tower_http::{ + compression::CompressionLayer, normalize_path::NormalizePathLayer, trace::TraceLayer, +}; use tracing_subscriber::EnvFilter; use crate::{ @@ -129,7 +134,16 @@ async fn main() { settings.port, settings.base_path ); - axum::serve(listener, router).await.unwrap(); + + let app = ServiceExt::::into_make_service( + ServiceBuilder::new() + .layer(map_request(lowercase_uri_path)) + .layer(TraceLayer::new_for_http()) + .layer(CompressionLayer::new()) + .layer(NormalizePathLayer::trim_trailing_slash()) + .service(router), + ); + axum::serve(listener, app).await.unwrap(); } Commands::Worker { auto_loop_seconds } => { if let Some(loop_seconds) = auto_loop_seconds { @@ -155,3 +169,17 @@ async fn main() { } } } + +async fn lowercase_uri_path(mut request: Request) -> Request { + let path = request.uri().path().to_lowercase(); + let path_and_query = match request.uri().query() { + Some(query) => format!("{}?{}", path, query), + None => path, + }; + let builder = + axum::http::uri::Builder::from(request.uri().clone()).path_and_query(path_and_query); + *request.uri_mut() = builder + .build() + .expect("lowercasing URI path should not break it"); + request +} diff --git a/src/router.rs b/src/router.rs index e2668b9..be42f21 100644 --- a/src/router.rs +++ b/src/router.rs @@ -13,12 +13,7 @@ use diesel::{delete, dsl::insert_into, prelude::*, update}; use rand::{distributions::Uniform, Rng}; use regex::Regex; use serde::Deserialize; -use tower::ServiceBuilder; -use tower_http::{ - compression::CompressionLayer, - services::{ServeDir, ServeFile}, - trace::TraceLayer, -}; +use tower_http::services::{ServeDir, ServeFile}; use uuid::Uuid; use crate::{ @@ -46,48 +41,50 @@ const MAX_VERIFICATION_GUESSES: u32 = 100; pub fn new_router(state: AppState) -> Router<()> { let base_path = state.settings.base_path.clone(); - Router::new().nest( - base_path.as_str(), - Router::new() - .route("/", get(landing_page)) - .merge(v0_router::new_router(state.clone())) - .route("/teams", get(teams_page)) - .route("/teams/{team_id}", get(team_page)) - .route("/teams/{team_id}/projects", get(projects_page)) - .route("/teams/{team_id}/projects/{project_id}", get(project_page)) - .route( - "/teams/{team_id}/projects/{project_id}/update-enabled-channels", - post(update_enabled_channels), - ) - .route("/teams/{team_id}/new-api-key", post(post_new_api_key)) - .route("/teams/{team_id}/channels", get(channels_page)) - .route("/teams/{team_id}/channels/{channel_id}", get(channel_page)) - .route( - "/teams/{team_id}/channels/{channel_id}/update-channel", - post(update_channel), - ) - .route( - "/teams/{team_id}/channels/{channel_id}/update-email-recipient", - post(update_channel_email_recipient), - ) - .route( - "/teams/{team_id}/channels/{channel_id}/verify-email", - post(verify_email), - ) - .route("/teams/{team_id}/new-channel", post(post_new_channel)) - .route("/new-team", get(new_team_page)) - .route("/new-team", post(post_new_team)) - .nest("/auth", auth::new_router()) - .fallback_service( + let app = Router::new() + .route("/", get(landing_page)) + .merge(v0_router::new_router(state.clone())) + .route("/teams", get(teams_page)) + .route("/teams/{team_id}", get(team_page)) + .route("/teams/{team_id}/projects", get(projects_page)) + .route("/teams/{team_id}/projects/{project_id}", get(project_page)) + .route( + "/teams/{team_id}/projects/{project_id}/update-enabled-channels", + post(update_enabled_channels), + ) + .route("/teams/{team_id}/new-api-key", post(post_new_api_key)) + .route("/teams/{team_id}/channels", get(channels_page)) + .route("/teams/{team_id}/channels/{channel_id}", get(channel_page)) + .route( + "/teams/{team_id}/channels/{channel_id}/update-channel", + post(update_channel), + ) + .route( + "/teams/{team_id}/channels/{channel_id}/update-email-recipient", + post(update_channel_email_recipient), + ) + .route( + "/teams/{team_id}/channels/{channel_id}/verify-email", + post(verify_email), + ) + .route("/teams/{team_id}/new-channel", post(post_new_channel)) + .route("/new-team", get(new_team_page)) + .route("/new-team", post(post_new_team)) + .nest("/auth", auth::new_router()) + .fallback_service( + ServeDir::new("static").not_found_service(ServeFile::new("static/_404.html")), + ) + .with_state(state); + let app = { + if base_path == "" { + app + } else { + Router::new().nest(&base_path, app).fallback_service( ServeDir::new("static").not_found_service(ServeFile::new("static/_404.html")), ) - .layer( - ServiceBuilder::new() - .layer(TraceLayer::new_for_http()) - .layer(CompressionLayer::new()), - ) - .with_state(state), - ) + } + }; + app } async fn landing_page(State(state): State) -> impl IntoResponse {