1 // Copyright 2012 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "net/spdy/spdy_http_utils.h"
6
7 #include <string>
8 #include <string_view>
9 #include <vector>
10
11 #include "base/check_op.h"
12 #include "base/feature_list.h"
13 #include "base/strings/strcat.h"
14 #include "base/strings/string_split.h"
15 #include "base/strings/string_util.h"
16 #include "base/types/expected.h"
17 #include "base/types/expected_macros.h"
18 #include "net/base/features.h"
19 #include "net/base/url_util.h"
20 #include "net/http/http_request_headers.h"
21 #include "net/http/http_request_info.h"
22 #include "net/http/http_response_headers.h"
23 #include "net/http/http_response_info.h"
24 #include "net/http/http_util.h"
25 #include "net/quic/quic_http_utils.h"
26 #include "net/third_party/quiche/src/quiche/quic/core/quic_stream_priority.h"
27 #include "net/third_party/quiche/src/quiche/spdy/core/http2_header_block.h"
28
29 namespace net {
30
31 const char* const kHttp2PriorityHeader = "priority";
32
33 namespace {
34
35 // The number of bytes to reserve for the raw headers string to avoid having to
36 // do reallocations most of the time. Equal to the 99th percentile of header
37 // sizes in ricea@'s cache on 3 Aug 2023.
38 constexpr size_t kExpectedRawHeaderSize = 4035;
39
40 // Add header `name` with `value` to `headers`. `name` must not already exist in
41 // `headers`.
AddUniqueSpdyHeader(std::string_view name,std::string_view value,spdy::Http2HeaderBlock * headers)42 void AddUniqueSpdyHeader(std::string_view name,
43 std::string_view value,
44 spdy::Http2HeaderBlock* headers) {
45 auto insert_result = headers->insert({name, value});
46 CHECK_EQ(insert_result, spdy::Http2HeaderBlock::InsertResult::kInserted);
47 }
48
49 // Convert `headers` to an HttpResponseHeaders object based on the features
50 // enabled at runtime.
51 base::expected<scoped_refptr<HttpResponseHeaders>, int>
SpdyHeadersToHttpResponseHeadersUsingFeatures(const spdy::Http2HeaderBlock & headers)52 SpdyHeadersToHttpResponseHeadersUsingFeatures(
53 const spdy::Http2HeaderBlock& headers) {
54 if (base::FeatureList::IsEnabled(
55 features::kSpdyHeadersToHttpResponseUseBuilder)) {
56 return SpdyHeadersToHttpResponseHeadersUsingBuilder(headers);
57 } else {
58 return SpdyHeadersToHttpResponseHeadersUsingRawString(headers);
59 }
60 }
61
62 } // namespace
63
SpdyHeadersToHttpResponse(const spdy::Http2HeaderBlock & headers,HttpResponseInfo * response)64 int SpdyHeadersToHttpResponse(const spdy::Http2HeaderBlock& headers,
65 HttpResponseInfo* response) {
66 ASSIGN_OR_RETURN(response->headers,
67 SpdyHeadersToHttpResponseHeadersUsingFeatures(headers));
68 response->was_fetched_via_spdy = true;
69 return OK;
70 }
71
72 NET_EXPORT_PRIVATE base::expected<scoped_refptr<HttpResponseHeaders>, int>
SpdyHeadersToHttpResponseHeadersUsingRawString(const spdy::Http2HeaderBlock & headers)73 SpdyHeadersToHttpResponseHeadersUsingRawString(
74 const spdy::Http2HeaderBlock& headers) {
75 // The ":status" header is required.
76 spdy::Http2HeaderBlock::const_iterator it =
77 headers.find(spdy::kHttp2StatusHeader);
78 if (it == headers.end()) {
79 return base::unexpected(ERR_INCOMPLETE_HTTP2_HEADERS);
80 }
81
82 const auto status = it->second;
83
84 std::string raw_headers =
85 base::StrCat({"HTTP/1.1 ", status, std::string_view("\0", 1)});
86 raw_headers.reserve(kExpectedRawHeaderSize);
87 for (const auto& [name, value] : headers) {
88 DCHECK_GT(name.size(), 0u);
89 if (name[0] == ':') {
90 // https://tools.ietf.org/html/rfc7540#section-8.1.2.4
91 // Skip pseudo headers.
92 continue;
93 }
94 // For each value, if the server sends a NUL-separated
95 // list of values, we separate that back out into
96 // individual headers for each value in the list.
97 // e.g.
98 // Set-Cookie "foo\0bar"
99 // becomes
100 // Set-Cookie: foo\0
101 // Set-Cookie: bar\0
102 size_t start = 0;
103 size_t end = 0;
104 do {
105 end = value.find('\0', start);
106 std::string_view tval;
107 if (end != value.npos) {
108 tval = value.substr(start, (end - start));
109 } else {
110 tval = value.substr(start);
111 }
112 base::StrAppend(&raw_headers,
113 {name, ":", tval, std::string_view("\0", 1)});
114 start = end + 1;
115 } while (end != value.npos);
116 }
117
118 auto response_headers =
119 base::MakeRefCounted<HttpResponseHeaders>(raw_headers);
120
121 // When there are multiple location headers the response is a potential
122 // response smuggling attack.
123 if (HttpUtil::HeadersContainMultipleCopiesOfField(*response_headers,
124 "location")) {
125 return base::unexpected(ERR_RESPONSE_HEADERS_MULTIPLE_LOCATION);
126 }
127
128 return response_headers;
129 }
130
131 NET_EXPORT_PRIVATE base::expected<scoped_refptr<HttpResponseHeaders>, int>
SpdyHeadersToHttpResponseHeadersUsingBuilder(const spdy::Http2HeaderBlock & headers)132 SpdyHeadersToHttpResponseHeadersUsingBuilder(
133 const spdy::Http2HeaderBlock& headers) {
134 // The ":status" header is required.
135 // TODO(ricea): The ":status" header should always come first. Skip this hash
136 // lookup after we no longer need to be compatible with the old
137 // implementation.
138 spdy::Http2HeaderBlock::const_iterator it =
139 headers.find(spdy::kHttp2StatusHeader);
140 if (it == headers.end()) {
141 return base::unexpected(ERR_INCOMPLETE_HTTP2_HEADERS);
142 }
143
144 const auto status = it->second;
145
146 HttpResponseHeaders::Builder builder({1, 1}, status);
147
148 for (const auto& [name, value] : headers) {
149 DCHECK_GT(name.size(), 0u);
150 if (name[0] == ':') {
151 // https://tools.ietf.org/html/rfc7540#section-8.1.2.4
152 // Skip pseudo headers.
153 continue;
154 }
155 // For each value, if the server sends a NUL-separated
156 // list of values, we separate that back out into
157 // individual headers for each value in the list.
158 // e.g.
159 // Set-Cookie "foo\0bar"
160 // becomes
161 // Set-Cookie: foo\0
162 // Set-Cookie: bar\0
163 size_t start = 0;
164 size_t end = 0;
165 std::optional<std::string_view> location_value;
166 do {
167 end = value.find('\0', start);
168 std::string_view tval;
169 if (end != value.npos) {
170 tval = value.substr(start, (end - start));
171
172 // TODO(ricea): Make this comparison case-sensitive when we are no
173 // longer maintaining compatibility with the old version of the
174 // function.
175 if (base::EqualsCaseInsensitiveASCII(name, "location") &&
176 !location_value.has_value()) {
177 location_value = HttpUtil::TrimLWS(tval);
178 }
179 } else {
180 tval = value.substr(start);
181 }
182 if (location_value.has_value() && start > 0) {
183 DCHECK(base::EqualsCaseInsensitiveASCII(name, "location"));
184 std::string_view trimmed_value = HttpUtil::TrimLWS(tval);
185 if (trimmed_value != location_value.value()) {
186 return base::unexpected(ERR_RESPONSE_HEADERS_MULTIPLE_LOCATION);
187 }
188 }
189 builder.AddHeader(name, tval);
190 start = end + 1;
191 } while (end != value.npos);
192 }
193
194 return builder.Build();
195 }
196
CreateSpdyHeadersFromHttpRequest(const HttpRequestInfo & info,std::optional<RequestPriority> priority,const HttpRequestHeaders & request_headers,spdy::Http2HeaderBlock * headers)197 void CreateSpdyHeadersFromHttpRequest(const HttpRequestInfo& info,
198 std::optional<RequestPriority> priority,
199 const HttpRequestHeaders& request_headers,
200 spdy::Http2HeaderBlock* headers) {
201 headers->insert({spdy::kHttp2MethodHeader, info.method});
202 if (info.method == "CONNECT") {
203 headers->insert({spdy::kHttp2AuthorityHeader, GetHostAndPort(info.url)});
204 } else {
205 headers->insert(
206 {spdy::kHttp2AuthorityHeader, GetHostAndOptionalPort(info.url)});
207 headers->insert({spdy::kHttp2SchemeHeader, info.url.scheme()});
208 headers->insert({spdy::kHttp2PathHeader, info.url.PathForRequest()});
209 }
210
211 HttpRequestHeaders::Iterator it(request_headers);
212 while (it.GetNext()) {
213 std::string name = base::ToLowerASCII(it.name());
214 if (name.empty() || name[0] == ':' || name == "connection" ||
215 name == "proxy-connection" || name == "transfer-encoding" ||
216 name == "host") {
217 continue;
218 }
219 AddUniqueSpdyHeader(name, it.value(), headers);
220 }
221
222 // Add the priority header if there is not already one set. This uses the
223 // quic helpers but the header values for HTTP extensible priorities are
224 // independent of quic.
225 if (priority &&
226 base::FeatureList::IsEnabled(net::features::kPriorityHeader) &&
227 headers->find(kHttp2PriorityHeader) == headers->end()) {
228 uint8_t urgency = ConvertRequestPriorityToQuicPriority(priority.value());
229 bool incremental = info.priority_incremental;
230 quic::HttpStreamPriority quic_priority{urgency, incremental};
231 std::string serialized_priority =
232 quic::SerializePriorityFieldValue(quic_priority);
233 if (!serialized_priority.empty()) {
234 AddUniqueSpdyHeader(kHttp2PriorityHeader, serialized_priority, headers);
235 }
236 }
237 }
238
CreateSpdyHeadersFromHttpRequestForExtendedConnect(const HttpRequestInfo & info,std::optional<RequestPriority> priority,const std::string & ext_connect_protocol,const HttpRequestHeaders & request_headers,spdy::Http2HeaderBlock * headers)239 void CreateSpdyHeadersFromHttpRequestForExtendedConnect(
240 const HttpRequestInfo& info,
241 std::optional<RequestPriority> priority,
242 const std::string& ext_connect_protocol,
243 const HttpRequestHeaders& request_headers,
244 spdy::Http2HeaderBlock* headers) {
245 CHECK_EQ(info.method, "CONNECT");
246
247 // Extended CONNECT, unlike CONNECT, requires scheme and path, and uses the
248 // default port in the authority header.
249 headers->insert({spdy::kHttp2SchemeHeader, info.url.scheme()});
250 headers->insert({spdy::kHttp2PathHeader, info.url.PathForRequest()});
251 headers->insert({spdy::kHttp2ProtocolHeader, ext_connect_protocol});
252
253 CreateSpdyHeadersFromHttpRequest(info, priority, request_headers, headers);
254
255 // Replace the existing `:authority` header. This will still be ordered
256 // correctly, since the header was first added before any regular headers.
257 headers->insert(
258 {spdy::kHttp2AuthorityHeader, GetHostAndOptionalPort(info.url)});
259 }
260
CreateSpdyHeadersFromHttpRequestForWebSocket(const GURL & url,const HttpRequestHeaders & request_headers,spdy::Http2HeaderBlock * headers)261 void CreateSpdyHeadersFromHttpRequestForWebSocket(
262 const GURL& url,
263 const HttpRequestHeaders& request_headers,
264 spdy::Http2HeaderBlock* headers) {
265 headers->insert({spdy::kHttp2MethodHeader, "CONNECT"});
266 headers->insert({spdy::kHttp2AuthorityHeader, GetHostAndOptionalPort(url)});
267 headers->insert({spdy::kHttp2SchemeHeader, "https"});
268 headers->insert({spdy::kHttp2PathHeader, url.PathForRequest()});
269 headers->insert({spdy::kHttp2ProtocolHeader, "websocket"});
270
271 HttpRequestHeaders::Iterator it(request_headers);
272 while (it.GetNext()) {
273 std::string name = base::ToLowerASCII(it.name());
274 if (name.empty() || name[0] == ':' || name == "upgrade" ||
275 name == "connection" || name == "proxy-connection" ||
276 name == "transfer-encoding" || name == "host") {
277 continue;
278 }
279 AddUniqueSpdyHeader(name, it.value(), headers);
280 }
281 }
282
283 static_assert(HIGHEST - LOWEST < 4 && HIGHEST - MINIMUM_PRIORITY < 6,
284 "request priority incompatible with spdy");
285
ConvertRequestPriorityToSpdyPriority(const RequestPriority priority)286 spdy::SpdyPriority ConvertRequestPriorityToSpdyPriority(
287 const RequestPriority priority) {
288 DCHECK_GE(priority, MINIMUM_PRIORITY);
289 DCHECK_LE(priority, MAXIMUM_PRIORITY);
290 return static_cast<spdy::SpdyPriority>(MAXIMUM_PRIORITY - priority +
291 spdy::kV3HighestPriority);
292 }
293
294 NET_EXPORT_PRIVATE RequestPriority
ConvertSpdyPriorityToRequestPriority(spdy::SpdyPriority priority)295 ConvertSpdyPriorityToRequestPriority(spdy::SpdyPriority priority) {
296 // Handle invalid values gracefully.
297 return ((priority - spdy::kV3HighestPriority) >
298 (MAXIMUM_PRIORITY - MINIMUM_PRIORITY))
299 ? IDLE
300 : static_cast<RequestPriority>(
301 MAXIMUM_PRIORITY - (priority - spdy::kV3HighestPriority));
302 }
303
ConvertHeaderBlockToHttpRequestHeaders(const spdy::Http2HeaderBlock & spdy_headers,HttpRequestHeaders * http_headers)304 NET_EXPORT_PRIVATE void ConvertHeaderBlockToHttpRequestHeaders(
305 const spdy::Http2HeaderBlock& spdy_headers,
306 HttpRequestHeaders* http_headers) {
307 for (const auto& it : spdy_headers) {
308 std::string_view key = it.first;
309 if (key[0] == ':') {
310 key.remove_prefix(1);
311 }
312 std::vector<std::string_view> values = base::SplitStringPiece(
313 it.second, "\0", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
314 for (const auto& value : values) {
315 http_headers->SetHeader(key, value);
316 }
317 }
318 }
319
320 } // namespace net
321