From f5d45dcdffce4ec78792fb80a85d211ef7c706a1 Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Mon, 3 Nov 2025 07:55:49 +0100 Subject: [PATCH] decode gzip bodies --- Cargo.lock | 55 +++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ src/logger.rs | 84 ++++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 130 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d732973..9e5d50d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "anstream" version = "0.6.21" @@ -52,6 +58,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + [[package]] name = "async-trait" version = "0.1.89" @@ -75,6 +87,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + [[package]] name = "clap" version = "4.5.51" @@ -121,6 +139,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "diff" version = "0.1.13" @@ -144,6 +171,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -440,6 +477,16 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.1.0" @@ -522,9 +569,11 @@ dependencies = [ name = "request-logging-proxy" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "bytes", "clap", + "flate2", "http", "http-body-util", "hyper", @@ -593,6 +642,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.11" diff --git a/Cargo.toml b/Cargo.toml index 0f67e86..013e910 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,9 +40,11 @@ unimplemented = { level = "warn", priority = 1 } unwrap_used = { level = "warn", priority = 1 } [dependencies] +anyhow = "1.0" async-trait = "0.1" bytes = "1.4" clap = { version = "4.5", features = ["env", "derive"] } +flate2 = "1.1" http = "1.3" http-body-util = "0.1" hyper = { version = "1.7", features = [] } diff --git a/src/logger.rs b/src/logger.rs index 5b1214d..748c9f9 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -1,10 +1,15 @@ +use anyhow::Context; use bytes::Bytes; +use flate2::read::GzDecoder; +use http::HeaderValue; +use http::header::CONTENT_ENCODING; use http_body_util::{BodyExt, Full}; use serde_json::Value; -use std::io::Write; use std::io::{self, BufWriter}; +use std::io::{Read, Write}; use std::sync::{Arc, Mutex, MutexGuard}; +#[derive(Debug)] pub struct LogEntry { pub request: hyper::Request>, pub response: hyper::Response>, @@ -12,7 +17,7 @@ pub struct LogEntry { #[async_trait::async_trait] pub trait RequestResponseLogger: Send + std::fmt::Debug { - async fn log_request_response(&mut self, entry: LogEntry) -> io::Result<()>; + async fn log_request_response(&mut self, entry: LogEntry) -> anyhow::Result<()>; } /// A logger that outputs request and response details to VSCode's REST log format. @@ -26,11 +31,21 @@ impl RequestResponseLogger for VSCodeRestLogger { async fn log_request_response( &mut self, LogEntry { request, response }: LogEntry, - ) -> io::Result<()> { + ) -> anyhow::Result<()> { let (request_header, request_body) = request.into_parts(); - let request_body = body_to_string(request_body).await?; + let request_body = body_to_string( + request_body, + ContentDecoder::try_from(request_header.headers.get(CONTENT_ENCODING))?, + ) + .await + .context("Failed to decode request body")?; let (response_header, response_body) = response.into_parts(); - let response_body = body_to_string(response_body).await?; + let response_body = body_to_string( + response_body, + ContentDecoder::try_from(response_header.headers.get(CONTENT_ENCODING))?, + ) + .await + .context("failed to decode response body")?; let mut w = self.writer.lock()?; Self::log_request(&mut w, request_header, request_body)?; Self::log_response(&mut w, response_header, response_body)?; @@ -58,7 +73,7 @@ impl VSCodeRestLogger { writer: &mut W, header: http::request::Parts, body: String, - ) -> io::Result<()> { + ) -> anyhow::Result<()> { writeln!( writer, "\n\ @@ -92,7 +107,7 @@ impl VSCodeRestLogger { writer: &mut W, header: http::response::Parts, body: String, - ) -> io::Result<()> { + ) -> anyhow::Result<()> { writeln!( writer, "\n\ @@ -129,13 +144,60 @@ impl VSCodeRestLogger { } } -async fn body_to_string(body: Full) -> io::Result { +async fn body_to_string( + body: Full, + content_encoding: ContentDecoder, +) -> anyhow::Result { let collected = body .collect() .await .map_err(|e| io::Error::other(format!("Failed to collect body: {e}")))?; - String::from_utf8(collected.to_bytes().to_vec()) - .map_err(|e| io::Error::other(format!("Failed to convert body to string: {e}"))) + let bytes = collected.to_bytes(); + + content_encoding + .decode(&bytes) + .with_context(|| format!("Failed to decode body: {}", String::from_utf8_lossy(&bytes))) +} + +enum ContentDecoder { + None, + Gzip, +} + +impl TryFrom> for ContentDecoder { + type Error = anyhow::Error; + + fn try_from(value: Option<&HeaderValue>) -> Result { + match value { + Some(header_value) => match header_value.to_str() { + Ok("gzip") => Ok(ContentDecoder::Gzip), + _ => anyhow::bail!("Unsupported content encoding: {header_value:?}"), + }, + None => Ok(ContentDecoder::None), + } + } +} + +impl ContentDecoder { + fn decode(&self, data: &[u8]) -> anyhow::Result { + if data.is_empty() { + return Ok(String::new()); + } + + match self { + ContentDecoder::None => { + String::from_utf8(data.to_vec()).context("Failed to convert body to string") + } + ContentDecoder::Gzip => { + let mut decoder = GzDecoder::new(data); + let mut decoded_data = String::new(); + decoder + .read_to_string(&mut decoded_data) + .context("Failed to decode gzip data")?; + Ok(decoded_data) + } + } + } } #[derive(Debug)] @@ -146,7 +208,7 @@ pub enum LockableWriter { } impl LockableWriter { - pub fn lock<'w>(&'w mut self) -> io::Result> { + pub fn lock<'w>(&'w mut self) -> anyhow::Result> { match self { LockableWriter::Stdout(stdout) => Ok(LockableWriterGuard::Stdout(stdout.lock())), LockableWriter::BufWriter(mutex) => {