blob: 70d3aafcc08e29f2280c3ec575f9c188d555b775 [file] [log] [blame]
// 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 "absl/container/flat_hash_map.h"
#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"
#include "quiche/common/platform/api/quiche_flag_utils.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::BALSA_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 `trailer` 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_CODE_COUNT(balsa_trailer_in_request);
}
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_