Open source Binary Http. Currently unused in production. cl/470499594 tried to do this but needs source_file_patterns update, which this CL does. Startblock: has LGTM from everyone PiperOrigin-RevId: 471034502
diff --git a/build/source_list.bzl b/build/source_list.bzl index deb4866..55336fa 100644 --- a/build/source_list.bzl +++ b/build/source_list.bzl
@@ -1469,6 +1469,13 @@ "quic/load_balancer/load_balancer_server_id_map_test.cc", "quic/load_balancer/load_balancer_server_id_test.cc", ] +binary_http_hdrs = [ + "binary_http/binary_http_message.h", +] +binary_http_srcs = [ + "binary_http/binary_http_message.cc", + "binary_http/binary_http_message_test.cc", +] qbone_hdrs = [ "quic/qbone/bonnet/icmp_reachable.h", "quic/qbone/bonnet/icmp_reachable_interface.h",
diff --git a/build/source_list.gni b/build/source_list.gni index fbcb0c4..802b493 100644 --- a/build/source_list.gni +++ b/build/source_list.gni
@@ -1469,6 +1469,13 @@ "src/quiche/quic/load_balancer/load_balancer_server_id_map_test.cc", "src/quiche/quic/load_balancer/load_balancer_server_id_test.cc", ] +binary_http_hdrs = [ + "src/quiche/binary_http/binary_http_message.h", +] +binary_http_srcs = [ + "src/quiche/binary_http/binary_http_message.cc", + "src/quiche/binary_http/binary_http_message_test.cc", +] qbone_hdrs = [ "src/quiche/quic/qbone/bonnet/icmp_reachable.h", "src/quiche/quic/qbone/bonnet/icmp_reachable_interface.h",
diff --git a/build/source_list.json b/build/source_list.json index dffd3bd..ea2cf5e 100644 --- a/build/source_list.json +++ b/build/source_list.json
@@ -1468,6 +1468,13 @@ "quiche/quic/load_balancer/load_balancer_server_id_map_test.cc", "quiche/quic/load_balancer/load_balancer_server_id_test.cc" ], + "binary_http_hdrs": [ + "quiche/binary_http/binary_http_message.h" + ], + "binary_http_srcs": [ + "quiche/binary_http/binary_http_message.cc", + "quiche/binary_http/binary_http_message_test.cc" + ], "qbone_hdrs": [ "quiche/quic/qbone/bonnet/icmp_reachable.h", "quiche/quic/qbone/bonnet/icmp_reachable_interface.h",
diff --git a/quiche/binary_http/binary_http_message.cc b/quiche/binary_http/binary_http_message.cc new file mode 100644 index 0000000..360c56d --- /dev/null +++ b/quiche/binary_http/binary_http_message.cc
@@ -0,0 +1,402 @@ +#include "quiche/binary_http/binary_http_message.h" + +#include <cstdint> +#include <functional> +#include <iterator> +#include <memory> +#include <string> +#include <utility> +#include <vector> + +#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/string_view.h" +#include "quiche/common/quiche_data_reader.h" +#include "quiche/common/quiche_data_writer.h" + +namespace quiche { +namespace { + +constexpr uint8_t kKnownLengthRequestFraming = 0; +constexpr uint8_t kKnownLengthResponseFraming = 1; + +bool ReadStringValue(quiche::QuicheDataReader& reader, std::string& data) { + absl::string_view data_view; + if (!reader.ReadStringPieceVarInt62(&data_view)) { + return false; + } + data = std::string(data_view); + return true; +} + +absl::StatusOr<BinaryHttpRequest::ControlData> DecodeControlData( + quiche::QuicheDataReader& reader) { + BinaryHttpRequest::ControlData control_data; + if (!ReadStringValue(reader, control_data.method)) { + return absl::InvalidArgumentError("Failed to read method."); + } + if (!ReadStringValue(reader, control_data.scheme)) { + return absl::InvalidArgumentError("Failed to read scheme."); + } + if (!ReadStringValue(reader, control_data.authority)) { + return absl::InvalidArgumentError("Failed to read authority."); + } + if (!ReadStringValue(reader, control_data.path)) { + return absl::InvalidArgumentError("Failed to read path."); + } + return control_data; +} + +absl::Status DecodeFields( + quiche::QuicheDataReader& reader, + const std::function<void(absl::string_view name, absl::string_view value)>& + callback) { + absl::string_view fields; + if (!reader.ReadStringPieceVarInt62(&fields)) { + return absl::InvalidArgumentError("Failed to read fields."); + } + quiche::QuicheDataReader fields_reader(fields); + while (!fields_reader.IsDoneReading()) { + absl::string_view name; + if (!fields_reader.ReadStringPieceVarInt62(&name)) { + return absl::InvalidArgumentError("Failed to read field name."); + } + absl::string_view value; + if (!fields_reader.ReadStringPieceVarInt62(&value)) { + return absl::InvalidArgumentError("Failed to read field value."); + } + callback(name, value); + } + return absl::OkStatus(); +} + +absl::Status DecodeFieldsAndBody(quiche::QuicheDataReader& reader, + BinaryHttpMessage& message) { + if (const absl::Status status = DecodeFields( + reader, + [&message](absl::string_view name, absl::string_view value) { + message.AddHeaderField({std::string(name), std::string(value)}); + }); + !status.ok()) { + return status; + } + // TODO(bschneider): Handle case where remaining message is truncated. + // Skip it on encode as well. + // https://www.ietf.org/archive/id/draft-ietf-httpbis-binary-message-06.html#name-padding-and-truncation + absl::string_view body; + if (!reader.ReadStringPieceVarInt62(&body)) { + return absl::InvalidArgumentError("Failed to read body."); + } + message.set_body(std::string(body)); + // TODO(bschneider): Check for / read-in any trailer-fields + return absl::OkStatus(); +} + +absl::StatusOr<BinaryHttpRequest> DecodeKnownLengthRequest( + quiche::QuicheDataReader& reader) { + const auto control_data = DecodeControlData(reader); + if (!control_data.ok()) { + return control_data.status(); + } + BinaryHttpRequest request(std::move(*control_data)); + if (const absl::Status status = DecodeFieldsAndBody(reader, request); + !status.ok()) { + return status; + } + return request; +} + +absl::StatusOr<BinaryHttpResponse> DecodeKnownLengthResponse( + quiche::QuicheDataReader& reader) { + std::vector<std::pair<uint16_t, std::vector<BinaryHttpMessage::Field>>> + informational_responses; + uint64_t status_code; + bool reading_response_control_data = true; + while (reading_response_control_data) { + if (!reader.ReadVarInt62(&status_code)) { + return absl::InvalidArgumentError("Failed to read status code."); + } + if (status_code >= 100 && status_code <= 199) { + std::vector<BinaryHttpMessage::Field> fields; + if (const absl::Status status = DecodeFields( + reader, + [&fields](absl::string_view name, absl::string_view value) { + fields.push_back({std::string(name), std::string(value)}); + }); + !status.ok()) { + return status; + } + informational_responses.emplace_back(status_code, std::move(fields)); + } else { + reading_response_control_data = false; + } + } + BinaryHttpResponse response(status_code); + for (const auto& informational_response : informational_responses) { + if (const absl::Status status = response.AddInformationalResponse( + informational_response.first, + std::move(informational_response.second)); + !status.ok()) { + return status; + } + } + if (const absl::Status status = DecodeFieldsAndBody(reader, response); + !status.ok()) { + return status; + } + 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) { + fields_.push_back(std::move(field)); +} + +// Encode fields in the order they were initially inserted. +// Updates do not change order. +absl::Status BinaryHttpMessage::Fields::Encode( + quiche::QuicheDataWriter& writer) const { + if (!writer.WriteVarInt62(EncodedFieldsSize())) { + return absl::InvalidArgumentError("Failed to write encoded field size."); + } + for (const BinaryHttpMessage::Field& field : fields_) { + if (!writer.WriteStringPieceVarInt62(field.name)) { + return absl::InvalidArgumentError("Failed to write field name."); + } + if (!writer.WriteStringPieceVarInt62(field.value)) { + return absl::InvalidArgumentError("Failed to write field value."); + } + } + return absl::OkStatus(); +} + +uint64_t BinaryHttpMessage::Fields::EncodedSize() const { + uint64_t size = EncodedFieldsSize(); + return size + quiche::QuicheDataWriter::GetVarInt62Len(size); +} + +uint64_t BinaryHttpMessage::Fields::EncodedFieldsSize() const { + uint64_t size = 0; + for (const BinaryHttpMessage::Field& field : fields_) { + size += StringPieceVarInt62Len(field.name) + + StringPieceVarInt62Len(field.value); + } + return size; +} + +BinaryHttpMessage* BinaryHttpMessage::AddHeaderField( + BinaryHttpMessage::Field field) { + const std::string lower_name = absl::AsciiStrToLower(field.name); + if (lower_name == "host") { + has_host_ = true; + } + header_fields_.AddField({std::move(lower_name), std::move(field.value)}); + return this; +} + +// Appends the encoded fields and body to data. +absl::Status BinaryHttpMessage::EncodeKnownLengthFieldsAndBody( + quiche::QuicheDataWriter& writer) const { + if (const absl::Status status = header_fields_.Encode(writer); !status.ok()) { + return status; + } + if (!writer.WriteStringPieceVarInt62(body_)) { + return absl::InvalidArgumentError("Failed to encode body."); + } + // TODO(bschneider): Consider support for trailer fields on known-length + // requests. Trailers are atypical for a known-length request. + return absl::OkStatus(); +} + +uint64_t BinaryHttpMessage::EncodedKnownLengthFieldsAndBodySize() const { + return header_fields_.EncodedSize() + StringPieceVarInt62Len(body_); +} + +absl::Status BinaryHttpResponse::AddInformationalResponse( + uint16_t status_code, std::vector<Field> header_fields) { + if (status_code < 100) { + return absl::InvalidArgumentError("status code < 100"); + } + if (status_code > 199) { + return absl::InvalidArgumentError("status code > 199"); + } + InformationalResponseInternal data(status_code); + for (Field& header : header_fields) { + data.AddField(header.name, std::move(header.value)); + } + informational_response_control_data_.push_back(std::move(data)); + return absl::OkStatus(); +} + +absl::StatusOr<std::string> BinaryHttpResponse::Serialize() const { + // Only supporting known length requests so far. + return EncodeAsKnownLength(); +} + +std::vector<BinaryHttpResponse::InformationalResponse> +BinaryHttpResponse::GetInformationalResponse() const { + std::vector<InformationalResponse> data; + data.reserve(informational_response_control_data_.size()); + for (const auto& control_data : informational_response_control_data_) { + data.push_back({control_data.response_code(), control_data.fields()}); + } + return data; +} + +absl::StatusOr<std::string> BinaryHttpResponse::EncodeAsKnownLength() const { + std::string data; + data.resize(EncodedSize()); + quiche::QuicheDataWriter writer(data.size(), data.data()); + if (!writer.WriteUInt8(kKnownLengthResponseFraming)) { + return absl::InvalidArgumentError("Failed to write framing indicator"); + } + // Informational response + for (const auto& informational : informational_response_control_data_) { + if (const absl::Status status = informational.Encode(writer); + !status.ok()) { + return status; + } + } + if (!writer.WriteVarInt62(status_code_)) { + return absl::InvalidArgumentError("Failed to write status code"); + } + if (const absl::Status status = EncodeKnownLengthFieldsAndBody(writer); + !status.ok()) { + return status; + } + QUICHE_DCHECK_EQ(writer.remaining(), 0u); + return data; +} + +uint64_t BinaryHttpResponse::EncodedSize() const { + uint64_t size = sizeof(kKnownLengthResponseFraming); + for (const auto& informational : informational_response_control_data_) { + size += informational.EncodedSize(); + } + return size + quiche::QuicheDataWriter::GetVarInt62Len(status_code_) + + EncodedKnownLengthFieldsAndBodySize(); +} + +void BinaryHttpResponse::InformationalResponseInternal::AddField( + absl::string_view name, std::string value) { + fields_.AddField({absl::AsciiStrToLower(name), std::move(value)}); +} + +// Appends the encoded fields and body to data. +absl::Status BinaryHttpResponse::InformationalResponseInternal::Encode( + quiche::QuicheDataWriter& writer) const { + writer.WriteVarInt62(response_code_); + return fields_.Encode(writer); +} + +uint64_t BinaryHttpResponse::InformationalResponseInternal::EncodedSize() + const { + return quiche::QuicheDataWriter::GetVarInt62Len(response_code_) + + fields_.EncodedSize(); +} + +absl::StatusOr<std::string> BinaryHttpRequest::Serialize() const { + // Only supporting known length requests so far. + return EncodeAsKnownLength(); +} + +// https://www.ietf.org/archive/id/draft-ietf-httpbis-binary-message-06.html#name-request-control-data +absl::Status BinaryHttpRequest::EncodeControlData( + quiche::QuicheDataWriter& writer) const { + if (!writer.WriteStringPieceVarInt62(control_data_.method)) { + return absl::InvalidArgumentError("Failed to encode method."); + } + if (!writer.WriteStringPieceVarInt62(control_data_.scheme)) { + return absl::InvalidArgumentError("Failed to encode scheme."); + } + // the Host header field is not replicated in the :authority field, as is + // required for ensuring that the request is reproduced accurately; see + // Section 8.1.2.3 of [H2]. + if (!has_host()) { + if (!writer.WriteStringPieceVarInt62(control_data_.authority)) { + return absl::InvalidArgumentError("Failed to encode authority."); + } + } else { + if (!writer.WriteStringPieceVarInt62("")) { + return absl::InvalidArgumentError("Failed to encode authority."); + } + } + if (!writer.WriteStringPieceVarInt62(control_data_.path)) { + return absl::InvalidArgumentError("Failed to encode path."); + } + return absl::OkStatus(); +} + +uint64_t BinaryHttpRequest::EncodedControlDataSize() const { + uint64_t size = StringPieceVarInt62Len(control_data_.method) + + StringPieceVarInt62Len(control_data_.scheme) + + StringPieceVarInt62Len(control_data_.path); + if (!has_host()) { + size += StringPieceVarInt62Len(control_data_.authority); + } else { + size += StringPieceVarInt62Len(""); + } + return size; +} + +uint64_t BinaryHttpRequest::EncodedSize() const { + return sizeof(kKnownLengthRequestFraming) + EncodedControlDataSize() + + EncodedKnownLengthFieldsAndBodySize(); +} + +// https://www.ietf.org/archive/id/draft-ietf-httpbis-binary-message-06.html#name-known-length-messages +absl::StatusOr<std::string> BinaryHttpRequest::EncodeAsKnownLength() const { + std::string data; + data.resize(EncodedSize()); + quiche::QuicheDataWriter writer(data.size(), data.data()); + if (!writer.WriteUInt8(kKnownLengthRequestFraming)) { + return absl::InvalidArgumentError("Failed to encode framing indicator."); + } + if (const absl::Status status = EncodeControlData(writer); !status.ok()) { + return status; + } + if (const absl::Status status = EncodeKnownLengthFieldsAndBody(writer); + !status.ok()) { + return status; + } + QUICHE_DCHECK_EQ(writer.remaining(), 0u); + return data; +} + +absl::StatusOr<BinaryHttpRequest> BinaryHttpRequest::Create( + absl::string_view data) { + quiche::QuicheDataReader reader(data); + uint8_t framing; + if (!reader.ReadUInt8(&framing)) { + return absl::InvalidArgumentError("Missing framing indicator."); + } + if (framing == kKnownLengthRequestFraming) { + return DecodeKnownLengthRequest(reader); + } + return absl::UnimplementedError( + absl::StrCat("Unsupported framing type ", framing)); +} + +absl::StatusOr<BinaryHttpResponse> BinaryHttpResponse::Create( + absl::string_view data) { + quiche::QuicheDataReader reader(data); + uint8_t framing; + if (!reader.ReadUInt8(&framing)) { + return absl::InvalidArgumentError("Missing framing indicator."); + } + if (framing == kKnownLengthResponseFraming) { + return DecodeKnownLengthResponse(reader); + } + return absl::UnimplementedError( + absl::StrCat("Unsupported framing type ", framing)); +} + +} // namespace quiche
diff --git a/quiche/binary_http/binary_http_message.h b/quiche/binary_http/binary_http_message.h new file mode 100644 index 0000000..843b860 --- /dev/null +++ b/quiche/binary_http/binary_http_message.h
@@ -0,0 +1,198 @@ +#ifndef QUICHE_BINARY_HTTP_BINARY_HTTP_MESSAGE_H_ +#define QUICHE_BINARY_HTTP_BINARY_HTTP_MESSAGE_H_ + +#include <cstdint> +#include <functional> +#include <memory> +#include <ostream> +#include <string> +#include <utility> +#include <vector> + +#include "absl/container/flat_hash_map.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_writer.h" + +namespace quiche { + +// Supports encoding and decoding Binary Http messages. +// Currently limited to known-length messages. +// https://www.ietf.org/archive/id/draft-ietf-httpbis-binary-message-06.html +class QUICHE_EXPORT_PRIVATE BinaryHttpMessage { + public: + // Name value pair of either a header or trailer field. + struct Field { + std::string name; + std::string value; + bool operator==(const BinaryHttpMessage::Field& rhs) const { + return name == rhs.name && value == rhs.value; + } + }; + virtual ~BinaryHttpMessage() = default; + + // TODO(bschneider): Switch to use existing Http2HeaderBlock + BinaryHttpMessage* AddHeaderField(Field header_field); + + const std::vector<Field>& GetHeaderFields() const { + return header_fields_.fields(); + } + + BinaryHttpMessage* set_body(std::string body) { + body_ = std::move(body); + return this; + } + + absl::string_view body() const { return body_; } + + // Returns the Binary Http formatted message. + virtual absl::StatusOr<std::string> Serialize() const = 0; + // TODO(bschneider): Add AddTrailerField for chunked messages + // TODO(bschneider): Add SetBodyCallback() for chunked messages + protected: + class Fields { + public: + // Appends `field` to list of fields. Can contain duplicates. + void AddField(BinaryHttpMessage::Field field); + + const std::vector<BinaryHttpMessage::Field>& fields() const { + return fields_; + } + + // Encode fields in insertion order. + // https://www.ietf.org/archive/id/draft-ietf-httpbis-binary-message-06.html#name-header-and-trailer-field-li + absl::Status Encode(quiche::QuicheDataWriter& writer) const; + + // The number of returned by EncodedFieldsSize + // plus the number of bytes used in the varint holding that value. + uint64_t EncodedSize() const; + + private: + // Number of bytes of just the set of fields. + uint64_t EncodedFieldsSize() const; + + // Fields in insertion order. + std::vector<BinaryHttpMessage::Field> fields_; + }; + + absl::Status EncodeKnownLengthFieldsAndBody( + quiche::QuicheDataWriter& writer) const; + uint64_t EncodedKnownLengthFieldsAndBodySize() const; + bool has_host() const { return has_host_; } + + private: + std::string body_; + Fields header_fields_; + bool has_host_ = false; +}; + +class QUICHE_EXPORT_PRIVATE BinaryHttpRequest : public BinaryHttpMessage { + public: + // HTTP request must have all of the following fields. + // Some examples are: + // scheme: HTTP + // authority: www.example.com + // path: /index.html + struct ControlData { + std::string method; + std::string scheme; + std::string authority; + std::string path; + bool operator==(const BinaryHttpRequest::ControlData& rhs) const { + return method == rhs.method && scheme == rhs.scheme && + authority == rhs.authority && path == rhs.path; + } + }; + explicit BinaryHttpRequest(ControlData control_data) + : control_data_(std::move(control_data)) {} + + // Deserialize + static absl::StatusOr<BinaryHttpRequest> Create(absl::string_view data); + + absl::StatusOr<std::string> Serialize() const override; + const ControlData& control_data() const { return control_data_; } + + private: + absl::Status EncodeControlData(quiche::QuicheDataWriter& writer) const; + + uint64_t EncodedControlDataSize() const; + + uint64_t EncodedSize() const; + + // Returns Binary Http known length request formatted request. + absl::StatusOr<std::string> EncodeAsKnownLength() const; + + const ControlData control_data_; +}; + +class QUICHE_EXPORT_PRIVATE BinaryHttpResponse : public BinaryHttpMessage { + public: + // https://www.ietf.org/archive/id/draft-ietf-httpbis-binary-message-06.html#name-response-control-data + struct InformationalResponse { + uint16_t status_code; + const std::vector<Field>& header_fields; + bool operator==( + const BinaryHttpResponse::InformationalResponse& rhs) const { + return status_code == rhs.status_code && + header_fields == rhs.header_fields; + } + }; + + explicit BinaryHttpResponse(uint16_t status_code) + : status_code_(status_code) {} + + // Deserialize + static absl::StatusOr<BinaryHttpResponse> Create(absl::string_view data); + + absl::StatusOr<std::string> Serialize() const override; + + // Informational status codes must be between 100 and 199 inclusive. + absl::Status AddInformationalResponse(uint16_t status_code, + std::vector<Field> header_fields); + + uint16_t status_code() const { return status_code_; } + // References in the returned `ResponseControlData` are invalidated on + // `BinaryHttpResponse` object mutations. + std::vector<InformationalResponse> GetInformationalResponse() const; + + private: + // + // Valid response codes are [100,199]. + // TODO(bschneider): Collapse this inner class into the public + // `InformationalResponse` struct. + class InformationalResponseInternal { + public: + explicit InformationalResponseInternal(uint16_t response_code) + : response_code_(response_code) {} + + void AddField(absl::string_view name, std::string value); + + // Appends the encoded fields and body to `writer`. + absl::Status Encode(quiche::QuicheDataWriter& writer) const; + + uint64_t EncodedSize() const; + + uint16_t response_code() const { return response_code_; } + + const std::vector<BinaryHttpMessage::Field>& fields() const { + return fields_.fields(); + } + + private: + const uint16_t response_code_; + BinaryHttpMessage::Fields fields_; + }; + + // Returns Binary Http known length request formatted response. + absl::StatusOr<std::string> EncodeAsKnownLength() const; + + uint64_t EncodedSize() const; + + std::vector<InformationalResponseInternal> + informational_response_control_data_; + const uint16_t status_code_; +}; +} // namespace quiche + +#endif // QUICHE_BINARY_HTTP_BINARY_HTTP_MESSAGE_H_
diff --git a/quiche/binary_http/binary_http_message_test.cc b/quiche/binary_http/binary_http_message_test.cc new file mode 100644 index 0000000..581848e --- /dev/null +++ b/quiche/binary_http/binary_http_message_test.cc
@@ -0,0 +1,489 @@ +#include "quiche/binary_http/binary_http_message.h" + +#include <cstdint> +#include <memory> +#include <string> +#include <vector> + +#include "absl/container/flat_hash_map.h" +#include "quiche/common/platform/api/quiche_test.h" + +using ::testing::ContainerEq; +using ::testing::FieldsAre; + +namespace quiche { +namespace { + +std::string WordToBytes(uint32_t word) { + return std::string( + {static_cast<uint8_t>(word >> 24), static_cast<uint8_t>(word >> 16), + static_cast<uint8_t>(word >> 8), static_cast<uint8_t>(word)}); +} + +} // namespace +// Test examples from +// https://www.ietf.org/archive/id/draft-ietf-httpbis-binary-message-06.html + +TEST(BinaryHttpRequest, EncodeGetNoBody) { + /* + 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", "www.example.com", "/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"}); + /* + 00000000: 00034745 54056874 74707300 0a2f6865 ..GET.https../he + 00000010: 6c6c6f2e 74787440 6c0a7573 65722d61 llo.txt@l.user-a + 00000020: 67656e74 34637572 6c2f372e 31362e33 gent4curl/7.16.3 + 00000030: 206c6962 6375726c 2f372e31 362e3320 libcurl/7.16.3 + 00000040: 4f70656e 53534c2f 302e392e 376c207a OpenSSL/0.9.7l z + 00000050: 6c69622f 312e322e 3304686f 73740f77 lib/1.2.3.host.w + 00000060: 77772e65 78616d70 6c652e63 6f6d0f61 ww.example.com.a + 00000070: 63636570 742d6c61 6e677561 67650665 ccept-language.e + 00000080: 6e2c206d 6900 n, mi.. + */ + const uint32_t expected_words[] = { + 0x00034745, 0x54056874, 0x74707300, 0x0a2f6865, 0x6c6c6f2e, 0x74787440, + 0x6c0a7573, 0x65722d61, 0x67656e74, 0x34637572, 0x6c2f372e, 0x31362e33, + 0x206c6962, 0x6375726c, 0x2f372e31, 0x362e3320, 0x4f70656e, 0x53534c2f, + 0x302e392e, 0x376c207a, 0x6c69622f, 0x312e322e, 0x3304686f, 0x73740f77, + 0x77772e65, 0x78616d70, 0x6c652e63, 0x6f6d0f61, 0x63636570, 0x742d6c61, + 0x6e677561, 0x67650665, 0x6e2c206d, 0x69000000}; + std::string expected; + for (const auto& word : expected_words) { + expected += WordToBytes(word); + } + // Remove padding. + expected.resize(expected.size() - 2); + + const auto result = request.Serialize(); + ASSERT_TRUE(result.ok()); + ASSERT_EQ(*result, expected); +} + +TEST(BinaryHttpRequest, DecodeGetNoBody) { + const uint32_t words[] = { + 0x00034745, 0x54056874, 0x74707300, 0x0a2f6865, 0x6c6c6f2e, 0x74787440, + 0x6c0a7573, 0x65722d61, 0x67656e74, 0x34637572, 0x6c2f372e, 0x31362e33, + 0x206c6962, 0x6375726c, 0x2f372e31, 0x362e3320, 0x4f70656e, 0x53534c2f, + 0x302e392e, 0x376c207a, 0x6c69622f, 0x312e322e, 0x3304686f, 0x73740f77, + 0x77772e65, 0x78616d70, 0x6c652e63, 0x6f6d0f61, 0x63636570, 0x742d6c61, + 0x6e677561, 0x67650665, 0x6e2c206d, 0x69000000}; + std::string data; + for (const auto& word : words) { + data += WordToBytes(word); + } + const auto request_so = BinaryHttpRequest::Create(data); + ASSERT_TRUE(request_so.ok()); + const BinaryHttpRequest request = *request_so; + ASSERT_THAT(request.control_data(), + FieldsAre("GET", "https", "", "/hello.txt")); + std::vector<BinaryHttpMessage::Field> expected_fields = { + {"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"}}; + ASSERT_THAT(request.GetHeaderFields(), ContainerEq(expected_fields)); + ASSERT_EQ(request.body(), ""); +} + +TEST(BinaryHttpRequest, EncodeGetWithAuthority) { + /* + GET https://www.example.com/hello.txt HTTP/1.1 + User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3 + Accept-Language: en, mi + */ + BinaryHttpRequest request({"GET", "https", "www.example.com", "/hello.txt"}); + request + .AddHeaderField({"User-Agent", + "curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3"}) + ->AddHeaderField({"Accept-Language", "en, mi"}); + /* + 00000000: 00034745 54056874 7470730f 7777772e ..GET.https.www. + 00000010: 6578616d 706c652e 636f6d0a 2f68656c example.com./hel + 00000020: 6c6f2e74 78744057 0a757365 722d6167 lo.txt@W.user-ag + 00000030: 656e7434 6375726c 2f372e31 362e3320 ent4curl/7.16.3 + 00000040: 6c696263 75726c2f 372e3136 2e33204f libcurl/7.16.3 O + 00000050: 70656e53 534c2f30 2e392e37 6c207a6c penSSL/0.9.7l zl + 00000060: 69622f31 2e322e33 0f616363 6570742d ib/1.2.3.accept- + 00000070: 6c616e67 75616765 06656e2c 206d6900 language.en, mi. + */ + + const uint32_t expected_words[] = { + 0x00034745, 0x54056874, 0x7470730f, 0x7777772e, 0x6578616d, 0x706c652e, + 0x636f6d0a, 0x2f68656c, 0x6c6f2e74, 0x78744057, 0x0a757365, 0x722d6167, + 0x656e7434, 0x6375726c, 0x2f372e31, 0x362e3320, 0x6c696263, 0x75726c2f, + 0x372e3136, 0x2e33204f, 0x70656e53, 0x534c2f30, 0x2e392e37, 0x6c207a6c, + 0x69622f31, 0x2e322e33, 0x0f616363, 0x6570742d, 0x6c616e67, 0x75616765, + 0x06656e2c, 0x206d6900}; + std::string expected; + for (const auto& word : expected_words) { + expected += WordToBytes(word); + } + const auto result = request.Serialize(); + ASSERT_TRUE(result.ok()); + ASSERT_EQ(*result, expected); +} + +TEST(BinaryHttpRequest, DecodeGetWithAuthority) { + const uint32_t words[] = { + 0x00034745, 0x54056874, 0x7470730f, 0x7777772e, 0x6578616d, 0x706c652e, + 0x636f6d0a, 0x2f68656c, 0x6c6f2e74, 0x78744057, 0x0a757365, 0x722d6167, + 0x656e7434, 0x6375726c, 0x2f372e31, 0x362e3320, 0x6c696263, 0x75726c2f, + 0x372e3136, 0x2e33204f, 0x70656e53, 0x534c2f30, 0x2e392e37, 0x6c207a6c, + 0x69622f31, 0x2e322e33, 0x0f616363, 0x6570742d, 0x6c616e67, 0x75616765, + 0x06656e2c, 0x206d6900, 0x00}; + std::string data; + for (const auto& word : words) { + data += WordToBytes(word); + } + const auto request_so = BinaryHttpRequest::Create(data); + ASSERT_TRUE(request_so.ok()); + const BinaryHttpRequest request = *request_so; + ASSERT_THAT(request.control_data(), + FieldsAre("GET", "https", "www.example.com", "/hello.txt")); + std::vector<BinaryHttpMessage::Field> expected_fields = { + {"user-agent", "curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3"}, + {"accept-language", "en, mi"}}; + ASSERT_THAT(request.GetHeaderFields(), ContainerEq(expected_fields)); + ASSERT_EQ(request.body(), ""); +} + +TEST(BinaryHttpRequest, EncodePostBody) { + /* + POST /hello.txt HTTP/1.1 + User-Agent: not/telling + Host: www.example.com + Accept-Language: en + + Some body that I used to post. + */ + BinaryHttpRequest request({"POST", "https", "www.example.com", "/hello.txt"}); + request.AddHeaderField({"User-Agent", "not/telling"}) + ->AddHeaderField({"Host", "www.example.com"}) + ->AddHeaderField({"Accept-Language", "en"}) + ->set_body({"Some body that I used to post.\r\n"}); + /* + 00000000: 0004504f 53540568 74747073 000a2f68 ..POST.https../h + 00000010: 656c6c6f 2e747874 3f0a7573 65722d61 ello.txt?.user-a + 00000020: 67656e74 0b6e6f74 2f74656c 6c696e67 gent.not/telling + 00000030: 04686f73 740f7777 772e6578 616d706c .host.www.exampl + 00000040: 652e636f 6d0f6163 63657074 2d6c616e e.com.accept-lan + 00000050: 67756167 6502656e 20536f6d 6520626f guage.en Some bo + 00000060: 64792074 68617420 49207573 65642074 dy that I used t + 00000070: 6f20706f 73742e0d 0a o post.... + */ + const uint32_t expected_words[] = { + 0x0004504f, 0x53540568, 0x74747073, 0x000a2f68, 0x656c6c6f, 0x2e747874, + 0x3f0a7573, 0x65722d61, 0x67656e74, 0x0b6e6f74, 0x2f74656c, 0x6c696e67, + 0x04686f73, 0x740f7777, 0x772e6578, 0x616d706c, 0x652e636f, 0x6d0f6163, + 0x63657074, 0x2d6c616e, 0x67756167, 0x6502656e, 0x20536f6d, 0x6520626f, + 0x64792074, 0x68617420, 0x49207573, 0x65642074, 0x6f20706f, 0x73742e0d, + 0x0a000000}; + std::string expected; + for (const auto& word : expected_words) { + expected += WordToBytes(word); + } + // Remove padding. + expected.resize(expected.size() - 3); + const auto result = request.Serialize(); + ASSERT_TRUE(result.ok()); + ASSERT_EQ(*result, expected); +} + +TEST(BinaryHttpRequest, DecodePostBody) { + const uint32_t words[] = { + 0x0004504f, 0x53540568, 0x74747073, 0x000a2f68, 0x656c6c6f, 0x2e747874, + 0x3f0a7573, 0x65722d61, 0x67656e74, 0x0b6e6f74, 0x2f74656c, 0x6c696e67, + 0x04686f73, 0x740f7777, 0x772e6578, 0x616d706c, 0x652e636f, 0x6d0f6163, + 0x63657074, 0x2d6c616e, 0x67756167, 0x6502656e, 0x20536f6d, 0x6520626f, + 0x64792074, 0x68617420, 0x49207573, 0x65642074, 0x6f20706f, 0x73742e0d, + 0x0a000000}; + std::string data; + for (const auto& word : words) { + data += WordToBytes(word); + } + const auto request_so = BinaryHttpRequest::Create(data); + ASSERT_TRUE(request_so.ok()); + BinaryHttpRequest request = *request_so; + ASSERT_THAT(request.control_data(), + FieldsAre("POST", "https", "", "/hello.txt")); + std::vector<BinaryHttpMessage::Field> expected_fields = { + {"user-agent", "not/telling"}, + {"host", "www.example.com"}, + {"accept-language", "en"}}; + ASSERT_THAT(request.GetHeaderFields(), ContainerEq(expected_fields)); + ASSERT_EQ(request.body(), "Some body that I used to post.\r\n"); +} + +TEST(BinaryHttpResponse, EncodeNoBody) { + /* + HTTP/1.1 404 Not Found + Server: Apache + */ + BinaryHttpResponse response(404); + response.AddHeaderField({"Server", "Apache"}); + /* + 0141940e 06736572 76657206 41706163 .A...server.Apac + 686500 he.. + */ + const uint32_t expected_words[] = {0x0141940e, 0x06736572, 0x76657206, + 0x41706163, 0x68650000}; + std::string expected; + for (const auto& word : expected_words) { + expected += WordToBytes(word); + } + // Remove padding. + expected.resize(expected.size() - 1); + const auto result = response.Serialize(); + ASSERT_TRUE(result.ok()); + ASSERT_EQ(*result, expected); +} + +TEST(BinaryHttpResponse, DecodeNoBody) { + /* + HTTP/1.1 404 Not Found + Server: Apache + */ + const uint32_t words[] = {0x0141940e, 0x06736572, 0x76657206, 0x41706163, + 0x68650000}; + std::string data; + for (const auto& word : words) { + data += WordToBytes(word); + } + const auto response_so = BinaryHttpResponse::Create(data); + ASSERT_TRUE(response_so.ok()); + const BinaryHttpResponse response = *response_so; + ASSERT_EQ(response.status_code(), 404); + std::vector<BinaryHttpMessage::Field> expected_fields = { + {"server", "Apache"}}; + ASSERT_THAT(response.GetHeaderFields(), ContainerEq(expected_fields)); + ASSERT_EQ(response.body(), ""); + ASSERT_TRUE(response.GetInformationalResponse().empty()); +} + +TEST(BinaryHttpResponse, EncodeBody) { + /* + HTTP/1.1 200 OK + Server: Apache + + Hello, world! + */ + BinaryHttpResponse response(200); + response.AddHeaderField({"Server", "Apache"}); + response.set_body("Hello, world!\r\n"); + /* + 0140c80e 06736572 76657206 41706163 .@...server.Apac + 68650f48 656c6c6f 2c20776f 726c6421 he.Hello, world! + 0d0a .... + */ + const uint32_t expected_words[] = {0x0140c80e, 0x06736572, 0x76657206, + 0x41706163, 0x68650f48, 0x656c6c6f, + 0x2c20776f, 0x726c6421, 0x0d0a0000}; + std::string expected; + for (const auto& word : expected_words) { + expected += WordToBytes(word); + } + // Remove padding. + expected.resize(expected.size() - 2); + + const auto result = response.Serialize(); + ASSERT_TRUE(result.ok()); + ASSERT_EQ(*result, expected); +} + +TEST(BinaryHttpResponse, DecodeBody) { + /* + HTTP/1.1 200 OK + + Hello, world! + */ + const uint32_t words[] = {0x0140c80e, 0x06736572, 0x76657206, + 0x41706163, 0x68650f48, 0x656c6c6f, + 0x2c20776f, 0x726c6421, 0x0d0a0000}; + std::string data; + for (const auto& word : words) { + data += WordToBytes(word); + } + const auto response_so = BinaryHttpResponse::Create(data); + ASSERT_TRUE(response_so.ok()); + const BinaryHttpResponse response = *response_so; + ASSERT_EQ(response.status_code(), 200); + std::vector<BinaryHttpMessage::Field> expected_fields = { + {"server", "Apache"}}; + ASSERT_THAT(response.GetHeaderFields(), ContainerEq(expected_fields)); + ASSERT_EQ(response.body(), "Hello, world!\r\n"); + ASSERT_TRUE(response.GetInformationalResponse().empty()); +} + +TEST(BHttpResponse, AddBadInformationalResponseCode) { + BinaryHttpResponse response(200); + ASSERT_FALSE(response.AddInformationalResponse(50, {}).ok()); + ASSERT_FALSE(response.AddInformationalResponse(300, {}).ok()); +} + +TEST(BinaryHttpResponse, EncodeMultiInformationalWithBody) { + /* + HTTP/1.1 102 Processing + Running: "sleep 15" + + HTTP/1.1 103 Early Hints + Link: </style.css>; rel=preload; as=style + Link: </script.js>; rel=preload; as=script + + HTTP/1.1 200 OK + Date: Mon, 27 Jul 2009 12:28:53 GMT + Server: Apache + Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT + ETag: "34aa387-d-1568eb00" + Accept-Ranges: bytes + Content-Length: 51 + Vary: Accept-Encoding + Content-Type: text/plain + + Hello World! My content includes a trailing CRLF. + */ + BinaryHttpResponse response(200); + response.AddHeaderField({"Date", "Mon, 27 Jul 2009 12:28:53 GMT"}) + ->AddHeaderField({"Server", "Apache"}) + ->AddHeaderField({"Last-Modified", "Wed, 22 Jul 2009 19:15:56 GMT"}) + ->AddHeaderField({"ETag", "\"34aa387-d-1568eb00\""}) + ->AddHeaderField({"Accept-Ranges", "bytes"}) + ->AddHeaderField({"Content-Length", "51"}) + ->AddHeaderField({"Vary", "Accept-Encoding"}) + ->AddHeaderField({"Content-Type", "text/plain"}); + response.set_body("Hello World! My content includes a trailing CRLF.\r\n"); + ASSERT_TRUE( + response.AddInformationalResponse(102, {{"Running", "\"sleep 15\""}}) + .ok()); + ASSERT_TRUE(response + .AddInformationalResponse( + 103, {{"Link", "</style.css>; rel=preload; as=style"}, + {"Link", "</script.js>; rel=preload; as=script"}}) + .ok()); + + /* + 01406613 0772756e 6e696e67 0a22736c .@f..running."sl + 65657020 31352240 67405304 6c696e6b eep 15"@g@S.link + 233c2f73 74796c65 2e637373 3e3b2072 #</style.css>; r + 656c3d70 72656c6f 61643b20 61733d73 el=preload; as=s + 74796c65 046c696e 6b243c2f 73637269 tyle.link$</scri + 70742e6a 733e3b20 72656c3d 7072656c pt.js>; rel=prel + 6f61643b 2061733d 73637269 707440c8 oad; as=script@. + 40ca0464 6174651d 4d6f6e2c 20323720 @..date.Mon, 27 + 4a756c20 32303039 2031323a 32383a35 Jul 2009 12:28:5 + 3320474d 54067365 72766572 06417061 3 GMT.server.Apa + 6368650d 6c617374 2d6d6f64 69666965 che.last-modifie + 641d5765 642c2032 32204a75 6c203230 d.Wed, 22 Jul 20 + 30392031 393a3135 3a353620 474d5404 09 19:15:56 GMT. + 65746167 14223334 61613338 372d642d etag."34aa387-d- + 31353638 65623030 220d6163 63657074 1568eb00".accept + 2d72616e 67657305 62797465 730e636f -ranges.bytes.co + 6e74656e 742d6c65 6e677468 02353104 ntent-length.51. + 76617279 0f416363 6570742d 456e636f vary.Accept-Enco + 64696e67 0c636f6e 74656e74 2d747970 ding.content-typ + 650a7465 78742f70 6c61696e 3348656c e.text/plain3Hel + 6c6f2057 6f726c64 21204d79 20636f6e lo World! My con + 74656e74 20696e63 6c756465 73206120 tent includes a + 74726169 6c696e67 2043524c 462e0d0a trailing CRLF... + */ + const uint32_t expected_words[] = { + 0x01406613, 0x0772756e, 0x6e696e67, 0x0a22736c, 0x65657020, 0x31352240, + 0x67405304, 0x6c696e6b, 0x233c2f73, 0x74796c65, 0x2e637373, 0x3e3b2072, + 0x656c3d70, 0x72656c6f, 0x61643b20, 0x61733d73, 0x74796c65, 0x046c696e, + 0x6b243c2f, 0x73637269, 0x70742e6a, 0x733e3b20, 0x72656c3d, 0x7072656c, + 0x6f61643b, 0x2061733d, 0x73637269, 0x707440c8, 0x40ca0464, 0x6174651d, + 0x4d6f6e2c, 0x20323720, 0x4a756c20, 0x32303039, 0x2031323a, 0x32383a35, + 0x3320474d, 0x54067365, 0x72766572, 0x06417061, 0x6368650d, 0x6c617374, + 0x2d6d6f64, 0x69666965, 0x641d5765, 0x642c2032, 0x32204a75, 0x6c203230, + 0x30392031, 0x393a3135, 0x3a353620, 0x474d5404, 0x65746167, 0x14223334, + 0x61613338, 0x372d642d, 0x31353638, 0x65623030, 0x220d6163, 0x63657074, + 0x2d72616e, 0x67657305, 0x62797465, 0x730e636f, 0x6e74656e, 0x742d6c65, + 0x6e677468, 0x02353104, 0x76617279, 0x0f416363, 0x6570742d, 0x456e636f, + 0x64696e67, 0x0c636f6e, 0x74656e74, 0x2d747970, 0x650a7465, 0x78742f70, + 0x6c61696e, 0x3348656c, 0x6c6f2057, 0x6f726c64, 0x21204d79, 0x20636f6e, + 0x74656e74, 0x20696e63, 0x6c756465, 0x73206120, 0x74726169, 0x6c696e67, + 0x2043524c, 0x462e0d0a}; + std::string expected; + for (const auto& word : expected_words) { + expected += WordToBytes(word); + } + const auto result = response.Serialize(); + ASSERT_TRUE(result.ok()); + ASSERT_EQ(*result, expected); +} + +TEST(BinaryHttpResponse, DecodeMultiInformationalWithBody) { + /* + HTTP/1.1 102 Processing + Running: "sleep 15" + + HTTP/1.1 103 Early Hints + Link: </style.css>; rel=preload; as=style + Link: </script.js>; rel=preload; as=script + + HTTP/1.1 200 OK + Date: Mon, 27 Jul 2009 12:28:53 GMT + Server: Apache + Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT + ETag: "34aa387-d-1568eb00" + Accept-Ranges: bytes + Content-Length: 51 + Vary: Accept-Encoding + Content-Type: text/plain + + Hello World! My content includes a trailing CRLF. + */ + const uint32_t words[] = { + 0x01406613, 0x0772756e, 0x6e696e67, 0x0a22736c, 0x65657020, 0x31352240, + 0x67405304, 0x6c696e6b, 0x233c2f73, 0x74796c65, 0x2e637373, 0x3e3b2072, + 0x656c3d70, 0x72656c6f, 0x61643b20, 0x61733d73, 0x74796c65, 0x046c696e, + 0x6b243c2f, 0x73637269, 0x70742e6a, 0x733e3b20, 0x72656c3d, 0x7072656c, + 0x6f61643b, 0x2061733d, 0x73637269, 0x707440c8, 0x40ca0464, 0x6174651d, + 0x4d6f6e2c, 0x20323720, 0x4a756c20, 0x32303039, 0x2031323a, 0x32383a35, + 0x3320474d, 0x54067365, 0x72766572, 0x06417061, 0x6368650d, 0x6c617374, + 0x2d6d6f64, 0x69666965, 0x641d5765, 0x642c2032, 0x32204a75, 0x6c203230, + 0x30392031, 0x393a3135, 0x3a353620, 0x474d5404, 0x65746167, 0x14223334, + 0x61613338, 0x372d642d, 0x31353638, 0x65623030, 0x220d6163, 0x63657074, + 0x2d72616e, 0x67657305, 0x62797465, 0x730e636f, 0x6e74656e, 0x742d6c65, + 0x6e677468, 0x02353104, 0x76617279, 0x0f416363, 0x6570742d, 0x456e636f, + 0x64696e67, 0x0c636f6e, 0x74656e74, 0x2d747970, 0x650a7465, 0x78742f70, + 0x6c61696e, 0x3348656c, 0x6c6f2057, 0x6f726c64, 0x21204d79, 0x20636f6e, + 0x74656e74, 0x20696e63, 0x6c756465, 0x73206120, 0x74726169, 0x6c696e67, + 0x2043524c, 0x462e0d0a, 0x00000000}; + std::string data; + for (const auto& word : words) { + data += WordToBytes(word); + } + const auto response_so = BinaryHttpResponse::Create(data); + ASSERT_TRUE(response_so.ok()); + const BinaryHttpResponse response = *response_so; + std::vector<BinaryHttpMessage::Field> expected_fields = { + {"date", "Mon, 27 Jul 2009 12:28:53 GMT"}, + {"server", "Apache"}, + {"last-modified", "Wed, 22 Jul 2009 19:15:56 GMT"}, + {"etag", "\"34aa387-d-1568eb00\""}, + {"accept-ranges", "bytes"}, + {"content-length", "51"}, + {"vary", "Accept-Encoding"}, + {"content-type", "text/plain"}}; + ASSERT_THAT(response.GetHeaderFields(), ContainerEq(expected_fields)); + ASSERT_EQ(response.body(), + "Hello World! My content includes a trailing CRLF.\r\n"); + std::vector<BinaryHttpMessage::Field> header102 = { + {"running", "\"sleep 15\""}}; + std::vector<BinaryHttpMessage::Field> header103 = { + {"link", "</style.css>; rel=preload; as=style"}, + {"link", "</script.js>; rel=preload; as=script"}}; + std::vector<BinaryHttpResponse::InformationalResponse> expected_control = { + {102, header102}, {103, header103}}; + ASSERT_THAT(response.GetInformationalResponse(), + ContainerEq(expected_control)); +} + +} // namespace quiche