// Copyright 2012 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "net/spdy/spdy_http_utils.h" #include #include #include #include "base/check_op.h" #include "base/feature_list.h" #include "base/strings/strcat.h" #include "base/strings/string_split.h" #include "base/strings/string_util.h" #include "base/types/expected.h" #include "base/types/expected_macros.h" #include "net/base/features.h" #include "net/base/url_util.h" #include "net/http/http_request_headers.h" #include "net/http/http_request_info.h" #include "net/http/http_response_headers.h" #include "net/http/http_response_info.h" #include "net/http/http_util.h" #include "net/quic/quic_http_utils.h" #include "net/third_party/quiche/src/quiche/quic/core/quic_stream_priority.h" #include "net/third_party/quiche/src/quiche/spdy/core/http2_header_block.h" namespace net { const char* const kHttp2PriorityHeader = "priority"; namespace { // The number of bytes to reserve for the raw headers string to avoid having to // do reallocations most of the time. Equal to the 99th percentile of header // sizes in ricea@'s cache on 3 Aug 2023. constexpr size_t kExpectedRawHeaderSize = 4035; // Add header `name` with `value` to `headers`. `name` must not already exist in // `headers`. void AddUniqueSpdyHeader(std::string_view name, std::string_view value, spdy::Http2HeaderBlock* headers) { auto insert_result = headers->insert({name, value}); CHECK_EQ(insert_result, spdy::Http2HeaderBlock::InsertResult::kInserted); } // Convert `headers` to an HttpResponseHeaders object based on the features // enabled at runtime. base::expected, int> SpdyHeadersToHttpResponseHeadersUsingFeatures( const spdy::Http2HeaderBlock& headers) { if (base::FeatureList::IsEnabled( features::kSpdyHeadersToHttpResponseUseBuilder)) { return SpdyHeadersToHttpResponseHeadersUsingBuilder(headers); } else { return SpdyHeadersToHttpResponseHeadersUsingRawString(headers); } } } // namespace int SpdyHeadersToHttpResponse(const spdy::Http2HeaderBlock& headers, HttpResponseInfo* response) { ASSIGN_OR_RETURN(response->headers, SpdyHeadersToHttpResponseHeadersUsingFeatures(headers)); response->was_fetched_via_spdy = true; return OK; } NET_EXPORT_PRIVATE base::expected, int> SpdyHeadersToHttpResponseHeadersUsingRawString( const spdy::Http2HeaderBlock& headers) { // The ":status" header is required. spdy::Http2HeaderBlock::const_iterator it = headers.find(spdy::kHttp2StatusHeader); if (it == headers.end()) { return base::unexpected(ERR_INCOMPLETE_HTTP2_HEADERS); } const auto status = it->second; std::string raw_headers = base::StrCat({"HTTP/1.1 ", status, std::string_view("\0", 1)}); raw_headers.reserve(kExpectedRawHeaderSize); for (const auto& [name, value] : headers) { DCHECK_GT(name.size(), 0u); if (name[0] == ':') { // https://tools.ietf.org/html/rfc7540#section-8.1.2.4 // Skip pseudo headers. continue; } // For each value, if the server sends a NUL-separated // list of values, we separate that back out into // individual headers for each value in the list. // e.g. // Set-Cookie "foo\0bar" // becomes // Set-Cookie: foo\0 // Set-Cookie: bar\0 size_t start = 0; size_t end = 0; do { end = value.find('\0', start); std::string_view tval; if (end != value.npos) { tval = value.substr(start, (end - start)); } else { tval = value.substr(start); } base::StrAppend(&raw_headers, {name, ":", tval, std::string_view("\0", 1)}); start = end + 1; } while (end != value.npos); } auto response_headers = base::MakeRefCounted(raw_headers); // When there are multiple location headers the response is a potential // response smuggling attack. if (HttpUtil::HeadersContainMultipleCopiesOfField(*response_headers, "location")) { return base::unexpected(ERR_RESPONSE_HEADERS_MULTIPLE_LOCATION); } return response_headers; } NET_EXPORT_PRIVATE base::expected, int> SpdyHeadersToHttpResponseHeadersUsingBuilder( const spdy::Http2HeaderBlock& headers) { // The ":status" header is required. // TODO(ricea): The ":status" header should always come first. Skip this hash // lookup after we no longer need to be compatible with the old // implementation. spdy::Http2HeaderBlock::const_iterator it = headers.find(spdy::kHttp2StatusHeader); if (it == headers.end()) { return base::unexpected(ERR_INCOMPLETE_HTTP2_HEADERS); } const auto status = it->second; HttpResponseHeaders::Builder builder({1, 1}, status); for (const auto& [name, value] : headers) { DCHECK_GT(name.size(), 0u); if (name[0] == ':') { // https://tools.ietf.org/html/rfc7540#section-8.1.2.4 // Skip pseudo headers. continue; } // For each value, if the server sends a NUL-separated // list of values, we separate that back out into // individual headers for each value in the list. // e.g. // Set-Cookie "foo\0bar" // becomes // Set-Cookie: foo\0 // Set-Cookie: bar\0 size_t start = 0; size_t end = 0; std::optional location_value; do { end = value.find('\0', start); std::string_view tval; if (end != value.npos) { tval = value.substr(start, (end - start)); // TODO(ricea): Make this comparison case-sensitive when we are no // longer maintaining compatibility with the old version of the // function. if (base::EqualsCaseInsensitiveASCII(name, "location") && !location_value.has_value()) { location_value = HttpUtil::TrimLWS(tval); } } else { tval = value.substr(start); } if (location_value.has_value() && start > 0) { DCHECK(base::EqualsCaseInsensitiveASCII(name, "location")); std::string_view trimmed_value = HttpUtil::TrimLWS(tval); if (trimmed_value != location_value.value()) { return base::unexpected(ERR_RESPONSE_HEADERS_MULTIPLE_LOCATION); } } builder.AddHeader(name, tval); start = end + 1; } while (end != value.npos); } return builder.Build(); } void CreateSpdyHeadersFromHttpRequest(const HttpRequestInfo& info, std::optional priority, const HttpRequestHeaders& request_headers, spdy::Http2HeaderBlock* headers) { headers->insert({spdy::kHttp2MethodHeader, info.method}); if (info.method == "CONNECT") { headers->insert({spdy::kHttp2AuthorityHeader, GetHostAndPort(info.url)}); } else { headers->insert( {spdy::kHttp2AuthorityHeader, GetHostAndOptionalPort(info.url)}); headers->insert({spdy::kHttp2SchemeHeader, info.url.scheme()}); headers->insert({spdy::kHttp2PathHeader, info.url.PathForRequest()}); } HttpRequestHeaders::Iterator it(request_headers); while (it.GetNext()) { std::string name = base::ToLowerASCII(it.name()); if (name.empty() || name[0] == ':' || name == "connection" || name == "proxy-connection" || name == "transfer-encoding" || name == "host") { continue; } AddUniqueSpdyHeader(name, it.value(), headers); } // Add the priority header if there is not already one set. This uses the // quic helpers but the header values for HTTP extensible priorities are // independent of quic. if (priority && base::FeatureList::IsEnabled(net::features::kPriorityHeader) && headers->find(kHttp2PriorityHeader) == headers->end()) { uint8_t urgency = ConvertRequestPriorityToQuicPriority(priority.value()); bool incremental = info.priority_incremental; quic::HttpStreamPriority quic_priority{urgency, incremental}; std::string serialized_priority = quic::SerializePriorityFieldValue(quic_priority); if (!serialized_priority.empty()) { AddUniqueSpdyHeader(kHttp2PriorityHeader, serialized_priority, headers); } } } void CreateSpdyHeadersFromHttpRequestForExtendedConnect( const HttpRequestInfo& info, std::optional priority, const std::string& ext_connect_protocol, const HttpRequestHeaders& request_headers, spdy::Http2HeaderBlock* headers) { CHECK_EQ(info.method, "CONNECT"); // Extended CONNECT, unlike CONNECT, requires scheme and path, and uses the // default port in the authority header. headers->insert({spdy::kHttp2SchemeHeader, info.url.scheme()}); headers->insert({spdy::kHttp2PathHeader, info.url.PathForRequest()}); headers->insert({spdy::kHttp2ProtocolHeader, ext_connect_protocol}); CreateSpdyHeadersFromHttpRequest(info, priority, request_headers, headers); // Replace the existing `:authority` header. This will still be ordered // correctly, since the header was first added before any regular headers. headers->insert( {spdy::kHttp2AuthorityHeader, GetHostAndOptionalPort(info.url)}); } void CreateSpdyHeadersFromHttpRequestForWebSocket( const GURL& url, const HttpRequestHeaders& request_headers, spdy::Http2HeaderBlock* headers) { headers->insert({spdy::kHttp2MethodHeader, "CONNECT"}); headers->insert({spdy::kHttp2AuthorityHeader, GetHostAndOptionalPort(url)}); headers->insert({spdy::kHttp2SchemeHeader, "https"}); headers->insert({spdy::kHttp2PathHeader, url.PathForRequest()}); headers->insert({spdy::kHttp2ProtocolHeader, "websocket"}); HttpRequestHeaders::Iterator it(request_headers); while (it.GetNext()) { std::string name = base::ToLowerASCII(it.name()); if (name.empty() || name[0] == ':' || name == "upgrade" || name == "connection" || name == "proxy-connection" || name == "transfer-encoding" || name == "host") { continue; } AddUniqueSpdyHeader(name, it.value(), headers); } } static_assert(HIGHEST - LOWEST < 4 && HIGHEST - MINIMUM_PRIORITY < 6, "request priority incompatible with spdy"); spdy::SpdyPriority ConvertRequestPriorityToSpdyPriority( const RequestPriority priority) { DCHECK_GE(priority, MINIMUM_PRIORITY); DCHECK_LE(priority, MAXIMUM_PRIORITY); return static_cast(MAXIMUM_PRIORITY - priority + spdy::kV3HighestPriority); } NET_EXPORT_PRIVATE RequestPriority ConvertSpdyPriorityToRequestPriority(spdy::SpdyPriority priority) { // Handle invalid values gracefully. return ((priority - spdy::kV3HighestPriority) > (MAXIMUM_PRIORITY - MINIMUM_PRIORITY)) ? IDLE : static_cast( MAXIMUM_PRIORITY - (priority - spdy::kV3HighestPriority)); } NET_EXPORT_PRIVATE void ConvertHeaderBlockToHttpRequestHeaders( const spdy::Http2HeaderBlock& spdy_headers, HttpRequestHeaders* http_headers) { for (const auto& it : spdy_headers) { std::string_view key = it.first; if (key[0] == ':') { key.remove_prefix(1); } std::vector values = base::SplitStringPiece( it.second, "\0", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); for (const auto& value : values) { http_headers->SetHeader(key, value); } } } } // namespace net