diff --git a/Cargo.lock b/Cargo.lock index 6f1c918..15a8c13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,10 +3,25 @@ version = 3 [[package]] -name = "anyhow" -version = "1.0.58" +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ef4730490ad1c4eae5c4325b2a95f521d023e5c885853ff7aca0a6a1631db3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" +checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2" +dependencies = [ + "alloc-no-stdlib", +] [[package]] name = "autocfg" @@ -20,6 +35,27 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.10.0" @@ -44,6 +80,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + [[package]] name = "encoding_rs" version = "0.8.31" @@ -53,6 +98,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "flate2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -294,6 +349,15 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "miniz_oxide" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +dependencies = [ + "adler", +] + [[package]] name = "mio" version = "0.8.4" @@ -527,16 +591,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] -name = "statue_rs" +name = "statue" version = "0.1.0" dependencies = [ - "anyhow", + "brotli", + "flate2", "hyper", "num_cpus", "reqwest", "serde", "serde_json", "tokio", + "tower", ] [[package]] @@ -621,6 +687,23 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" + [[package]] name = "tower-service" version = "0.3.2" @@ -634,6 +717,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" dependencies = [ "cfg-if", + "log", "pin-project-lite", "tracing-core", ] diff --git a/Cargo.toml b/Cargo.toml index a56ad7a..96fb1a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,17 @@ [package] -name = "statue_rs" +name = "statue" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0.58" +brotli = "3.3.4" +flate2 = "1.0.24" hyper = { version = "0.14.20", default-features = false, features = ["http1", "http2", "server", "tcp", "client"] } num_cpus = "1.13.1" reqwest = { version = "0.11.11", default-features = false, features = ["rustls-tls"] } serde = { version = "1.0.140", features = ["derive"] } serde_json = "1.0.82" tokio = { version = "1.20.1", features = ["rt", "macros", "rt-multi-thread", "fs", "process"] } +tower = "0.4.13" diff --git a/src/main.rs b/src/main.rs index 791b58f..061924e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,19 @@ mod api; +mod util; -use std::{collections::HashMap, net::ToSocketAddrs}; +use std::{collections::BTreeMap, net::ToSocketAddrs}; +use flate2::Compression; use hyper::{ header::HeaderValue, service::{make_service_fn, service_fn}, Body, Request as Request_, Response as Response_, }; use serde::{Deserialize, Serialize}; +use tower::ServiceBuilder; type Request = Request_; -type Response = Response_; +type Response = Response_; pub type Result = core::result::Result; @@ -27,16 +30,16 @@ struct ConfigSchema { listen_addr: Option, title: Option, description: Option, - hosts: HashMap, - services: HashMap, + hosts: BTreeMap, + services: BTreeMap, } pub struct Config { listen_addr: String, title: String, description: String, - hosts: HashMap, - services: HashMap, + hosts: BTreeMap, + services: BTreeMap, } fn not_found() -> Result { @@ -56,9 +59,8 @@ fn internal_server_error() -> Result { async fn index() -> Result { let config = read_config().await?; - let mut resp = Response::new( - format!( - r###" + let mut resp = Response::new(format!( + r###" @@ -79,13 +81,11 @@ async fn index() -> Result { "###, - title = config.title, - description = config.description, - style = include_str!("../frontend/style.css"), - script = include_str!(concat!(env!("OUT_DIR"), "/frontend.js")), - ) - .into(), - ); + title = config.title, + description = config.description, + style = include_str!("../frontend/style.css"), + script = include_str!(concat!(env!("OUT_DIR"), "/frontend.js")), + )); resp.headers_mut().insert( "content-type", HeaderValue::from_static("text/html; charset=utf-8"), @@ -97,7 +97,7 @@ async fn index() -> Result { fn json(v: &T) -> Result { let json = serde_json::to_string(&v)?; - let mut resp = Response::new(json.into()); + let mut resp = Response::new(json); resp.headers_mut() .insert("content-type", HeaderValue::from_static("application/json")); @@ -157,19 +157,35 @@ pub async fn read_config() -> Result { async fn serve() -> Result<()> { let config = read_config().await?; let addr = config.listen_addr.to_socket_addrs()?.next().unwrap(); - let make_service = make_service_fn(|_conn| async { - Ok::<_, std::io::Error>(service_fn(|req| async { - let resp = route(req).await; + let service = service_fn(|req| async { + let resp = route(req).await; - match resp { - Ok(r) => Ok(r), - Err(e) => { - eprintln!("{}", e); + match resp { + Ok(r) => Ok(r), + Err(e) => { + eprintln!("{}", e); - internal_server_error() - } + internal_server_error() } - })) + } + }); + let make_service = make_service_fn(|_conn| async move { + let service = ServiceBuilder::new() + .layer(util::Log) + .layer(util::Compress { + algorithms: util::AlgorithmPreferences { + brotli: Some((3, util::brotli_default_params(4, true))), + gzip: Some((2, Compression::fast())), + deflate: Some((1, Compression::fast())), + }, + predicate: util::CompressPredicate { + min_size: 256, + content_type_predicate: |_: &str| true, + }, + }) + .service(service); + + Ok::<_, std::convert::Infallible>(service) }); let server = hyper::server::Server::bind(&addr).serve(make_service); diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..9414537 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,669 @@ +use std::{ + cmp::Ordering, + future::Future, + pin::Pin, + task::{Context, Poll}, + time::Instant, +}; + +use brotli::{enc::BrotliEncoderParams, CompressorWriter}; +use flate2::{ + write::{DeflateEncoder, GzEncoder}, + Compression, +}; +use hyper::{ + body::{Buf, Bytes, HttpBody}, + header::HeaderValue, + Method, Request, Response, Uri, +}; +use tower::{Layer, Service}; + +#[derive(Clone)] +pub struct Log; + +impl Layer for Log { + type Service = LogService; + + fn layer(&self, inner: S) -> Self::Service { + LogService::new(inner) + } +} + +#[derive(Clone)] +pub struct LogService { + inner: S, +} + +impl LogService { + fn new(inner: S) -> Self { + Self { inner } + } +} + +impl Service> for LogService +where + S: Service, Response = Response>, +{ + type Response = S::Response; + + type Error = S::Error; + + type Future = LogServiceFuture; + + fn poll_ready( + &mut self, + cx: &mut Context<'_>, + ) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: Request) -> Self::Future { + let uri = req.uri().clone(); + let method = req.method().clone(); + + let fut = self.inner.call(req); + + LogServiceFuture { + inner: fut, + uri, + method, + start: None, + } + } +} + +pub struct LogServiceFuture { + uri: Uri, + method: Method, + inner: InnerFut, + start: Option, +} + +impl Future for LogServiceFuture +where + InnerFut: Future, InnerFutError>>, +{ + type Output = InnerFut::Output; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let (uri, method, inner, mut start) = unsafe { + let this = self.get_unchecked_mut(); + ( + &this.uri, + &this.method, + Pin::new_unchecked(&mut this.inner), + Pin::new_unchecked(&mut this.start), + ) + }; + start.get_or_insert(Instant::now()); + + match inner.poll(cx) { + Poll::Ready(r) => { + let res = match r { + Ok(res) => res, + e => return Poll::Ready(e), + }; + + let start = unsafe { start.unwrap_unchecked() }; + let now = Instant::now(); + let diff = now - start; + + println!( + "{} {} {} [{}ms]", + method, + uri, + res.status(), + diff.as_millis() + ); + + Poll::Ready(Ok(res)) + } + p => p, + } + } +} + +pub type Preference = u8; +/// Compression algorithms and their associated prefence level. Higher is +/// better +#[derive(Clone)] +pub struct AlgorithmPreferences { + pub brotli: Option<(Preference, BrotliEncoderParams)>, + pub gzip: Option<(Preference, Compression)>, + pub deflate: Option<(Preference, Compression)>, +} + +pub fn brotli_default_params( + quality: u8, + favor_cpu_efficiency: bool, +) -> BrotliEncoderParams { + BrotliEncoderParams { + quality: quality as i32, + favor_cpu_efficiency, + ..Default::default() + } +} + +#[derive(Clone)] +pub struct CompressPredicate { + pub min_size: usize, + pub content_type_predicate: ContentTypePredicateFn, +} + +#[derive(Clone)] +pub struct Compress { + pub algorithms: AlgorithmPreferences, + pub predicate: CompressPredicate, +} + +impl Layer for Compress { + type Service = CompressService; + + fn layer(&self, inner: S) -> Self::Service { + CompressService { + compress: self.clone(), + inner, + } + } +} + +#[derive(Clone)] +pub struct CompressService { + compress: Compress, + inner: S, +} + +pub enum Algorithm { + Brotli(BrotliEncoderParams), + Deflate(Compression), + Gzip(Compression), +} + +impl Service> + for CompressService +where + S: Service, Response = Response>, + CTPF: Clone + FnOnce(&str) -> bool, + ResBody: HttpBody, +{ + type Response = Response>; + + type Error = S::Error; + + type Future = CompressFuture; + + fn poll_ready( + &mut self, + cx: &mut Context<'_>, + ) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, req: Request) -> Self::Future { + let chosen_algorithm = match req.headers().get("accept-encoding") { + None => None, + Some(s) => choose_algorithm(&self.compress.algorithms, s), + }; + + let fut = self.inner.call(req); + + CompressFuture { + inner: fut, + chosen_algorithm, + predicate: self.compress.predicate.clone(), + } + } +} + +fn choose_algorithm( + preferences: &AlgorithmPreferences, + accept_encodings: &HeaderValue, +) -> Option { + let accept_encodings = match accept_encodings.to_str().ok() { + None => return None, + Some(s) => s, + }; + + accept_encodings + .split(',') + .flat_map(|s| s.split(';').next()) + .map(|s| s.trim().to_string()) + .flat_map(|s| match s.as_str() { + "br" => preferences + .brotli + .as_ref() + .map(|p| (Algorithm::Brotli(p.1.clone()), p.0)), + "gzip" => preferences + .gzip + .as_ref() + .map(|p| (Algorithm::Gzip(p.1), p.0)), + "deflate" => preferences + .deflate + .as_ref() + .map(|p| (Algorithm::Deflate(p.1), p.0)), + _ => None, + }) + .max_by(|a, b| { + if a.1 == b.1 { + return Ordering::Greater; + } + + a.1.cmp(&b.1) + }) + .map(|(e, _)| e) +} + +pub struct CompressFuture { + inner: InnerFut, + chosen_algorithm: Option, + predicate: CompressPredicate, +} +impl Future + for CompressFuture +where + InnerFut: Future, InnerFutError>>, + CTPF: FnOnce(&str) -> bool + Clone, + ResBody: HttpBody, +{ + type Output = Result>, InnerFutError>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let (chosen_algorithm, predicate, inner) = unsafe { + let this = self.get_unchecked_mut(); + ( + &this.chosen_algorithm, + &mut this.predicate, + Pin::new_unchecked(&mut this.inner), + ) + }; + + match inner.poll(cx) { + Poll::Ready(r) => { + let res = match r { + Ok(res) => res, + Err(e) => return Poll::Ready(Err(e)), + }; + + let res = choose_body(res, chosen_algorithm, predicate.clone()); + + Poll::Ready(Ok(res)) + } + _ => Poll::Pending, + } + } +} + +pub enum CompressBody { + None(B), + Brotli(BrotliBody), + Gzip(GzipBody), + Deflate(DeflateBody), +} +impl HttpBody for CompressBody +where + B: HttpBody, + B::Data: Send + 'static, +{ + type Data = Box; + + type Error = B::Error; + + fn poll_data( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + match unsafe { self.get_unchecked_mut() } { + CompressBody::Brotli(b) => unsafe { Pin::new_unchecked(b) } + .poll_data(cx) + .map(|o| o.map(|r| r.map(|b| Box::new(b) as Box<_>))), + CompressBody::Gzip(b) => unsafe { Pin::new_unchecked(b) } + .poll_data(cx) + .map(|o| o.map(|r| r.map(|b| Box::new(b) as Box<_>))), + CompressBody::Deflate(b) => unsafe { Pin::new_unchecked(b) } + .poll_data(cx) + .map(|o| o.map(|r| r.map(|b| Box::new(b) as Box<_>))), + CompressBody::None(b) => unsafe { Pin::new_unchecked(b) } + .poll_data(cx) + .map(|o| o.map(|r| r.map(|b| Box::new(b) as Box<_>))), + } + } + + fn poll_trailers( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + match unsafe { self.get_unchecked_mut() } { + CompressBody::None(b) => { + unsafe { Pin::new_unchecked(b) }.poll_trailers(cx) + } + CompressBody::Brotli(b) => { + unsafe { Pin::new_unchecked(b) }.poll_trailers(cx) + } + CompressBody::Gzip(b) => { + unsafe { Pin::new_unchecked(b) }.poll_trailers(cx) + } + CompressBody::Deflate(b) => { + unsafe { Pin::new_unchecked(b) }.poll_trailers(cx) + } + } + } +} + +pub struct BrotliBody { + inner: B, + compressor: Option>>, +} +impl HttpBody for BrotliBody +where + B::Data: Send + 'static, +{ + type Data = Bytes; + + type Error = B::Error; + + fn poll_data( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + let (inner, compressor) = unsafe { + let this = self.get_unchecked_mut(); + + (Pin::new_unchecked(&mut this.inner), &mut this.compressor) + }; + + if compressor.is_none() { + return Poll::Ready(None); + } + + let data = match inner.poll_data(cx) { + Poll::Ready(Some(Ok(d))) => Some(d), + Poll::Ready(None) => None, + Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))), + Poll::Pending => return Poll::Pending, + }; + + match data { + None => { + let compressor = + unsafe { compressor.take().unwrap_unchecked() }; + let buf = compressor.into_inner(); + + Poll::Ready(Some(Ok(Bytes::from(buf)))) + } + Some(d) => { + let mut compressor = + unsafe { compressor.as_mut().unwrap_unchecked() }; + let mut reader = d.reader(); + use std::io::Write; + let _ = std::io::copy(&mut reader, &mut compressor); + let _ = compressor.flush(); + let buf = compressor.get_ref().clone(); + compressor.get_mut().clear(); + + Poll::Ready(Some(Ok(Bytes::from(buf)))) + } + } + } + + fn poll_trailers( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + let inner = unsafe { + let this = self.get_unchecked_mut(); + + Pin::new_unchecked(&mut this.inner) + }; + + inner.poll_trailers(cx) + } +} + +impl BrotliBody { + pub fn new(inner: B, params: &BrotliEncoderParams) -> Self { + Self { + inner, + compressor: Some(CompressorWriter::with_params( + Vec::new(), + 4096, + params, + )), + } + } +} + +pub struct GzipBody { + inner: B, + encoder: Option>>, +} +impl GzipBody { + pub fn new(inner: B, compression: Compression) -> Self { + Self { + inner, + encoder: Some(GzEncoder::new(Vec::new(), compression)), + } + } +} +impl HttpBody for GzipBody +where + B: HttpBody, +{ + type Data = Bytes; + + type Error = B::Error; + + fn poll_data( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + let (inner, encoder) = unsafe { + let this = self.get_unchecked_mut(); + (Pin::new_unchecked(&mut this.inner), &mut this.encoder) + }; + + if encoder.is_none() { + return Poll::Ready(None); + } + + let data = match inner.poll_data(cx) { + Poll::Ready(Some(Ok(d))) => Some(d), + Poll::Ready(None) => None, + Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))), + Poll::Pending => return Poll::Pending, + }; + + match data { + None => { + let encoder = unsafe { encoder.take().unwrap_unchecked() }; + let buf = encoder.finish().unwrap(); + + Poll::Ready(Some(Ok(Bytes::from(buf)))) + } + Some(d) => { + let mut encoder = + unsafe { encoder.as_mut().unwrap_unchecked() }; + let mut reader = d.reader(); + let _ = std::io::copy(&mut reader, &mut encoder); + let buf = encoder.get_ref().clone(); + encoder.get_mut().clear(); + + Poll::Ready(Some(Ok(Bytes::from(buf)))) + } + } + } + + fn poll_trailers( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + let inner = unsafe { + let this = self.get_unchecked_mut(); + Pin::new_unchecked(&mut this.inner) + }; + + inner.poll_trailers(cx) + } +} + +pub struct DeflateBody { + inner: B, + encoder: Option>>, +} +impl DeflateBody { + pub fn new(inner: B, compression: Compression) -> Self { + Self { + inner, + encoder: Some(DeflateEncoder::new(Vec::new(), compression)), + } + } +} +impl HttpBody for DeflateBody +where + B: HttpBody, +{ + type Data = Bytes; + + type Error = B::Error; + + fn poll_data( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + let (inner, encoder) = unsafe { + let this = self.get_unchecked_mut(); + (Pin::new_unchecked(&mut this.inner), &mut this.encoder) + }; + + if encoder.is_none() { + return Poll::Ready(None); + } + + let data = match inner.poll_data(cx) { + Poll::Ready(Some(Ok(d))) => Some(d), + Poll::Ready(None) => None, + Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))), + Poll::Pending => return Poll::Pending, + }; + + match data { + None => { + let encoder = unsafe { encoder.take().unwrap_unchecked() }; + let buf = encoder.finish().unwrap(); + + Poll::Ready(Some(Ok(Bytes::from(buf)))) + } + Some(d) => { + let mut encoder = + unsafe { encoder.as_mut().unwrap_unchecked() }; + let mut reader = d.reader(); + let _ = std::io::copy(&mut reader, &mut encoder); + let buf = encoder.get_ref().clone(); + encoder.get_mut().clear(); + + Poll::Ready(Some(Ok(Bytes::from(buf)))) + } + } + } + + fn poll_trailers( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>> { + let inner = unsafe { + let this = self.get_unchecked_mut(); + Pin::new_unchecked(&mut this.inner) + }; + + inner.poll_trailers(cx) + } +} + +fn replace_body( + r: Response, + replace: impl FnOnce(B) -> B_, +) -> Response { + let (parts, body) = r.into_parts(); + + Response::from_parts(parts, replace(body)) +} + +pub fn choose_body( + res: Response, + chosen_algorithm: &Option, + predicate: CompressPredicate, +) -> Response> +where + CTPF: FnOnce(&str) -> bool, + B: HttpBody, +{ + let chosen_algorithm = match chosen_algorithm { + None => return replace_body(res, CompressBody::None), + Some(a) => a, + }; + + let headers = res.headers(); + let content_length = match headers + .get("transfer-encoding") + .and_then(|v| v.to_str().ok()) + { + None => Some( + headers + .get("content-length") + .and_then(|v| v.to_str().ok()) + .and_then(|s| s.parse::().ok()) + .unwrap_or_else(|| res.body().size_hint().lower() as usize), + ), + Some("chunked") => None, + _ => return replace_body(res, CompressBody::None), + }; + match content_length { + Some(l) if l < predicate.min_size => { + return replace_body(res, CompressBody::None) + } + _ => (), + } + + let content_type = + match headers.get("content-type").and_then(|v| v.to_str().ok()) { + Some(ct) => ct, + None => return replace_body(res, CompressBody::None), + }; + + if !(predicate.content_type_predicate)(content_type) { + return replace_body(res, CompressBody::None); + } + + match chosen_algorithm { + Algorithm::Brotli(p) => { + let mut res = replace_body(res, |b| { + CompressBody::Brotli(BrotliBody::new(b, p)) + }); + + res.headers_mut() + .insert("content-encoding", HeaderValue::from_static("br")); + + res + } + Algorithm::Gzip(c) => { + let mut res = + replace_body(res, |b| CompressBody::Gzip(GzipBody::new(b, *c))); + + res.headers_mut() + .insert("content-encoding", HeaderValue::from_static("gzip")); + + res + } + Algorithm::Deflate(c) => { + let mut res = replace_body(res, |b| { + CompressBody::Deflate(DeflateBody::new(b, *c)) + }); + + res.headers_mut().insert( + "content-encoding", + HeaderValue::from_static("deflate"), + ); + + res + } + } +}