From 1dd761c87de226261599ff2518fe9d231ba1c82d Mon Sep 17 00:00:00 2001 From: Alex Rebert Date: Tue, 15 Dec 2020 17:23:07 -0500 Subject: [PATCH] fix(http1): ignore chunked trailers (#2357) Previously, hyper returned an "Invalid chunk end CR" error on chunked responses with trailers, as described in RFC 7230 Section 4.1.2. This commit adds code to ignore the trailers. Closes #2171 --- src/proto/h1/decode.rs | 41 ++++++++++++++++++++++++--- tests/client.rs | 63 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/src/proto/h1/decode.rs b/src/proto/h1/decode.rs index 15c934d1..ddca5f9d 100644 --- a/src/proto/h1/decode.rs +++ b/src/proto/h1/decode.rs @@ -55,6 +55,8 @@ enum ChunkedState { Body, BodyCr, BodyLf, + Trailer, + TrailerLf, EndCr, EndLf, End, @@ -196,6 +198,8 @@ impl ChunkedState { Body => ChunkedState::read_body(cx, body, size, buf), BodyCr => ChunkedState::read_body_cr(cx, body), BodyLf => ChunkedState::read_body_lf(cx, body), + Trailer => ChunkedState::read_trailer(cx, body), + TrailerLf => ChunkedState::read_trailer_lf(cx, body), EndCr => ChunkedState::read_end_cr(cx, body), EndLf => ChunkedState::read_end_lf(cx, body), End => Poll::Ready(Ok(ChunkedState::End)), @@ -340,16 +344,36 @@ impl ChunkedState { } } + fn read_trailer( + cx: &mut task::Context<'_>, + rdr: &mut R, + ) -> Poll> { + trace!("read_trailer"); + match byte!(rdr, cx) { + b'\r' => Poll::Ready(Ok(ChunkedState::TrailerLf)), + _ => Poll::Ready(Ok(ChunkedState::Trailer)), + } + } + fn read_trailer_lf( + cx: &mut task::Context<'_>, + rdr: &mut R, + ) -> Poll> { + match byte!(rdr, cx) { + b'\n' => Poll::Ready(Ok(ChunkedState::EndCr)), + _ => Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid trailer end LF", + ))), + } + } + fn read_end_cr( cx: &mut task::Context<'_>, rdr: &mut R, ) -> Poll> { match byte!(rdr, cx) { b'\r' => Poll::Ready(Ok(ChunkedState::EndLf)), - _ => Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk end CR", - ))), + _ => Poll::Ready(Ok(ChunkedState::Trailer)), } } fn read_end_lf( @@ -538,6 +562,15 @@ mod tests { assert_eq!("1234567890abcdef", &result); } + #[tokio::test] + async fn test_read_chunked_trailer_with_missing_lf() { + let mut mock_buf = &b"10\r\n1234567890abcdef\r\n0\r\nbad\r\r\n"[..]; + let mut decoder = Decoder::chunked(); + decoder.decode_fut(&mut mock_buf).await.expect("decode"); + let e = decoder.decode_fut(&mut mock_buf).await.unwrap_err(); + assert_eq!(e.kind(), io::ErrorKind::InvalidInput); + } + #[tokio::test] async fn test_read_chunked_after_eof() { let mut mock_buf = &b"10\r\n1234567890abcdef\r\n0\r\n\r\n"[..]; diff --git a/tests/client.rs b/tests/client.rs index 92ba534f..d5092e35 100644 --- a/tests/client.rs +++ b/tests/client.rs @@ -430,6 +430,69 @@ test! { body: None, } +test! { + name: client_get_req_body_chunked_with_trailer, + + server: + expected: "\ + GET / HTTP/1.1\r\n\ + host: {addr}\r\n\ + \r\n\ + ", + reply: "\ + HTTP/1.1 200 OK\r\n\ + Transfer-Encoding: chunked\r\n\ + \r\n\ + 5\r\n\ + hello\r\n\ + 0\r\n\ + Trailer: value\r\n\ + \r\n\ + ", + + client: + request: { + method: GET, + url: "http://{addr}/", + }, + response: + status: OK, + headers: {}, + body: &b"hello"[..], +} + +test! { + name: client_get_req_body_chunked_with_multiple_trailers, + + server: + expected: "\ + GET / HTTP/1.1\r\n\ + host: {addr}\r\n\ + \r\n\ + ", + reply: "\ + HTTP/1.1 200 OK\r\n\ + Transfer-Encoding: chunked\r\n\ + \r\n\ + 5\r\n\ + hello\r\n\ + 0\r\n\ + Trailer: value\r\n\ + another-trainer: another-value\r\n\ + \r\n\ + ", + + client: + request: { + method: GET, + url: "http://{addr}/", + }, + response: + status: OK, + headers: {}, + body: &b"hello"[..], +} + test! { name: client_get_req_body_sized,