From 8d46314faf3744a4aa117d4c707acbcd4e69be8e Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Thu, 30 May 2024 12:37:49 +0200 Subject: [PATCH 1/7] fix: check subdomains for length --- src/app.rs | 6 +++--- src/artifact_api.rs | 2 +- src/cache.rs | 2 +- src/query.rs | 15 +++++++++------ src/templates.rs | 13 +++++++++---- 5 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/app.rs b/src/app.rs index 512a5ad..35ff32d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -154,7 +154,7 @@ impl App { url: state .i .cfg - .url_with_subdomain(&query.subdomain_with_artifact(None)), + .url_with_subdomain(&query.subdomain_with_artifact(None)?), }, LinkItem { name: entry.name.to_owned(), @@ -210,7 +210,7 @@ impl App { artifacts: artifacts .into_iter() .map(|a| ArtifactItem::from_artifact(a, &query, &state.i.cfg)) - .collect(), + .collect::>>()?, }; Ok(Response::builder() .typed_header(headers::ContentType::html()) @@ -229,7 +229,7 @@ impl App { if subdomain.is_empty() { let query = Query::from_forge_url(&url.url)?; - let subdomain = query.subdomain(); + let subdomain = query.subdomain()?; let target = format!( "{}{}.{}", state.i.cfg.url_proto(), diff --git a/src/artifact_api.rs b/src/artifact_api.rs index 8841a72..b9b7d5f 100644 --- a/src/artifact_api.rs +++ b/src/artifact_api.rs @@ -112,7 +112,7 @@ impl ArtifactApi { } pub async fn list(&self, query: &QueryData) -> Result> { - let subdomain = query.subdomain_with_artifact(None); + let subdomain = query.subdomain_with_artifact(None)?; self.qc .get_or_insert_async(&subdomain, async { if query.is_github() { diff --git a/src/cache.rs b/src/cache.rs index 28a05fe..8881db2 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -114,7 +114,7 @@ impl Cache { query: &ArtifactQuery, ip: &IpAddr, ) -> Result { - let subdomain = query.subdomain(); + let subdomain = query.subdomain()?; let zip_path = path!(self.cfg.load().cache_dir / format!("{subdomain}.zip")); let downloaded = !zip_path.is_file(); if downloaded { diff --git a/src/query.rs b/src/query.rs index c96c9a6..8d0fca2 100644 --- a/src/query.rs +++ b/src/query.rs @@ -107,7 +107,7 @@ impl Query { })) } - pub fn subdomain(&self) -> String { + pub fn subdomain(&self) -> Result { match self { Query::Artifact(q) => q.subdomain(), Query::Run(q) => q.subdomain(), @@ -130,19 +130,19 @@ impl Query { } impl ArtifactQuery { - pub fn subdomain(&self) -> String { + pub fn subdomain(&self) -> Result { self.subdomain_with_artifact(Some(self.artifact)) } } impl RunQuery { - pub fn subdomain(&self) -> String { + pub fn subdomain(&self) -> Result { self.subdomain_with_artifact(None) } } impl QueryData { - pub fn subdomain_with_artifact(&self, artifact: Option) -> String { + pub fn subdomain_with_artifact(&self, artifact: Option) -> Result { let mut res = format!( "{}--{}--{}--{}", encode_domain(&self.host, '.'), @@ -153,7 +153,10 @@ impl QueryData { if let Some(artifact) = artifact { write!(res, "-{artifact}").unwrap(); } - res + if res.len() > 63 { + return Err(Error::BadRequest("subdomain too long".into())); + } + Ok(res) } pub fn shortid(&self) -> String { @@ -399,7 +402,7 @@ mod tests { artifact: 123 }) ); - assert_eq!(query.subdomain(), d1); + assert_eq!(query.subdomain().unwrap(), d1); } #[rstest] diff --git a/src/templates.rs b/src/templates.rs index a1b042d..bc36b21 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -2,6 +2,7 @@ use crate::{ artifact_api::Artifact, cache::{ListingEntry, Size}, config::Config, + error::Result, query::QueryData, }; use yarte::{Render, Template}; @@ -61,14 +62,18 @@ pub struct ArtifactItem { } impl ArtifactItem { - pub fn from_artifact(artifact: Artifact, query: &QueryData, cfg: &Config) -> Self { - Self { + pub fn from_artifact( + artifact: Artifact, + query: &QueryData, + cfg: &Config, + ) -> Result { + Ok(Self { name: artifact.name, - url: cfg.url_with_subdomain(&query.subdomain_with_artifact(Some(artifact.id))), + url: cfg.url_with_subdomain(&query.subdomain_with_artifact(Some(artifact.id))?), size: Size(artifact.size as u32), expired: artifact.expired, download_url: artifact.user_download_url.unwrap_or(artifact.download_url), - } + }) } } From 9360f5837c4bbe1d6240869befcee609de5162fb Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Thu, 30 May 2024 17:58:54 +0200 Subject: [PATCH 2/7] feat: add site aliases --- crates/envy/src/lib.rs | 127 ++++++++++++++++++++++++++++++++++------- src/app.rs | 10 ++-- src/artifact_api.rs | 2 + src/cache.rs | 6 +- src/config.rs | 6 ++ src/query.rs | 69 ++++++++++++++++++---- 6 files changed, 180 insertions(+), 40 deletions(-) diff --git a/crates/envy/src/lib.rs b/crates/envy/src/lib.rs index 75835d9..3f00ae4 100644 --- a/crates/envy/src/lib.rs +++ b/crates/envy/src/lib.rs @@ -62,11 +62,7 @@ use serde::de::{ value::{MapDeserializer, SeqDeserializer}, IntoDeserializer, }; -use std::{ - borrow::Cow, - env, - iter::{empty, IntoIterator}, -}; +use std::{borrow::Cow, env, iter::IntoIterator, str::MatchIndices}; // Ours mod error; @@ -137,19 +133,19 @@ impl<'de> de::Deserializer<'de> for Val { where V: de::Visitor<'de>, { - // std::str::split doesn't work as expected for our use case: when we - // get an empty string we want to produce an empty Vec, but split would - // still yield an iterator with an empty string in it. So we need to - // special case empty strings. - if self.1.is_empty() { - SeqDeserializer::new(empty::()).deserialize_seq(visitor) - } else { - let values = self - .1 - .split(',') - .map(|v| Val(self.0.clone(), v.trim().to_owned())); - SeqDeserializer::new(values).deserialize_seq(visitor) - } + let values = SplitEscaped::new(&self.1, ";", true); + SeqDeserializer::new(values.map(|v| Val(self.0.clone(), v))).deserialize_seq(visitor) + } + + fn deserialize_map(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + MapDeserializer::new(SplitEscaped::new(&self.1, ";", false).filter_map(|pair| { + let mut parts = SplitEscaped::new(&pair, "=>", true); + parts.next().zip(parts.next()) + })) + .deserialize_map(visitor) } fn deserialize_option(self, visitor: V) -> Result @@ -216,7 +212,7 @@ impl<'de> de::Deserializer<'de> for Val { serde::forward_to_deserialize_any! { char str string unit - bytes byte_buf map unit_struct tuple_struct + bytes byte_buf unit_struct tuple_struct identifier tuple ignored_any struct } @@ -372,6 +368,63 @@ where Prefixed(prefix.into()) } +struct SplitEscaped<'a> { + input: &'a str, + pat: &'a str, + epat: String, + fin: bool, + separators: MatchIndices<'a, &'a str>, + start: usize, +} + +impl<'a> SplitEscaped<'a> { + pub fn new(input: &'a str, pat: &'a str, fin: bool) -> Self { + Self { + separators: input.match_indices(pat), + input, + pat, + fin, + epat: format!("\\{pat}"), + start: 0, + } + } +} + +impl<'a> Iterator for SplitEscaped<'a> { + type Item = String; + + fn next(&mut self) -> Option { + let res = match self.separators.find(|(i, _)| { + let b1 = self.input.get(i - 1..*i); + let b2 = if *i > 1 { + self.input.get(i - 2..i - 1) + } else { + None + }; + b1 != Some(r"\") || b2 == Some(r"\") + }) { + Some((pos, _)) => Some(&self.input[self.start..pos]), + None => { + if self.start >= self.input.len() { + None + } else { + Some(&self.input[self.start..]) + } + } + }; + if let Some(res) = res { + self.start += res.len() + self.pat.len(); + let mut out = res.trim().replace(&self.epat, self.pat); + if self.fin { + out = out.replace(r"\\", r"\"); + } + Some(out) + } else { + None + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -416,7 +469,7 @@ mod tests { let data = vec![ (String::from("BAR"), String::from("test")), (String::from("BAZ"), String::from("true")), - (String::from("DOOM"), String::from("1, 2, 3 ")), + (String::from("DOOM"), String::from("1; 2; 3 ")), // Empty string should result in empty vector. (String::from("BOOM"), String::from("")), (String::from("SIZE"), String::from("small")), @@ -479,7 +532,7 @@ mod tests { Ok(_) => panic!("expected failure"), Err(e) => assert_eq!( e, - Error::Custom(String::from("provided string was not `true` or `false` while parsing value \'notabool\' provided by BAZ")) + Error::Custom(String::from("error parsing boolean value: 'notabool'")) ), } } @@ -490,7 +543,7 @@ mod tests { (String::from("APP_BAR"), String::from("test")), (String::from("APP_BAZ"), String::from("true")), (String::from("APP_DOOM"), String::from("")), - (String::from("APP_BOOM"), String::from("4,5")), + (String::from("APP_BOOM"), String::from("4;5")), (String::from("APP_SIZE"), String::from("small")), (String::from("APP_PROVIDED"), String::from("test")), (String::from("APP_NEWTYPE"), String::from("42")), @@ -557,4 +610,34 @@ mod tests { let res = from_iter::<_, X>(data).unwrap(); assert_eq!(res.val, None) } + + #[test] + fn deserialize_map() { + #[derive(Deserialize)] + struct X { + val: HashMap, + } + + let data = vec![( + String::from("VAL"), + String::from("He\\\\llo\\=>W=>orld;this is => me"), + )]; + let res = from_iter::<_, X>(data).unwrap(); + assert_eq!(res.val.len(), 2); + assert_eq!(&res.val["He\\llo=>W"], "orld"); + assert_eq!(&res.val["this is"], "me"); + } + + #[test] + fn split_escaped() { + let input = r"He\\llo=> Wor\=>ld "; + let res = SplitEscaped::new(input, "=>", true).collect::>(); + assert_eq!(res, [r"He\llo", "Wor=>ld"]); + + let input = r"4;5"; + let res = SplitEscaped::new(input, ";", true).collect::>(); + assert_eq!(res, ["4", "5"]); + + assert!(SplitEscaped::new("", ";", true).next().is_none()); + } } diff --git a/src/app.rs b/src/app.rs index 35ff32d..0fd6374 100644 --- a/src/app.rs +++ b/src/app.rs @@ -124,7 +124,7 @@ impl App { .typed_header(headers::ContentType::html()) .body(templates::Index::default().to_string().into())?) } else { - let query = Query::from_subdomain(subdomain)?; + let query = Query::from_subdomain(subdomain, &state.i.cfg.load().site_aliases)?; state.i.cfg.check_filterlist(&query)?; let path = percent_encoding::percent_decode_str(uri.path()).decode_utf8_lossy(); let hdrs = request.headers(); @@ -228,7 +228,7 @@ impl App { let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?; if subdomain.is_empty() { - let query = Query::from_forge_url(&url.url)?; + let query = Query::from_forge_url(&url.url, &state.i.cfg.load().site_aliases)?; let subdomain = query.subdomain()?; let target = format!( "{}{}.{}", @@ -368,7 +368,7 @@ impl App { Host(host): Host, ) -> Result>> { let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?; - let query = Query::from_subdomain(subdomain)?; + 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)) @@ -380,7 +380,7 @@ impl App { Host(host): Host, ) -> Result> { let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?; - let query = Query::from_subdomain(subdomain)?; + 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)) @@ -394,7 +394,7 @@ impl App { ) -> Result>> { 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)?; + let query = Query::from_subdomain(subdomain, &state.i.cfg.load().site_aliases)?; state.i.cfg.check_filterlist(&query)?; let entry_res = state .i diff --git a/src/artifact_api.rs b/src/artifact_api.rs index b9b7d5f..47013e6 100644 --- a/src/artifact_api.rs +++ b/src/artifact_api.rs @@ -277,6 +277,7 @@ mod tests { async fn fetch_forgejo() { let query = ArtifactQuery { host: "code.thetadev.de".to_owned(), + host_alias: None, user: "HSA".to_owned(), repo: "Visitenbuch".to_owned(), run: 32, @@ -293,6 +294,7 @@ mod tests { async fn fetch_github() { let query = ArtifactQuery { host: "github.com".to_owned(), + host_alias: None, user: "actions".to_owned(), repo: "upload-artifact".to_owned(), run: 8805345396, diff --git a/src/cache.rs b/src/cache.rs index 8881db2..f3db3ec 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -3,7 +3,7 @@ use std::{ collections::{BTreeMap, HashMap}, fs::FileTimes, net::IpAddr, - num::{NonZeroU32, NonZeroUsize}, + num::NonZeroUsize, path::{Path, PathBuf}, sync::Arc, time::{Duration, SystemTime}, @@ -103,7 +103,7 @@ impl Cache { .load() .limit_artifacts_per_min .map(|lim| RateLimiter::keyed(Quota::per_minute(lim))), - lim_gc: RateLimiter::direct(Quota::per_hour(NonZeroU32::MIN)), + lim_gc: RateLimiter::direct(Quota::with_period(Duration::from_secs(1800)).unwrap()), cfg, } } @@ -114,7 +114,7 @@ impl Cache { query: &ArtifactQuery, ip: &IpAddr, ) -> Result { - let subdomain = query.subdomain()?; + let subdomain = query.subdomain_noalias(); let zip_path = path!(self.cfg.load().cache_dir / format!("{subdomain}.zip")); let downloaded = !zip_path.is_file(); if downloaded { diff --git a/src/config.rs b/src/config.rs index 7a7aeb9..2b09e91 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashMap, num::{NonZeroU32, NonZeroUsize}, path::{Path, PathBuf}, sync::Arc, @@ -58,8 +59,12 @@ pub struct ConfigData { pub real_ip_header: Option, /// Limit the amount of downloaded artifacts per IP address and minute pub limit_artifacts_per_min: Option, + /// List of sites/users/repos that can NOT be accessed pub repo_blacklist: QueryFilterList, + /// List of sites/users/repos that can ONLY be accessed pub repo_whitelist: QueryFilterList, + /// Aliases for sites (Example: `gh => github.com`) + pub site_aliases: HashMap, } impl Default for ConfigData { @@ -79,6 +84,7 @@ impl Default for ConfigData { limit_artifacts_per_min: Some(NonZeroU32::new(5).unwrap()), repo_blacklist: QueryFilterList::default(), repo_whitelist: QueryFilterList::default(), + site_aliases: HashMap::new(), } } } diff --git a/src/query.rs b/src/query.rs index 8d0fca2..8776abc 100644 --- a/src/query.rs +++ b/src/query.rs @@ -1,4 +1,4 @@ -use std::{fmt::Write, hash::Hash, str::FromStr}; +use std::{collections::HashMap, fmt::Write, str::FromStr}; use once_cell::sync::Lazy; use regex::{Captures, Regex}; @@ -18,10 +18,12 @@ pub enum Query { pub type RunQuery = QueryData<()>; pub type ArtifactQuery = QueryData; -#[derive(Debug, PartialEq, Eq, Hash)] +#[derive(Debug, PartialEq, Eq)] pub struct QueryData { /// Forge host pub host: String, + /// Host alias if the query was constructed using one + pub host_alias: Option, /// User/org name (case-insensitive) pub user: String, /// Repository name (case-insensitive) @@ -35,7 +37,7 @@ pub struct QueryData { static RE_REPO_NAME: Lazy = Lazy::new(|| Regex::new("^[a-z0-9\\-_\\.]+$").unwrap()); impl Query { - pub fn from_subdomain(subdomain: &str) -> Result { + pub fn from_subdomain(subdomain: &str, aliases: &HashMap) -> Result { let segments = subdomain.split("--").collect::>(); if segments.len() != 4 { return Err(Error::InvalidUrl); @@ -46,14 +48,22 @@ impl Query { return Err(Error::InvalidUrl); } - let host = decode_domain(segments[0], '.'); + let mut host = decode_domain(segments[0], '.'); + let mut host_alias = None; let user = decode_domain(segments[1], '-'); let repo = decode_domain(segments[2], '-'); let run = run_and_artifact[0].parse().ok().ok_or(Error::InvalidUrl)?; + #[allow(clippy::assigning_clones)] + if let Some(alias) = aliases.get(&host) { + host_alias = Some(host); + host = alias.clone(); + } + Ok(match run_and_artifact.get(1) { Some(x) => Self::Artifact(QueryData { host, + host_alias, user, repo, run, @@ -61,6 +71,7 @@ impl Query { }), None => Self::Run(QueryData { host, + host_alias, user, repo, run, @@ -69,7 +80,7 @@ impl Query { }) } - pub fn from_forge_url(url: &str) -> Result { + pub fn from_forge_url(url: &str, aliases: &HashMap) -> Result { let (host, mut path_segs) = util::parse_url(url)?; let user = path_segs @@ -93,13 +104,20 @@ impl Query { return Err(Error::BadRequest("invalid repository name".into())); } + let host = aliases + .iter() + .find(|(_, v)| *v == host) + .map(|(k, _)| k.to_owned()) + .unwrap_or_else(|| host.to_owned()); + let run = path_segs .next() .and_then(|s| s.parse::().ok()) .ok_or(Error::BadRequest("no run ID".into()))?; Ok(Self::Run(RunQuery { - host: host.to_owned(), + host, + host_alias: None, user, repo, run, @@ -107,6 +125,19 @@ impl Query { })) } + pub fn with_aliases(mut self, aliases: &HashMap) -> Self { + let (host, host_alias) = match &mut self { + Query::Artifact(q) => (&mut q.host, &mut q.host_alias), + Query::Run(q) => (&mut q.host, &mut q.host_alias), + }; + + if let Some(alias) = aliases.get(host) { + *host_alias = Some(host.clone()); + host.clone_from(alias); + } + self + } + pub fn subdomain(&self) -> Result { match self { Query::Artifact(q) => q.subdomain(), @@ -133,6 +164,11 @@ impl ArtifactQuery { pub fn subdomain(&self) -> Result { self.subdomain_with_artifact(Some(self.artifact)) } + + /// Non-shortened subdomain (used for cache storage) + pub fn subdomain_noalias(&self) -> String { + self._subdomain(Some(self.artifact), false) + } } impl RunQuery { @@ -142,10 +178,16 @@ impl RunQuery { } impl QueryData { - pub fn subdomain_with_artifact(&self, artifact: Option) -> Result { + pub fn _subdomain(&self, artifact: Option, use_alias: bool) -> String { + let host = if use_alias { + self.host_alias.as_deref().unwrap_or(&self.host) + } else { + &self.host + }; + let mut res = format!( "{}--{}--{}--{}", - encode_domain(&self.host, '.'), + encode_domain(host, '.'), encode_domain(&self.user, '-'), encode_domain(&self.repo, '-'), self.run, @@ -153,6 +195,11 @@ impl QueryData { if let Some(artifact) = artifact { write!(res, "-{artifact}").unwrap(); } + res + } + + pub fn subdomain_with_artifact(&self, artifact: Option) -> Result { + let res = self._subdomain(artifact, true); if res.len() > 63 { return Err(Error::BadRequest("subdomain too long".into())); } @@ -177,6 +224,7 @@ impl QueryData { pub fn into_runquery(self) -> RunQuery { RunQuery { host: self.host, + host_alias: self.host_alias, user: self.user, repo: self.repo, run: self.run, @@ -351,7 +399,7 @@ impl<'de> Deserialize<'de> for QueryFilterList { #[cfg(test)] mod tests { - use std::str::FromStr; + use std::{collections::HashMap, str::FromStr}; use crate::query::{QueryFilter, QueryFilterList}; @@ -391,11 +439,12 @@ mod tests { #[test] fn query_from_subdomain() { let d1 = "github-com--thetadev--newpipe-extractor--14-123"; - let query = Query::from_subdomain(d1).unwrap(); + let query = Query::from_subdomain(d1, &HashMap::new()).unwrap(); assert_eq!( query, Query::Artifact(ArtifactQuery { host: "github.com".to_owned(), + host_alias: None, user: "thetadev".to_owned(), repo: "newpipe-extractor".to_owned(), run: 14, From fe820fa69803cbaac52c511a9eb1c4b91cea67a5 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Thu, 30 May 2024 18:30:04 +0200 Subject: [PATCH 3/7] feat: return API errors in JSON format --- src/app.rs | 20 ++++++++++---------- src/error.rs | 2 +- src/util.rs | 49 ++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/src/app.rs b/src/app.rs index 0fd6374..2395802 100644 --- a/src/app.rs +++ b/src/app.rs @@ -26,11 +26,11 @@ use crate::{ artifact_api::{Artifact, ArtifactApi}, cache::{Cache, CacheEntry, GetFileResult, GetFileResultFile, IndexEntry}, config::Config, - error::{Error, Result}, + error::Error, gzip_reader::{PrecompressedGzipReader, GZIP_EXTRA_LEN}, query::Query, templates::{self, ArtifactItem, LinkItem}, - util::{self, InsertTypedHeader}, + util::{self, ErrorJson, ResponseBuilderExt}, App, }; @@ -65,7 +65,7 @@ impl App { AppState::new() } - pub async fn run(&self) -> Result<()> { + pub async fn run(&self) -> Result<(), Error> { let address = "0.0.0.0:3000"; let listener = tokio::net::TcpListener::bind(address).await?; tracing::info!("Listening on http://{address}"); @@ -112,7 +112,7 @@ impl App { Host(host): Host, uri: Uri, request: Request, - ) -> Result> { + ) -> Result, Error> { let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?; if subdomain.is_empty() { @@ -210,7 +210,7 @@ impl App { artifacts: artifacts .into_iter() .map(|a| ArtifactItem::from_artifact(a, &query, &state.i.cfg)) - .collect::>>()?, + .collect::, _>>()?, }; Ok(Response::builder() .typed_header(headers::ContentType::html()) @@ -224,7 +224,7 @@ impl App { State(state): State, Host(host): Host, Form(url): Form, - ) -> Result { + ) -> Result { let subdomain = util::get_subdomain(&host, &state.i.cfg.load().root_domain)?; if subdomain.is_empty() { @@ -248,7 +248,7 @@ impl App { zip_path: PathBuf, res: GetFileResultFile, hdrs: &HeaderMap, - ) -> Result> { + ) -> Result, Error> { let file = res.file; // Dont serve files above the configured size limit @@ -366,7 +366,7 @@ impl App { async fn get_artifacts( State(state): State, Host(host): Host, - ) -> Result>> { + ) -> Result>, 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)?; @@ -378,7 +378,7 @@ impl App { async fn get_artifact( State(state): State, Host(host): Host, - ) -> Result> { + ) -> Result, 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)?; @@ -391,7 +391,7 @@ impl App { State(state): State, Host(host): Host, request: Request, - ) -> Result>> { + ) -> Result>, 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)?; diff --git a/src/error.rs b/src/error.rs index d5710db..336cc47 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,7 +6,7 @@ use axum::{ }; use http::StatusCode; -use crate::{templates, util::InsertTypedHeader}; +use crate::{templates, util::ResponseBuilderExt}; pub type Result = core::result::Result; diff --git a/src/util.rs b/src/util.rs index 21099ea..651d961 100644 --- a/src/util.rs +++ b/src/util.rs @@ -8,26 +8,48 @@ use async_zip::error::ZipError; use axum::{ extract::{ConnectInfo, Request}, http::HeaderMap, + response::{IntoResponse, Response}, }; use headers::{Header, HeaderMapExt}; -use http::header; +use http::{header, StatusCode}; use mime_guess::Mime; +use serde::Serialize; use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt}; +use tokio_util::bytes::{BufMut, BytesMut}; use crate::error::{Error, Result}; -pub trait InsertTypedHeader { +/// HTTP response builder extensions +pub trait ResponseBuilderExt { /// Inserts a typed header to this response. fn typed_header(self, header: T) -> Self; + /// Consumes this builder, using the provided json-serializable `val` to return a constructed [`Response`] + fn json(self, val: &T) -> core::result::Result; } -impl InsertTypedHeader for axum::http::response::Builder { +impl ResponseBuilderExt for axum::http::response::Builder { fn typed_header(mut self, header: T) -> Self { if let Some(headers) = self.headers_mut() { headers.typed_insert(header); } self } + + fn json(self, val: &T) -> core::result::Result { + // copied from axum::json::into_response + // Use a small initial capacity of 128 bytes like serde_json::to_vec + // https://docs.rs/serde_json/1.0.82/src/serde_json/ser.rs.html#2189 + let mut buf = BytesMut::with_capacity(128).writer(); + match serde_json::to_writer(&mut buf, val) { + Ok(()) => self + .typed_header(headers::ContentType::json()) + .body(buf.into_inner().freeze().into()), + Err(err) => self + .status(StatusCode::INTERNAL_SERVER_ERROR) + .typed_header(headers::ContentType::text()) + .body(err.to_string().into()), + } + } } pub fn accepts_gzip(headers: &HeaderMap) -> bool { @@ -203,6 +225,27 @@ pub fn parse_url(input: &str) -> Result<(&str, std::str::Split)> { Ok((host, parts)) } +#[derive(Serialize)] +pub struct ErrorJson { + status: u16, + msg: String, +} + +impl From for ErrorJson { + fn from(value: Error) -> Self { + Self { + status: value.status().as_u16(), + msg: value.to_string(), + } + } +} + +impl IntoResponse for ErrorJson { + fn into_response(self) -> Response { + Response::builder().json(&self).unwrap() + } +} + #[cfg(test)] mod tests { use http::{header, HeaderMap}; From 6b865aaf5a2c9e268d6f82f270452e67332cfbf1 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Thu, 30 May 2024 20:33:33 +0200 Subject: [PATCH 4/7] feat: add caching headers --- Cargo.lock | 44 ++++++++++++++++++++++++++++++++++++++++---- Cargo.toml | 4 ++-- src/app.rs | 35 +++++++++++++++++++++++------------ src/util.rs | 30 ++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8caebb..3b33250 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 3b624d6..ee93f72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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/*"] diff --git a/src/app.rs b/src/app.rs index 2395802..27f683a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -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::(), @@ -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, Host(host): Host, - ) -> Result>, ErrorJson> { + ) -> Result, 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, Host(host): Host, - ) -> Result, ErrorJson> { + ) -> Result, 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, Host(host): Host, request: Request, - ) -> Result>, ErrorJson> { + ) -> Result, 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)?) } } diff --git a/src/util.rs b/src/util.rs index 651d961..3d99897 100644 --- a/src/util.rs +++ b/src/util.rs @@ -23,6 +23,9 @@ use crate::error::{Error, Result}; pub trait ResponseBuilderExt { /// Inserts a typed header to this response. fn typed_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(self, val: &T) -> core::result::Result; } @@ -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(self, val: &T) -> core::result::Result { // 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 for ErrorJson { } } +impl From 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() From 3711eb12315e756db911c72a5d4d4c6a8d89acf1 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Thu, 30 May 2024 20:36:40 +0200 Subject: [PATCH 5/7] fix: mangled filter javascript --- templates/listing.hbs | 15 +++++---------- templates/selection.hbs | 15 +++++---------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/templates/listing.hbs b/templates/listing.hbs index 635c9f6..6c3c476 100644 --- a/templates/listing.hbs +++ b/templates/listing.hbs @@ -158,16 +158,11 @@ diff --git a/templates/selection.hbs b/templates/selection.hbs index 12ab94f..7171640 100644 --- a/templates/selection.hbs +++ b/templates/selection.hbs @@ -170,16 +170,11 @@ From 8ada5a3d9f692ca6242b3d5ee4d4fa2577f8c09c Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Thu, 30 May 2024 21:11:19 +0200 Subject: [PATCH 6/7] feat: add default favicon --- resources/favicon.ico | Bin 0 -> 268 bytes resources/favicon.xcf | Bin 0 -> 2040 bytes src/app.rs | 32 ++++++++++++++++++++++++++++---- 3 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 resources/favicon.ico create mode 100644 resources/favicon.xcf diff --git a/resources/favicon.ico b/resources/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8525003a304197258ff55a816db723d80d55e45d GIT binary patch literal 268 zcmZQzU<5)11py$*!tjlOfk6z2I|KaOdAX#xfJ|Ob50@YytpLItY(TQOs_F}n+Ux1! z7!uKXb-Jfeg8~oB!B6LF7~^`?bzBlBB}6=Hi{`z%Bl7*;nKOf{78UC~do0j&w(X)o zu?P18r^DPL8-GmbW7>MtOCv*2B78waO{Is_4OW4)b!~e^uDtsz$E(F+A@wbgL2hpn zZx+i6sc!)d9~Qq|w$O1ZgX>`r*7+g{j9UM0)wv}0yUu7z^FQ(WPa5OPbTRkCySR3A z_fI@tdW9pvbj?Z!!TM9RwxLWSystHXNZVIA^Qi~;T{!p8c0a>9#xO3f1D(r(9%1lw L^>bP0l+XkKG96$Y literal 0 HcmV?d00001 diff --git a/resources/favicon.xcf b/resources/favicon.xcf new file mode 100644 index 0000000000000000000000000000000000000000..dbb418a0c3c19b241d85c1509fa698421ca72bf2 GIT binary patch literal 2040 zcmeH{OK#gR6h$f8@h^6pen$nu{s3nnz*<2E85A89=%{l?mK7DY71(N<%sS)>I?e*J zyDosemy*>-w;L#sXFR?`@`{ov@#e+)%3P;Q^KF(S3SpWg|3p4QMiH7P$iX>=n24vy zF0zk&-Y|8~kiU?V981S(xn5_R3Vp@N$LHH@US$jOy{InD`wt&KapuHJi}gIu;(S{y zI7ao|bXVQ1GP5gJ#o}xZLtj>n2hz|E>@lu!$moDa zel2SHwq|NV9zxUg>-22R)Py{Mrsp+Bwq|PnuvT5$hlX>}U#uP1)v9BAsC8c&@y(_2?sdf)&bxm dtpfxs9KZ<&2v|4(9IJJJfQ17%;Q;Pb{Ra3Nr>p<~ literal 0 HcmV?d00001 diff --git a/src/app.rs b/src/app.rs index 27f683a..44f1ec9 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,4 +1,4 @@ -use std::{net::SocketAddr, ops::Bound, path::PathBuf, sync::Arc}; +use std::{net::SocketAddr, ops::Bound, path::PathBuf, str::FromStr, sync::Arc}; use async_zip::tokio::read::ZipEntryReader; use axum::{ @@ -59,6 +59,9 @@ struct UrlForm { url: String, } +const FAVICON_PATH: &str = "/favicon.ico"; +const FAVICON_BYTES: &[u8; 268] = include_bytes!("../resources/favicon.ico"); + impl App { pub fn new() -> Self { Self @@ -121,6 +124,9 @@ impl App { if subdomain.is_empty() { // Main page + if uri.path() == FAVICON_PATH { + return Self::favicon(); + } if uri.path() != "/" { return Err(Error::NotFound("path".into())); } @@ -143,12 +149,12 @@ impl App { state.garbage_collect(); } - match entry.get_file(&path, uri.query().unwrap_or_default())? { - GetFileResult::File(res) => { + match entry.get_file(&path, uri.query().unwrap_or_default()) { + Ok(GetFileResult::File(res)) => { Self::serve_artifact_file(state, entry, entry_res.zip_path, res, hdrs) .await } - GetFileResult::Listing(listing) => { + Ok(GetFileResult::Listing(listing)) => { if !path.ends_with('/') { return Ok(Redirect::to(&format!("{path}/")).into_response()); } @@ -193,11 +199,22 @@ impl App { .cache_immutable() .body(tmpl.to_string().into())?) } + Err(Error::NotFound(e)) => { + if path == FAVICON_PATH { + Self::favicon() + } else { + Err(Error::NotFound(e)) + } + } + Err(e) => Err(e), } } Query::Run(query) => { let artifacts = state.i.api.list(&query).await?; + if uri.path() == FAVICON_PATH { + return Self::favicon(); + } if uri.path() != "/" { return Err(Error::NotFound("path".into())); } @@ -418,6 +435,13 @@ impl App { .cache_immutable() .json(&files)?) } + + fn favicon() -> Result, Error> { + Ok(Response::builder() + .typed_header(headers::ContentType::from_str("image/x-icon").unwrap()) + .cache_immutable() + .body(FAVICON_BYTES.as_slice().into())?) + } } impl AppState { From 7bf4728416fdc53fc32abf2da0d282bbd9156fd1 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Thu, 30 May 2024 21:12:48 +0200 Subject: [PATCH 7/7] chore: remove dead code --- Cargo.lock | 29 ++++++++++++++--------------- src/lib.rs | 2 -- src/query.rs | 13 ------------- src/util.rs | 8 -------- 4 files changed, 14 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b33250..3e413d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -184,9 +184,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c90a406b4495d129f00461241616194cb8a032c8d1c53c657f0961d5f8e0498" +checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5" dependencies = [ "bzip2", "deflate64", @@ -319,9 +319,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11" dependencies = [ "addr2line", "cc", @@ -828,9 +828,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "glob" @@ -1028,9 +1028,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d8d52be92d09acc2e01dddb7fde3ad983fc6489c7db4837e605bc3fca4cb63e" +checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" dependencies = [ "bytes", "futures-channel", @@ -1241,11 +1241,10 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" dependencies = [ - "lazy_static", "libc", "log", "openssl", @@ -1313,9 +1312,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" dependencies = [ "memchr", ] diff --git a/src/lib.rs b/src/lib.rs index f6cdddd..c3adf4a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - mod app; mod artifact_api; mod cache; diff --git a/src/query.rs b/src/query.rs index 8776abc..5e9d872 100644 --- a/src/query.rs +++ b/src/query.rs @@ -125,19 +125,6 @@ impl Query { })) } - pub fn with_aliases(mut self, aliases: &HashMap) -> Self { - let (host, host_alias) = match &mut self { - Query::Artifact(q) => (&mut q.host, &mut q.host_alias), - Query::Run(q) => (&mut q.host, &mut q.host_alias), - }; - - if let Some(alias) = aliases.get(host) { - *host_alias = Some(host.clone()); - host.clone_from(alias); - } - self - } - pub fn subdomain(&self) -> Result { match self { Query::Artifact(q) => q.subdomain(), diff --git a/src/util.rs b/src/util.rs index 3d99897..2a0d3f7 100644 --- a/src/util.rs +++ b/src/util.rs @@ -23,7 +23,6 @@ use crate::error::{Error, Result}; pub trait ResponseBuilderExt { /// Inserts a typed header to this response. fn typed_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`] @@ -38,13 +37,6 @@ 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,