http: teach the Client how to decode deflate content

The Backup Server can compress the content using deflate so we teach the
client how to decode it.

If a request is sent with the `Accept-Encoding` [2] header set to
`deflate`, and the response's `Content-Encoding` [1] header is equal to
`deflate` we wrap the Body stream with a stream that can decode `zlib`
on the run.

Note that from the `Accept-Encoding` docs [2], the `deflate` encoding is
actually `zlib`.

This can be also tested against
http://eu.httpbin.org/#/Response_formats/get_deflate by adding the
following test:

```rust
    #[tokio::test]
    async fn test_client() {
        let client = Client::new();
        let headers = HashMap::from([(
            hyper::header::ACCEPT_ENCODING.to_string(),
            "deflate".to_string(),
        )]);
        let response = client
            .get_string("https://eu.httpbin.org/deflate", Some(&headers))
            .await;
        assert!(response.is_ok());
    }
```

at `proxmox-http/src/client/simple.rs` and running

```
cargo test --features=client,client-trait
```

[1] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
[2] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding

Suggested-by: Lukas Wagner <l.wagner@proxmox.com>
Signed-off-by: Maximiliano Sandoval <m.sandoval@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
Tested-by: Max Carrara <m.carrara@proxmox.com>
This commit is contained in:
Maximiliano Sandoval 2024-07-05 15:04:32 +02:00 committed by Wolfgang Bumiller
parent 8b8957b5ba
commit 4d1c4ec829
2 changed files with 71 additions and 1 deletions

View File

@ -26,6 +26,11 @@ proxmox-async = { workspace = true, optional = true }
proxmox-sys = { workspace = true, optional = true }
proxmox-io = { workspace = true, optional = true }
proxmox-lang = { workspace = true, optional = true }
proxmox-compression = { workspace = true, optional = true }
[dev-dependencies]
tokio = { workspace = true, features = [ "macros" ] }
flate2 = { workspace = true }
[features]
default = []
@ -42,12 +47,14 @@ client = [
"dep:futures",
"dep:hyper",
"dep:openssl",
"dep:proxmox-compression",
"dep:tokio",
"dep:tokio-openssl",
"http-helpers",
"hyper?/client",
"hyper?/http1",
"hyper?/http2",
"hyper?/stream",
"hyper?/tcp",
"rate-limited-stream",
"tokio?/io-util",

View File

@ -78,7 +78,8 @@ impl Client {
self.add_proxy_headers(&mut request)?;
self.client.request(request).map_err(Error::from).await
let encoded_response = self.client.request(request).map_err(Error::from).await?;
decode_response(encoded_response).await
}
pub async fn post(
@ -245,3 +246,65 @@ impl crate::HttpClient<String, String> for Client {
})
}
}
/// Wraps the `Body` stream in a DeflateDecoder stream if the `Content-Encoding`
/// header of the response is `deflate`, otherwise returns the original
/// response.
async fn decode_response(mut res: Response<Body>) -> Result<Response<Body>, Error> {
let Some(content_encoding) = res.headers_mut().remove(&hyper::header::CONTENT_ENCODING) else {
return Ok(res);
};
let encodings = content_encoding.to_str()?;
if encodings == "deflate" {
let (parts, body) = res.into_parts();
let decoder = proxmox_compression::DeflateDecoder::builder(body)
.zlib(true)
.build();
let decoded_body = Body::wrap_stream(decoder);
Ok(Response::from_parts(parts, decoded_body))
} else {
bail!("Unknown encoding format: {encodings}");
}
}
#[cfg(test)]
mod test {
use super::*;
use std::io::Write;
const BODY: &str = r#"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut
enim aeque doleamus animo, cum corpore dolemus, fieri tamen permagna accessio potest,
si aliquod aeternum et infinitum impendere."#;
#[tokio::test]
async fn test_parse_response_deflate() {
let encoded = encode_deflate(BODY.as_bytes()).unwrap();
let encoded_body = Body::from(encoded);
let encoded_response = Response::builder()
.header(hyper::header::CONTENT_ENCODING, "deflate")
.body(encoded_body)
.unwrap();
let decoded_response = decode_response(encoded_response).await.unwrap();
assert_eq!(
Client::response_body_string(decoded_response)
.await
.unwrap(),
BODY
);
}
fn encode_deflate(bytes: &[u8]) -> Result<Vec<u8>, std::io::Error> {
use flate2::write::ZlibEncoder;
use flate2::Compression;
let mut e = ZlibEncoder::new(Vec::new(), Compression::default());
e.write_all(bytes).unwrap();
e.finish()
}
}