Implementing Indeterminate-Length Binary HTTP decoding [RFC 9292](https://www.rfc-editor.org/rfc/rfc9292.html) Protected by tests, this is new code not used by GFE. PiperOrigin-RevId: 778634399
diff --git a/quiche/binary_http/binary_http_message.cc b/quiche/binary_http/binary_http_message.cc index 815b30e..bc7a9b0 100644 --- a/quiche/binary_http/binary_http_message.cc +++ b/quiche/binary_http/binary_http_message.cc
@@ -10,11 +10,13 @@ #include <utility> #include <vector> +#include "absl/base/attributes.h" #include "absl/container/flat_hash_map.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/ascii.h" #include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" #include "absl/strings/str_join.h" #include "absl/strings/string_view.h" #include "quiche/common/quiche_callbacks.h" @@ -26,6 +28,15 @@ constexpr uint64_t kKnownLengthRequestFraming = 0; constexpr uint64_t kKnownLengthResponseFraming = 1; +constexpr uint64_t kIndeterminateLengthRequestFraming = 2; +constexpr uint64_t kContentTerminator = 0; + +// A view of a field name and value. Used to pass around a field without owning +// or copying the backing data. +struct FieldView { + absl::string_view name; + absl::string_view value; +}; bool ReadStringValue(quiche::QuicheDataReader& reader, std::string& data) { absl::string_view data_view; @@ -59,6 +70,21 @@ return control_data; } +// Decodes a header/trailer name and value. This takes a length which represents +// only the name length. +absl::StatusOr<FieldView> DecodeField(QuicheDataReader& reader, + uint64_t name_length) { + absl::string_view name; + if (!reader.ReadStringPiece(&name, name_length)) { + return absl::OutOfRangeError("Not enough data to read field name."); + } + absl::string_view value; + if (!reader.ReadStringPieceVarInt62(&value)) { + return absl::OutOfRangeError("Not enough data to read field value."); + } + return FieldView{name, value}; +} + absl::Status DecodeFields(quiche::QuicheDataReader& reader, quiche::UnretainedCallback<void( absl::string_view name, absl::string_view value)> @@ -402,6 +428,184 @@ absl::StrCat("Unsupported framing type ", framing)); } +absl::Status +BinaryHttpRequest::IndeterminateLengthDecoder::DecodeContentTerminatedSection( + QuicheDataReader& reader) { + uint64_t length_or_content_terminator; + do { + if (!reader.ReadVarInt62(&length_or_content_terminator)) { + return absl::OutOfRangeError("Not enough data to read section."); + } + if (length_or_content_terminator != kContentTerminator) { + switch (current_section_) { + case MessageSection::kHeader: { + const absl::StatusOr<FieldView> field = + DecodeField(reader, length_or_content_terminator); + if (!field.ok()) { + return field.status(); + } + message_section_handler_.OnHeader(field->name, field->value); + break; + } + case MessageSection::kBody: { + absl::string_view body_chunk; + if (!reader.ReadStringPiece(&body_chunk, + length_or_content_terminator)) { + return absl::OutOfRangeError("Failed to read body chunk."); + } + message_section_handler_.OnBodyChunk(body_chunk); + break; + } + case MessageSection::kTrailer: { + const absl::StatusOr<FieldView> field = + DecodeField(reader, length_or_content_terminator); + if (!field.ok()) { + return field.status(); + } + message_section_handler_.OnTrailer(field->name, field->value); + break; + } + default: + return absl::InternalError( + "Unexpected section in DecodeContentTerminatedSection."); + } + } + // Either a section was successfully decoded or a content terminator was + // encountered, save the checkpoint. + SaveCheckpoint(reader); + } while (length_or_content_terminator != kContentTerminator); + return absl::OkStatus(); +} + +// Returns Ok status only if the decoding processes the Padding section +// successfully or if the message is truncated properly. All other points of +// return are errors. +absl::Status +BinaryHttpRequest::IndeterminateLengthDecoder::DecodeCheckpointData( + bool end_stream) { + QuicheDataReader reader(checkpoint_view_); + switch (current_section_) { + case MessageSection::kEnd: + return absl::InternalError("Decoder is invalid."); + case MessageSection::kControlData: { + uint64_t framing; + if (!reader.ReadVarInt62(&framing)) { + return absl::OutOfRangeError("Failed to read framing."); + } + if (framing != kIndeterminateLengthRequestFraming) { + return absl::InvalidArgumentError( + absl::StrFormat("Unsupported framing type: 0x%02x", framing)); + } + + const absl::StatusOr<BinaryHttpRequest::ControlData> control_data = + DecodeControlData(reader); + // Only fails if there is not enough data to read the entire control data. + if (!control_data.ok()) { + return absl::OutOfRangeError("Failed to read control data."); + } + + message_section_handler_.OnControlData(control_data.value()); + SaveCheckpoint(reader); + current_section_ = MessageSection::kHeader; + } + ABSL_FALLTHROUGH_INTENDED; + case MessageSection::kHeader: { + const absl::Status status = DecodeContentTerminatedSection(reader); + if (!status.ok()) { + return status; + } + message_section_handler_.OnHeadersDone(); + current_section_ = MessageSection::kBody; + } + ABSL_FALLTHROUGH_INTENDED; + case MessageSection::kBody: { + if (!reader.IsDoneReading()) { + maybe_truncated_ = false; + } + // Body and trailers truncation is valid only if: + // 1. There is no data to read after the headers section. + // 2. This is signaled as the last piece of data (end_stream). + if (maybe_truncated_ && end_stream) { + message_section_handler_.OnBodyChunksDone(); + message_section_handler_.OnTrailersDone(); + return absl::OkStatus(); + } + + const absl::Status status = DecodeContentTerminatedSection(reader); + if (!status.ok()) { + return status; + } + message_section_handler_.OnBodyChunksDone(); + current_section_ = MessageSection::kTrailer; + // Reset the truncation flag before entering the trailers section. + maybe_truncated_ = true; + } + ABSL_FALLTHROUGH_INTENDED; + case MessageSection::kTrailer: { + if (!reader.IsDoneReading()) { + maybe_truncated_ = false; + } + // Trailers truncation is valid only if: + // 1. There is no data to read after the body section. + // 2. This is signaled as the last piece of data (end_stream). + if (maybe_truncated_ && end_stream) { + message_section_handler_.OnTrailersDone(); + return absl::OkStatus(); + } + + const absl::Status status = DecodeContentTerminatedSection(reader); + if (!status.ok()) { + return status; + } + message_section_handler_.OnTrailersDone(); + current_section_ = MessageSection::kPadding; + } + ABSL_FALLTHROUGH_INTENDED; + case MessageSection::kPadding: { + if (!IsValidPadding(reader.PeekRemainingPayload())) { + return absl::InvalidArgumentError("Non-zero padding."); + } + return absl::OkStatus(); + } + } +} + +void BinaryHttpRequest::IndeterminateLengthDecoder::InitializeCheckpoint( + absl::string_view data) { + checkpoint_view_ = data; + // Prepend buffered data if present. This is the data from a previous call to + // Decode that could not finish because it needed this new data. + if (!buffer_.empty()) { + absl::StrAppend(&buffer_, data); + checkpoint_view_ = buffer_; + } +} + +absl::Status BinaryHttpRequest::IndeterminateLengthDecoder::Decode( + absl::string_view data, bool end_stream) { + if (current_section_ == MessageSection::kEnd) { + return absl::InternalError("Decoder is invalid."); + } + + InitializeCheckpoint(data); + absl::Status status = DecodeCheckpointData(end_stream); + if (end_stream) { + current_section_ = MessageSection::kEnd; + buffer_.clear(); + return status; + } + if (absl::IsOutOfRange(status)) { + BufferCheckpoint(); + return absl::OkStatus(); + } + if (!status.ok()) { + current_section_ = MessageSection::kEnd; + } + + buffer_.clear(); + return status; +} + absl::StatusOr<BinaryHttpResponse> BinaryHttpResponse::Create( absl::string_view data) { quiche::QuicheDataReader reader(data);
diff --git a/quiche/binary_http/binary_http_message.h b/quiche/binary_http/binary_http_message.h index 8f1f8f8..85fb1dd 100644 --- a/quiche/binary_http/binary_http_message.h +++ b/quiche/binary_http/binary_http_message.h
@@ -11,9 +11,11 @@ #include <vector> #include "absl/container/flat_hash_map.h" +#include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/string_view.h" #include "quiche/common/platform/api/quiche_export.h" +#include "quiche/common/quiche_data_reader.h" #include "quiche/common/quiche_data_writer.h" namespace quiche { @@ -171,6 +173,91 @@ return !(*this == rhs); } + // Provides a Decode method that can be called multiple times as data is + // received. The relevant MessageSectionHandler method will be called when + // its corresponding section is successfully decoded. + class IndeterminateLengthDecoder { + public: + // The handler to invoke when a section is decoded successfully. + class MessageSectionHandler { + public: + virtual ~MessageSectionHandler() = default; + virtual void OnControlData(const ControlData& control_data) = 0; + virtual void OnHeader(absl::string_view name, + absl::string_view value) = 0; + virtual void OnHeadersDone() = 0; + virtual void OnBodyChunk(absl::string_view body_chunk) = 0; + virtual void OnBodyChunksDone() = 0; + virtual void OnTrailer(absl::string_view name, + absl::string_view value) = 0; + virtual void OnTrailersDone() = 0; + }; + + explicit IndeterminateLengthDecoder( + MessageSectionHandler& message_section_handler) + : message_section_handler_(message_section_handler) {} + + // Decodes an Indeterminate-Length BHTTP request. As the caller receives + // portions of the request, the caller can call this method with the request + // portion. The class keeps track of the current message section that is + // being decoded and buffers data if the section is not fully decoded so + // that the next call can continue decoding from where it left off. It will + // also invoke the appropriate MessageSectionHandler method when a section + // is decoded successfully. + // `end_stream` indicates that no more data will be provided to the decoder. + // This is used to determine if a valid message was decoded properly given + // the last piece of data provided, handling both complete messages and + // truncated messages. + absl::Status Decode(absl::string_view data, bool end_stream); + + private: + // The sections of an Indeterminate-Length BHTTP request. + enum class MessageSection { + kControlData, + kHeader, + kBody, + kTrailer, + kPadding, + // The decoder is set to end after end_stream is received or when an error + // occurs while decoding. + kEnd, + }; + + // Initializes the checkpoint with the provided data and any buffered data. + void InitializeCheckpoint(absl::string_view data); + // Carries out the decode logic from the checkpoint. Returns + // OutOfRangeError if there is not enough data to decode the current + // section. When a section is fully decoded, the checkpoint is updated. + absl::Status DecodeCheckpointData(bool end_stream); + // Saves the checkpoint based on the current position of the reader. + void SaveCheckpoint(const QuicheDataReader& reader) { + checkpoint_view_ = reader.PeekRemainingPayload(); + } + // Buffers the checkpoint. + void BufferCheckpoint() { buffer_ = std::string(checkpoint_view_); } + // Decodes a section 0 or more times until a content terminator is + // encountered. + absl::Status DecodeContentTerminatedSection(QuicheDataReader& reader); + + MessageSectionHandler& message_section_handler_; + // Stores the data that could not be processed due to missing data. + std::string buffer_; + // Tracks the remaining data to be processed or buffered. + // When decoding fails due to missing data, we buffer based on this + // checkpoint and return. When decoding succeeds, we update the checkpoint + // to not buffer the already processed data. + absl::string_view checkpoint_view_; + // The current section that is being decoded. + MessageSection current_section_ = MessageSection::kControlData; + // Upon initial entry of the body or trailer section, the message is assumed + // to be truncated. This will be set to `false` upon the detection of data, + // and the state remains consistent for the remainder of the section. This + // serves to differentiate between true truncation and an `end_stream` + // occurring after partial processing of the section's content but before + // its content terminator. + bool maybe_truncated_ = true; + }; + private: absl::Status EncodeControlData(quiche::QuicheDataWriter& writer) const;
diff --git a/quiche/binary_http/binary_http_message_test.cc b/quiche/binary_http/binary_http_message_test.cc index 0ae8e88..0a6e3d8 100644 --- a/quiche/binary_http/binary_http_message_test.cc +++ b/quiche/binary_http/binary_http_message_test.cc
@@ -2,14 +2,19 @@ #include <cstdint> #include <memory> +#include <optional> #include <sstream> #include <string> +#include <utility> #include <vector> #include "absl/container/flat_hash_map.h" +#include "absl/status/status.h" #include "absl/strings/escaping.h" +#include "absl/strings/str_cat.h" #include "absl/strings/string_view.h" #include "quiche/common/platform/api/quiche_test.h" +#include "quiche/common/test_tools/quiche_test_utils.h" using ::testing::ContainerEq; using ::testing::FieldsAre; @@ -30,6 +35,81 @@ PrintTo(resp, &os); EXPECT_EQ(os.str(), resp.DebugString()); } + +class RequestMessageSectionTestHandler + : public BinaryHttpRequest::IndeterminateLengthDecoder:: + MessageSectionHandler { + public: + struct MessageData { + std::optional<BinaryHttpRequest::ControlData> control_data_; + std::vector<std::pair<std::string, std::string>> headers_; + bool headers_done_ = false; + std::vector<std::string> body_chunks_; + bool body_chunks_done_ = false; + std::vector<std::pair<std::string, std::string>> trailers_; + bool trailers_done_ = false; + }; + RequestMessageSectionTestHandler() = default; + void OnControlData( + const BinaryHttpRequest::ControlData& control_data) override { + EXPECT_FALSE(message_data_.control_data_.has_value()); + message_data_.control_data_ = control_data; + } + void OnHeader(absl::string_view name, absl::string_view value) override { + EXPECT_FALSE(message_data_.headers_done_); + message_data_.headers_.push_back({std::string(name), std::string(value)}); + } + void OnHeadersDone() override { + EXPECT_FALSE(message_data_.headers_done_); + message_data_.headers_done_ = true; + } + void OnBodyChunk(absl::string_view body_chunk) override { + EXPECT_FALSE(message_data_.body_chunks_done_); + message_data_.body_chunks_.push_back(std::string(body_chunk)); + } + void OnBodyChunksDone() override { + EXPECT_FALSE(message_data_.body_chunks_done_); + message_data_.body_chunks_done_ = true; + } + void OnTrailer(absl::string_view name, absl::string_view value) override { + EXPECT_FALSE(message_data_.trailers_done_); + message_data_.trailers_.push_back({std::string(name), std::string(value)}); + } + void OnTrailersDone() override { + EXPECT_FALSE(message_data_.trailers_done_); + message_data_.trailers_done_ = true; + } + MessageData& GetMessageData() { return message_data_; } + + private: + MessageData message_data_; +}; + +constexpr absl::string_view kIndeterminateLengthEncodedRequestHeaders = + "4002" // 2-byte framing indicator + "04504F5354" // :method = POST + "056874747073" // :scheme = https + "0A676F6F676C652E636F6D" // :authority = "google.com" + "062F68656C6C6F" // :path = /hello + "0A757365722D6167656E74" // user-agent + "346375726C2F372E31362E33206C69626375726C2F372E31362E33204F70656E53534C2F" + "302E392E376C207A6C69622F312E322E33" // curl/7.16.3 libcurl/7.16.3 + // OpenSSL/0.9.7l zlib/1.2.3 + "0F6163636570742D6C616E6775616765" // accept-language + "06656E2C206D69" // en, mi + "C000000000000000"; // 8-byte content terminator +constexpr absl::string_view kIndeterminateLengthEncodedRequestBodyChunks = + "066368756E6B31" // chunk1 + "066368756E6B32" // chunk2 + "066368756E6B33" // chunk3 + "80000000"; // 4-byte content terminator +constexpr absl::string_view kIndeterminateLengthEncodedRequestTrailers = + "08747261696C657231" // trailer1 + "0676616C756531" // value1 + "08747261696C657232" // trailer2 + "0676616C756532" // value2 + "00" // 1-byte content terminator + "000000"; // padding } // namespace // Test examples from // https://www.ietf.org/archive/id/draft-ietf-httpbis-binary-message-06.html @@ -388,6 +468,219 @@ EXPECT_NE(request, no_body); } +void ExpectRequestMessageSectionHandler( + const RequestMessageSectionTestHandler::MessageData& message_data) { + EXPECT_TRUE(message_data.control_data_.has_value()); + if (message_data.control_data_.has_value()) { + EXPECT_THAT(*message_data.control_data_, + FieldsAre("POST", "https", "google.com", "/hello")); + } + std::vector<std::pair<std::string, std::string>> expected_headers = { + {"user-agent", "curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3"}, + {"accept-language", "en, mi"}}; + EXPECT_TRUE(message_data.headers_done_); + EXPECT_THAT(message_data.headers_, ContainerEq(expected_headers)); + std::vector<std::string> expected_body_chunks = {"chunk1", "chunk2", + "chunk3"}; + EXPECT_TRUE(message_data.body_chunks_done_); + EXPECT_THAT(message_data.body_chunks_, ContainerEq(expected_body_chunks)); + std::vector<std::pair<std::string, std::string>> expected_trailers = { + {"trailer1", "value1"}, {"trailer2", "value2"}}; + EXPECT_TRUE(message_data.trailers_done_); + EXPECT_THAT(message_data.trailers_, ContainerEq(expected_trailers)); +} + +TEST(IndeterminateLengthDecoder, FullRequestDecodingSuccess) { + std::string request_bytes; + EXPECT_TRUE(absl::HexStringToBytes( + absl::StrCat(kIndeterminateLengthEncodedRequestHeaders, + kIndeterminateLengthEncodedRequestBodyChunks, + kIndeterminateLengthEncodedRequestTrailers), + &request_bytes)); + RequestMessageSectionTestHandler handler; + BinaryHttpRequest::IndeterminateLengthDecoder decoder(handler); + QUICHE_EXPECT_OK(decoder.Decode(request_bytes, true)); + ExpectRequestMessageSectionHandler(handler.GetMessageData()); +} + +TEST(IndeterminateLengthDecoder, BufferedRequestDecodingSuccess) { + std::string request_bytes; + EXPECT_TRUE(absl::HexStringToBytes( + absl::StrCat(kIndeterminateLengthEncodedRequestHeaders, + kIndeterminateLengthEncodedRequestBodyChunks, + kIndeterminateLengthEncodedRequestTrailers), + &request_bytes)); + RequestMessageSectionTestHandler handler; + BinaryHttpRequest::IndeterminateLengthDecoder decoder(handler); + for (uint64_t i = 0; i < request_bytes.size() - 1; i++) { + QUICHE_EXPECT_OK( + decoder.Decode(absl::string_view(&request_bytes[i], 1), false)); + } + // Decode the last byte, send end_stream. + QUICHE_EXPECT_OK(decoder.Decode( + absl::string_view(&request_bytes[request_bytes.size() - 1], 1), true)); + ExpectRequestMessageSectionHandler(handler.GetMessageData()); +} + +TEST(IndeterminateLengthDecoder, InvalidFramingError) { + RequestMessageSectionTestHandler handler; + BinaryHttpRequest::IndeterminateLengthDecoder decoder(handler); + std::string request_bytes; + EXPECT_TRUE(absl::HexStringToBytes("00", &request_bytes)); + absl::Status status = decoder.Decode(request_bytes, false); + EXPECT_THAT(status, test::StatusIs(absl::StatusCode::kInvalidArgument)); +} + +TEST(IndeterminateLengthDecoder, InvalidPaddingError) { + RequestMessageSectionTestHandler handler; + BinaryHttpRequest::IndeterminateLengthDecoder decoder(handler); + std::string request_bytes; + EXPECT_TRUE(absl::HexStringToBytes( + absl::StrCat(absl::StrCat(kIndeterminateLengthEncodedRequestHeaders, + kIndeterminateLengthEncodedRequestBodyChunks, + kIndeterminateLengthEncodedRequestTrailers)), + &request_bytes)); + QUICHE_EXPECT_OK(decoder.Decode(request_bytes, false)); + absl::Status status = decoder.Decode("\x01", false); + EXPECT_THAT(status, test::StatusIs(absl::StatusCode::kInvalidArgument)); +} + +void ExpectTruncatedTrailerSection( + const RequestMessageSectionTestHandler::MessageData& message_data) { + EXPECT_TRUE(message_data.headers_done_); + EXPECT_TRUE(message_data.trailers_done_); + std::vector<std::pair<std::string, std::string>> expected_headers = { + {"user-agent", "curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3"}, + {"accept-language", "en, mi"}}; + EXPECT_THAT(message_data.headers_, ContainerEq(expected_headers)); + EXPECT_THAT(message_data.trailers_, testing::IsEmpty()); +} + +TEST(IndeterminateLengthDecoder, TruncatedBodyAndTrailers) { + RequestMessageSectionTestHandler handler; + BinaryHttpRequest::IndeterminateLengthDecoder decoder(handler); + std::string request_bytes; + EXPECT_TRUE(absl::HexStringToBytes(kIndeterminateLengthEncodedRequestHeaders, + &request_bytes)); + + QUICHE_EXPECT_OK(decoder.Decode(request_bytes, true)); + auto message_data = handler.GetMessageData(); + EXPECT_TRUE(message_data.body_chunks_done_); + EXPECT_THAT(message_data.body_chunks_, testing::IsEmpty()); + ExpectTruncatedTrailerSection(message_data); +} + +TEST(IndeterminateLengthDecoder, TruncatedBodyAndTrailersSplitEndStream) { + RequestMessageSectionTestHandler handler; + BinaryHttpRequest::IndeterminateLengthDecoder decoder(handler); + std::string request_bytes; + EXPECT_TRUE(absl::HexStringToBytes(kIndeterminateLengthEncodedRequestHeaders, + &request_bytes)); + + QUICHE_EXPECT_OK(decoder.Decode(request_bytes, false)); + // Send `end_stream` with no data. + QUICHE_EXPECT_OK(decoder.Decode("", true)); + auto message_data = handler.GetMessageData(); + EXPECT_TRUE(message_data.body_chunks_done_); + EXPECT_THAT(message_data.body_chunks_, testing::IsEmpty()); + ExpectTruncatedTrailerSection(message_data); +} + +TEST(IndeterminateLengthDecoder, TruncatedTrailers) { + RequestMessageSectionTestHandler handler; + BinaryHttpRequest::IndeterminateLengthDecoder decoder(handler); + std::string request_bytes; + EXPECT_TRUE(absl::HexStringToBytes( + absl::StrCat(kIndeterminateLengthEncodedRequestHeaders, + kIndeterminateLengthEncodedRequestBodyChunks), + &request_bytes)); + + QUICHE_EXPECT_OK(decoder.Decode(request_bytes, true)); + auto message_data = handler.GetMessageData(); + EXPECT_TRUE(message_data.body_chunks_done_); + std::vector<std::string> expected_body_chunks = {"chunk1", "chunk2", + "chunk3"}; + EXPECT_THAT(message_data.body_chunks_, ContainerEq(expected_body_chunks)); + ExpectTruncatedTrailerSection(message_data); +} + +TEST(IndeterminateLengthDecoder, TruncatedTrailersSplitEndStream) { + RequestMessageSectionTestHandler handler; + BinaryHttpRequest::IndeterminateLengthDecoder decoder(handler); + std::string request_bytes; + EXPECT_TRUE(absl::HexStringToBytes( + absl::StrCat(kIndeterminateLengthEncodedRequestHeaders, + kIndeterminateLengthEncodedRequestBodyChunks), + &request_bytes)); + + QUICHE_EXPECT_OK(decoder.Decode(request_bytes, false)); + // Send `end_stream` with no data. + QUICHE_EXPECT_OK(decoder.Decode("", true)); + auto message_data = handler.GetMessageData(); + EXPECT_TRUE(message_data.body_chunks_done_); + std::vector<std::string> expected_body_chunks = {"chunk1", "chunk2", + "chunk3"}; + EXPECT_THAT(message_data.body_chunks_, ContainerEq(expected_body_chunks)); + ExpectTruncatedTrailerSection(message_data); +} + +TEST(IndeterminateLengthDecoder, InvalidDecodeAfterEndStream) { + std::string request_bytes; + EXPECT_TRUE(absl::HexStringToBytes(kIndeterminateLengthEncodedRequestHeaders, + &request_bytes)); + RequestMessageSectionTestHandler handler; + BinaryHttpRequest::IndeterminateLengthDecoder decoder(handler); + QUICHE_EXPECT_OK(decoder.Decode(request_bytes, true)); + absl::Status status = decoder.Decode(request_bytes, false); + EXPECT_THAT(status, test::StatusIs(absl::StatusCode::kInternal)); +} + +struct InvalidEndStreamTestCase { + std::string name; + std::string request; +}; + +using InvalidEndStreamTest = + quiche::test::QuicheTestWithParam<InvalidEndStreamTestCase>; + +TEST_P(InvalidEndStreamTest, InvalidEndStreamError) { + const InvalidEndStreamTestCase& test_case = GetParam(); + RequestMessageSectionTestHandler handler; + BinaryHttpRequest::IndeterminateLengthDecoder decoder(handler); + std::string request_bytes; + EXPECT_TRUE(absl::HexStringToBytes(test_case.request, &request_bytes)); + absl::Status status = decoder.Decode(request_bytes, true); + EXPECT_THAT(status, test::StatusIs(absl::StatusCode::kOutOfRange)); +} + +INSTANTIATE_TEST_SUITE_P( + InvalidEndStreamTestInstantiation, InvalidEndStreamTest, + testing::ValuesIn<InvalidEndStreamTestCase>({ + { + "headers_not_terminated", + "02" // Indeterminate length request frame + "04504F5354" // :method = POST + "056874747073" // :scheme = https + "0A676F6F676C652E636F6D" // :authority = "google.com" + "062F68656C6C6F" // :path = /hello + "0A757365722D6167656E74" // user-agent + }, + {"body_not_terminated", + absl::StrCat(kIndeterminateLengthEncodedRequestHeaders, + "066368756E6B31" // chunk1 + )}, + {"trailers_not_terminated", + absl::StrCat(kIndeterminateLengthEncodedRequestHeaders, + kIndeterminateLengthEncodedRequestBodyChunks, + "08747261696C657231" // trailer1 + "0676616C756531" // value1 + "08747261696C657232" // trailer2 + )}, + }), + [](const testing::TestParamInfo<InvalidEndStreamTest::ParamType>& info) { + return info.param.name; + }); + TEST(BinaryHttpResponse, EncodeNoBody) { /* HTTP/1.1 404 Not Found