From 3ff61593b7026f3b4fae9f6d5c3642358e7d7a57 Mon Sep 17 00:00:00 2001 From: Luke Frisken Date: Sat, 27 Jan 2024 15:00:34 +0400 Subject: [PATCH] Update dependencies and add Cache-Control header to dynamic routes with no-store --- Cargo.lock | 224 +++++++++++++++++++++++++++-------- Cargo.toml | 15 +-- src/admin/analytics/index.rs | 4 +- src/analytics.rs | 12 +- src/auth.rs | 4 +- src/cache_control.rs | 12 ++ src/database/backup.rs | 19 ++- src/database/mod.rs | 10 +- src/disclaimer.rs | 5 +- src/i18n.rs | 25 ++-- src/index.rs | 9 +- src/isbot.rs | 15 ++- src/main.rs | 53 +++++---- src/templates.rs | 10 +- src/user_preferences.rs | 6 +- 15 files changed, 289 insertions(+), 134 deletions(-) create mode 100644 src/cache_control.rs diff --git a/Cargo.lock b/Cargo.lock index 40e489d..8d3fb29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,6 +87,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ansi-to-html" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d73c455ae09fa2223a75114789f30ad605e9e297f79537953523366c05995f5f" +dependencies = [ + "regex", + "thiserror", +] + [[package]] name = "approx" version = "0.5.1" @@ -197,7 +207,7 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" name = "avalanche-report" version = "0.1.0" dependencies = [ - "ansi-to-html", + "ansi-to-html 0.1.3", "assert-json-diff", "async-trait", "average", @@ -220,8 +230,9 @@ dependencies = [ "fs_extra", "futures", "governor", - "http", - "http-body", + "headers", + "http 1.0.0", + "http-body 1.0.0", "humansize", "humantime", "i18n-embed", @@ -288,19 +299,19 @@ dependencies = [ [[package]] name = "axum" -version = "0.6.20" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +checksum = "1236b4b292f6c4d6dc34604bb5120d85c3fe1d1aa596bd5cc52ca054d13e7b9e" dependencies = [ "async-trait", "axum-core", - "bitflags 1.3.2", "bytes", "futures-util", - "headers", - "http", - "http-body", - "hyper", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.1.0", + "hyper-util", "itoa", "matchit", "memchr", @@ -317,42 +328,47 @@ dependencies = [ "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] name = "axum-core" -version = "0.3.4" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", "mime", + "pin-project-lite", "rustversion", + "sync_wrapper", "tower-layer", "tower-service", + "tracing", ] [[package]] name = "axum-extra" -version = "0.7.7" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93e433be9382c737320af3924f7d5fc6f89c155cf2bf88949d8f5126fab283f" +checksum = "895ff42f72016617773af68fb90da2a9677d89c62338ec09162d4909d86fdd8f" dependencies = [ "axum", "axum-core", "bytes", "cookie", "futures-util", - "http", - "http-body", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", "mime", "pin-project-lite", "serde", - "tokio", "tower", "tower-layer", "tower-service", @@ -361,9 +377,9 @@ dependencies = [ [[package]] name = "axum-reporting" version = "0.1.0" -source = "git+https://github.com/kellpossible/axum-reporting.git#0f6af4273246fce76724d0e1198d107fcfdde843" +source = "git+https://github.com/kellpossible/axum-reporting.git#f690a1b79ffd994468354e6ca0cb560381b000f3" dependencies = [ - "ansi-to-html", + "ansi-to-html 0.2.1", "axum", "bytesize", "color-eyre", @@ -782,9 +798,9 @@ dependencies = [ [[package]] name = "cookie" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +checksum = "3cd91cf61412820176e137621345ee43b3f4423e589e7ae4e50d601d93e35ef8" dependencies = [ "percent-encoding", "time", @@ -1802,7 +1818,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.9", "indexmap 1.9.3", "slab", "tokio", @@ -1810,6 +1826,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 1.0.0", + "indexmap 2.0.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1840,14 +1875,14 @@ dependencies = [ [[package]] name = "headers" -version = "0.3.9" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" dependencies = [ "base64 0.21.4", "bytes", "headers-core", - "http", + "http 1.0.0", "httpdate", "mime", "sha1", @@ -1855,11 +1890,11 @@ dependencies = [ [[package]] name = "headers-core" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http", + "http 1.0.0", ] [[package]] @@ -1933,6 +1968,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.5" @@ -1940,15 +1986,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", - "http", + "http 0.2.9", "pin-project-lite", ] [[package]] -name = "http-range-header" -version = "0.3.1" +name = "http-body" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.0.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" +dependencies = [ + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "pin-project-lite", +] [[package]] name = "httparse" @@ -1987,9 +2050,9 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.3.21", + "http 0.2.9", + "http-body 0.4.5", "httparse", "httpdate", "itoa", @@ -2001,6 +2064,25 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5aa53871fc917b1a9ed87b683a5d86db645e23acb32c2e0785a353e522fb75" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.2", + "http 1.0.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "tokio", +] + [[package]] name = "hyper-rustls" version = "0.24.1" @@ -2008,13 +2090,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" dependencies = [ "futures-util", - "http", - "hyper", + "http 0.2.9", + "hyper 0.14.27", "rustls", "tokio", "tokio-rustls", ] +[[package]] +name = "hyper-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdea9aac0dbe5a9240d68cfd9501e2db94222c6dc06843e06640b9e07f0fdc67" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "hyper 1.1.0", + "pin-project-lite", + "socket2 0.5.4", + "tokio", + "tracing", +] + [[package]] name = "i18n-config" version = "0.4.6" @@ -3431,19 +3531,19 @@ checksum = "f1382d1f0a252c4bf97dc20d979a2fdd05b024acd7c2ed0f7595d7817666a157" [[package]] name = "reqwest" -version = "0.11.20" +version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ "base64 0.21.4", "bytes", "encoding_rs", "futures-core", "futures-util", - "h2", - "http", - "http-body", - "hyper", + "h2 0.3.21", + "http 0.2.9", + "http-body 0.4.5", + "hyper 0.14.27", "hyper-rustls", "ipnet", "js-sys", @@ -3457,6 +3557,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", + "system-configuration", "tokio", "tokio-rustls", "tokio-util", @@ -4292,6 +4393,27 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tar" version = "0.4.40" @@ -4632,18 +4754,16 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.4.4" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" +checksum = "0da193277a4e2c33e59e09b5861580c33dd0a637c3883d0fa74ba40c0374af2e" dependencies = [ "base64 0.21.4", "bitflags 2.4.0", "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-range-header", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", "mime", "pin-project-lite", "tower-layer", diff --git a/Cargo.toml b/Cargo.toml index 2cc296e..f3f39b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,8 +11,9 @@ forecast-spreadsheet = { path = "./forecast-spreadsheet" } utils = { path = "./utils" } # geo = { path = "./geo" } average = "0.14.1" -axum = { version = "0.6.20", features = ["headers"] } -axum-extra = { version = "0.7.7", default-features = false, features = ["cookie"] } +axum = { version = "0.7.4" } +headers = "0.4.0" +axum-extra = { version = "0.9.2", default-features = false, features = ["cookie"] } base64 = "0.21.0" bcrypt = "0.15.0" ansi-to-html = "0.1.2" @@ -30,7 +31,7 @@ fluent-langneg = "0.13.0" futures = "0.3.26" tracing-appender = "0.2" tracing-subscriber = "0.3" -http = "0.2.8" +http = "1.0.0" governor = "0.6.0" tracing = "0.1" mime_guess = "2.0.4" @@ -40,11 +41,11 @@ regex = "1.7.1" tower = "0.4" humantime = "2.1.0" nonzero_ext = "0.3.0" -http-body = "0.4" +http-body = "1.0.0" time-tz = { workspace = true } -tower-http = { version = "0.4", features = ["trace", "auth"] } +tower-http = { version = "0.5.1", features = ["trace", "auth"] } tokio = { version = "1.24.1", features = ["macros", "rt-multi-thread"] } -tokio-stream = { version = "0.1.11" } +tokio-stream = { version = "0.1.14" } deadpool-sqlite = { git = "https://github.com/bikeshedder/deadpool.git" } # Needed for new version of rusqlite thiserror = "1.0.38" libsqlite3-sys = { version = "0.26", features = ["bundled"] } @@ -60,7 +61,7 @@ once_cell = { workspace = true } unic-langid = { workspace = true, features = ["serde"] } time = { workspace = true, features = ["serde", "parsing", "formatting", "macros"] } resvg = { version = "0.29.0", default-features = false, features = ["filter", "text", "memmap-fonts"] } # required only for svg to png diagram generation -reqwest = { version = "0.11.14", default-features = false, features = ["json", "stream", "rustls-tls"] } +reqwest = { version = "0.11.23", default-features = false, features = ["json", "stream", "rustls-tls"] } secrecy = { version = "0.8.0", features = ["serde"] } usvg-text-layout = { version = "0.29.0", default-features = false, features = ["memmap-fonts"]} buildstructor = "0.5.4" diff --git a/src/admin/analytics/index.rs b/src/admin/analytics/index.rs index 1bc0d16..cc8f613 100644 --- a/src/admin/analytics/index.rs +++ b/src/admin/analytics/index.rs @@ -17,7 +17,7 @@ use sea_query::{Alias, Expr, IntoIden, Order, SimpleExpr, SqliteQueryBuilder}; use sea_query_rusqlite::RusqliteBinder; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; -use utils::serde::{duration_seconds_option, rfc3339_option}; +use utils::serde::rfc3339_option; use crate::{ analytics::AnalyticsIden, error::map_eyre_error, state::AppState, @@ -171,7 +171,7 @@ pub struct Query { pub async fn handler( Extension(templates): Extension, axum::extract::Query(mut query): axum::extract::Query, - headers: axum::headers::HeaderMap, + headers: headers::HeaderMap, State(state): State, ) -> axum::response::Result { let empty_uri_filter: bool = query diff --git a/src/analytics.rs b/src/analytics.rs index 3c05cb0..9d40e58 100644 --- a/src/analytics.rs +++ b/src/analytics.rs @@ -1,15 +1,19 @@ use std::{collections::HashMap, num::NonZeroU32, sync::Arc}; use average::WeightedMean; -use axum::{extract::State, middleware::Next, response::Response}; +use axum::{ + extract::{Request, State}, + middleware::Next, + response::Response, +}; use cronchik::CronSchedule; use eyre::{Context, ContextCompat}; use futures::{lock::Mutex, StreamExt}; use governor::{state::StreamRateLimitExt, Quota, RateLimiter}; -use http::{Request, StatusCode}; +use http::StatusCode; use nonzero_ext::nonzero; use rusqlite::Row; -use sea_query::{ConditionalStatement, Expr, Func, Order, Query, SimpleExpr, SqliteQueryBuilder}; +use sea_query::{ConditionalStatement, Expr, Order, Query, SimpleExpr, SqliteQueryBuilder}; use sea_query_rusqlite::RusqliteBinder; use serde::Serialize; use time::{format_description::well_known::Rfc3339, Duration, OffsetDateTime}; @@ -471,7 +475,7 @@ pub fn channel() -> (mpsc::Sender, mpsc::Receiver) { /// Middleware for performing analytics on incoming requests. #[tracing::instrument(skip_all)] -pub async fn middleware(state: State, request: Request, next: Next) -> Response { +pub async fn middleware(state: State, request: Request, next: Next) -> Response { let uri = Uri::from(request.uri().clone()); let is_bot = request .extensions() diff --git a/src/auth.rs b/src/auth.rs index 8ff5469..78a684f 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -5,7 +5,7 @@ use http::{HeaderValue, StatusCode}; use secrecy::{ExposeSecret, SecretString}; use std::{pin::Pin, sync::Arc}; use tokio::sync::OnceCell; -use tower_http::auth::AsyncAuthorizeRequest; +use tower_http::{auth::AsyncAuthorizeRequest, body::UnsyncBoxBody}; /// Basic authentication for accessing logs. #[derive(Clone)] @@ -27,7 +27,7 @@ impl MyBasicAuth { } impl AsyncAuthorizeRequest for MyBasicAuth { - type ResponseBody = http_body::combinators::UnsyncBoxBody; + type ResponseBody = axum::body::Body; type RequestBody = B; type Future = Pin< Box< diff --git a/src/cache_control.rs b/src/cache_control.rs new file mode 100644 index 0000000..4f5e162 --- /dev/null +++ b/src/cache_control.rs @@ -0,0 +1,12 @@ +use axum::{extract::Request, middleware::Next, response::Response}; +use headers::{CacheControl, HeaderMapExt}; + +/// Middleware to set the [`CacheControl`] header on all reponses to `no-store` to prevent browsers +/// from caching dynamic pages and causing unexpected lag in updates. +pub async fn no_store_middleware(request: Request, next: Next) -> Response { + let mut response = next.run(request).await; + response + .headers_mut() + .typed_insert(CacheControl::new().with_no_store()); + response +} diff --git a/src/database/backup.rs b/src/database/backup.rs index fd82944..7324ea2 100644 --- a/src/database/backup.rs +++ b/src/database/backup.rs @@ -2,7 +2,6 @@ use std::time::Duration; use base64::Engine; use eyre::{bail, Context, ContextCompat}; -use http::{HeaderValue, StatusCode}; use humansize::format_size; use md5::{Digest, Md5}; use rusty_s3::{Credentials, S3Action, UrlStyle}; @@ -51,7 +50,7 @@ async fn perform_backup(config: &Config) -> eyre::Result { .head(head_bucket.sign(Duration::from_secs(60 * 60))) .send() .await?; - if response.status() == StatusCode::NOT_FOUND { + if response.status() == reqwest::StatusCode::NOT_FOUND { bail!("Unable to perform backup, the bucket {s3_bucket_name} does not exist in the region {s3_bucket_region}") } @@ -121,10 +120,18 @@ async fn perform_backup(config: &Config) -> eyre::Result { .to_str()? .to_owned() .replace('"', ""), - version_id: Option::transpose(headers.get("x-amz-version-id").map(HeaderValue::to_str))? - .map(ToOwned::to_owned), - expiration: Option::transpose(headers.get("x-amz-expiration").map(HeaderValue::to_str))? - .map(ToOwned::to_owned), + version_id: Option::transpose( + headers + .get("x-amz-version-id") + .map(reqwest::header::HeaderValue::to_str), + )? + .map(ToOwned::to_owned), + expiration: Option::transpose( + headers + .get("x-amz-expiration") + .map(reqwest::header::HeaderValue::to_str), + )? + .map(ToOwned::to_owned), }; let backup_size = format_size(info.size, humansize::BINARY); diff --git a/src/database/mod.rs b/src/database/mod.rs index cdbcdd8..6a902a2 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,8 +1,8 @@ -use axum::extract::State; +use axum::extract::{Request, State}; use axum::middleware::Next; use axum::response::{IntoResponse, Response}; use deadpool_sqlite::PoolError; -use http::{Request, StatusCode}; +use http::StatusCode; use nonzero_ext::nonzero; use std::path::Path; use std::sync::Arc; @@ -101,11 +101,7 @@ impl DatabaseInstance { } #[tracing::instrument(skip_all)] -pub async fn middleware( - state: State, - mut request: Request, - next: Next, -) -> Response { +pub async fn middleware(state: State, mut request: Request, next: Next) -> Response { let database = match state.database.get().await { Ok(database) => database, Err(error) => { diff --git a/src/disclaimer.rs b/src/disclaimer.rs index 368fb0c..646b402 100644 --- a/src/disclaimer.rs +++ b/src/disclaimer.rs @@ -6,12 +6,13 @@ use crate::{ templates::{render, TemplatesWithContext}, }; use axum::{ + extract::Request, middleware::Next, response::{IntoResponse, Redirect, Response}, }; use axum_extra::extract::CookieJar; use eyre::{Context, ContextCompat}; -use http::{header::SET_COOKIE, HeaderMap, HeaderValue, Request}; +use http::{header::SET_COOKIE, HeaderMap, HeaderValue}; const DISCLAIMER_COOKIE_NAME: &str = "disclaimer"; /// TODO: if this version is updated we need new logic to require the current version of the @@ -40,7 +41,7 @@ pub async fn handler(headers: HeaderMap) -> axum::response::Result(request: Request, next: Next) -> Response { +pub async fn middleware(request: Request, next: Next) -> Response { let is_bot = request .extensions() .get::() diff --git a/src/i18n.rs b/src/i18n.rs index 288c7f6..eff9026 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -1,31 +1,24 @@ use axum::{ - extract::State, - http::{HeaderMap, HeaderValue, Request}, + extract::{Request, State}, + http::{HeaderMap, HeaderValue}, middleware::Next, - response::{IntoResponse, Redirect, Response}, + response::Response, }; -use axum_extra::extract::CookieJar; -use eyre::{Context, ContextCompat}; -use http::{header::SET_COOKIE, StatusCode}; use i18n_embed::{ fluent::{fluent_language_loader, FluentLanguageLoader, NegotiationStrategy}, LanguageLoader, }; use rust_embed::RustEmbed; -use serde::Deserialize; -use std::{collections::HashMap, str::FromStr, sync::Arc}; +use std::{collections::HashMap, sync::Arc}; use time::OffsetDateTime; -use crate::{ - error::{map_eyre_error, map_std_error}, - state::AppState, - user_preferences::UserPreferences, -}; +use crate::{state::AppState, user_preferences::UserPreferences}; #[derive(RustEmbed)] #[folder = "i18n/"] struct Localizations; +#[derive(Clone)] pub struct RequestedLanguages(pub Vec); impl std::fmt::Display for RequestedLanguages { @@ -87,11 +80,11 @@ pub fn load_languages(loader: &I18nLoader) -> eyre::Result<()> { Ok(()) } -pub async fn middleware( +pub async fn middleware( State(state): State, headers: HeaderMap, - mut request: Request, - next: Next, + mut request: Request, + next: Next, ) -> Response { let preferences: &UserPreferences = request .extensions() diff --git a/src/index.rs b/src/index.rs index 74fc589..860c794 100644 --- a/src/index.rs +++ b/src/index.rs @@ -2,6 +2,7 @@ use axum::{extract::State, response::IntoResponse, Extension}; use color_eyre::Help; use eyre::{bail, eyre, Context, ContextCompat}; use futures::{stream, StreamExt, TryStreamExt}; +use headers::{CacheControl, Header, HeaderMapExt}; use i18n_embed::LanguageLoader; use serde::Serialize; use unic_langid::LanguageIdentifier; @@ -232,5 +233,11 @@ pub async fn handler( forecasts, errors, }; - render(&templates.environment, "index.html", &index).map_err(map_eyre_error) + let mut response = render(&templates.environment, "index.html", &index) + .map_err(map_eyre_error)? + .into_response(); + response + .headers_mut() + .typed_insert(CacheControl::new().with_no_store()); + Ok(response) } diff --git a/src/isbot.rs b/src/isbot.rs index 96b7ea4..10d5537 100644 --- a/src/isbot.rs +++ b/src/isbot.rs @@ -1,7 +1,12 @@ use async_trait::async_trait; -use axum::{extract::FromRequestParts, middleware::Next, response::Response}; -use http::{header::USER_AGENT, request::Parts, HeaderMap, Request, StatusCode}; +use axum::{ + extract::{FromRequestParts, Request}, + middleware::Next, + response::Response, +}; +use http::{header::USER_AGENT, request::Parts, HeaderMap, StatusCode}; +#[derive(Copy, Clone)] pub struct IsBot(bool); impl IsBot { @@ -22,10 +27,10 @@ pub fn is_bot(headers: &HeaderMap) -> bool { } /// Middleware to detect whether the request is from a bot based on the [`USER_AGENT`] header. -pub async fn middleware( +pub async fn middleware( is_bot: IsBot, - mut request: Request, - next: Next, + mut request: Request, + next: Next, ) -> Result { request.extensions_mut().insert(is_bot); Ok(next.run(request).await) diff --git a/src/main.rs b/src/main.rs index 6042dc0..d8242dd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ use axum::{ - body::{boxed, Full}, handler::HandlerWithoutStateExt, http::{header, StatusCode, Uri}, middleware, @@ -7,6 +6,7 @@ use axum::{ routing::{get, post}, Extension, Router, }; +use bytes::Bytes; use error::map_std_error; use eyre::Context; use rust_embed::RustEmbed; @@ -23,6 +23,7 @@ use crate::{ mod admin; mod analytics; mod auth; +mod cache_control; mod database; mod diagrams; mod disclaimer; @@ -110,31 +111,39 @@ async fn main() -> eyre::Result<()> { // build our application with a route let app = Router::new() - // Using a GET request because this supports a redirect. - .route( - "/user-preferences-redirect", - get(user_preferences::query_set_redirect_handler), - ) - .route("/disclaimer", post(disclaimer::handler)) - .route("/weather-forecast", get(weather_forecast::handler)) - // These routes expose public forecast information and thus have the disclaimer middleware - // applied to them. + // All these pages are dynamic and should have the Cache-Control: no-store header set + // using the cache_control::no_store_middleware to help prevent browsers from caching them + // and preventing updates during refresh. .nest( "/", Router::new() - .route("/", get(index::handler)) - .route("/forecasts/:file_name", get(forecasts::handler)) - .nest("/observations", observations::router()) - .layer(middleware::from_fn(disclaimer::middleware)), + // Using a GET request because this supports a redirect. + .route( + "/user-preferences-redirect", + get(user_preferences::query_set_redirect_handler), + ) + .route("/disclaimer", post(disclaimer::handler)) + .route("/weather-forecast", get(weather_forecast::handler)) + // These routes expose public forecast information and thus have the disclaimer middleware + // applied to them. + .nest( + "/", + Router::new() + .route("/", get(index::handler)) + .route("/forecasts/:file_name", get(forecasts::handler)) + .nest("/observations", observations::router()) + .layer(middleware::from_fn(disclaimer::middleware)), + ) + .nest( + "/admin", + admin::router(reporting_options, &options.admin_password_hash), + ) + .layer(middleware::from_fn(cache_control::no_store_middleware)), ) .nest("/diagrams", diagrams::router()) .nest("/forecast-areas", forecast_areas::router()) .route_service("/dist/*file", dist_handler.into_service()) .route_service("/static/*file", static_handler.into_service()) - .nest( - "/admin", - admin::router(reporting_options, &options.admin_password_hash), - ) .fallback(not_found_handler) .layer(middleware::from_fn_with_state( state.clone(), @@ -159,9 +168,8 @@ async fn main() -> eyre::Result<()> { let url = &options.base_url(); tracing::info!("listening on {url}"); - axum::Server::bind(&options.listen_address) - .serve(app.into_make_service()) - .await?; + let listener = tokio::net::TcpListener::bind(&options.listen_address).await?; + axum::serve(listener, app).await?; Ok(()) } @@ -235,7 +243,8 @@ where let path: &str = self.path.as_ref(); match E::get(path) { Some(content) => { - let body = boxed(Full::from(content.data)); + let bytes = Bytes::from(content.data.to_vec()); + let body = axum::body::Body::from(bytes); let mime = mime_guess::from_path(path).first_or_octet_stream(); Response::builder() .header(header::CONTENT_TYPE, mime.as_ref()) diff --git a/src/templates.rs b/src/templates.rs index 41c3b85..41b7e57 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -8,13 +8,13 @@ use std::{ }; use axum::{ - extract::State, + extract::{Request, State}, middleware::Next, response::{IntoResponse, Response}, Extension, }; use fluent::{types::FluentNumber, FluentValue}; -use http::{header::CONTENT_TYPE, Request, StatusCode}; +use http::{header::CONTENT_TYPE, StatusCode}; use minijinja::{ value::{Value, ValueKind}, Error, ErrorKind, @@ -226,11 +226,11 @@ pub fn mapremove(map: Value, key: Cow<'_, str>) -> Result { } /// Middleware that provides access to all available templates with context injected. -pub async fn middleware( +pub async fn middleware( State(state): State, Extension(i18n): Extension, - mut request: Request, - next: Next, + mut request: Request, + next: Next, ) -> axum::response::Result { let mut environment = (*state.templates.reloader.acquire_env().map_err(|error| { ( diff --git a/src/user_preferences.rs b/src/user_preferences.rs index 1082936..09a5cad 100644 --- a/src/user_preferences.rs +++ b/src/user_preferences.rs @@ -1,12 +1,12 @@ use axum::{ - extract::Query, + extract::{Query, Request}, middleware::Next, response::{IntoResponse, Redirect, Response}, Extension, }; use axum_extra::extract::CookieJar; use eyre::{Context, ContextCompat}; -use http::{header::SET_COOKIE, HeaderMap, HeaderValue, Request, StatusCode}; +use http::{header::SET_COOKIE, HeaderMap, HeaderValue, StatusCode}; use serde::{Deserialize, Serialize}; use crate::error::map_eyre_error; @@ -95,7 +95,7 @@ pub async fn query_set_redirect_handler( } /// Middleware for extracting user preferences from cookie that was set using [`set_handler`]. -pub async fn middleware(mut request: Request, next: Next) -> Response { +pub async fn middleware(mut request: Request, next: Next) -> Response { let cookies = CookieJar::from_headers(request.headers()); let preferences: UserPreferences = match Option::transpose(cookies.get(COOKIE_NAME).map(|cookie| {