| #ifndef QUICHE_BINARY_HTTP_BINARY_HTTP_MESSAGE_H_ |
| #define QUICHE_BINARY_HTTP_BINARY_HTTP_MESSAGE_H_ |
| |
| #include <cstddef> |
| #include <cstdint> |
| #include <functional> |
| #include <memory> |
| #include <optional> |
| #include <ostream> |
| #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/string_view.h" |
| #include "absl/types/span.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 { |
| |
| // 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 BinaryHttpMessage { |
| public: |
| // A view of a field name and value. Used to pass around a field without |
| // owning or copying the backing data. |
| struct QUICHE_EXPORT FieldView { |
| absl::string_view name; |
| absl::string_view value; |
| }; |
| // Name value pair of either a header or trailer field. |
| struct QUICHE_EXPORT Field { |
| std::string name; |
| std::string value; |
| bool operator==(const BinaryHttpMessage::Field& rhs) const { |
| return name == rhs.name && value == rhs.value; |
| } |
| |
| bool operator!=(const BinaryHttpMessage::Field& rhs) const { |
| return !(*this == rhs); |
| } |
| |
| std::string DebugString() const; |
| }; |
| 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; |
| } |
| |
| 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 |
| // TODO(bschneider): Add SetBodyCallback() for chunked messages |
| |
| virtual std::string DebugString() const; |
| |
| 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_; |
| } |
| |
| bool operator==(const BinaryHttpMessage::Fields& rhs) const { |
| return fields_ == rhs.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. |
| size_t EncodedSize() const; |
| |
| private: |
| // Number of bytes of just the set of fields. |
| size_t EncodedFieldsSize() const; |
| |
| // Fields in insertion order. |
| std::vector<BinaryHttpMessage::Field> fields_; |
| }; |
| |
| // 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_; |
| } |
| |
| absl::Status EncodeKnownLengthFieldsAndBody( |
| quiche::QuicheDataWriter& writer) 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, |
| std::ostream* os); |
| |
| class QUICHE_EXPORT BinaryHttpRequest : public BinaryHttpMessage { |
| public: |
| // 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 |
| // path: /index.html |
| struct QUICHE_EXPORT 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; |
| } |
| bool operator!=(const BinaryHttpRequest::ControlData& rhs) const { |
| return !(*this == rhs); |
| } |
| }; |
| explicit BinaryHttpRequest(ControlData control_data) |
| : control_data_(std::move(control_data)) {} |
| |
| // 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; |
| |
| // 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::IsPayloadEqual(rhs); |
| } |
| |
| bool operator==(const BinaryHttpRequest& rhs) const { |
| return IsPayloadEqual(rhs) && |
| num_padding_bytes() == rhs.num_padding_bytes(); |
| } |
| |
| bool operator!=(const BinaryHttpRequest& rhs) const { |
| 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 QUICHE_EXPORT IndeterminateLengthDecoder { |
| public: |
| // The handler to invoke when a section is decoded successfully. The |
| // handler can return an error if the decoded data cannot be processed |
| // successfully. |
| class QUICHE_EXPORT MessageSectionHandler { |
| public: |
| virtual ~MessageSectionHandler() = default; |
| virtual absl::Status OnControlData(const ControlData& control_data) = 0; |
| virtual absl::Status OnHeader(absl::string_view name, |
| absl::string_view value) = 0; |
| virtual absl::Status OnHeadersDone() = 0; |
| virtual absl::Status OnBodyChunk(absl::string_view body_chunk) = 0; |
| virtual absl::Status OnBodyChunksDone() = 0; |
| virtual absl::Status OnTrailer(absl::string_view name, |
| absl::string_view value) = 0; |
| virtual absl::Status 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; |
| |
| size_t EncodedControlDataSize() const; |
| |
| // Returns Binary Http known length request formatted request. |
| absl::StatusOr<std::string> EncodeAsKnownLength() const; |
| |
| const ControlData control_data_; |
| }; |
| |
| void QUICHE_EXPORT PrintTo(const BinaryHttpRequest& msg, std::ostream* os); |
| |
| class QUICHE_EXPORT BinaryHttpResponse : public BinaryHttpMessage { |
| public: |
| // https://www.ietf.org/archive/id/draft-ietf-httpbis-binary-message-06.html#name-response-control-data |
| // A response can contain 0 to N informational responses. Each informational |
| // response contains a status code followed by a header field. Valid status |
| // codes are [100,199]. |
| class QUICHE_EXPORT InformationalResponse { |
| public: |
| explicit InformationalResponse(uint16_t status_code) |
| : status_code_(status_code) {} |
| InformationalResponse(uint16_t status_code, |
| const std::vector<BinaryHttpMessage::Field>& fields) |
| : status_code_(status_code) { |
| for (const BinaryHttpMessage::Field& field : fields) { |
| AddField(field.name, field.value); |
| } |
| } |
| |
| bool operator==( |
| const BinaryHttpResponse::InformationalResponse& rhs) const { |
| return status_code_ == rhs.status_code_ && fields_ == rhs.fields_; |
| } |
| |
| bool operator!=( |
| const BinaryHttpResponse::InformationalResponse& rhs) const { |
| return !(*this == rhs); |
| } |
| |
| // Adds a field with the provided name, converted to lower case. |
| // Fields are in the order they are added. |
| void AddField(absl::string_view name, std::string value); |
| |
| const std::vector<BinaryHttpMessage::Field>& fields() const { |
| return fields_.fields(); |
| } |
| |
| uint16_t status_code() const { return status_code_; } |
| |
| std::string DebugString() const; |
| |
| private: |
| // Give BinaryHttpResponse access to Encoding functionality. |
| friend class BinaryHttpResponse; |
| |
| size_t EncodedSize() const; |
| |
| // Appends the encoded fields and body to `writer`. |
| absl::Status Encode(quiche::QuicheDataWriter& writer) const; |
| |
| const uint16_t status_code_; |
| BinaryHttpMessage::Fields fields_; |
| }; |
| |
| explicit BinaryHttpResponse(uint16_t status_code) |
| : status_code_(status_code) {} |
| |
| // 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. |
| 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. |
| const std::vector<InformationalResponse>& informational_responses() const { |
| return informational_response_control_data_; |
| } |
| |
| virtual std::string DebugString() const override; |
| |
| // 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::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); |
| } |
| |
| // Provides encoding methods for an Indeterminate-Length BHTTP response. The |
| // encoder keeps track of what has been encoded so far to ensure sections are |
| // encoded in the correct order, this means it can only be used for a single |
| // BHTTP response message. |
| class QUICHE_EXPORT IndeterminateLengthEncoder { |
| public: |
| // Encodes the specified informational response status code, fields, and its |
| // content terminator. |
| absl::StatusOr<std::string> EncodeInformationalResponse( |
| uint16_t status_code, absl::Span<FieldView> fields); |
| // Encodes the specified status code, headers, and its content terminator. |
| absl::StatusOr<std::string> EncodeHeaders(uint16_t status_code, |
| absl::Span<FieldView> headers); |
| // Encodes the specified body chunks. If 'body_chunks_done' is true, the |
| // encoded body chunks are followed by the content terminator. |
| absl::StatusOr<std::string> EncodeBodyChunks( |
| absl::Span<absl::string_view> body_chunks, bool body_chunks_done); |
| // Encodes the specified trailers and its content terminator. |
| absl::StatusOr<std::string> EncodeTrailers(absl::Span<FieldView> trailers); |
| |
| private: |
| enum class MessageSection { |
| kInformationalResponseOrHeader, |
| kBody, |
| kTrailer, |
| kEnd, |
| }; |
| absl::StatusOr<std::string> EncodeFieldSection( |
| std::optional<uint16_t> status_code, absl::Span<FieldView> fields); |
| std::string GetMessageSectionString(MessageSection section) const; |
| |
| MessageSection current_section_ = |
| MessageSection::kInformationalResponseOrHeader; |
| bool framing_indicator_encoded_ = false; |
| }; |
| |
| private: |
| // Returns Binary Http known length request formatted response. |
| absl::StatusOr<std::string> EncodeAsKnownLength() const; |
| |
| std::vector<InformationalResponse> informational_response_control_data_; |
| const uint16_t status_code_; |
| }; |
| |
| void QUICHE_EXPORT PrintTo(const BinaryHttpResponse& msg, std::ostream* os); |
| } // namespace quiche |
| |
| #endif // QUICHE_BINARY_HTTP_BINARY_HTTP_MESSAGE_H_ |