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