• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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