| // 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. |
| |
| #include "quiche/balsa/balsa_headers.h" |
| |
| #include <sys/types.h> |
| |
| #include <cstdint> |
| #include <functional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "absl/container/flat_hash_set.h" |
| #include "absl/strings/ascii.h" |
| #include "absl/strings/match.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/str_format.h" |
| #include "absl/strings/str_join.h" |
| #include "absl/strings/string_view.h" |
| #include "quiche/balsa/balsa_enums.h" |
| #include "quiche/balsa/header_properties.h" |
| #include "quiche/common/platform/api/quiche_header_policy.h" |
| #include "quiche/common/platform/api/quiche_logging.h" |
| |
| namespace { |
| |
| constexpr absl::string_view kContentLength("Content-Length"); |
| constexpr absl::string_view kCookie("Cookie"); |
| constexpr absl::string_view kHost("Host"); |
| constexpr absl::string_view kTransferEncoding("Transfer-Encoding"); |
| |
| // The following list defines list of headers that Envoy considers multivalue. |
| // Headers on this list are coalesced by EFG in order to provide forward |
| // compatibility with Envoy behavior. See b/143490671 for details. |
| // Date, Last-Modified and Location are excluded because they're found on Chrome |
| // HttpUtil::IsNonCoalescingHeader() list. |
| #define ALL_ENVOY_HEADERS(HEADER_FUNC) \ |
| HEADER_FUNC("Accept") \ |
| HEADER_FUNC("Accept-Encoding") \ |
| HEADER_FUNC("Access-Control-Request-Headers") \ |
| HEADER_FUNC("Access-Control-Request-Method") \ |
| HEADER_FUNC("Access-Control-Allow-Origin") \ |
| HEADER_FUNC("Access-Control-Allow-Headers") \ |
| HEADER_FUNC("Access-Control-Allow-Methods") \ |
| HEADER_FUNC("Access-Control-Allow-Credentials") \ |
| HEADER_FUNC("Access-Control-Expose-Headers") \ |
| HEADER_FUNC("Access-Control-Max-Age") \ |
| HEADER_FUNC("Authorization") \ |
| HEADER_FUNC("Cache-Control") \ |
| HEADER_FUNC("X-Client-Trace-Id") \ |
| HEADER_FUNC("Connection") \ |
| HEADER_FUNC("Content-Encoding") \ |
| HEADER_FUNC("Content-Length") \ |
| HEADER_FUNC("Content-Type") \ |
| /* HEADER_FUNC("Date") */ \ |
| HEADER_FUNC("Envoy-Attempt-Count") \ |
| HEADER_FUNC("Envoy-Degraded") \ |
| HEADER_FUNC("Envoy-Decorator-Operation") \ |
| HEADER_FUNC("Envoy-Downstream-Service-Cluster") \ |
| HEADER_FUNC("Envoy-Downstream-Service-Node") \ |
| HEADER_FUNC("Envoy-Expected-Request-Timeout-Ms") \ |
| HEADER_FUNC("Envoy-External-Address") \ |
| HEADER_FUNC("Envoy-Force-Trace") \ |
| HEADER_FUNC("Envoy-Hedge-On-Per-Try-Timeout") \ |
| HEADER_FUNC("Envoy-Immediate-Health-Check-Fail") \ |
| HEADER_FUNC("Envoy-Internal-Request") \ |
| HEADER_FUNC("Envoy-Ip-Tags") \ |
| HEADER_FUNC("Envoy-Max-Retries") \ |
| HEADER_FUNC("Envoy-Original-Path") \ |
| HEADER_FUNC("Envoy-Original-Url") \ |
| HEADER_FUNC("Envoy-Overloaded") \ |
| HEADER_FUNC("Envoy-Rate-Limited") \ |
| HEADER_FUNC("Envoy-Retry-On") \ |
| HEADER_FUNC("Envoy-Retry-Grpc-On") \ |
| HEADER_FUNC("Envoy-Retriable-StatusCodes") \ |
| HEADER_FUNC("Envoy-Retriable-HeaderNames") \ |
| HEADER_FUNC("Envoy-Upstream-AltStatName") \ |
| HEADER_FUNC("Envoy-Upstream-Canary") \ |
| HEADER_FUNC("Envoy-Upstream-HealthCheckedCluster") \ |
| HEADER_FUNC("Envoy-Upstream-RequestPerTryTimeoutMs") \ |
| HEADER_FUNC("Envoy-Upstream-RequestTimeoutAltResponse") \ |
| HEADER_FUNC("Envoy-Upstream-RequestTimeoutMs") \ |
| HEADER_FUNC("Envoy-Upstream-ServiceTime") \ |
| HEADER_FUNC("Etag") \ |
| HEADER_FUNC("Expect") \ |
| HEADER_FUNC("X-Forwarded-Client-Cert") \ |
| HEADER_FUNC("X-Forwarded-For") \ |
| HEADER_FUNC("X-Forwarded-Proto") \ |
| HEADER_FUNC("Grpc-Accept-Encoding") \ |
| HEADER_FUNC("Grpc-Message") \ |
| HEADER_FUNC("Grpc-Status") \ |
| HEADER_FUNC("Grpc-Timeout") \ |
| HEADER_FUNC("Host") \ |
| HEADER_FUNC("Keep-Alive") \ |
| /* HEADER_FUNC("Last-Modified") */ \ |
| /* HEADER_FUNC("Location") */ \ |
| HEADER_FUNC("Method") \ |
| HEADER_FUNC("No-Chunks") \ |
| HEADER_FUNC("Origin") \ |
| HEADER_FUNC("X-Ot-Span-Context") \ |
| HEADER_FUNC("Path") \ |
| HEADER_FUNC("Protocol") \ |
| HEADER_FUNC("Proxy-Connection") \ |
| HEADER_FUNC("Referer") \ |
| HEADER_FUNC("X-Request-Id") \ |
| HEADER_FUNC("Scheme") \ |
| HEADER_FUNC("Server") \ |
| HEADER_FUNC("Status") \ |
| HEADER_FUNC("TE") \ |
| HEADER_FUNC("Transfer-Encoding") \ |
| HEADER_FUNC("Upgrade") \ |
| HEADER_FUNC("User-Agent") \ |
| HEADER_FUNC("Vary") \ |
| HEADER_FUNC("Via") |
| |
| // HEADER_FUNC to insert "name" into the MultivaluedHeadersSet of Envoy headers. |
| #define MULTIVALUE_ENVOY_HEADER(name) {name}, |
| |
| absl::string_view::difference_type FindIgnoreCase(absl::string_view haystack, |
| absl::string_view needle) { |
| absl::string_view::difference_type pos = 0; |
| while (haystack.size() >= needle.size()) { |
| if (absl::StartsWithIgnoreCase(haystack, needle)) { |
| return pos; |
| } |
| ++pos; |
| haystack.remove_prefix(1); |
| } |
| |
| return absl::string_view::npos; |
| } |
| |
| absl::string_view::difference_type RemoveLeadingWhitespace( |
| absl::string_view* text) { |
| size_t count = 0; |
| const char* ptr = text->data(); |
| while (count < text->size() && absl::ascii_isspace(*ptr)) { |
| count++; |
| ptr++; |
| } |
| text->remove_prefix(count); |
| return count; |
| } |
| |
| absl::string_view::difference_type RemoveTrailingWhitespace( |
| absl::string_view* text) { |
| size_t count = 0; |
| const char* ptr = text->data() + text->size() - 1; |
| while (count < text->size() && absl::ascii_isspace(*ptr)) { |
| ++count; |
| --ptr; |
| } |
| text->remove_suffix(count); |
| return count; |
| } |
| |
| absl::string_view::difference_type RemoveWhitespaceContext( |
| absl::string_view* text) { |
| return RemoveLeadingWhitespace(text) + RemoveTrailingWhitespace(text); |
| } |
| |
| } // namespace |
| |
| namespace quiche { |
| |
| const size_t BalsaBuffer::kDefaultBlocksize; |
| |
| const BalsaHeaders::MultivaluedHeadersSet& |
| BalsaHeaders::multivalued_envoy_headers() { |
| static const MultivaluedHeadersSet* multivalued_envoy_headers = |
| new MultivaluedHeadersSet({ALL_ENVOY_HEADERS(MULTIVALUE_ENVOY_HEADER)}); |
| return *multivalued_envoy_headers; |
| } |
| |
| void BalsaHeaders::ParseTokenList(absl::string_view header_value, |
| HeaderTokenList* tokens) { |
| if (header_value.empty()) { |
| return; |
| } |
| const char* start = header_value.begin(); |
| const char* end = header_value.end(); |
| while (true) { |
| // Cast `*start` to unsigned char to make values above 127 rank as expected |
| // on platforms with signed char, where such values are represented as |
| // negative numbers before the cast. |
| |
| // search for first nonwhitespace, non separator char. |
| while (*start == ',' || static_cast<unsigned char>(*start) <= ' ') { |
| ++start; |
| if (start == end) { |
| return; |
| } |
| } |
| // found. marked. |
| const char* nws = start; |
| |
| // search for next whitspace or separator char. |
| while (*start != ',' && static_cast<unsigned char>(*start) > ' ') { |
| ++start; |
| if (start == end) { |
| if (nws != start) { |
| tokens->push_back(absl::string_view(nws, start - nws)); |
| } |
| return; |
| } |
| } |
| tokens->push_back(absl::string_view(nws, start - nws)); |
| } |
| } |
| |
| // This can be called after a std::move() operation, so things might be |
| // in an unspecified state after the move. |
| void BalsaHeaders::Clear() { |
| balsa_buffer_.Clear(); |
| transfer_encoding_is_chunked_ = false; |
| content_length_ = 0; |
| content_length_status_ = BalsaHeadersEnums::NO_CONTENT_LENGTH; |
| parsed_response_code_ = 0; |
| firstline_buffer_base_idx_ = 0; |
| whitespace_1_idx_ = 0; |
| non_whitespace_1_idx_ = 0; |
| whitespace_2_idx_ = 0; |
| non_whitespace_2_idx_ = 0; |
| whitespace_3_idx_ = 0; |
| non_whitespace_3_idx_ = 0; |
| whitespace_4_idx_ = 0; |
| header_lines_.clear(); |
| header_lines_.shrink_to_fit(); |
| } |
| |
| void BalsaHeaders::CopyFrom(const BalsaHeaders& other) { |
| // Protect against copying with self. |
| if (this == &other) { |
| return; |
| } |
| |
| balsa_buffer_.CopyFrom(other.balsa_buffer_); |
| transfer_encoding_is_chunked_ = other.transfer_encoding_is_chunked_; |
| content_length_ = other.content_length_; |
| content_length_status_ = other.content_length_status_; |
| parsed_response_code_ = other.parsed_response_code_; |
| firstline_buffer_base_idx_ = other.firstline_buffer_base_idx_; |
| whitespace_1_idx_ = other.whitespace_1_idx_; |
| non_whitespace_1_idx_ = other.non_whitespace_1_idx_; |
| whitespace_2_idx_ = other.whitespace_2_idx_; |
| non_whitespace_2_idx_ = other.non_whitespace_2_idx_; |
| whitespace_3_idx_ = other.whitespace_3_idx_; |
| non_whitespace_3_idx_ = other.non_whitespace_3_idx_; |
| whitespace_4_idx_ = other.whitespace_4_idx_; |
| header_lines_ = other.header_lines_; |
| } |
| |
| void BalsaHeaders::AddAndMakeDescription(absl::string_view key, |
| absl::string_view value, |
| HeaderLineDescription* d) { |
| QUICHE_CHECK(d != nullptr); |
| |
| if (enforce_header_policy_) { |
| QuicheHandleHeaderPolicy(key); |
| } |
| |
| // + 2 to size for ": " |
| size_t line_size = key.size() + 2 + value.size(); |
| BalsaBuffer::Blocks::size_type block_buffer_idx = 0; |
| char* storage = balsa_buffer_.Reserve(line_size, &block_buffer_idx); |
| size_t base_idx = storage - GetPtr(block_buffer_idx); |
| |
| char* cur_loc = storage; |
| memcpy(cur_loc, key.data(), key.size()); |
| cur_loc += key.size(); |
| *cur_loc = ':'; |
| ++cur_loc; |
| *cur_loc = ' '; |
| ++cur_loc; |
| memcpy(cur_loc, value.data(), value.size()); |
| *d = HeaderLineDescription( |
| base_idx, base_idx + key.size(), base_idx + key.size() + 2, |
| base_idx + key.size() + 2 + value.size(), block_buffer_idx); |
| } |
| |
| void BalsaHeaders::AppendAndMakeDescription(absl::string_view key, |
| absl::string_view value, |
| HeaderLineDescription* d) { |
| // Figure out how much space we need to reserve for the new header size. |
| size_t old_value_size = d->last_char_idx - d->value_begin_idx; |
| if (old_value_size == 0) { |
| AddAndMakeDescription(key, value, d); |
| return; |
| } |
| absl::string_view old_value(GetPtr(d->buffer_base_idx) + d->value_begin_idx, |
| old_value_size); |
| |
| BalsaBuffer::Blocks::size_type block_buffer_idx = 0; |
| // + 3 because we potentially need to add ": ", and "," to the line. |
| size_t new_size = key.size() + 3 + old_value_size + value.size(); |
| char* storage = balsa_buffer_.Reserve(new_size, &block_buffer_idx); |
| size_t base_idx = storage - GetPtr(block_buffer_idx); |
| |
| absl::string_view first_value = old_value; |
| absl::string_view second_value = value; |
| char* cur_loc = storage; |
| memcpy(cur_loc, key.data(), key.size()); |
| cur_loc += key.size(); |
| *cur_loc = ':'; |
| ++cur_loc; |
| *cur_loc = ' '; |
| ++cur_loc; |
| memcpy(cur_loc, first_value.data(), first_value.size()); |
| cur_loc += first_value.size(); |
| *cur_loc = ','; |
| ++cur_loc; |
| memcpy(cur_loc, second_value.data(), second_value.size()); |
| |
| *d = HeaderLineDescription(base_idx, base_idx + key.size(), |
| base_idx + key.size() + 2, base_idx + new_size, |
| block_buffer_idx); |
| } |
| |
| // Reset internal flags for chunked transfer encoding or content length if a |
| // header we're removing is one of those headers. |
| void BalsaHeaders::MaybeClearSpecialHeaderValues(absl::string_view key) { |
| if (absl::EqualsIgnoreCase(key, kContentLength)) { |
| if (transfer_encoding_is_chunked_) { |
| return; |
| } |
| |
| content_length_status_ = BalsaHeadersEnums::NO_CONTENT_LENGTH; |
| content_length_ = 0; |
| return; |
| } |
| |
| if (absl::EqualsIgnoreCase(key, kTransferEncoding)) { |
| transfer_encoding_is_chunked_ = false; |
| } |
| } |
| |
| // Removes all keys value pairs with key 'key' starting at 'start'. |
| void BalsaHeaders::RemoveAllOfHeaderStartingAt(absl::string_view key, |
| HeaderLines::iterator start) { |
| MaybeClearSpecialHeaderValues(key); |
| while (start != header_lines_.end()) { |
| start->skip = true; |
| ++start; |
| start = GetHeaderLinesIterator(key, start); |
| } |
| } |
| |
| void BalsaHeaders::ReplaceOrAppendHeader(absl::string_view key, |
| absl::string_view value) { |
| const HeaderLines::iterator end = header_lines_.end(); |
| const HeaderLines::iterator begin = header_lines_.begin(); |
| HeaderLines::iterator i = GetHeaderLinesIterator(key, begin); |
| if (i != end) { |
| // First, remove all of the header lines including this one. We want to |
| // remove before replacing, in case our replacement ends up being appended |
| // at the end (and thus would be removed by this call) |
| RemoveAllOfHeaderStartingAt(key, i); |
| // Now, take the first instance and replace it. This will remove the |
| // 'skipped' tag if the replacement is done in-place. |
| AddAndMakeDescription(key, value, &(*i)); |
| return; |
| } |
| AppendHeader(key, value); |
| } |
| |
| void BalsaHeaders::AppendHeader(absl::string_view key, |
| absl::string_view value) { |
| HeaderLineDescription hld; |
| AddAndMakeDescription(key, value, &hld); |
| header_lines_.push_back(hld); |
| } |
| |
| void BalsaHeaders::AppendToHeader(absl::string_view key, |
| absl::string_view value) { |
| HeaderLines::iterator i = GetHeaderLinesIterator(key, header_lines_.begin()); |
| if (i == header_lines_.end()) { |
| // The header did not exist already. Instead of appending to an existing |
| // header simply append the key/value pair to the headers. |
| AppendHeader(key, value); |
| return; |
| } |
| HeaderLineDescription hld = *i; |
| |
| AppendAndMakeDescription(key, value, &hld); |
| |
| // Invalidate the old header line and add the new one. |
| i->skip = true; |
| header_lines_.push_back(hld); |
| } |
| |
| void BalsaHeaders::AppendToHeaderWithCommaAndSpace(absl::string_view key, |
| absl::string_view value) { |
| HeaderLines::iterator i = GetHeaderLinesIteratorForLastMultivaluedHeader(key); |
| if (i == header_lines_.end()) { |
| // The header did not exist already. Instead of appending to an existing |
| // header simply append the key/value pair to the headers. No extra |
| // space will be added before the value. |
| AppendHeader(key, value); |
| return; |
| } |
| |
| std::string space_and_value = absl::StrCat(" ", value); |
| |
| HeaderLineDescription hld = *i; |
| AppendAndMakeDescription(key, space_and_value, &hld); |
| |
| // Invalidate the old header line and add the new one. |
| i->skip = true; |
| header_lines_.push_back(hld); |
| } |
| |
| absl::string_view BalsaHeaders::GetValueFromHeaderLineDescription( |
| const HeaderLineDescription& line) const { |
| QUICHE_DCHECK_GE(line.last_char_idx, line.value_begin_idx); |
| return absl::string_view(GetPtr(line.buffer_base_idx) + line.value_begin_idx, |
| line.last_char_idx - line.value_begin_idx); |
| } |
| |
| absl::string_view BalsaHeaders::GetHeader(absl::string_view key) const { |
| QUICHE_DCHECK(!header_properties::IsMultivaluedHeader(key)) |
| << "Header '" << key << "' may consist of multiple lines. Do not " |
| << "use BalsaHeaders::GetHeader() or you may be missing some of its " |
| << "values."; |
| const HeaderLines::const_iterator end = header_lines_.end(); |
| HeaderLines::const_iterator i = GetConstHeaderLinesIterator(key); |
| if (i == end) { |
| return absl::string_view(); |
| } |
| return GetValueFromHeaderLineDescription(*i); |
| } |
| |
| BalsaHeaders::const_header_lines_iterator BalsaHeaders::GetHeaderPosition( |
| absl::string_view key) const { |
| const HeaderLines::const_iterator end = header_lines_.end(); |
| HeaderLines::const_iterator i = GetConstHeaderLinesIterator(key); |
| if (i == end) { |
| // TODO(tgreer) Convert from HeaderLines::const_iterator to |
| // const_header_lines_iterator without calling lines().end(), which is |
| // nontrivial. Look for other needless calls to lines().end(), or make |
| // lines().end() trivial. |
| return lines().end(); |
| } |
| |
| return const_header_lines_iterator(this, (i - header_lines_.begin())); |
| } |
| |
| BalsaHeaders::const_header_lines_key_iterator BalsaHeaders::GetIteratorForKey( |
| absl::string_view key) const { |
| HeaderLines::const_iterator i = GetConstHeaderLinesIterator(key); |
| if (i == header_lines_.end()) { |
| return header_lines_key_end(); |
| } |
| |
| return const_header_lines_key_iterator(this, (i - header_lines_.begin()), |
| key); |
| } |
| |
| BalsaHeaders::HeaderLines::const_iterator |
| BalsaHeaders::GetConstHeaderLinesIterator(absl::string_view key) const { |
| const HeaderLines::const_iterator end = header_lines_.end(); |
| for (HeaderLines::const_iterator i = header_lines_.begin(); i != end; ++i) { |
| const HeaderLineDescription& line = *i; |
| if (line.skip) { |
| continue; |
| } |
| const absl::string_view current_key( |
| GetPtr(line.buffer_base_idx) + line.first_char_idx, |
| line.key_end_idx - line.first_char_idx); |
| if (absl::EqualsIgnoreCase(current_key, key)) { |
| QUICHE_DCHECK_GE(line.last_char_idx, line.value_begin_idx); |
| return i; |
| } |
| } |
| return end; |
| } |
| |
| BalsaHeaders::HeaderLines::iterator BalsaHeaders::GetHeaderLinesIterator( |
| absl::string_view key, BalsaHeaders::HeaderLines::iterator start) { |
| const HeaderLines::iterator end = header_lines_.end(); |
| for (HeaderLines::iterator i = start; i != end; ++i) { |
| const HeaderLineDescription& line = *i; |
| if (line.skip) { |
| continue; |
| } |
| const absl::string_view current_key( |
| GetPtr(line.buffer_base_idx) + line.first_char_idx, |
| line.key_end_idx - line.first_char_idx); |
| if (absl::EqualsIgnoreCase(current_key, key)) { |
| QUICHE_DCHECK_GE(line.last_char_idx, line.value_begin_idx); |
| return i; |
| } |
| } |
| return end; |
| } |
| |
| BalsaHeaders::HeaderLines::iterator |
| BalsaHeaders::GetHeaderLinesIteratorForLastMultivaluedHeader( |
| absl::string_view key) { |
| const HeaderLines::iterator end = header_lines_.end(); |
| HeaderLines::iterator last_found_match; |
| bool found_a_match = false; |
| for (HeaderLines::iterator i = header_lines_.begin(); i != end; ++i) { |
| const HeaderLineDescription& line = *i; |
| if (line.skip) { |
| continue; |
| } |
| const absl::string_view current_key( |
| GetPtr(line.buffer_base_idx) + line.first_char_idx, |
| line.key_end_idx - line.first_char_idx); |
| if (absl::EqualsIgnoreCase(current_key, key)) { |
| QUICHE_DCHECK_GE(line.last_char_idx, line.value_begin_idx); |
| last_found_match = i; |
| found_a_match = true; |
| } |
| } |
| return (found_a_match ? last_found_match : end); |
| } |
| |
| void BalsaHeaders::GetAllOfHeader(absl::string_view key, |
| std::vector<absl::string_view>* out) const { |
| for (const_header_lines_key_iterator it = GetIteratorForKey(key); |
| it != lines().end(); ++it) { |
| out->push_back(it->second); |
| } |
| } |
| |
| void BalsaHeaders::GetAllOfHeaderIncludeRemoved( |
| absl::string_view key, std::vector<absl::string_view>* out) const { |
| const HeaderLines::const_iterator begin = header_lines_.begin(); |
| const HeaderLines::const_iterator end = header_lines_.end(); |
| for (bool add_removed : {false, true}) { |
| for (HeaderLines::const_iterator i = begin; i != end; ++i) { |
| const HeaderLineDescription& line = *i; |
| if ((!add_removed && line.skip) || (add_removed && !line.skip)) { |
| continue; |
| } |
| const absl::string_view current_key( |
| GetPtr(line.buffer_base_idx) + line.first_char_idx, |
| line.key_end_idx - line.first_char_idx); |
| if (absl::EqualsIgnoreCase(current_key, key)) { |
| QUICHE_DCHECK_GE(line.last_char_idx, line.value_begin_idx); |
| out->push_back(GetValueFromHeaderLineDescription(line)); |
| } |
| } |
| } |
| } |
| |
| namespace { |
| |
| // Helper function for HeaderHasValue that checks that the specified region |
| // within line is preceded by whitespace and a comma or beginning of line, |
| // and followed by whitespace and a comma or end of line. |
| bool SurroundedOnlyBySpacesAndCommas(absl::string_view::difference_type idx, |
| absl::string_view::difference_type end_idx, |
| absl::string_view line) { |
| for (idx = idx - 1; idx >= 0; --idx) { |
| if (line[idx] == ',') { |
| break; |
| } |
| if (line[idx] != ' ') { |
| return false; |
| } |
| } |
| |
| for (; end_idx < static_cast<int64_t>(line.size()); ++end_idx) { |
| if (line[end_idx] == ',') { |
| break; |
| } |
| if (line[end_idx] != ' ') { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| } // namespace |
| |
| bool BalsaHeaders::HeaderHasValueHelper(absl::string_view key, |
| absl::string_view value, |
| bool case_sensitive) const { |
| for (const_header_lines_key_iterator it = GetIteratorForKey(key); |
| it != lines().end(); ++it) { |
| absl::string_view line = it->second; |
| absl::string_view::size_type idx = |
| case_sensitive ? line.find(value, 0) : FindIgnoreCase(line, value); |
| while (idx != absl::string_view::npos) { |
| absl::string_view::difference_type end_idx = idx + value.size(); |
| if (SurroundedOnlyBySpacesAndCommas(idx, end_idx, line)) { |
| return true; |
| } |
| idx = line.find(value, idx + 1); |
| } |
| } |
| return false; |
| } |
| |
| bool BalsaHeaders::HasNonEmptyHeader(absl::string_view key) const { |
| for (const_header_lines_key_iterator it = GetIteratorForKey(key); |
| it != header_lines_key_end(); ++it) { |
| if (!it->second.empty()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| std::string BalsaHeaders::GetAllOfHeaderAsString(absl::string_view key) const { |
| // Use custom formatter to ignore header key and join only header values. |
| // absl::AlphaNumFormatter is the default formatter for absl::StrJoin(). |
| auto formatter = [](std::string* out, |
| std::pair<absl::string_view, absl::string_view> header) { |
| return absl::AlphaNumFormatter()(out, header.second); |
| }; |
| return absl::StrJoin(GetIteratorForKey(key), header_lines_key_end(), ",", |
| formatter); |
| } |
| |
| void BalsaHeaders::RemoveAllOfHeaderInList(const HeaderTokenList& keys) { |
| if (keys.empty()) { |
| return; |
| } |
| |
| // This extra copy sacrifices some performance to prevent the possible |
| // mistakes that the caller does not lower case the headers in keys. |
| // Better performance can be achieved by asking caller to lower case |
| // the keys and RemoveAllOfheaderInlist just does lookup. |
| absl::flat_hash_set<std::string> lowercase_keys; |
| for (const auto& key : keys) { |
| MaybeClearSpecialHeaderValues(key); |
| lowercase_keys.insert(absl::AsciiStrToLower(key)); |
| } |
| |
| for (HeaderLineDescription& line : header_lines_) { |
| if (line.skip) { |
| continue; |
| } |
| // Remove the header if it matches any of the keys to remove. |
| const size_t key_len = line.key_end_idx - line.first_char_idx; |
| absl::string_view key(GetPtr(line.buffer_base_idx) + line.first_char_idx, |
| key_len); |
| |
| std::string lowercase_key = absl::AsciiStrToLower(key); |
| if (lowercase_keys.count(lowercase_key) != 0) { |
| line.skip = true; |
| } |
| } |
| } |
| |
| void BalsaHeaders::RemoveAllOfHeader(absl::string_view key) { |
| HeaderLines::iterator it = GetHeaderLinesIterator(key, header_lines_.begin()); |
| RemoveAllOfHeaderStartingAt(key, it); |
| } |
| |
| void BalsaHeaders::RemoveAllHeadersWithPrefix(absl::string_view prefix) { |
| for (HeaderLines::size_type i = 0; i < header_lines_.size(); ++i) { |
| if (header_lines_[i].skip) { |
| continue; |
| } |
| |
| HeaderLineDescription& line = header_lines_[i]; |
| const size_t key_len = line.key_end_idx - line.first_char_idx; |
| if (key_len < prefix.size()) { |
| continue; |
| } |
| |
| const absl::string_view current_key_prefix( |
| GetPtr(line.buffer_base_idx) + line.first_char_idx, prefix.size()); |
| if (absl::EqualsIgnoreCase(current_key_prefix, prefix)) { |
| const absl::string_view current_key( |
| GetPtr(line.buffer_base_idx) + line.first_char_idx, key_len); |
| MaybeClearSpecialHeaderValues(current_key); |
| line.skip = true; |
| } |
| } |
| } |
| |
| bool BalsaHeaders::HasHeadersWithPrefix(absl::string_view prefix) const { |
| for (HeaderLines::size_type i = 0; i < header_lines_.size(); ++i) { |
| if (header_lines_[i].skip) { |
| continue; |
| } |
| |
| const HeaderLineDescription& line = header_lines_[i]; |
| if (line.key_end_idx - line.first_char_idx < prefix.size()) { |
| continue; |
| } |
| |
| const absl::string_view current_key_prefix( |
| GetPtr(line.buffer_base_idx) + line.first_char_idx, prefix.size()); |
| if (absl::EqualsIgnoreCase(current_key_prefix, prefix)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| void BalsaHeaders::GetAllOfHeaderWithPrefix( |
| absl::string_view prefix, |
| std::vector<std::pair<absl::string_view, absl::string_view>>* out) const { |
| for (HeaderLines::size_type i = 0; i < header_lines_.size(); ++i) { |
| if (header_lines_[i].skip) { |
| continue; |
| } |
| const HeaderLineDescription& line = header_lines_[i]; |
| absl::string_view key(GetPtr(line.buffer_base_idx) + line.first_char_idx, |
| line.key_end_idx - line.first_char_idx); |
| if (absl::StartsWithIgnoreCase(key, prefix)) { |
| out->push_back(std::make_pair( |
| key, |
| absl::string_view(GetPtr(line.buffer_base_idx) + line.value_begin_idx, |
| line.last_char_idx - line.value_begin_idx))); |
| } |
| } |
| } |
| |
| void BalsaHeaders::GetAllHeadersWithLimit( |
| std::vector<std::pair<absl::string_view, absl::string_view>>* out, |
| int limit) const { |
| for (HeaderLines::size_type i = 0; i < header_lines_.size(); ++i) { |
| if (limit >= 0 && out->size() >= static_cast<size_t>(limit)) { |
| return; |
| } |
| if (header_lines_[i].skip) { |
| continue; |
| } |
| const HeaderLineDescription& line = header_lines_[i]; |
| absl::string_view key(GetPtr(line.buffer_base_idx) + line.first_char_idx, |
| line.key_end_idx - line.first_char_idx); |
| out->push_back(std::make_pair( |
| key, |
| absl::string_view(GetPtr(line.buffer_base_idx) + line.value_begin_idx, |
| line.last_char_idx - line.value_begin_idx))); |
| } |
| } |
| |
| size_t BalsaHeaders::RemoveValue(absl::string_view key, |
| absl::string_view search_value) { |
| // Remove whitespace around search value. |
| absl::string_view needle = search_value; |
| RemoveWhitespaceContext(&needle); |
| QUICHE_BUG_IF(bug_22783_2, needle != search_value) |
| << "Search value should not be surrounded by spaces."; |
| |
| // We have nothing to do for empty needle strings. |
| if (needle.empty()) { |
| return 0; |
| } |
| |
| // The return value: number of removed values. |
| size_t removals = 0; |
| |
| // Iterate over all header lines matching key with skip=false. |
| for (HeaderLines::iterator it = |
| GetHeaderLinesIterator(key, header_lines_.begin()); |
| it != header_lines_.end(); it = GetHeaderLinesIterator(key, ++it)) { |
| HeaderLineDescription* line = &(*it); |
| |
| // If needle given to us is longer than this header, don't consider it. |
| if (line->ValuesLength() < needle.size()) { |
| continue; |
| } |
| |
| // If the values are equivalent, just remove the whole line. |
| char* buf = GetPtr(line->buffer_base_idx); // The head of our buffer. |
| char* value_begin = buf + line->value_begin_idx; |
| // StringPiece containing values that have yet to be processed. The head of |
| // this stringpiece will continually move forward, and its tail |
| // (head+length) will always remain the same. |
| absl::string_view values(value_begin, line->ValuesLength()); |
| RemoveWhitespaceContext(&values); |
| if (values.size() == needle.size()) { |
| if (values == needle) { |
| line->skip = true; |
| removals++; |
| } |
| continue; |
| } |
| |
| // Find all occurrences of the needle to be removed. |
| char* insertion = value_begin; |
| while (values.size() >= needle.size()) { |
| // Strip leading whitespace. |
| ssize_t cur_leading_whitespace = RemoveLeadingWhitespace(&values); |
| |
| // See if we've got a match (at least as a prefix). |
| bool found = absl::StartsWith(values, needle); |
| |
| // Find the entirety of this value (including trailing comma if existent). |
| const size_t next_comma = |
| values.find(',', /* pos = */ (found ? needle.size() : 0)); |
| const bool comma_found = next_comma != absl::string_view::npos; |
| const size_t cur_size = (comma_found ? next_comma + 1 : values.size()); |
| |
| // Make sure that our prefix match is a full match. |
| if (found && cur_size != needle.size()) { |
| absl::string_view cur(values.data(), cur_size); |
| if (comma_found) { |
| cur.remove_suffix(1); |
| } |
| RemoveTrailingWhitespace(&cur); |
| found = (cur.size() == needle.size()); |
| } |
| |
| // Move as necessary (avoid move just for the sake of leading whitespace). |
| if (found) { |
| removals++; |
| // Remove trailing comma if we happen to have found the last value. |
| if (!comma_found) { |
| // We modify insertion since it'll be used to update last_char_idx. |
| insertion--; |
| } |
| } else { |
| if (insertion + cur_leading_whitespace != values.data()) { |
| // Has the side-effect of also copying any trailing whitespace. |
| memmove(insertion, values.data(), cur_size); |
| insertion += cur_size; |
| } else { |
| insertion += cur_leading_whitespace + cur_size; |
| } |
| } |
| |
| // No longer consider the current value. (Increment.) |
| values.remove_prefix(cur_size); |
| } |
| // Move remaining data. |
| if (!values.empty()) { |
| if (insertion != values.data()) { |
| memmove(insertion, values.data(), values.size()); |
| } |
| insertion += values.size(); |
| } |
| // Set new line size. |
| if (insertion <= value_begin) { |
| // All values removed. |
| line->skip = true; |
| } else { |
| line->last_char_idx = insertion - buf; |
| } |
| } |
| |
| return removals; |
| } |
| |
| size_t BalsaHeaders::GetSizeForWriteBuffer() const { |
| // First add the space required for the first line + line separator. |
| size_t write_buf_size = whitespace_4_idx_ - non_whitespace_1_idx_ + 2; |
| // Then add the space needed for each header line to write out + line |
| // separator. |
| const HeaderLines::size_type end = header_lines_.size(); |
| for (HeaderLines::size_type i = 0; i < end; ++i) { |
| const HeaderLineDescription& line = header_lines_[i]; |
| if (!line.skip) { |
| // Add the key size and ": ". |
| write_buf_size += line.key_end_idx - line.first_char_idx + 2; |
| // Add the value size and the line separator. |
| write_buf_size += line.last_char_idx - line.value_begin_idx + 2; |
| } |
| } |
| // Finally tack on the terminal line separator. |
| return write_buf_size + 2; |
| } |
| |
| void BalsaHeaders::DumpToString(std::string* str) const { |
| DumpToPrefixedString(" ", str); |
| } |
| |
| std::string BalsaHeaders::DebugString() const { |
| std::string s; |
| DumpToString(&s); |
| return s; |
| } |
| |
| bool BalsaHeaders::ForEachHeader( |
| std::function<bool(const absl::string_view key, |
| const absl::string_view value)> |
| fn) const { |
| int s = header_lines_.size(); |
| for (int i = 0; i < s; ++i) { |
| const HeaderLineDescription& desc = header_lines_[i]; |
| if (!desc.skip && desc.KeyLength() > 0) { |
| const char* stream_begin = GetPtr(desc.buffer_base_idx); |
| if (!fn(absl::string_view(stream_begin + desc.first_char_idx, |
| desc.KeyLength()), |
| absl::string_view(stream_begin + desc.value_begin_idx, |
| desc.ValuesLength()))) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| void BalsaHeaders::DumpToPrefixedString(const char* spaces, |
| std::string* str) const { |
| const absl::string_view firstline = first_line(); |
| const int buffer_length = GetReadableBytesFromHeaderStream(); |
| // First check whether the header object is empty. |
| if (firstline.empty() && buffer_length == 0) { |
| absl::StrAppend(str, "\n", spaces, "<empty header>\n"); |
| return; |
| } |
| |
| // Then check whether the header is in a partially parsed state. If so, just |
| // dump the raw data. |
| if (!FramerIsDoneWriting()) { |
| absl::StrAppendFormat(str, "\n%s<incomplete header len: %d>\n%s%.*s\n", |
| spaces, buffer_length, spaces, buffer_length, |
| OriginalHeaderStreamBegin()); |
| return; |
| } |
| |
| // If the header is complete, then just dump them with the logical key value |
| // pair. |
| str->reserve(str->size() + GetSizeForWriteBuffer()); |
| absl::StrAppend(str, "\n", spaces, firstline, "\n"); |
| for (const auto& line : lines()) { |
| absl::StrAppend(str, spaces, line.first, ": ", line.second, "\n"); |
| } |
| } |
| |
| void BalsaHeaders::SetContentLength(size_t length) { |
| // If the content-length is already the one we want, don't do anything. |
| if (content_length_status_ == BalsaHeadersEnums::VALID_CONTENT_LENGTH && |
| content_length_ == length) { |
| return; |
| } |
| // If header state indicates that there is either a content length or |
| // transfer encoding header, remove them before adding the new content |
| // length. There is always the possibility that client can manually add |
| // either header directly and cause content_length_status_ or |
| // transfer_encoding_is_chunked_ to be inconsistent with the actual header. |
| // In the interest of efficiency, however, we will assume that clients will |
| // use the header object correctly and thus we will not scan the all headers |
| // each time this function is called. |
| if (content_length_status_ != BalsaHeadersEnums::NO_CONTENT_LENGTH) { |
| RemoveAllOfHeader(kContentLength); |
| } else if (transfer_encoding_is_chunked_) { |
| RemoveAllOfHeader(kTransferEncoding); |
| } |
| content_length_status_ = BalsaHeadersEnums::VALID_CONTENT_LENGTH; |
| content_length_ = length; |
| |
| AppendHeader(kContentLength, absl::StrCat(length)); |
| } |
| |
| void BalsaHeaders::SetTransferEncodingToChunkedAndClearContentLength() { |
| if (transfer_encoding_is_chunked_) { |
| return; |
| } |
| if (content_length_status_ != BalsaHeadersEnums::NO_CONTENT_LENGTH) { |
| // Per https://httpwg.org/specs/rfc7230.html#header.content-length, we can't |
| // send both transfer-encoding and content-length. |
| ClearContentLength(); |
| } |
| ReplaceOrAppendHeader(kTransferEncoding, "chunked"); |
| transfer_encoding_is_chunked_ = true; |
| } |
| |
| void BalsaHeaders::SetNoTransferEncoding() { |
| if (transfer_encoding_is_chunked_) { |
| // clears transfer_encoding_is_chunked_ |
| RemoveAllOfHeader(kTransferEncoding); |
| } |
| } |
| |
| void BalsaHeaders::ClearContentLength() { RemoveAllOfHeader(kContentLength); } |
| |
| bool BalsaHeaders::IsEmpty() const { |
| return balsa_buffer_.GetTotalBytesUsed() == 0; |
| } |
| |
| absl::string_view BalsaHeaders::Authority() const { return GetHeader(kHost); } |
| |
| void BalsaHeaders::ReplaceOrAppendAuthority(absl::string_view value) { |
| ReplaceOrAppendHeader(kHost, value); |
| } |
| |
| void BalsaHeaders::RemoveAuthority() { RemoveAllOfHeader(kHost); } |
| |
| void BalsaHeaders::ApplyToCookie( |
| std::function<void(absl::string_view cookie)> f) const { |
| f(GetHeader(kCookie)); |
| } |
| |
| void BalsaHeaders::SetResponseFirstline(absl::string_view version, |
| size_t parsed_response_code, |
| absl::string_view reason_phrase) { |
| SetFirstlineFromStringPieces(version, absl::StrCat(parsed_response_code), |
| reason_phrase); |
| parsed_response_code_ = parsed_response_code; |
| } |
| |
| void BalsaHeaders::SetFirstlineFromStringPieces(absl::string_view firstline_a, |
| absl::string_view firstline_b, |
| absl::string_view firstline_c) { |
| size_t line_size = |
| (firstline_a.size() + firstline_b.size() + firstline_c.size() + 2); |
| char* storage = balsa_buffer_.Reserve(line_size, &firstline_buffer_base_idx_); |
| char* cur_loc = storage; |
| |
| memcpy(cur_loc, firstline_a.data(), firstline_a.size()); |
| cur_loc += firstline_a.size(); |
| |
| *cur_loc = ' '; |
| ++cur_loc; |
| |
| memcpy(cur_loc, firstline_b.data(), firstline_b.size()); |
| cur_loc += firstline_b.size(); |
| |
| *cur_loc = ' '; |
| ++cur_loc; |
| |
| memcpy(cur_loc, firstline_c.data(), firstline_c.size()); |
| |
| whitespace_1_idx_ = storage - BeginningOfFirstLine(); |
| non_whitespace_1_idx_ = whitespace_1_idx_; |
| whitespace_2_idx_ = non_whitespace_1_idx_ + firstline_a.size(); |
| non_whitespace_2_idx_ = whitespace_2_idx_ + 1; |
| whitespace_3_idx_ = non_whitespace_2_idx_ + firstline_b.size(); |
| non_whitespace_3_idx_ = whitespace_3_idx_ + 1; |
| whitespace_4_idx_ = non_whitespace_3_idx_ + firstline_c.size(); |
| } |
| |
| void BalsaHeaders::SetRequestMethod(absl::string_view method) { |
| // This is the first of the three parts of the firstline. |
| if (method.size() <= (whitespace_2_idx_ - non_whitespace_1_idx_)) { |
| non_whitespace_1_idx_ = whitespace_2_idx_ - method.size(); |
| if (!method.empty()) { |
| char* stream_begin = BeginningOfFirstLine(); |
| memcpy(stream_begin + non_whitespace_1_idx_, method.data(), |
| method.size()); |
| } |
| } else { |
| // The new method is too large to fit in the space available for the old |
| // one, so we have to reformat the firstline. |
| SetRequestFirstlineFromStringPieces(method, request_uri(), |
| request_version()); |
| } |
| } |
| |
| void BalsaHeaders::SetResponseVersion(absl::string_view version) { |
| // Note: There is no difference between request_method() and |
| // response_Version(). Thus, a function to set one is equivalent to a |
| // function to set the other. We maintain two functions for this as it is |
| // much more descriptive, and makes code more understandable. |
| SetRequestMethod(version); |
| } |
| |
| void BalsaHeaders::SetRequestUri(absl::string_view uri) { |
| SetRequestFirstlineFromStringPieces(request_method(), uri, request_version()); |
| } |
| |
| void BalsaHeaders::SetResponseCode(absl::string_view code) { |
| // Note: There is no difference between request_uri() and response_code(). |
| // Thus, a function to set one is equivalent to a function to set the other. |
| // We maintain two functions for this as it is much more descriptive, and |
| // makes code more understandable. |
| SetRequestUri(code); |
| } |
| |
| void BalsaHeaders::SetParsedResponseCodeAndUpdateFirstline( |
| size_t parsed_response_code) { |
| parsed_response_code_ = parsed_response_code; |
| SetResponseCode(absl::StrCat(parsed_response_code)); |
| } |
| |
| void BalsaHeaders::SetRequestVersion(absl::string_view version) { |
| // This is the last of the three parts of the firstline. |
| // Since whitespace_3_idx and non_whitespace_3_idx may point to the same |
| // place, we ensure below that any available space includes space for a |
| // literal space (' ') character between the second component and the third |
| // component. |
| bool fits_in_space_allowed = |
| version.size() + 1 <= whitespace_4_idx_ - whitespace_3_idx_; |
| |
| if (!fits_in_space_allowed) { |
| // If the new version is too large, then reformat the firstline. |
| SetRequestFirstlineFromStringPieces(request_method(), request_uri(), |
| version); |
| return; |
| } |
| |
| char* stream_begin = BeginningOfFirstLine(); |
| *(stream_begin + whitespace_3_idx_) = ' '; |
| non_whitespace_3_idx_ = whitespace_3_idx_ + 1; |
| whitespace_4_idx_ = non_whitespace_3_idx_ + version.size(); |
| memcpy(stream_begin + non_whitespace_3_idx_, version.data(), version.size()); |
| } |
| |
| void BalsaHeaders::SetResponseReasonPhrase(absl::string_view reason) { |
| // Note: There is no difference between request_version() and |
| // response_reason_phrase(). Thus, a function to set one is equivalent to a |
| // function to set the other. We maintain two functions for this as it is |
| // much more descriptive, and makes code more understandable. |
| SetRequestVersion(reason); |
| } |
| |
| void BalsaHeaders::RemoveLastTokenFromHeaderValue(absl::string_view key) { |
| BalsaHeaders::HeaderLines::iterator it = |
| GetHeaderLinesIterator(key, header_lines_.begin()); |
| if (it == header_lines_.end()) { |
| QUICHE_DLOG(WARNING) |
| << "Attempting to remove last token from a non-existent " |
| << "header \"" << key << "\""; |
| return; |
| } |
| |
| // Find the last line with that key. |
| BalsaHeaders::HeaderLines::iterator header_line; |
| do { |
| header_line = it; |
| it = GetHeaderLinesIterator(key, it + 1); |
| } while (it != header_lines_.end()); |
| |
| // Tokenize just that line. |
| BalsaHeaders::HeaderTokenList tokens; |
| // Find where this line is stored. |
| const char* stream_begin = GetPtr(header_line->buffer_base_idx); |
| absl::string_view value( |
| stream_begin + header_line->value_begin_idx, |
| header_line->last_char_idx - header_line->value_begin_idx); |
| // Tokenize. |
| ParseTokenList(value, &tokens); |
| |
| if (tokens.empty()) { |
| QUICHE_DLOG(WARNING) |
| << "Attempting to remove a token from an empty header value " |
| << "for header \"" << key << "\""; |
| header_line->skip = true; // remove the whole line |
| } else if (tokens.size() == 1) { |
| header_line->skip = true; // remove the whole line |
| } else { |
| // Shrink the line size and leave the extra data in the buffer. |
| absl::string_view new_last_token = tokens[tokens.size() - 2]; |
| const char* last_char_address = |
| new_last_token.data() + new_last_token.size() - 1; |
| const char* stream_begin = GetPtr(header_line->buffer_base_idx); |
| |
| header_line->last_char_idx = last_char_address - stream_begin + 1; |
| } |
| } |
| |
| bool BalsaHeaders::ResponseCanHaveBody(int response_code) { |
| // For responses, can't have a body if the request was a HEAD, or if it is |
| // one of these response-codes. rfc2616 section 4.3 |
| if (response_code >= 100 && response_code < 200) { |
| // 1xx responses can't have bodies. |
| return false; |
| } |
| |
| // No content and Not modified responses have no body. |
| return (response_code != 204) && (response_code != 304); |
| } |
| |
| } // namespace quiche |