feat: add caching headers

This commit is contained in:
ThetaDev 2024-05-30 20:33:33 +02:00
parent fe820fa698
commit 6b865aaf5a
Signed by: ThetaDev
GPG key ID: E319D3C5148D65B6
4 changed files with 95 additions and 18 deletions

44
Cargo.lock generated
View file

@ -1506,6 +1506,15 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "proc-macro-crate"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro2"
version = "1.0.84"
@ -1715,9 +1724,9 @@ dependencies = [
[[package]]
name = "rstest"
version = "0.19.0"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5316d2a1479eeef1ea21e7f9ddc67c191d497abc8fc3ba2467857abbb68330"
checksum = "27059f51958c5f8496a6f79511e7c0ac396dd815dc8894e9b6e2efb5779cf6f0"
dependencies = [
"rstest_macros",
"rustc_version",
@ -1725,12 +1734,13 @@ dependencies = [
[[package]]
name = "rstest_macros"
version = "0.19.0"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04a9df72cc1f67020b0d63ad9bfe4a323e459ea7eb68e03bd9824db49f9a4c25"
checksum = "e6132d64df104c0b3ea7a6ad7766a43f587bd773a4a9cf4cd59296d426afaf3a"
dependencies = [
"cfg-if",
"glob",
"proc-macro-crate",
"proc-macro2",
"quote",
"regex",
@ -2210,6 +2220,23 @@ dependencies = [
"serde",
]
[[package]]
name = "toml_datetime"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
[[package]]
name = "toml_edit"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
dependencies = [
"indexmap",
"toml_datetime",
"winnow",
]
[[package]]
name = "tower"
version = "0.4.13"
@ -2707,6 +2734,15 @@ version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
[[package]]
name = "winnow"
version = "0.5.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.52.0"

View file

@ -33,7 +33,7 @@ serde_json = "1.0.117"
thiserror = "1.0.61"
tokio = { version = "1.37.0", features = ["macros", "fs", "rt-multi-thread"] }
tokio-util = { version = "0.7.11", features = ["io"] }
tower-http = { version = "0.5.2", features = ["trace"] }
tower-http = { version = "0.5.2", features = ["trace", "set-header"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
url = "2.5.0"
@ -44,7 +44,7 @@ yarte_helpers = "0.15.8"
[dev-dependencies]
proptest = "1.4.0"
rstest = { version = "0.19.0", default-features = false }
rstest = { version = "0.20.0", default-features = false }
[workspace]
members = [".", "crates/*"]

View file

@ -7,7 +7,7 @@ use axum::{
http::{Response, Uri},
response::{IntoResponse, Redirect},
routing::{any, get, post},
Form, Json, Router,
Form, Router,
};
use headers::HeaderMapExt;
use http::{HeaderMap, StatusCode};
@ -20,11 +20,14 @@ use tokio_util::{
compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt},
io::ReaderStream,
};
use tower_http::trace::{DefaultOnResponse, TraceLayer};
use tower_http::{
set_header::SetResponseHeaderLayer,
trace::{DefaultOnResponse, TraceLayer},
};
use crate::{
artifact_api::{Artifact, ArtifactApi},
cache::{Cache, CacheEntry, GetFileResult, GetFileResultFile, IndexEntry},
artifact_api::ArtifactApi,
cache::{Cache, CacheEntry, GetFileResult, GetFileResultFile},
config::Config,
error::Error,
gzip_reader::{PrecompressedGzipReader, GZIP_EXTRA_LEN},
@ -98,7 +101,8 @@ impl App {
tracing::error_span!("request", url = util::full_url_from_request(request), ip)
})
.on_response(DefaultOnResponse::new().level(tracing::Level::INFO)),
);
)
.layer(SetResponseHeaderLayer::appending(http::header::X_CONTENT_TYPE_OPTIONS, http::HeaderValue::from_static("nosniff")));
axum::serve(
listener,
router.into_make_service_with_connect_info::<SocketAddr>(),
@ -122,6 +126,7 @@ impl App {
}
Ok(Response::builder()
.typed_header(headers::ContentType::html())
.cache()
.body(templates::Index::default().to_string().into())?)
} else {
let query = Query::from_subdomain(subdomain, &state.i.cfg.load().site_aliases)?;
@ -185,6 +190,7 @@ impl App {
Ok(Response::builder()
.typed_header(headers::ContentType::html())
.cache_immutable()
.body(tmpl.to_string().into())?)
}
}
@ -214,6 +220,7 @@ impl App {
};
Ok(Response::builder()
.typed_header(headers::ContentType::html())
.cache()
.body(tmpl.to_string().into())?)
}
}
@ -267,7 +274,8 @@ impl App {
let mut resp = Response::builder()
.status(res.status)
.typed_header(headers::AcceptRanges::bytes())
.typed_header(headers::LastModified::from(entry.last_modified));
.typed_header(headers::LastModified::from(entry.last_modified))
.cache_immutable();
if let Some(mime) = res.mime {
resp = resp.typed_header(headers::ContentType::from(mime));
}
@ -366,24 +374,24 @@ impl App {
async fn get_artifacts(
State(state): State<AppState>,
Host(host): Host,
) -> Result<Json<Vec<Artifact>>, ErrorJson> {
) -> Result<Response<Body>, ErrorJson> {
let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
let query = Query::from_subdomain(subdomain, &state.i.cfg.load().site_aliases)?;
state.i.cfg.check_filterlist(&query)?;
let artifacts = state.i.api.list(&query.into_runquery()).await?;
Ok(Json(artifacts))
Ok(Response::builder().cache().json(&artifacts)?)
}
/// API endpoint to get the metadata of the current artifact
async fn get_artifact(
State(state): State<AppState>,
Host(host): Host,
) -> Result<Json<Artifact>, ErrorJson> {
) -> Result<Response<Body>, ErrorJson> {
let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
let query = Query::from_subdomain(subdomain, &state.i.cfg.load().site_aliases)?;
state.i.cfg.check_filterlist(&query)?;
let artifact = state.i.api.fetch(&query.try_into_artifactquery()?).await?;
Ok(Json(artifact))
Ok(Response::builder().cache().json(&artifact)?)
}
/// API endpoint to get a file listing
@ -391,7 +399,7 @@ impl App {
State(state): State<AppState>,
Host(host): Host,
request: Request,
) -> Result<Json<Vec<IndexEntry>>, ErrorJson> {
) -> Result<Response<Body>, ErrorJson> {
let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?;
let ip = util::get_ip_address(&request, state.i.cfg.load().real_ip_header.as_deref())?;
let query = Query::from_subdomain(subdomain, &state.i.cfg.load().site_aliases)?;
@ -405,7 +413,10 @@ impl App {
state.garbage_collect();
}
let files = entry_res.entry.get_files();
Ok(Json(files))
Ok(Response::builder()
.typed_header(headers::LastModified::from(entry_res.entry.last_modified))
.cache_immutable()
.json(&files)?)
}
}

View file

@ -23,6 +23,9 @@ use crate::error::{Error, Result};
pub trait ResponseBuilderExt {
/// Inserts a typed header to this response.
fn typed_header<T: Header>(self, header: T) -> Self;
fn cache_none(self) -> Self;
fn cache(self) -> Self;
fn cache_immutable(self) -> Self;
/// Consumes this builder, using the provided json-serializable `val` to return a constructed [`Response`]
fn json<T: Serialize>(self, val: &T) -> core::result::Result<Response, http::Error>;
}
@ -35,6 +38,27 @@ impl ResponseBuilderExt for axum::http::response::Builder {
self
}
fn cache_none(self) -> Self {
self.header(
http::header::CACHE_CONTROL,
http::HeaderValue::from_static("no-cache"),
)
}
fn cache(self) -> Self {
self.header(
http::header::CACHE_CONTROL,
http::HeaderValue::from_static("max-age=1800,public"),
)
}
fn cache_immutable(self) -> Self {
self.header(
http::header::CACHE_CONTROL,
http::HeaderValue::from_static("max-age=31536000,public,immutable"),
)
}
fn json<T: Serialize>(self, val: &T) -> core::result::Result<Response, http::Error> {
// copied from axum::json::into_response
// Use a small initial capacity of 128 bytes like serde_json::to_vec
@ -240,6 +264,12 @@ impl From<Error> for ErrorJson {
}
}
impl From<http::Error> for ErrorJson {
fn from(value: http::Error) -> Self {
Self::from(Error::from(value))
}
}
impl IntoResponse for ErrorJson {
fn into_response(self) -> Response {
Response::builder().json(&self).unwrap()