vector/sources/util/http/
encoding.rs1use bytes::{Buf, BufMut, Bytes, BytesMut};
2use futures_util::StreamExt;
3use snap::raw::Decoder as SnappyDecoder;
4use warp::http::StatusCode;
5use warp::{Filter, filters::BoxedFilter};
6
7#[cfg(test)]
8use crate::sources::util::decompression::DEFAULT_MAX_DECOMPRESSED_SIZE_BYTES;
9pub use crate::sources::util::decompression::set_max_decompressed_size_bytes;
10use crate::sources::util::decompression::{
11 CappedDecoder, is_decompressed_size_limit_error, max_decompressed_size_bytes,
12};
13use crate::{common::http::ErrorMessage, internal_events::HttpDecompressError};
14
15pub(crate) fn capped_body() -> BoxedFilter<(Bytes,)> {
21 let max_body_size = max_decompressed_size_bytes();
22 let max_body_size_header = u64::try_from(max_body_size).unwrap_or(u64::MAX);
23
24 warp::header::optional::<u64>("content-length")
25 .and_then(move |declared: Option<u64>| async move {
26 if declared.is_some_and(|len| len > max_body_size_header) {
27 Err(warp::reject::custom(request_body_too_large_error(
28 max_body_size,
29 )))
30 } else {
31 Ok(())
32 }
33 })
34 .untuple_one()
35 .and(warp::body::stream())
36 .and_then(move |body| async move {
37 collect_body_with_limit(body, max_body_size)
38 .await
39 .map_err(warp::reject::custom)
40 })
41 .boxed()
42}
43
44pub fn decompress_body(header: Option<&str>, body: Bytes) -> Result<Bytes, ErrorMessage> {
50 decompress_body_with_limit(header, body, max_decompressed_size_bytes())
51}
52
53pub(crate) fn decompress_body_with_limit(
55 header: Option<&str>,
56 mut body: Bytes,
57 max_decompressed_size: usize,
58) -> Result<Bytes, ErrorMessage> {
59 if let Some(encodings) = header {
60 for encoding in encodings.rsplit(',').map(str::trim) {
61 body = match encoding {
62 "identity" => body,
63 "gzip" => CappedDecoder::gzip_with_limit(body.reader(), max_decompressed_size)
64 .decompress()
65 .map(Bytes::from)
66 .map_err(|error| {
67 emit_decompress_error(encoding, error, max_decompressed_size)
68 })?,
69 "deflate" => CappedDecoder::zlib_with_limit(body.reader(), max_decompressed_size)
70 .decompress()
71 .map(Bytes::from)
72 .map_err(|error| {
73 emit_decompress_error(encoding, error, max_decompressed_size)
74 })?,
75 "snappy" => decompress_snappy(&body, max_decompressed_size)?,
76 "zstd" => CappedDecoder::zstd_http_with_limit(body.reader(), max_decompressed_size)
77 .map_err(|error| emit_decompress_error(encoding, error, max_decompressed_size))?
78 .decompress()
79 .map(Bytes::from)
80 .map_err(|error| {
81 emit_decompress_error(encoding, error, max_decompressed_size)
82 })?,
83 encoding => {
84 return Err(ErrorMessage::new(
85 StatusCode::UNSUPPORTED_MEDIA_TYPE,
86 format!("Unsupported encoding {encoding}"),
87 ));
88 }
89 }
90 }
91 }
92
93 ensure_body_within_limit(&body, "identity", max_decompressed_size)?;
94 Ok(body)
95}
96
97fn decompress_snappy(body: &Bytes, max_decompressed_size: usize) -> Result<Bytes, ErrorMessage> {
98 let len = snap::raw::decompress_len(body).map_err(|error| {
101 emit_decompress_error(
102 "snappy",
103 std::io::Error::other(error),
104 max_decompressed_size,
105 )
106 })?;
107 if len > max_decompressed_size {
108 return Err(decompressed_too_large_error(
109 "snappy",
110 max_decompressed_size,
111 ));
112 }
113 let decoded = SnappyDecoder::new().decompress_vec(body).map_err(|error| {
114 emit_decompress_error(
115 "snappy",
116 std::io::Error::other(error),
117 max_decompressed_size,
118 )
119 })?;
120 Ok(decoded.into())
121}
122
123const ADDITIONAL_CAPACITY_FOR_CHUNKS_BEYOND_FIRST_TWO: usize = 16 * 1024;
125
126async fn collect_body_with_limit<S, B>(body: S, max_body_size: usize) -> Result<Bytes, ErrorMessage>
133where
134 S: futures_util::Stream<Item = Result<B, warp::Error>>,
135 B: Buf,
136{
137 futures_util::pin_mut!(body);
138
139 let mut total_body_size: usize = 0;
140 let mut admit_chunk_within_limit = |chunk: Result<B, warp::Error>| -> Result<B, ErrorMessage> {
141 let chunk = chunk.map_err(|error| {
142 ErrorMessage::new(
143 StatusCode::BAD_REQUEST,
144 format!("Failed reading request body: {error}"),
145 )
146 })?;
147
148 total_body_size = total_body_size.saturating_add(chunk.remaining());
149 if total_body_size > max_body_size {
150 return Err(request_body_too_large_error(max_body_size));
151 }
152
153 Ok(chunk)
154 };
155
156 let Some(chunk) = body.next().await else {
157 return Ok(Bytes::new());
158 };
159 let mut first = admit_chunk_within_limit(chunk)?;
160
161 let Some(chunk) = body.next().await else {
162 return Ok(first.copy_to_bytes(first.remaining()));
163 };
164 let second = admit_chunk_within_limit(chunk)?;
165
166 let mut bytes = BytesMut::with_capacity(
167 first.remaining() + second.remaining() + ADDITIONAL_CAPACITY_FOR_CHUNKS_BEYOND_FIRST_TWO,
168 );
169 bytes.put(first);
170 bytes.put(second);
171
172 while let Some(chunk) = body.next().await {
173 bytes.put(admit_chunk_within_limit(chunk)?);
174 }
175
176 Ok(bytes.freeze())
177}
178
179fn ensure_body_within_limit(
180 body: &Bytes,
181 encoding: &str,
182 max_decompressed_size: usize,
183) -> Result<(), ErrorMessage> {
184 if body.len() > max_decompressed_size {
185 return Err(decompressed_too_large_error(
186 encoding,
187 max_decompressed_size,
188 ));
189 }
190 Ok(())
191}
192
193fn request_body_too_large_error(max: usize) -> ErrorMessage {
194 ErrorMessage::new(
195 StatusCode::PAYLOAD_TOO_LARGE,
196 format!("Request body exceeds limit of {max} bytes."),
197 )
198}
199
200fn decompressed_too_large_error(encoding: &str, max: usize) -> ErrorMessage {
201 ErrorMessage::new(
202 StatusCode::PAYLOAD_TOO_LARGE,
203 format!("Decompressed {encoding} body exceeds limit of {max} bytes."),
204 )
205}
206
207pub fn emit_decompress_error(
218 encoding: &str,
219 error: std::io::Error,
220 max_decompressed_size: usize,
221) -> ErrorMessage {
222 if is_decompressed_size_limit_error(&error) {
223 return decompressed_too_large_error(encoding, max_decompressed_size);
224 }
225 emit!(HttpDecompressError {
226 encoding,
227 error: &error
228 });
229 ErrorMessage::new(
230 StatusCode::UNPROCESSABLE_ENTITY,
231 format!("Failed decompressing payload with {encoding} decoder."),
232 )
233}
234
235#[cfg(test)]
236mod tests {
237 use std::io::Write;
238
239 use flate2::{Compression, write::GzEncoder};
240 use futures_util::stream;
241 use zstd::stream::Encoder as ZstdEncoder;
242
243 use super::*;
244
245 fn gzip_payload(plaintext: &[u8]) -> Bytes {
246 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
247 encoder.write_all(plaintext).unwrap();
248 encoder.finish().unwrap().into()
249 }
250
251 fn zstd_payload_with_window_log(plaintext: &[u8], window_log: u32) -> Bytes {
252 let mut encoder = ZstdEncoder::new(Vec::new(), 0).unwrap();
253 encoder.window_log(window_log).unwrap();
254 encoder.write_all(plaintext).unwrap();
255 encoder.finish().unwrap().into()
256 }
257
258 #[test]
259 fn gzip_within_limit_succeeds() {
260 let plaintext = vec![0u8; 10_000];
261 let body = gzip_payload(&plaintext);
262
263 let decoded = decompress_body_with_limit(Some("gzip"), body, 100_000).unwrap();
264 assert_eq!(decoded.len(), plaintext.len());
265 }
266
267 #[test]
268 fn gzip_exceeding_limit_returns_413() {
269 let plaintext = vec![0u8; 1_000_000];
271 let body = gzip_payload(&plaintext);
272
273 let err = decompress_body_with_limit(Some("gzip"), body, 1024).expect_err("should reject");
274 assert_eq!(err.status_code(), StatusCode::PAYLOAD_TOO_LARGE);
275 }
276
277 #[test]
278 fn snappy_exceeding_limit_returns_413_before_allocating() {
279 let plaintext = vec![0u8; 2 * 1024 * 1024];
281 let compressed = snap::raw::Encoder::new().compress_vec(&plaintext).unwrap();
282
283 let err = decompress_body_with_limit(Some("snappy"), compressed.into(), 1024)
284 .expect_err("should reject");
285 assert_eq!(err.status_code(), StatusCode::PAYLOAD_TOO_LARGE);
286 }
287
288 #[test]
289 fn zstd_exceeding_limit_returns_413() {
290 let plaintext = vec![0u8; 10_000];
291 let compressed = zstd_payload_with_window_log(plaintext.as_slice(), 10);
292
293 let err =
294 decompress_body_with_limit(Some("zstd"), compressed, 1024).expect_err("should reject");
295 assert_eq!(err.status_code(), StatusCode::PAYLOAD_TOO_LARGE);
296 }
297
298 #[test]
299 fn identity_passes_through() {
300 let body: Bytes = Bytes::from_static(b"hello world");
301 let decoded = decompress_body(Some("identity"), body.clone()).unwrap();
302 assert_eq!(decoded, body);
303 }
304
305 #[test]
306 fn identity_exceeding_limit_returns_413() {
307 let body = Bytes::from_static(b"hello world");
308
309 let err = decompress_body_with_limit(Some("identity"), body, 5).expect_err("should reject");
310 assert_eq!(err.status_code(), StatusCode::PAYLOAD_TOO_LARGE);
311 }
312
313 #[test]
314 fn missing_content_encoding_exceeding_limit_returns_413() {
315 let body = Bytes::from_static(b"hello world");
316
317 let err = decompress_body_with_limit(None, body, 5).expect_err("should reject");
318 assert_eq!(err.status_code(), StatusCode::PAYLOAD_TOO_LARGE);
319 }
320
321 #[test]
322 fn zstd_window_log_tracks_limit() {
323 use crate::sources::util::decompression::zstd_window_log_max;
324 assert_eq!(zstd_window_log_max(0), Some(10));
328 assert_eq!(zstd_window_log_max(1), Some(10));
329 assert_eq!(zstd_window_log_max(1024), Some(10));
330 assert_eq!(zstd_window_log_max(1025), Some(11));
331 assert_eq!(
332 zstd_window_log_max(DEFAULT_MAX_DECOMPRESSED_SIZE_BYTES),
333 Some(27)
334 );
335 }
336
337 #[test]
338 fn http_zstd_window_log_clamps_to_rfc9659_ceiling() {
339 use crate::sources::util::decompression::http_zstd_window_log_max;
340 assert_eq!(http_zstd_window_log_max(1024), Some(10));
342 assert_eq!(http_zstd_window_log_max(8 * 1024 * 1024), Some(23));
343 assert_eq!(http_zstd_window_log_max(16 * 1024 * 1024), Some(23));
345 assert_eq!(
346 http_zstd_window_log_max(DEFAULT_MAX_DECOMPRESSED_SIZE_BYTES),
347 Some(23)
348 );
349 }
350
351 #[tokio::test]
352 async fn collect_body_with_limit_succeeds_within_limit() {
353 let body = stream::iter([
354 Ok::<_, warp::Error>(Bytes::from_static(b"hello")),
355 Ok::<_, warp::Error>(Bytes::from_static(b" world")),
356 ]);
357
358 let collected = collect_body_with_limit(body, 11).await.unwrap();
359 assert_eq!(collected, Bytes::from_static(b"hello world"));
360 }
361
362 #[tokio::test]
363 async fn collect_body_with_limit_rejects_oversized_stream() {
364 let body = stream::iter([
365 Ok::<_, warp::Error>(Bytes::from_static(b"hello")),
366 Ok::<_, warp::Error>(Bytes::from_static(b" world")),
367 ]);
368
369 let err = collect_body_with_limit(body, 5)
370 .await
371 .expect_err("should reject");
372 assert_eq!(err.status_code(), StatusCode::PAYLOAD_TOO_LARGE);
373 }
374}