xref: /aosp_15_r20/external/cronet/net/spdy/spdy_http_utils.cc (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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