0.34.6_repo_cleanup
This commit is contained in:
1 parent
ce1a90397c
commit
133d2f0f91
273 files changed
+5969
-6926
No files matched your search
@@ -0,0 +1,345 @@
|
||||
use anyhow::{ensure, Result};
|
||||
use axum::{body::Body, response::Response};
|
||||
use http::{HeaderMap, HeaderValue, Method, Request};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::Infallible;
|
||||
use tower::{service_fn, ServiceExt};
|
||||
use tower_http::compression::{
|
||||
predicate::{DefaultPredicate, Predicate},
|
||||
Compression, CompressionLevel,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct MinimumSize(u64);
|
||||
impl Predicate for MinimumSize {
|
||||
fn should_compress<B: http_body::Body>(&self, response: &http::Response<B>) -> bool {
|
||||
let size = response
|
||||
.headers()
|
||||
.get(http::header::CONTENT_LENGTH)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.or_else(|| response.body().size_hint().exact());
|
||||
size.is_none_or(|size| size >= self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct Settings {
|
||||
pub gzip: bool,
|
||||
pub br: bool,
|
||||
pub zstd: bool,
|
||||
pub deflate: bool,
|
||||
pub min_bytes: u64,
|
||||
}
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
gzip: true,
|
||||
br: true,
|
||||
zstd: true,
|
||||
deflate: false,
|
||||
min_bytes: 1024,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Settings {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
ensure!(
|
||||
(32..=1_048_576).contains(&self.min_bytes),
|
||||
"compression.min_bytes must be 32..1048576"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
pub fn enabled(&self) -> bool {
|
||||
self.gzip || self.br || self.zstd || self.deflate
|
||||
}
|
||||
pub fn scoped(&self, enabled: bool) -> Self {
|
||||
if enabled {
|
||||
return self.clone();
|
||||
}
|
||||
let mut disabled = self.clone();
|
||||
disabled.gzip = false;
|
||||
disabled.br = false;
|
||||
disabled.zstd = false;
|
||||
disabled.deflate = false;
|
||||
disabled
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct Overrides {
|
||||
pub enabled: Option<bool>,
|
||||
pub gzip: Option<bool>,
|
||||
pub br: Option<bool>,
|
||||
pub zstd: Option<bool>,
|
||||
pub deflate: Option<bool>,
|
||||
}
|
||||
impl Overrides {
|
||||
pub fn resolve(&self, global: &Settings, inherited: &Settings) -> Settings {
|
||||
if self.enabled == Some(false) {
|
||||
return global.scoped(false);
|
||||
}
|
||||
let mut resolved = if self.enabled == Some(true) {
|
||||
global.clone()
|
||||
} else {
|
||||
inherited.clone()
|
||||
};
|
||||
if let Some(enabled) = self.gzip {
|
||||
resolved.gzip = enabled && global.gzip;
|
||||
}
|
||||
if let Some(enabled) = self.br {
|
||||
resolved.br = enabled && global.br;
|
||||
}
|
||||
if let Some(enabled) = self.zstd {
|
||||
resolved.zstd = enabled && global.zstd;
|
||||
}
|
||||
if let Some(enabled) = self.deflate {
|
||||
resolved.deflate = enabled && global.deflate;
|
||||
}
|
||||
resolved.min_bytes = global.min_bytes;
|
||||
resolved
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Skip;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AppliedEncoding {
|
||||
Gzip,
|
||||
Brotli,
|
||||
Zstd,
|
||||
Deflate,
|
||||
}
|
||||
impl AppliedEncoding {
|
||||
const PREFERENCE: [Self; 4] = [Self::Zstd, Self::Brotli, Self::Gzip, Self::Deflate];
|
||||
|
||||
pub(crate) fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Gzip => "gzip",
|
||||
Self::Brotli => "br",
|
||||
Self::Zstd => "zstd",
|
||||
Self::Deflate => "deflate",
|
||||
}
|
||||
}
|
||||
|
||||
fn enabled(self, settings: &Settings) -> bool {
|
||||
match self {
|
||||
Self::Gzip => settings.gzip,
|
||||
Self::Brotli => settings.br,
|
||||
Self::Zstd => settings.zstd,
|
||||
Self::Deflate => settings.deflate,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_header(value: &str) -> Option<Self> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"gzip" => Some(Self::Gzip),
|
||||
"br" => Some(Self::Brotli),
|
||||
"zstd" => Some(Self::Zstd),
|
||||
"deflate" => Some(Self::Deflate),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn no_transform(headers: &HeaderMap) -> bool {
|
||||
headers
|
||||
.get_all("cache-control")
|
||||
.iter()
|
||||
.filter_map(|v| v.to_str().ok())
|
||||
.any(|v| {
|
||||
v.split(',').any(|directive| {
|
||||
directive
|
||||
.trim()
|
||||
.split('=')
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("no-transform")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn request_headers(headers: &HeaderMap) -> HeaderMap {
|
||||
let mut selected = HeaderMap::new();
|
||||
for name in ["accept-encoding", "range", "cache-control"] {
|
||||
for value in headers.get_all(name) {
|
||||
selected.append(name, value.clone());
|
||||
}
|
||||
}
|
||||
selected
|
||||
}
|
||||
|
||||
fn eligible(
|
||||
response: &Response,
|
||||
request: &HeaderMap,
|
||||
method: &Method,
|
||||
settings: &Settings,
|
||||
) -> bool {
|
||||
if response.extensions().get::<Skip>().is_some()
|
||||
|| !settings.enabled()
|
||||
|| method == Method::HEAD
|
||||
|| response.status().is_informational()
|
||||
|| response.status().as_u16() == 204
|
||||
|| response.status().as_u16() == 304
|
||||
|| response.headers().contains_key("content-encoding")
|
||||
|| response.headers().contains_key("content-range")
|
||||
|| response.headers().contains_key("upgrade")
|
||||
|| request.contains_key("range")
|
||||
|| no_transform(request)
|
||||
|| no_transform(response.headers())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
!response
|
||||
.headers()
|
||||
.get("content-type")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.is_some_and(|v| {
|
||||
v.split(';')
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("text/event-stream")
|
||||
})
|
||||
}
|
||||
|
||||
fn quality(raw: &str) -> Option<u16> {
|
||||
let value = raw.trim().parse::<f32>().ok()?;
|
||||
if !(0.0..=1.0).contains(&value) {
|
||||
return None;
|
||||
}
|
||||
Some((value * 1000.0).round() as u16)
|
||||
}
|
||||
|
||||
pub(crate) fn preferred_encoding(
|
||||
headers: &HeaderMap,
|
||||
settings: &Settings,
|
||||
) -> Option<AppliedEncoding> {
|
||||
let mut explicit = [None; 4];
|
||||
let mut wildcard = None;
|
||||
for line in headers.get_all("accept-encoding").iter() {
|
||||
let Ok(line) = line.to_str() else { continue };
|
||||
for item in line.split(',') {
|
||||
let mut parts = item.split(';');
|
||||
let coding = parts.next().unwrap_or("").trim().to_ascii_lowercase();
|
||||
if coding.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut q = 1000;
|
||||
let mut valid = true;
|
||||
for parameter in parts {
|
||||
let Some((name, value)) = parameter.trim().split_once('=') else {
|
||||
continue;
|
||||
};
|
||||
if name.trim().eq_ignore_ascii_case("q") {
|
||||
if let Some(parsed) = quality(value) {
|
||||
q = parsed;
|
||||
} else {
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
continue;
|
||||
}
|
||||
if coding == "*" {
|
||||
wildcard = Some(wildcard.map_or(q, |current: u16| current.max(q)));
|
||||
continue;
|
||||
}
|
||||
if let Some(encoding) = AppliedEncoding::from_header(&coding) {
|
||||
let index = match encoding {
|
||||
AppliedEncoding::Zstd => 0,
|
||||
AppliedEncoding::Brotli => 1,
|
||||
AppliedEncoding::Gzip => 2,
|
||||
AppliedEncoding::Deflate => 3,
|
||||
};
|
||||
explicit[index] = Some(explicit[index].map_or(q, |current: u16| current.max(q)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut best = None;
|
||||
let mut best_q = 0;
|
||||
for (index, encoding) in AppliedEncoding::PREFERENCE.into_iter().enumerate() {
|
||||
if !encoding.enabled(settings) {
|
||||
continue;
|
||||
}
|
||||
let q = explicit[index].or(wildcard).unwrap_or(0);
|
||||
if q > best_q {
|
||||
best = Some(encoding);
|
||||
best_q = q;
|
||||
}
|
||||
}
|
||||
best.filter(|_| best_q > 0)
|
||||
}
|
||||
|
||||
pub async fn encode(
|
||||
response: Response,
|
||||
headers: &HeaderMap,
|
||||
method: &Method,
|
||||
settings: &Settings,
|
||||
) -> Response {
|
||||
if !eligible(&response, headers, method, settings) {
|
||||
return response;
|
||||
}
|
||||
let Some(selected) = preferred_encoding(headers, settings) else {
|
||||
return response;
|
||||
};
|
||||
let mut response = Some(response);
|
||||
let service = service_fn(move |_: Request<Body>| {
|
||||
let response = response.take().expect("one-shot compression service");
|
||||
async move { Ok::<_, Infallible>(response) }
|
||||
});
|
||||
let compression = Compression::new(service)
|
||||
.gzip(settings.gzip)
|
||||
.br(settings.br)
|
||||
.zstd(settings.zstd)
|
||||
.deflate(settings.deflate)
|
||||
.quality(CompressionLevel::Fastest)
|
||||
.compress_when(DefaultPredicate::new().and(MinimumSize(settings.min_bytes)));
|
||||
let mut request = Request::new(Body::empty());
|
||||
*request.method_mut() = method.clone();
|
||||
request.headers_mut().insert(
|
||||
"accept-encoding",
|
||||
HeaderValue::from_static(selected.as_str()),
|
||||
);
|
||||
let mut response = match compression.oneshot(request).await {
|
||||
Ok(response) => response.map(Body::new),
|
||||
Err(never) => match never {},
|
||||
};
|
||||
if response.headers().contains_key("content-encoding") {
|
||||
if let Some(applied) = response
|
||||
.headers()
|
||||
.get("content-encoding")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.and_then(AppliedEncoding::from_header)
|
||||
{
|
||||
response.extensions_mut().insert(applied);
|
||||
}
|
||||
if let Some(value) = response
|
||||
.headers()
|
||||
.get("etag")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(str::to_owned)
|
||||
{
|
||||
if !value.starts_with("W/") {
|
||||
if let Ok(weak) = HeaderValue::from_str(&format!("W/{value}")) {
|
||||
response.headers_mut().insert("etag", weak);
|
||||
} else {
|
||||
response.headers_mut().remove("etag");
|
||||
}
|
||||
}
|
||||
}
|
||||
for field in ["content-md5", "digest", "content-digest", "accept-ranges"] {
|
||||
response.headers_mut().remove(field);
|
||||
}
|
||||
}
|
||||
response
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
include!("tests.rs");
|
||||
Reference in new issue
Block a user