| // Copyright 2022 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #ifndef QUICHE_BALSA_BALSA_FRAME_H_ |
| #define QUICHE_BALSA_BALSA_FRAME_H_ |
| |
| #include <cstddef> |
| #include <cstdint> |
| #include <utility> |
| #include <vector> |
| |
| #include "quiche/balsa/balsa_enums.h" |
| #include "quiche/balsa/balsa_headers.h" |
| #include "quiche/balsa/balsa_visitor_interface.h" |
| #include "quiche/balsa/framer_interface.h" |
| #include "quiche/balsa/http_validation_policy.h" |
| #include "quiche/balsa/noop_balsa_visitor.h" |
| #include "quiche/common/platform/api/quiche_bug_tracker.h" |
| #include "quiche/common/platform/api/quiche_export.h" |
| |
| namespace quiche { |
| |
| namespace test { |
| class BalsaFrameTestPeer; |
| } // namespace test |
| |
| // BalsaFrame is a lightweight HTTP framer. |
| class QUICHE_EXPORT_PRIVATE BalsaFrame : public FramerInterface { |
| public: |
| typedef std::vector<std::pair<size_t, size_t> > Lines; |
| |
| typedef BalsaHeaders::HeaderLineDescription HeaderLineDescription; |
| typedef BalsaHeaders::HeaderLines HeaderLines; |
| typedef BalsaHeaders::HeaderTokenList HeaderTokenList; |
| |
| enum class InvalidCharsLevel { kOff, kWarning, kError }; |
| |
| // TODO(fenix): get rid of the 'kValidTerm*' stuff by using the 'since last |
| // index' strategy. Note that this implies getting rid of the HeaderFramed() |
| |
| static constexpr int32_t kValidTerm1 = '\n' << 16 | '\r' << 8 | '\n'; |
| static constexpr int32_t kValidTerm1Mask = 0xFF << 16 | 0xFF << 8 | 0xFF; |
| static constexpr int32_t kValidTerm2 = '\n' << 8 | '\n'; |
| static constexpr int32_t kValidTerm2Mask = 0xFF << 8 | 0xFF; |
| BalsaFrame() |
| : last_char_was_slash_r_(false), |
| saw_non_newline_char_(false), |
| start_was_space_(true), |
| chunk_length_character_extracted_(false), |
| is_request_(true), |
| allow_reading_until_close_for_request_(false), |
| request_was_head_(false), |
| max_header_length_(16 * 1024), |
| visitor_(&do_nothing_visitor_), |
| chunk_length_remaining_(0), |
| content_length_remaining_(0), |
| last_slash_n_loc_(nullptr), |
| last_recorded_slash_n_loc_(nullptr), |
| last_slash_n_idx_(0), |
| term_chars_(0), |
| parse_state_(BalsaFrameEnums::READING_HEADER_AND_FIRSTLINE), |
| last_error_(BalsaFrameEnums::NO_ERROR), |
| continue_headers_(nullptr), |
| headers_(nullptr), |
| start_of_trailer_line_(0), |
| trailer_length_(0), |
| trailer_(nullptr), |
| invalid_chars_level_(InvalidCharsLevel::kOff), |
| http_validation_policy_(HttpValidationPolicy::CreateDefault()) {} |
| |
| ~BalsaFrame() override {} |
| |
| // Reset reinitializes all the member variables of the framer and clears the |
| // attached header object (but doesn't change the pointer value headers_). |
| void Reset(); |
| |
| // The method set_balsa_headers clears the headers provided and attaches them |
| // to the framer. This is a required step before the framer will process any |
| // input message data. |
| // To detach the header object from the framer, use |
| // set_balsa_headers(nullptr). |
| void set_balsa_headers(BalsaHeaders* headers) { |
| if (headers_ != headers) { |
| headers_ = headers; |
| } |
| if (headers_ != nullptr) { |
| // Clear the headers if they are non-null, even if the new headers are |
| // the same as the old. |
| headers_->Clear(); |
| } |
| } |
| |
| // If set to non-null, allow 100 Continue headers before the main headers. |
| void set_continue_headers(BalsaHeaders* continue_headers) { |
| if (continue_headers_ != continue_headers) { |
| continue_headers_ = continue_headers; |
| } |
| if (continue_headers_ != nullptr) { |
| // Clear the headers if they are non-null, even if the new headers are |
| // the same as the old. |
| continue_headers_->Clear(); |
| } |
| } |
| |
| // The method set_balsa_trailer clears the trailer provided and attaches it |
| // to the framer. This is a required step before the framer will process any |
| // input message data. |
| // To detach the trailer object from the framer, use |
| // set_balsa_trailer(nullptr). |
| void set_balsa_trailer(BalsaHeaders* trailer) { |
| if (trailer != nullptr && is_request()) { |
| QUICHE_BUG(bug_1317_1) << "Trailer in request is not allowed."; |
| return; |
| } |
| |
| if (trailer_ != trailer) { |
| trailer_ = trailer; |
| } |
| if (trailer_ != nullptr) { |
| // Clear the trailer if it is non-null, even if the new trailer is |
| // the same as the old. |
| trailer_->Clear(); |
| } |
| } |
| |
| void set_balsa_visitor(BalsaVisitorInterface* visitor) { |
| visitor_ = visitor; |
| if (visitor_ == nullptr) { |
| visitor_ = &do_nothing_visitor_; |
| } |
| } |
| |
| void set_invalid_chars_level(InvalidCharsLevel v) { |
| invalid_chars_level_ = v; |
| } |
| |
| bool track_invalid_chars() { |
| return invalid_chars_level_ != InvalidCharsLevel::kOff; |
| } |
| |
| bool invalid_chars_error_enabled() { |
| return invalid_chars_level_ == InvalidCharsLevel::kError; |
| } |
| |
| void set_http_validation_policy(const quiche::HttpValidationPolicy& policy) { |
| http_validation_policy_ = policy; |
| } |
| const quiche::HttpValidationPolicy& http_validation_policy() const { |
| return http_validation_policy_; |
| } |
| |
| void set_is_request(bool is_request) { is_request_ = is_request; } |
| |
| bool is_request() const { return is_request_; } |
| |
| void set_request_was_head(bool request_was_head) { |
| request_was_head_ = request_was_head; |
| } |
| |
| void set_max_header_length(size_t max_header_length) { |
| max_header_length_ = max_header_length; |
| } |
| |
| size_t max_header_length() const { return max_header_length_; } |
| |
| bool MessageFullyRead() const { |
| return parse_state_ == BalsaFrameEnums::MESSAGE_FULLY_READ; |
| } |
| |
| BalsaFrameEnums::ParseState ParseState() const { return parse_state_; } |
| |
| bool Error() const { return parse_state_ == BalsaFrameEnums::ERROR; } |
| |
| BalsaFrameEnums::ErrorCode ErrorCode() const { return last_error_; } |
| |
| const absl::flat_hash_map<char, int>& get_invalid_chars() const { |
| return invalid_chars_; |
| } |
| |
| const BalsaHeaders* headers() const { return headers_; } |
| BalsaHeaders* mutable_headers() { return headers_; } |
| |
| const BalsaHeaders* trailer() const { return trailer_; } |
| BalsaHeaders* mutable_trailer() { return trailer_; } |
| |
| size_t BytesSafeToSplice() const; |
| void BytesSpliced(size_t bytes_spliced); |
| |
| size_t ProcessInput(const char* input, size_t size) override; |
| |
| void set_allow_reading_until_close_for_request(bool set) { |
| allow_reading_until_close_for_request_ = set; |
| } |
| |
| // For websockets and possibly other uses, we suspend the usual expectations |
| // about when a message has a body and how long it should be. |
| void AllowArbitraryBody() { |
| parse_state_ = BalsaFrameEnums::READING_UNTIL_CLOSE; |
| } |
| |
| protected: |
| inline BalsaHeadersEnums::ContentLengthStatus ProcessContentLengthLine( |
| size_t line_idx, size_t* length); |
| |
| inline void ProcessTransferEncodingLine(size_t line_idx); |
| |
| void ProcessFirstLine(const char* begin, const char* end); |
| |
| void CleanUpKeyValueWhitespace(const char* stream_begin, |
| const char* line_begin, const char* current, |
| const char* line_end, |
| HeaderLineDescription* current_header_line); |
| |
| void ProcessHeaderLines(const Lines& lines, bool is_trailer, |
| BalsaHeaders* headers); |
| |
| // Returns true if there are invalid characters, false otherwise. |
| // Will also update counts per invalid character in invalid_chars_. |
| bool CheckHeaderLinesForInvalidChars(const Lines& lines, |
| const BalsaHeaders* headers); |
| |
| inline size_t ProcessHeaders(const char* message_start, |
| size_t message_length); |
| |
| void AssignParseStateAfterHeadersHaveBeenParsed(); |
| |
| inline bool LineFramingFound(char current_char) { |
| return current_char == '\n'; |
| } |
| |
| // TODO(fenix): get rid of the following function and its uses (and |
| // replace with something more efficient). |
| // Return header framing pattern. Non-zero return value indicates found, |
| // which has two possible outcomes: kValidTerm1, which means \n\r\n |
| // or kValidTerm2, which means \n\n. Zero return value means not found. |
| inline int32_t HeaderFramingFound(char current_char) { |
| // Note that the 'if (current_char == '\n' ...)' test exists to ensure that |
| // the HeaderFramingMayBeFound test works properly. In benchmarking done on |
| // 2/13/2008, the 'if' actually speeds up performance of the function |
| // anyway.. |
| if (current_char == '\n' || current_char == '\r') { |
| term_chars_ <<= 8; |
| // This is necessary IFF architecture has > 8 bit char. Alas, I'm |
| // paranoid. |
| term_chars_ |= current_char & 0xFF; |
| |
| if ((term_chars_ & kValidTerm1Mask) == kValidTerm1) { |
| term_chars_ = 0; |
| return kValidTerm1; |
| } |
| if ((term_chars_ & kValidTerm2Mask) == kValidTerm2) { |
| term_chars_ = 0; |
| return kValidTerm2; |
| } |
| } else { |
| term_chars_ = 0; |
| } |
| return 0; |
| } |
| |
| inline bool HeaderFramingMayBeFound() const { return term_chars_ != 0; } |
| |
| private: |
| friend class test::BalsaFrameTestPeer; |
| |
| // Calls HandleError() and returns false on error. |
| bool FindColonsAndParseIntoKeyValue(const Lines& lines, bool is_trailer, |
| BalsaHeaders* headers); |
| |
| void HandleError(BalsaFrameEnums::ErrorCode error_code); |
| void HandleWarning(BalsaFrameEnums::ErrorCode error_code); |
| |
| bool last_char_was_slash_r_; |
| bool saw_non_newline_char_; |
| bool start_was_space_; |
| bool chunk_length_character_extracted_; |
| bool is_request_; // This is not reset in Reset() |
| // Generally, requests are not allowed to frame with connection: close. For |
| // protocols which do their own protocol-specific chunking, such as streamed |
| // stubby, we allow connection close semantics for requests. |
| bool allow_reading_until_close_for_request_; |
| bool request_was_head_; // This is not reset in Reset() |
| size_t max_header_length_; // This is not reset in Reset() |
| BalsaVisitorInterface* visitor_; |
| size_t chunk_length_remaining_; |
| size_t content_length_remaining_; |
| const char* last_slash_n_loc_; |
| const char* last_recorded_slash_n_loc_; |
| size_t last_slash_n_idx_; |
| uint32_t term_chars_; |
| BalsaFrameEnums::ParseState parse_state_; |
| BalsaFrameEnums::ErrorCode last_error_; |
| absl::flat_hash_map<char, int> invalid_chars_; |
| |
| Lines lines_; |
| |
| BalsaHeaders* continue_headers_; // This is not reset to nullptr in Reset(). |
| BalsaHeaders* headers_; // This is not reset to nullptr in Reset(). |
| NoOpBalsaVisitor do_nothing_visitor_; |
| |
| Lines trailer_lines_; |
| size_t start_of_trailer_line_; |
| size_t trailer_length_; |
| BalsaHeaders* trailer_; // Does not own and is not reset to nullptr |
| // in Reset(). |
| InvalidCharsLevel invalid_chars_level_; // This is not reset in Reset() |
| |
| quiche::HttpValidationPolicy http_validation_policy_; |
| }; |
| |
| } // namespace quiche |
| |
| #endif // QUICHE_BALSA_BALSA_FRAME_H_ |