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 <stdint.h>
8
9 #include <limits>
10
11 #include "base/test/gmock_expected_support.h"
12 #include "base/test/scoped_feature_list.h"
13 #include "net/base/features.h"
14 #include "net/base/ip_endpoint.h"
15 #include "net/http/http_request_info.h"
16 #include "net/http/http_response_headers.h"
17 #include "net/http/http_response_headers_test_util.h"
18 #include "net/http/http_response_info.h"
19 #include "net/third_party/quiche/src/quiche/spdy/core/http2_header_block.h"
20 #include "net/third_party/quiche/src/quiche/spdy/core/spdy_framer.h"
21 #include "net/third_party/quiche/src/quiche/spdy/core/spdy_protocol.h"
22 #include "net/third_party/quiche/src/quiche/spdy/test_tools/spdy_test_utils.h"
23 #include "testing/gtest/include/gtest/gtest.h"
24
25 namespace net {
26
27 namespace {
28
29 using ::testing::Values;
30
31 class SpdyHttpUtilsTestParam : public testing::TestWithParam<bool> {
32 public:
SpdyHttpUtilsTestParam()33 SpdyHttpUtilsTestParam() {
34 if (PriorityHeaderEnabled()) {
35 feature_list_.InitAndEnableFeature(net::features::kPriorityHeader);
36 } else {
37 feature_list_.InitAndDisableFeature(net::features::kPriorityHeader);
38 }
39 }
40
41 protected:
PriorityHeaderEnabled() const42 bool PriorityHeaderEnabled() const { return GetParam(); }
43
44 private:
45 base::test::ScopedFeatureList feature_list_;
46 };
47
48 INSTANTIATE_TEST_SUITE_P(All, SpdyHttpUtilsTestParam, Values(true, false));
49
50 // Check that the headers are ordered correctly, with pseudo-headers
51 // preceding HTTP headers per
52 // https://datatracker.ietf.org/doc/html/rfc9114#section-4.3
CheckOrdering(const spdy::Http2HeaderBlock & headers)53 void CheckOrdering(const spdy::Http2HeaderBlock& headers) {
54 bool seen_http_header = false;
55
56 for (auto& header : headers) {
57 bool is_pseudo = header.first.starts_with(':');
58 if (is_pseudo) {
59 if (seen_http_header) {
60 EXPECT_TRUE(false) << "Header order is incorrect:\n"
61 << headers.DebugString();
62 return;
63 }
64 } else {
65 seen_http_header = true;
66 }
67 }
68 }
69
TEST(SpdyHttpUtilsTest,ConvertRequestPriorityToSpdy3Priority)70 TEST(SpdyHttpUtilsTest, ConvertRequestPriorityToSpdy3Priority) {
71 EXPECT_EQ(0, ConvertRequestPriorityToSpdyPriority(HIGHEST));
72 EXPECT_EQ(1, ConvertRequestPriorityToSpdyPriority(MEDIUM));
73 EXPECT_EQ(2, ConvertRequestPriorityToSpdyPriority(LOW));
74 EXPECT_EQ(3, ConvertRequestPriorityToSpdyPriority(LOWEST));
75 EXPECT_EQ(4, ConvertRequestPriorityToSpdyPriority(IDLE));
76 EXPECT_EQ(5, ConvertRequestPriorityToSpdyPriority(THROTTLED));
77 }
78
TEST(SpdyHttpUtilsTest,ConvertSpdy3PriorityToRequestPriority)79 TEST(SpdyHttpUtilsTest, ConvertSpdy3PriorityToRequestPriority) {
80 EXPECT_EQ(HIGHEST, ConvertSpdyPriorityToRequestPriority(0));
81 EXPECT_EQ(MEDIUM, ConvertSpdyPriorityToRequestPriority(1));
82 EXPECT_EQ(LOW, ConvertSpdyPriorityToRequestPriority(2));
83 EXPECT_EQ(LOWEST, ConvertSpdyPriorityToRequestPriority(3));
84 EXPECT_EQ(IDLE, ConvertSpdyPriorityToRequestPriority(4));
85 EXPECT_EQ(THROTTLED, ConvertSpdyPriorityToRequestPriority(5));
86 // These are invalid values, but we should still handle them
87 // gracefully.
88 for (int i = 6; i < std::numeric_limits<uint8_t>::max(); ++i) {
89 EXPECT_EQ(IDLE, ConvertSpdyPriorityToRequestPriority(i));
90 }
91 }
92
TEST_P(SpdyHttpUtilsTestParam,CreateSpdyHeadersFromHttpRequestHTTP2)93 TEST_P(SpdyHttpUtilsTestParam, CreateSpdyHeadersFromHttpRequestHTTP2) {
94 GURL url("https://www.google.com/index.html");
95 HttpRequestInfo request;
96 request.method = "GET";
97 request.url = url;
98 request.priority_incremental = true;
99 request.extra_headers.SetHeader(HttpRequestHeaders::kUserAgent, "Chrome/1.1");
100 spdy::Http2HeaderBlock headers;
101 CreateSpdyHeadersFromHttpRequest(request, RequestPriority::HIGHEST,
102 request.extra_headers, &headers);
103 CheckOrdering(headers);
104 EXPECT_EQ("GET", headers[":method"]);
105 EXPECT_EQ("https", headers[":scheme"]);
106 EXPECT_EQ("www.google.com", headers[":authority"]);
107 EXPECT_EQ("/index.html", headers[":path"]);
108 if (base::FeatureList::IsEnabled(net::features::kPriorityHeader)) {
109 EXPECT_EQ("u=0, i", headers[net::kHttp2PriorityHeader]);
110 } else {
111 EXPECT_EQ(headers.end(), headers.find(net::kHttp2PriorityHeader));
112 }
113 EXPECT_EQ(headers.end(), headers.find(":version"));
114 EXPECT_EQ("Chrome/1.1", headers["user-agent"]);
115 }
116
TEST_P(SpdyHttpUtilsTestParam,CreateSpdyHeadersFromHttpRequestForExtendedConnect)117 TEST_P(SpdyHttpUtilsTestParam,
118 CreateSpdyHeadersFromHttpRequestForExtendedConnect) {
119 GURL url("https://www.google.com/index.html");
120 HttpRequestInfo request;
121 request.method = "CONNECT";
122 request.url = url;
123 request.priority_incremental = true;
124 request.extra_headers.SetHeader(HttpRequestHeaders::kUserAgent, "Chrome/1.1");
125 spdy::Http2HeaderBlock headers;
126 CreateSpdyHeadersFromHttpRequestForExtendedConnect(
127 request, RequestPriority::HIGHEST, "connect-ftp", request.extra_headers,
128 &headers);
129 CheckOrdering(headers);
130 EXPECT_EQ("CONNECT", headers[":method"]);
131 EXPECT_EQ("https", headers[":scheme"]);
132 EXPECT_EQ("www.google.com", headers[":authority"]);
133 EXPECT_EQ("connect-ftp", headers[":protocol"]);
134 EXPECT_EQ("/index.html", headers[":path"]);
135 if (base::FeatureList::IsEnabled(net::features::kPriorityHeader)) {
136 EXPECT_EQ("u=0, i", headers[net::kHttp2PriorityHeader]);
137 } else {
138 EXPECT_EQ(headers.end(), headers.find(net::kHttp2PriorityHeader));
139 }
140 EXPECT_EQ("Chrome/1.1", headers["user-agent"]);
141 }
142
TEST_P(SpdyHttpUtilsTestParam,CreateSpdyHeadersWithDefaultPriority)143 TEST_P(SpdyHttpUtilsTestParam, CreateSpdyHeadersWithDefaultPriority) {
144 GURL url("https://www.google.com/index.html");
145 HttpRequestInfo request;
146 request.method = "GET";
147 request.url = url;
148 request.priority_incremental = false;
149 request.extra_headers.SetHeader(HttpRequestHeaders::kUserAgent, "Chrome/1.1");
150 spdy::Http2HeaderBlock headers;
151 CreateSpdyHeadersFromHttpRequest(request, RequestPriority::DEFAULT_PRIORITY,
152 request.extra_headers, &headers);
153 CheckOrdering(headers);
154 EXPECT_EQ("GET", headers[":method"]);
155 EXPECT_EQ("https", headers[":scheme"]);
156 EXPECT_EQ("www.google.com", headers[":authority"]);
157 EXPECT_EQ("/index.html", headers[":path"]);
158 EXPECT_FALSE(headers.contains(net::kHttp2PriorityHeader));
159 EXPECT_FALSE(headers.contains(":version"));
160 EXPECT_EQ("Chrome/1.1", headers["user-agent"]);
161 }
162
TEST_P(SpdyHttpUtilsTestParam,CreateSpdyHeadersWithExistingPriority)163 TEST_P(SpdyHttpUtilsTestParam, CreateSpdyHeadersWithExistingPriority) {
164 GURL url("https://www.google.com/index.html");
165 HttpRequestInfo request;
166 request.method = "GET";
167 request.url = url;
168 request.priority_incremental = true;
169 request.extra_headers.SetHeader(HttpRequestHeaders::kUserAgent, "Chrome/1.1");
170 request.extra_headers.SetHeader(net::kHttp2PriorityHeader,
171 "explicit-priority");
172 spdy::Http2HeaderBlock headers;
173 CreateSpdyHeadersFromHttpRequest(request, RequestPriority::HIGHEST,
174 request.extra_headers, &headers);
175 CheckOrdering(headers);
176 EXPECT_EQ("GET", headers[":method"]);
177 EXPECT_EQ("https", headers[":scheme"]);
178 EXPECT_EQ("www.google.com", headers[":authority"]);
179 EXPECT_EQ("/index.html", headers[":path"]);
180 EXPECT_EQ("explicit-priority", headers[net::kHttp2PriorityHeader]);
181 EXPECT_EQ(headers.end(), headers.find(":version"));
182 EXPECT_EQ("Chrome/1.1", headers["user-agent"]);
183 }
184
TEST(SpdyHttpUtilsTest,CreateSpdyHeadersFromHttpRequestConnectHTTP2)185 TEST(SpdyHttpUtilsTest, CreateSpdyHeadersFromHttpRequestConnectHTTP2) {
186 GURL url("https://www.google.com/index.html");
187 HttpRequestInfo request;
188 request.method = "CONNECT";
189 request.url = url;
190 request.extra_headers.SetHeader(HttpRequestHeaders::kUserAgent, "Chrome/1.1");
191 spdy::Http2HeaderBlock headers;
192 CreateSpdyHeadersFromHttpRequest(request, RequestPriority::DEFAULT_PRIORITY,
193 request.extra_headers, &headers);
194 CheckOrdering(headers);
195 EXPECT_EQ("CONNECT", headers[":method"]);
196 EXPECT_TRUE(headers.end() == headers.find(":scheme"));
197 EXPECT_EQ("www.google.com:443", headers[":authority"]);
198 EXPECT_EQ(headers.end(), headers.find(":path"));
199 EXPECT_EQ(headers.end(), headers.find(":scheme"));
200 EXPECT_TRUE(headers.end() == headers.find(":version"));
201 EXPECT_EQ("Chrome/1.1", headers["user-agent"]);
202 }
203
204 constexpr auto ToSimpleString = test::HttpResponseHeadersToSimpleString;
205
206 enum class SpdyHeadersToHttpResponseHeadersFeatureConfig {
207 kUseRawString,
208 kUseBuilder
209 };
210
PrintToString(SpdyHeadersToHttpResponseHeadersFeatureConfig config)211 std::string PrintToString(
212 SpdyHeadersToHttpResponseHeadersFeatureConfig config) {
213 switch (config) {
214 case SpdyHeadersToHttpResponseHeadersFeatureConfig::kUseRawString:
215 return "RawString";
216
217 case SpdyHeadersToHttpResponseHeadersFeatureConfig::kUseBuilder:
218 return "UseBuilder";
219 }
220 }
221
222 class SpdyHeadersToHttpResponseTest
223 : public ::testing::TestWithParam<
224 SpdyHeadersToHttpResponseHeadersFeatureConfig> {
225 public:
SpdyHeadersToHttpResponseTest()226 SpdyHeadersToHttpResponseTest() {
227 switch (GetParam()) {
228 case SpdyHeadersToHttpResponseHeadersFeatureConfig::kUseRawString:
229 feature_list_.InitWithFeatures(
230 {}, {features::kSpdyHeadersToHttpResponseUseBuilder});
231 break;
232
233 case SpdyHeadersToHttpResponseHeadersFeatureConfig::kUseBuilder:
234 feature_list_.InitWithFeatures(
235 {features::kSpdyHeadersToHttpResponseUseBuilder}, {});
236 break;
237 }
238 }
239
240 private:
241 base::test::ScopedFeatureList feature_list_;
242 };
243
244 // This test behaves the same regardless of which features are enabled.
TEST_P(SpdyHeadersToHttpResponseTest,SpdyHeadersToHttpResponse)245 TEST_P(SpdyHeadersToHttpResponseTest, SpdyHeadersToHttpResponse) {
246 constexpr char kExpectedSimpleString[] =
247 "HTTP/1.1 200\n"
248 "content-type: text/html\n"
249 "cache-control: no-cache, no-store\n"
250 "set-cookie: test_cookie=1234567890; Max-Age=3600; Secure; HttpOnly\n"
251 "set-cookie: session_id=abcdefghijklmnopqrstuvwxyz; Path=/; HttpOnly\n";
252 spdy::Http2HeaderBlock input;
253 input[spdy::kHttp2StatusHeader] = "200";
254 input["content-type"] = "text/html";
255 input["cache-control"] = "no-cache, no-store";
256 input.AppendValueOrAddHeader(
257 "set-cookie", "test_cookie=1234567890; Max-Age=3600; Secure; HttpOnly");
258 input.AppendValueOrAddHeader(
259 "set-cookie", "session_id=abcdefghijklmnopqrstuvwxyz; Path=/; HttpOnly");
260
261 net::HttpResponseInfo output;
262 output.remote_endpoint = {{127, 0, 0, 1}, 80};
263
264 EXPECT_EQ(OK, SpdyHeadersToHttpResponse(input, &output));
265
266 // This should be set.
267 EXPECT_TRUE(output.was_fetched_via_spdy);
268
269 // This should be untouched.
270 EXPECT_EQ(output.remote_endpoint, IPEndPoint({127, 0, 0, 1}, 80));
271
272 EXPECT_EQ(kExpectedSimpleString, ToSimpleString(output.headers));
273 }
274
275 INSTANTIATE_TEST_SUITE_P(
276 SpdyHttpUtils,
277 SpdyHeadersToHttpResponseTest,
278 Values(SpdyHeadersToHttpResponseHeadersFeatureConfig::kUseRawString,
279 SpdyHeadersToHttpResponseHeadersFeatureConfig::kUseBuilder),
280 ::testing::PrintToStringParamName());
281
282 // TODO(ricea): Once SpdyHeadersToHttpResponseHeadersUsingRawString has been
283 // removed, remove the parameterization and make these into
284 // SpdyHeadersToHttpResponse tests.
285
286 using SpdyHeadersToHttpResponseHeadersFunctionPtrType =
287 base::expected<scoped_refptr<HttpResponseHeaders>, int> (*)(
288 const spdy::Http2HeaderBlock&);
289
290 class SpdyHeadersToHttpResponseHeadersTest
291 : public testing::TestWithParam<
292 SpdyHeadersToHttpResponseHeadersFunctionPtrType> {
293 public:
PerformConversion(const spdy::Http2HeaderBlock & headers)294 base::expected<scoped_refptr<HttpResponseHeaders>, int> PerformConversion(
295 const spdy::Http2HeaderBlock& headers) {
296 return GetParam()(headers);
297 }
298 };
299
TEST_P(SpdyHeadersToHttpResponseHeadersTest,NoStatus)300 TEST_P(SpdyHeadersToHttpResponseHeadersTest, NoStatus) {
301 spdy::Http2HeaderBlock headers;
302 EXPECT_THAT(PerformConversion(headers),
303 base::test::ErrorIs(ERR_INCOMPLETE_HTTP2_HEADERS));
304 }
305
TEST_P(SpdyHeadersToHttpResponseHeadersTest,EmptyStatus)306 TEST_P(SpdyHeadersToHttpResponseHeadersTest, EmptyStatus) {
307 constexpr char kRawHeaders[] = "HTTP/1.1 200\n";
308 spdy::Http2HeaderBlock headers;
309 headers[":status"] = "";
310 ASSERT_OK_AND_ASSIGN(const auto output, PerformConversion(headers));
311 EXPECT_EQ(kRawHeaders, ToSimpleString(output));
312 }
313
TEST_P(SpdyHeadersToHttpResponseHeadersTest,Plain200)314 TEST_P(SpdyHeadersToHttpResponseHeadersTest, Plain200) {
315 // ":status" does not appear as a header in the output.
316 constexpr char kRawHeaders[] = "HTTP/1.1 200\n";
317 spdy::Http2HeaderBlock headers;
318 headers[spdy::kHttp2StatusHeader] = "200";
319 ASSERT_OK_AND_ASSIGN(const auto output, PerformConversion(headers));
320 EXPECT_EQ(kRawHeaders, ToSimpleString(output));
321 }
322
TEST_P(SpdyHeadersToHttpResponseHeadersTest,MultipleLocation)323 TEST_P(SpdyHeadersToHttpResponseHeadersTest, MultipleLocation) {
324 spdy::Http2HeaderBlock headers;
325 headers[spdy::kHttp2StatusHeader] = "304";
326 headers["Location"] = "https://example.com/1";
327 headers.AppendValueOrAddHeader("location", "https://example.com/2");
328 EXPECT_THAT(PerformConversion(headers),
329 base::test::ErrorIs(ERR_RESPONSE_HEADERS_MULTIPLE_LOCATION));
330 }
331
TEST_P(SpdyHeadersToHttpResponseHeadersTest,SpacesAmongValues)332 TEST_P(SpdyHeadersToHttpResponseHeadersTest, SpacesAmongValues) {
333 constexpr char kRawHeaders[] =
334 "HTTP/1.1 200\n"
335 "spaces: foo , bar\n";
336 spdy::Http2HeaderBlock headers;
337 headers[spdy::kHttp2StatusHeader] = "200";
338 headers["spaces"] = "foo , bar";
339 ASSERT_OK_AND_ASSIGN(const auto output, PerformConversion(headers));
340 EXPECT_EQ(kRawHeaders, ToSimpleString(output));
341 }
342
TEST_P(SpdyHeadersToHttpResponseHeadersTest,RepeatedHeader)343 TEST_P(SpdyHeadersToHttpResponseHeadersTest, RepeatedHeader) {
344 constexpr char kRawHeaders[] =
345 "HTTP/1.1 200\n"
346 "name: value1\n"
347 "name: value2\n";
348 spdy::Http2HeaderBlock headers;
349 headers[spdy::kHttp2StatusHeader] = "200";
350 headers.AppendValueOrAddHeader("name", "value1");
351 headers.AppendValueOrAddHeader("name", "value2");
352 ASSERT_OK_AND_ASSIGN(const auto output, PerformConversion(headers));
353 EXPECT_EQ(kRawHeaders, ToSimpleString(output));
354 }
355
TEST_P(SpdyHeadersToHttpResponseHeadersTest,EmptyValue)356 TEST_P(SpdyHeadersToHttpResponseHeadersTest, EmptyValue) {
357 constexpr char kRawHeaders[] =
358 "HTTP/1.1 200\n"
359 "empty: \n";
360 spdy::Http2HeaderBlock headers;
361 headers[spdy::kHttp2StatusHeader] = "200";
362 headers.AppendValueOrAddHeader("empty", "");
363 ASSERT_OK_AND_ASSIGN(const auto output, PerformConversion(headers));
364 EXPECT_EQ(kRawHeaders, ToSimpleString(output));
365 }
366
TEST_P(SpdyHeadersToHttpResponseHeadersTest,PseudoHeadersAreDropped)367 TEST_P(SpdyHeadersToHttpResponseHeadersTest, PseudoHeadersAreDropped) {
368 constexpr char kRawHeaders[] =
369 "HTTP/1.1 200\n"
370 "Content-Length: 5\n";
371 spdy::Http2HeaderBlock headers;
372 headers[spdy::kHttp2StatusHeader] = "200";
373 headers[spdy::kHttp2MethodHeader] = "GET";
374 headers["Content-Length"] = "5";
375 headers[":fake"] = "ignored";
376 ASSERT_OK_AND_ASSIGN(const auto output, PerformConversion(headers));
377 EXPECT_EQ(kRawHeaders, ToSimpleString(output));
378 }
379
TEST_P(SpdyHeadersToHttpResponseHeadersTest,DoubleEmptyLocationHeader)380 TEST_P(SpdyHeadersToHttpResponseHeadersTest, DoubleEmptyLocationHeader) {
381 constexpr char kRawHeaders[] =
382 "HTTP/1.1 200\n"
383 "location: \n"
384 "location: \n";
385 spdy::Http2HeaderBlock headers;
386 headers[spdy::kHttp2StatusHeader] = "200";
387 headers.AppendValueOrAddHeader("location", "");
388 headers.AppendValueOrAddHeader("location", "");
389 ASSERT_OK_AND_ASSIGN(const auto output, PerformConversion(headers));
390 EXPECT_EQ(kRawHeaders, ToSimpleString(output));
391 }
392
TEST_P(SpdyHeadersToHttpResponseHeadersTest,DifferentLocationHeaderTriggersError)393 TEST_P(SpdyHeadersToHttpResponseHeadersTest,
394 DifferentLocationHeaderTriggersError) {
395 spdy::Http2HeaderBlock headers;
396 headers[spdy::kHttp2StatusHeader] = "200";
397 headers.AppendValueOrAddHeader("location", "https://same/");
398 headers.AppendValueOrAddHeader("location", "https://same/");
399 headers.AppendValueOrAddHeader("location", "https://different/");
400 EXPECT_THAT(PerformConversion(headers),
401 base::test::ErrorIs(ERR_RESPONSE_HEADERS_MULTIPLE_LOCATION));
402 }
403
404 // TODO(ricea): Ensure that QUICHE will never send us header values with leading
405 // or trailing whitespace and remove this test.
TEST_P(SpdyHeadersToHttpResponseHeadersTest,LocationEquivalenceIgnoresSurroundingSpace)406 TEST_P(SpdyHeadersToHttpResponseHeadersTest,
407 LocationEquivalenceIgnoresSurroundingSpace) {
408 constexpr char kRawHeaders[] =
409 "HTTP/1.1 200\n"
410 "location: https://same/\n"
411 "location: https://same/\n";
412 spdy::Http2HeaderBlock headers;
413 headers[spdy::kHttp2StatusHeader] = "200";
414 headers.AppendValueOrAddHeader("location", " https://same/");
415 headers.AppendValueOrAddHeader("location", "https://same/ ");
416 ASSERT_OK_AND_ASSIGN(const auto output, PerformConversion(headers));
417 EXPECT_EQ(kRawHeaders, ToSimpleString(output));
418 }
419
420 INSTANTIATE_TEST_SUITE_P(
421 SpdyHttpUtils,
422 SpdyHeadersToHttpResponseHeadersTest,
423 Values(SpdyHeadersToHttpResponseHeadersUsingRawString,
424 SpdyHeadersToHttpResponseHeadersUsingBuilder),
425 [](const testing::TestParamInfo<
__anond7b2e8a80202(const testing::TestParamInfo< SpdyHeadersToHttpResponseHeadersTest::ParamType>& info) 426 SpdyHeadersToHttpResponseHeadersTest::ParamType>& info) {
427 return info.param == SpdyHeadersToHttpResponseHeadersUsingRawString
428 ? "SpdyHeadersToHttpResponseHeadersUsingRawString"
429 : "SpdyHeadersToHttpResponseHeadersUsingBuilder";
430 });
431
432 } // namespace
433
434 } // namespace net
435