| #include "http2/adapter/header_validator.h" |
| |
| #include <array> |
| |
| #include "absl/strings/escaping.h" |
| #include "absl/strings/numbers.h" |
| #include "common/platform/api/quiche_logging.h" |
| |
| namespace http2 { |
| namespace adapter { |
| |
| namespace { |
| |
| const absl::string_view kHttp2HeaderNameAllowedChars = |
| "!#$%&\'*+-.0123456789" |
| "^_`abcdefghijklmnopqrstuvwxyz|~"; |
| |
| const absl::string_view kHttp2HeaderValueAllowedChars = |
| "\t " |
| "!\"#$%&'()*+,-./" |
| "0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`" |
| "abcdefghijklmnopqrstuvwxyz{|}~"; |
| |
| const absl::string_view kHttp2StatusValueAllowedChars = "0123456789"; |
| |
| const absl::string_view kValidAuthorityChars = |
| "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~%!$&'()[" |
| "]*+,;=:"; |
| |
| using CharMap = std::array<bool, 256>; |
| |
| CharMap BuildValidCharMap(absl::string_view valid_chars) { |
| CharMap map; |
| map.fill(false); |
| for (char c : valid_chars) { |
| // Cast to uint8_t, guaranteed to have 8 bits. A char may have more, leading |
| // to possible indices above 256. |
| map[static_cast<uint8_t>(c)] = true; |
| } |
| return map; |
| } |
| |
| bool AllCharsInMap(absl::string_view str, const CharMap& map) { |
| for (char c : str) { |
| if (!map[static_cast<uint8_t>(c)]) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| // Returns whether `authority` contains only characters from the `host` ABNF |
| // from RFC 3986 section 3.2.2. |
| bool IsValidAuthority(absl::string_view authority) { |
| static const CharMap valid_chars = BuildValidCharMap(kValidAuthorityChars); |
| return AllCharsInMap(authority, valid_chars); |
| } |
| |
| bool IsValidHeaderName(absl::string_view name) { |
| static const CharMap valid_chars = |
| BuildValidCharMap(kHttp2HeaderNameAllowedChars); |
| return AllCharsInMap(name, valid_chars); |
| } |
| |
| bool IsValidHeaderValue(absl::string_view value) { |
| static const CharMap valid_chars = |
| BuildValidCharMap(kHttp2HeaderValueAllowedChars); |
| return AllCharsInMap(value, valid_chars); |
| } |
| |
| bool IsValidStatus(absl::string_view status) { |
| static const CharMap valid_chars = |
| BuildValidCharMap(kHttp2StatusValueAllowedChars); |
| return AllCharsInMap(status, valid_chars); |
| } |
| |
| bool ValidateRequestHeaders(const std::vector<std::string>& pseudo_headers, |
| absl::string_view method, absl::string_view path, |
| bool allow_connect) { |
| QUICHE_VLOG(2) << "Request pseudo-headers: [" |
| << absl::StrJoin(pseudo_headers, ", ") |
| << "], allow_connect: " << allow_connect |
| << ", method: " << method << ", path: " << path; |
| if (allow_connect && method == "CONNECT") { |
| static const std::vector<std::string>* kConnectHeaders = |
| new std::vector<std::string>( |
| {":authority", ":method", ":path", ":protocol", ":scheme"}); |
| return pseudo_headers == *kConnectHeaders; |
| } |
| |
| if (path.empty()) { |
| return false; |
| } |
| if (path == "*") { |
| if (method != "OPTIONS") { |
| return false; |
| } |
| } else if (path[0] != '/') { |
| return false; |
| } |
| |
| static const std::vector<std::string>* kRequiredHeaders = |
| new std::vector<std::string>( |
| {":authority", ":method", ":path", ":scheme"}); |
| return pseudo_headers == *kRequiredHeaders; |
| } |
| |
| bool ValidateRequestTrailers(const std::vector<std::string>& pseudo_headers) { |
| return pseudo_headers.empty(); |
| } |
| |
| bool ValidateResponseHeaders(const std::vector<std::string>& pseudo_headers) { |
| static const std::vector<std::string>* kRequiredHeaders = |
| new std::vector<std::string>({":status"}); |
| return pseudo_headers == *kRequiredHeaders; |
| } |
| |
| bool ValidateResponseTrailers(const std::vector<std::string>& pseudo_headers) { |
| return pseudo_headers.empty(); |
| } |
| |
| } // namespace |
| |
| void HeaderValidator::StartHeaderBlock() { |
| pseudo_headers_.clear(); |
| status_.clear(); |
| method_.clear(); |
| path_.clear(); |
| content_length_.reset(); |
| } |
| |
| HeaderValidator::HeaderStatus HeaderValidator::ValidateSingleHeader( |
| absl::string_view key, absl::string_view value) { |
| if (key.empty()) { |
| return HEADER_FIELD_INVALID; |
| } |
| if (max_field_size_.has_value() && |
| key.size() + value.size() > max_field_size_.value()) { |
| QUICHE_VLOG(2) << "Header field size is " << key.size() + value.size() |
| << ", exceeds max size of " << max_field_size_.value(); |
| return HEADER_FIELD_TOO_LONG; |
| } |
| const absl::string_view validated_key = key[0] == ':' ? key.substr(1) : key; |
| if (!IsValidHeaderName(validated_key)) { |
| QUICHE_VLOG(2) << "invalid chars in header name: [" |
| << absl::CEscape(validated_key) << "]"; |
| return HEADER_FIELD_INVALID; |
| } |
| if (!IsValidHeaderValue(value)) { |
| QUICHE_VLOG(2) << "invalid chars in header value: [" << absl::CEscape(value) |
| << "]"; |
| return HEADER_FIELD_INVALID; |
| } |
| if (key[0] == ':') { |
| if (key == ":status") { |
| if (value.size() != 3 || !IsValidStatus(value)) { |
| QUICHE_VLOG(2) << "malformed status value: [" << absl::CEscape(value) |
| << "]"; |
| return HEADER_FIELD_INVALID; |
| } |
| if (value == "101") { |
| // Switching protocols is not allowed on a HTTP/2 stream. |
| return HEADER_FIELD_INVALID; |
| } |
| status_ = std::string(value); |
| } else if (key == ":method") { |
| method_ = std::string(value); |
| } else if (key == ":authority" && !IsValidAuthority(value)) { |
| return HEADER_FIELD_INVALID; |
| } else if (key == ":path") { |
| if (value.empty()) { |
| // For now, reject an empty path regardless of scheme. |
| return HEADER_FIELD_INVALID; |
| } |
| path_ = std::string(value); |
| } |
| pseudo_headers_.push_back(std::string(key)); |
| } else if (key == "content-length") { |
| const bool success = HandleContentLength(value); |
| if (!success) { |
| return HEADER_FIELD_INVALID; |
| } |
| } else if (key == "te" && value != "trailers") { |
| return HEADER_FIELD_INVALID; |
| } |
| return HEADER_OK; |
| } |
| |
| // Returns true if all required pseudoheaders and no extra pseudoheaders are |
| // present for the given header type. |
| bool HeaderValidator::FinishHeaderBlock(HeaderType type) { |
| std::sort(pseudo_headers_.begin(), pseudo_headers_.end()); |
| switch (type) { |
| case HeaderType::REQUEST: |
| return ValidateRequestHeaders(pseudo_headers_, method_, path_, |
| allow_connect_); |
| case HeaderType::REQUEST_TRAILER: |
| return ValidateRequestTrailers(pseudo_headers_); |
| case HeaderType::RESPONSE_100: |
| case HeaderType::RESPONSE: |
| return ValidateResponseHeaders(pseudo_headers_); |
| case HeaderType::RESPONSE_TRAILER: |
| return ValidateResponseTrailers(pseudo_headers_); |
| } |
| return false; |
| } |
| |
| bool HeaderValidator::HandleContentLength(absl::string_view value) { |
| if (value.empty()) { |
| return false; |
| } |
| |
| if (status_ == "204" && value != "0") { |
| // There should be no body in a "204 No Content" response. |
| return false; |
| } |
| if (!status_.empty() && status_[0] == '1' && value != "0") { |
| // There should also be no body in a 1xx response. |
| return false; |
| } |
| |
| size_t content_length = 0; |
| const bool valid = absl::SimpleAtoi(value, &content_length); |
| if (!valid) { |
| return false; |
| } |
| |
| content_length_ = content_length; |
| return true; |
| } |
| |
| } // namespace adapter |
| } // namespace http2 |