Support padding of binary http request and response messages. PiperOrigin-RevId: 486635936
diff --git a/quiche/binary_http/binary_http_message.cc b/quiche/binary_http/binary_http_message.cc index 24feedf..6d333ac 100644 --- a/quiche/binary_http/binary_http_message.cc +++ b/quiche/binary_http/binary_http_message.cc
@@ -33,6 +33,11 @@ return true; } +bool IsValidPadding(absl::string_view data) { + return std::all_of(data.begin(), data.end(), + [](char c) { return c == '\0'; }); +} + absl::StatusOr<BinaryHttpRequest::ControlData> DecodeControlData( quiche::QuicheDataReader& reader) { BinaryHttpRequest::ControlData control_data; @@ -107,6 +112,10 @@ !status.ok()) { return status; } + if (!IsValidPadding(reader.PeekRemainingPayload())) { + return absl::InvalidArgumentError("Non-zero padding."); + } + request.set_num_padding_bytes(reader.BytesRemaining()); return request; } @@ -148,13 +157,16 @@ !status.ok()) { return status; } + if (!IsValidPadding(reader.PeekRemainingPayload())) { + return absl::InvalidArgumentError("Non-zero padding."); + } + response.set_num_padding_bytes(reader.BytesRemaining()); return response; } uint64_t StringPieceVarInt62Len(absl::string_view s) { return quiche::QuicheDataWriter::GetVarInt62Len(s.length()) + s.length(); } - } // namespace void BinaryHttpMessage::Fields::AddField(BinaryHttpMessage::Field field) { @@ -179,13 +191,13 @@ return absl::OkStatus(); } -uint64_t BinaryHttpMessage::Fields::EncodedSize() const { - uint64_t size = EncodedFieldsSize(); +size_t BinaryHttpMessage::Fields::EncodedSize() const { + const size_t size = EncodedFieldsSize(); return size + quiche::QuicheDataWriter::GetVarInt62Len(size); } -uint64_t BinaryHttpMessage::Fields::EncodedFieldsSize() const { - uint64_t size = 0; +size_t BinaryHttpMessage::Fields::EncodedFieldsSize() const { + size_t size = 0; for (const BinaryHttpMessage::Field& field : fields_) { size += StringPieceVarInt62Len(field.name) + StringPieceVarInt62Len(field.value); @@ -217,7 +229,7 @@ return absl::OkStatus(); } -uint64_t BinaryHttpMessage::EncodedKnownLengthFieldsAndBodySize() const { +size_t BinaryHttpMessage::EncodedKnownLengthFieldsAndBodySize() const { return header_fields_.EncodedSize() + StringPieceVarInt62Len(body_); } @@ -263,17 +275,18 @@ !status.ok()) { return status; } - QUICHE_DCHECK_EQ(writer.remaining(), 0u); + QUICHE_DCHECK_EQ(writer.remaining(), num_padding_bytes()); + writer.WritePadding(); return data; } -uint64_t BinaryHttpResponse::EncodedSize() const { - uint64_t size = sizeof(kKnownLengthResponseFraming); +size_t BinaryHttpResponse::EncodedSize() const { + size_t size = sizeof(kKnownLengthResponseFraming); for (const auto& informational : informational_response_control_data_) { size += informational.EncodedSize(); } return size + quiche::QuicheDataWriter::GetVarInt62Len(status_code_) + - EncodedKnownLengthFieldsAndBodySize(); + EncodedKnownLengthFieldsAndBodySize() + num_padding_bytes(); } void BinaryHttpResponse::InformationalResponse::AddField(absl::string_view name, @@ -288,7 +301,7 @@ return fields_.Encode(writer); } -uint64_t BinaryHttpResponse::InformationalResponse::EncodedSize() const { +size_t BinaryHttpResponse::InformationalResponse::EncodedSize() const { return quiche::QuicheDataWriter::GetVarInt62Len(status_code_) + fields_.EncodedSize(); } @@ -325,10 +338,10 @@ return absl::OkStatus(); } -uint64_t BinaryHttpRequest::EncodedControlDataSize() const { - uint64_t size = StringPieceVarInt62Len(control_data_.method) + - StringPieceVarInt62Len(control_data_.scheme) + - StringPieceVarInt62Len(control_data_.path); +size_t BinaryHttpRequest::EncodedControlDataSize() const { + size_t size = StringPieceVarInt62Len(control_data_.method) + + StringPieceVarInt62Len(control_data_.scheme) + + StringPieceVarInt62Len(control_data_.path); if (!has_host()) { size += StringPieceVarInt62Len(control_data_.authority); } else { @@ -337,9 +350,9 @@ return size; } -uint64_t BinaryHttpRequest::EncodedSize() const { +size_t BinaryHttpRequest::EncodedSize() const { return sizeof(kKnownLengthRequestFraming) + EncodedControlDataSize() + - EncodedKnownLengthFieldsAndBodySize(); + EncodedKnownLengthFieldsAndBodySize() + num_padding_bytes(); } // https://www.ietf.org/archive/id/draft-ietf-httpbis-binary-message-06.html#name-known-length-messages @@ -357,7 +370,8 @@ !status.ok()) { return status; } - QUICHE_DCHECK_EQ(writer.remaining(), 0u); + QUICHE_DCHECK_EQ(writer.remaining(), num_padding_bytes()); + writer.WritePadding(); return data; }
diff --git a/quiche/binary_http/binary_http_message.h b/quiche/binary_http/binary_http_message.h index 70b8106..8f1f8f8 100644 --- a/quiche/binary_http/binary_http_message.h +++ b/quiche/binary_http/binary_http_message.h
@@ -1,6 +1,7 @@ #ifndef QUICHE_BINARY_HTTP_BINARY_HTTP_MESSAGE_H_ #define QUICHE_BINARY_HTTP_BINARY_HTTP_MESSAGE_H_ +#include <cstddef> #include <cstdint> #include <functional> #include <memory> @@ -51,9 +52,16 @@ } void swap_body(std::string& body) { body_.swap(body); } + void set_num_padding_bytes(size_t num_padding_bytes) { + num_padding_bytes_ = num_padding_bytes; + } + size_t num_padding_bytes() const { return num_padding_bytes_; } absl::string_view body() const { return body_; } + // Returns the number of bytes `Serialize` will return, including padding. + virtual size_t EncodedSize() const = 0; + // Returns the Binary Http formatted message. virtual absl::StatusOr<std::string> Serialize() const = 0; // TODO(bschneider): Add AddTrailerField for chunked messages @@ -81,17 +89,18 @@ // The number of returned by EncodedFieldsSize // plus the number of bytes used in the varint holding that value. - uint64_t EncodedSize() const; + size_t EncodedSize() const; private: // Number of bytes of just the set of fields. - uint64_t EncodedFieldsSize() const; + size_t EncodedFieldsSize() const; // Fields in insertion order. std::vector<BinaryHttpMessage::Field> fields_; }; - bool operator==(const BinaryHttpMessage& rhs) const { + // Checks equality excluding padding. + bool IsPayloadEqual(const BinaryHttpMessage& rhs) const { // `has_host_` is derived from `header_fields_` so it doesn't need to be // tested directly. return body_ == rhs.body_ && header_fields_ == rhs.header_fields_; @@ -99,13 +108,14 @@ absl::Status EncodeKnownLengthFieldsAndBody( quiche::QuicheDataWriter& writer) const; - uint64_t EncodedKnownLengthFieldsAndBodySize() const; + size_t EncodedKnownLengthFieldsAndBodySize() const; bool has_host() const { return has_host_; } private: std::string body_; Fields header_fields_; bool has_host_ = false; + size_t num_padding_bytes_ = 0; }; void QUICHE_EXPORT PrintTo(const BinaryHttpMessage::Field& msg, @@ -113,7 +123,10 @@ class QUICHE_EXPORT BinaryHttpRequest : public BinaryHttpMessage { public: - // HTTP request must have all of the following fields. + // HTTP request must have method, scheme, and path fields. + // The `authority` field is required unless a `host` header field is added. + // If a `host` header field is added, `authority` is serialized as the empty + // string. // Some examples are: // scheme: HTTP // authority: www.example.com @@ -137,14 +150,21 @@ // Deserialize static absl::StatusOr<BinaryHttpRequest> Create(absl::string_view data); + size_t EncodedSize() const override; absl::StatusOr<std::string> Serialize() const override; const ControlData& control_data() const { return control_data_; } virtual std::string DebugString() const override; - bool operator==(const BinaryHttpRequest& rhs) const { + // Returns true if the contents of the requests are equal, excluding padding. + bool IsPayloadEqual(const BinaryHttpRequest& rhs) const { return control_data_ == rhs.control_data_ && - BinaryHttpMessage::operator==(rhs); + BinaryHttpMessage::IsPayloadEqual(rhs); + } + + bool operator==(const BinaryHttpRequest& rhs) const { + return IsPayloadEqual(rhs) && + num_padding_bytes() == rhs.num_padding_bytes(); } bool operator!=(const BinaryHttpRequest& rhs) const { @@ -154,9 +174,7 @@ private: absl::Status EncodeControlData(quiche::QuicheDataWriter& writer) const; - uint64_t EncodedControlDataSize() const; - - uint64_t EncodedSize() const; + size_t EncodedControlDataSize() const; // Returns Binary Http known length request formatted request. absl::StatusOr<std::string> EncodeAsKnownLength() const; @@ -210,7 +228,7 @@ // Give BinaryHttpResponse access to Encoding functionality. friend class BinaryHttpResponse; - uint64_t EncodedSize() const; + size_t EncodedSize() const; // Appends the encoded fields and body to `writer`. absl::Status Encode(quiche::QuicheDataWriter& writer) const; @@ -225,6 +243,7 @@ // Deserialize static absl::StatusOr<BinaryHttpResponse> Create(absl::string_view data); + size_t EncodedSize() const override; absl::StatusOr<std::string> Serialize() const override; // Informational status codes must be between 100 and 199 inclusive. @@ -241,12 +260,19 @@ virtual std::string DebugString() const override; - bool operator==(const BinaryHttpResponse& rhs) const { + // Returns true if the contents of the requests are equal, excluding padding. + bool IsPayloadEqual(const BinaryHttpResponse& rhs) const { return informational_response_control_data_ == rhs.informational_response_control_data_ && status_code_ == rhs.status_code_ && - BinaryHttpMessage::operator==(rhs); + BinaryHttpMessage::IsPayloadEqual(rhs); } + + bool operator==(const BinaryHttpResponse& rhs) const { + return IsPayloadEqual(rhs) && + num_padding_bytes() == rhs.num_padding_bytes(); + } + bool operator!=(const BinaryHttpResponse& rhs) const { return !(*this == rhs); } @@ -255,8 +281,6 @@ // Returns Binary Http known length request formatted response. absl::StatusOr<std::string> EncodeAsKnownLength() const; - uint64_t EncodedSize() const; - std::vector<InformationalResponse> informational_response_control_data_; const uint16_t status_code_; };
diff --git a/quiche/binary_http/binary_http_message_test.cc b/quiche/binary_http/binary_http_message_test.cc index 1ecd9f9..df8c036 100644 --- a/quiche/binary_http/binary_http_message_test.cc +++ b/quiche/binary_http/binary_http_message_test.cc
@@ -707,4 +707,80 @@ EXPECT_NE(response, no_informational); } +MATCHER_P(HasEqPayload, value, "Payloads of messages are equivalent.") { + return arg.IsPayloadEqual(value); +} + +template <typename T> +void TestPadding(T& message) { + const auto data_so = message.Serialize(); + ASSERT_TRUE(data_so.ok()); + auto data = *data_so; + ASSERT_EQ(data.size(), message.EncodedSize()); + + message.set_num_padding_bytes(10); + const auto padded_data_so = message.Serialize(); + ASSERT_TRUE(padded_data_so.ok()); + const auto padded_data = *padded_data_so; + ASSERT_EQ(padded_data.size(), message.EncodedSize()); + + // Check padding size output. + ASSERT_EQ(data.size() + 10, padded_data.size()); + // Check for valid null byte padding output + data.resize(data.size() + 10); + ASSERT_EQ(data, padded_data); + + // Deserialize padded and not padded, and verify they are the same. + const auto deserialized_padded_message_so = T::Create(data); + ASSERT_TRUE(deserialized_padded_message_so.ok()); + const auto deserialized_padded_message = *deserialized_padded_message_so; + ASSERT_EQ(deserialized_padded_message, message); + ASSERT_EQ(deserialized_padded_message.num_padding_bytes(), size_t(10)); + + // Invalid padding + data[data.size() - 1] = 'a'; + const auto bad_so = T::Create(data); + ASSERT_FALSE(bad_so.ok()); + + // Check that padding does not impact equality. + data.resize(data.size() - 10); + const auto deserialized_message_so = T::Create(data); + ASSERT_TRUE(deserialized_message_so.ok()); + const auto deserialized_message = *deserialized_message_so; + ASSERT_EQ(deserialized_message.num_padding_bytes(), size_t(0)); + // Confirm that the message payloads are equal, but not fully equivalent due + // to padding. + ASSERT_THAT(deserialized_message, HasEqPayload(deserialized_padded_message)); + ASSERT_NE(deserialized_message, deserialized_padded_message); +} + +TEST(BinaryHttpRequest, Padding) { + /* + GET /hello.txt HTTP/1.1 + User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3 + Host: www.example.com + Accept-Language: en, mi + */ + BinaryHttpRequest request({"GET", "https", "", "/hello.txt"}); + request + .AddHeaderField({"User-Agent", + "curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3"}) + ->AddHeaderField({"Host", "www.example.com"}) + ->AddHeaderField({"Accept-Language", "en, mi"}); + TestPadding(request); +} + +TEST(BinaryHttpResponse, Padding) { + /* + HTTP/1.1 200 OK + Server: Apache + + Hello, world! + */ + BinaryHttpResponse response(200); + response.AddHeaderField({"Server", "Apache"}); + response.set_body("Hello, world!\r\n"); + TestPadding(response); +} + } // namespace quiche