blob: e030ae5356a6575d87a576f6689d8cf957095afd [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/spdy/spdy_http_utils.h"
#include <stdint.h>
#include <limits>
#include "base/test/gmock_expected_support.h"
#include "base/test/scoped_feature_list.h"
#include "net/base/features.h"
#include "net/base/ip_endpoint.h"
#include "net/http/http_request_info.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_response_headers_test_util.h"
#include "net/http/http_response_info.h"
#include "net/third_party/quiche/src/quiche/common/http/http_header_block.h"
#include "net/third_party/quiche/src/quiche/http2/core/spdy_framer.h"
#include "net/third_party/quiche/src/quiche/http2/core/spdy_protocol.h"
#include "net/third_party/quiche/src/quiche/http2/test_tools/spdy_test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace net {
namespace {
using ::testing::Values;
// Check that the headers are ordered correctly, with pseudo-headers
// preceding HTTP headers per
// https://datatracker.ietf.org/doc/html/rfc9114#section-4.3
void CheckOrdering(const quiche::HttpHeaderBlock& headers) {
bool seen_http_header = false;
for (auto& header : headers) {
const bool is_pseudo = header.first.starts_with(':');
if (is_pseudo) {
ASSERT_FALSE(seen_http_header) << "Header order is incorrect:\n"
<< headers.DebugString();
} else {
seen_http_header = true;
}
}
}
TEST(SpdyHttpUtilsTest, ConvertRequestPriorityToSpdy3Priority) {
EXPECT_EQ(0, ConvertRequestPriorityToSpdyPriority(HIGHEST));
EXPECT_EQ(1, ConvertRequestPriorityToSpdyPriority(MEDIUM));
EXPECT_EQ(2, ConvertRequestPriorityToSpdyPriority(LOW));
EXPECT_EQ(3, ConvertRequestPriorityToSpdyPriority(LOWEST));
EXPECT_EQ(4, ConvertRequestPriorityToSpdyPriority(IDLE));
EXPECT_EQ(5, ConvertRequestPriorityToSpdyPriority(THROTTLED));
}
TEST(SpdyHttpUtilsTest, ConvertSpdy3PriorityToRequestPriority) {
EXPECT_EQ(HIGHEST, ConvertSpdyPriorityToRequestPriority(0));
EXPECT_EQ(MEDIUM, ConvertSpdyPriorityToRequestPriority(1));
EXPECT_EQ(LOW, ConvertSpdyPriorityToRequestPriority(2));
EXPECT_EQ(LOWEST, ConvertSpdyPriorityToRequestPriority(3));
EXPECT_EQ(IDLE, ConvertSpdyPriorityToRequestPriority(4));
EXPECT_EQ(THROTTLED, ConvertSpdyPriorityToRequestPriority(5));
// These are invalid values, but we should still handle them
// gracefully.
for (int i = 6; i < std::numeric_limits<uint8_t>::max(); ++i) {
EXPECT_EQ(IDLE, ConvertSpdyPriorityToRequestPriority(i));
}
}
TEST(SpdyHttpUtilsTest, CreateSpdyHeadersFromHttpRequestHTTP2) {
GURL url("https://www.google.com/index.html");
HttpRequestInfo request;
request.method = "GET";
request.url = url;
request.priority_incremental = true;
request.extra_headers.SetHeader(HttpRequestHeaders::kUserAgent, "Chrome/1.1");
quiche::HttpHeaderBlock headers;
CreateSpdyHeadersFromHttpRequest(request, RequestPriority::HIGHEST,
request.extra_headers, &headers);
CheckOrdering(headers);
EXPECT_EQ("GET", headers[":method"]);
EXPECT_EQ("https", headers[":scheme"]);
EXPECT_EQ("www.google.com", headers[":authority"]);
EXPECT_EQ("/index.html", headers[":path"]);
EXPECT_EQ("u=0, i", headers[net::kHttp2PriorityHeader]);
EXPECT_EQ(headers.end(), headers.find(":version"));
EXPECT_EQ("Chrome/1.1", headers["user-agent"]);
}
TEST(SpdyHttpUtilsTest, CreateSpdyHeadersFromHttpRequestForExtendedConnect) {
GURL url("https://www.google.com/index.html");
HttpRequestInfo request;
request.method = "CONNECT";
request.url = url;
request.priority_incremental = true;
request.extra_headers.SetHeader(HttpRequestHeaders::kUserAgent, "Chrome/1.1");
quiche::HttpHeaderBlock headers;
CreateSpdyHeadersFromHttpRequestForExtendedConnect(
request, RequestPriority::HIGHEST, "connect-ftp", request.extra_headers,
&headers);
CheckOrdering(headers);
EXPECT_EQ("CONNECT", headers[":method"]);
EXPECT_EQ("https", headers[":scheme"]);
EXPECT_EQ("www.google.com", headers[":authority"]);
EXPECT_EQ("connect-ftp", headers[":protocol"]);
EXPECT_EQ("/index.html", headers[":path"]);
EXPECT_EQ("u=0, i", headers[net::kHttp2PriorityHeader]);
EXPECT_EQ("Chrome/1.1", headers["user-agent"]);
}
TEST(SpdyHttpUtilsTest, CreateSpdyHeadersWithDefaultPriority) {
GURL url("https://www.google.com/index.html");
HttpRequestInfo request;
request.method = "GET";
request.url = url;
request.priority_incremental = false;
request.extra_headers.SetHeader(HttpRequestHeaders::kUserAgent, "Chrome/1.1");
quiche::HttpHeaderBlock headers;
CreateSpdyHeadersFromHttpRequest(request, RequestPriority::DEFAULT_PRIORITY,
request.extra_headers, &headers);
CheckOrdering(headers);
EXPECT_EQ("GET", headers[":method"]);
EXPECT_EQ("https", headers[":scheme"]);
EXPECT_EQ("www.google.com", headers[":authority"]);
EXPECT_EQ("/index.html", headers[":path"]);
EXPECT_FALSE(headers.contains(net::kHttp2PriorityHeader));
EXPECT_FALSE(headers.contains(":version"));
EXPECT_EQ("Chrome/1.1", headers["user-agent"]);
}
TEST(SpdyHttpUtilsTest, CreateSpdyHeadersWithExistingPriority) {
GURL url("https://www.google.com/index.html");
HttpRequestInfo request;
request.method = "GET";
request.url = url;
request.priority_incremental = true;
request.extra_headers.SetHeader(HttpRequestHeaders::kUserAgent, "Chrome/1.1");
request.extra_headers.SetHeader(net::kHttp2PriorityHeader,
"explicit-priority");
quiche::HttpHeaderBlock headers;
CreateSpdyHeadersFromHttpRequest(request, RequestPriority::HIGHEST,
request.extra_headers, &headers);
CheckOrdering(headers);
EXPECT_EQ("GET", headers[":method"]);
EXPECT_EQ("https", headers[":scheme"]);
EXPECT_EQ("www.google.com", headers[":authority"]);
EXPECT_EQ("/index.html", headers[":path"]);
EXPECT_EQ("explicit-priority", headers[net::kHttp2PriorityHeader]);
EXPECT_EQ(headers.end(), headers.find(":version"));
EXPECT_EQ("Chrome/1.1", headers["user-agent"]);
}
TEST(SpdyHttpUtilsTest, CreateSpdyHeadersFromHttpRequestConnectHTTP2) {
GURL url("https://www.google.com/index.html");
HttpRequestInfo request;
request.method = "CONNECT";
request.url = url;
request.extra_headers.SetHeader(HttpRequestHeaders::kUserAgent, "Chrome/1.1");
quiche::HttpHeaderBlock headers;
CreateSpdyHeadersFromHttpRequest(request, RequestPriority::DEFAULT_PRIORITY,
request.extra_headers, &headers);
CheckOrdering(headers);
EXPECT_EQ("CONNECT", headers[":method"]);
EXPECT_TRUE(headers.end() == headers.find(":scheme"));
EXPECT_EQ("www.google.com:443", headers[":authority"]);
EXPECT_EQ(headers.end(), headers.find(":path"));
EXPECT_EQ(headers.end(), headers.find(":scheme"));
EXPECT_TRUE(headers.end() == headers.find(":version"));
EXPECT_EQ("Chrome/1.1", headers["user-agent"]);
}
constexpr auto ToSimpleString = test::HttpResponseHeadersToSimpleString;
enum class SpdyHeadersToHttpResponseHeadersFeatureConfig {
kUseRawString,
kUseBuilder
};
std::string PrintToString(
SpdyHeadersToHttpResponseHeadersFeatureConfig config) {
switch (config) {
case SpdyHeadersToHttpResponseHeadersFeatureConfig::kUseRawString:
return "RawString";
case SpdyHeadersToHttpResponseHeadersFeatureConfig::kUseBuilder:
return "UseBuilder";
}
}
class SpdyHeadersToHttpResponseTest
: public ::testing::TestWithParam<
SpdyHeadersToHttpResponseHeadersFeatureConfig> {
public:
SpdyHeadersToHttpResponseTest() {
switch (GetParam()) {
case SpdyHeadersToHttpResponseHeadersFeatureConfig::kUseRawString:
feature_list_.InitWithFeatures(
{}, {features::kSpdyHeadersToHttpResponseUseBuilder});
break;
case SpdyHeadersToHttpResponseHeadersFeatureConfig::kUseBuilder:
feature_list_.InitWithFeatures(
{features::kSpdyHeadersToHttpResponseUseBuilder}, {});
break;
}
}
private:
base::test::ScopedFeatureList feature_list_;
};
// This test behaves the same regardless of which features are enabled.
TEST_P(SpdyHeadersToHttpResponseTest, SpdyHeadersToHttpResponse) {
constexpr char kExpectedSimpleString[] =
"HTTP/1.1 200\n"
"content-type: text/html\n"
"cache-control: no-cache, no-store\n"
"set-cookie: test_cookie=1234567890; Max-Age=3600; Secure; HttpOnly\n"
"set-cookie: session_id=abcdefghijklmnopqrstuvwxyz; Path=/; HttpOnly\n";
quiche::HttpHeaderBlock input;
input[spdy::kHttp2StatusHeader] = "200";
input["content-type"] = "text/html";
input["cache-control"] = "no-cache, no-store";
input.AppendValueOrAddHeader(
"set-cookie", "test_cookie=1234567890; Max-Age=3600; Secure; HttpOnly");
input.AppendValueOrAddHeader(
"set-cookie", "session_id=abcdefghijklmnopqrstuvwxyz; Path=/; HttpOnly");
net::HttpResponseInfo output;
output.remote_endpoint = {{127, 0, 0, 1}, 80};
EXPECT_EQ(OK, SpdyHeadersToHttpResponse(input, &output));
// This should be set.
EXPECT_TRUE(output.was_fetched_via_spdy);
// This should be untouched.
EXPECT_EQ(output.remote_endpoint, IPEndPoint({127, 0, 0, 1}, 80));
EXPECT_EQ(kExpectedSimpleString, ToSimpleString(output.headers));
}
INSTANTIATE_TEST_SUITE_P(
SpdyHttpUtils,
SpdyHeadersToHttpResponseTest,
Values(SpdyHeadersToHttpResponseHeadersFeatureConfig::kUseRawString,
SpdyHeadersToHttpResponseHeadersFeatureConfig::kUseBuilder),
::testing::PrintToStringParamName());
// TODO(ricea): Once SpdyHeadersToHttpResponseHeadersUsingRawString has been
// removed, remove the parameterization and make these into
// SpdyHeadersToHttpResponse tests.
using SpdyHeadersToHttpResponseHeadersFunctionPtrType =
base::expected<scoped_refptr<HttpResponseHeaders>, int> (*)(
const quiche::HttpHeaderBlock&);
class SpdyHeadersToHttpResponseHeadersTest
: public testing::TestWithParam<
SpdyHeadersToHttpResponseHeadersFunctionPtrType> {
public:
base::expected<scoped_refptr<HttpResponseHeaders>, int> PerformConversion(
const quiche::HttpHeaderBlock& headers) {
return GetParam()(headers);
}
};
TEST_P(SpdyHeadersToHttpResponseHeadersTest, NoStatus) {
quiche::HttpHeaderBlock headers;
EXPECT_THAT(PerformConversion(headers),
base::test::ErrorIs(ERR_INCOMPLETE_HTTP2_HEADERS));
}
TEST_P(SpdyHeadersToHttpResponseHeadersTest, EmptyStatus) {
constexpr char kRawHeaders[] = "HTTP/1.1 200\n";
quiche::HttpHeaderBlock headers;
headers[":status"] = "";
ASSERT_OK_AND_ASSIGN(const auto output, PerformConversion(headers));
EXPECT_EQ(kRawHeaders, ToSimpleString(output));
}
TEST_P(SpdyHeadersToHttpResponseHeadersTest, Plain200) {
// ":status" does not appear as a header in the output.
constexpr char kRawHeaders[] = "HTTP/1.1 200\n";
quiche::HttpHeaderBlock headers;
headers[spdy::kHttp2StatusHeader] = "200";
ASSERT_OK_AND_ASSIGN(const auto output, PerformConversion(headers));
EXPECT_EQ(kRawHeaders, ToSimpleString(output));
}
TEST_P(SpdyHeadersToHttpResponseHeadersTest, MultipleLocation) {
quiche::HttpHeaderBlock headers;
headers[spdy::kHttp2StatusHeader] = "304";
headers["Location"] = "https://example.com/1";
headers.AppendValueOrAddHeader("location", "https://example.com/2");
EXPECT_THAT(PerformConversion(headers),
base::test::ErrorIs(ERR_RESPONSE_HEADERS_MULTIPLE_LOCATION));
}
TEST_P(SpdyHeadersToHttpResponseHeadersTest, SpacesAmongValues) {
constexpr char kRawHeaders[] =
"HTTP/1.1 200\n"
"spaces: foo , bar\n";
quiche::HttpHeaderBlock headers;
headers[spdy::kHttp2StatusHeader] = "200";
headers["spaces"] = "foo , bar";
ASSERT_OK_AND_ASSIGN(const auto output, PerformConversion(headers));
EXPECT_EQ(kRawHeaders, ToSimpleString(output));
}
TEST_P(SpdyHeadersToHttpResponseHeadersTest, RepeatedHeader) {
constexpr char kRawHeaders[] =
"HTTP/1.1 200\n"
"name: value1\n"
"name: value2\n";
quiche::HttpHeaderBlock headers;
headers[spdy::kHttp2StatusHeader] = "200";
headers.AppendValueOrAddHeader("name", "value1");
headers.AppendValueOrAddHeader("name", "value2");
ASSERT_OK_AND_ASSIGN(const auto output, PerformConversion(headers));
EXPECT_EQ(kRawHeaders, ToSimpleString(output));
}
TEST_P(SpdyHeadersToHttpResponseHeadersTest, EmptyValue) {
constexpr char kRawHeaders[] =
"HTTP/1.1 200\n"
"empty: \n";
quiche::HttpHeaderBlock headers;
headers[spdy::kHttp2StatusHeader] = "200";
headers.AppendValueOrAddHeader("empty", "");
ASSERT_OK_AND_ASSIGN(const auto output, PerformConversion(headers));
EXPECT_EQ(kRawHeaders, ToSimpleString(output));
}
TEST_P(SpdyHeadersToHttpResponseHeadersTest, PseudoHeadersAreDropped) {
constexpr char kRawHeaders[] =
"HTTP/1.1 200\n"
"Content-Length: 5\n";
quiche::HttpHeaderBlock headers;
headers[spdy::kHttp2StatusHeader] = "200";
headers[spdy::kHttp2MethodHeader] = "GET";
headers["Content-Length"] = "5";
headers[":fake"] = "ignored";
ASSERT_OK_AND_ASSIGN(const auto output, PerformConversion(headers));
EXPECT_EQ(kRawHeaders, ToSimpleString(output));
}
TEST_P(SpdyHeadersToHttpResponseHeadersTest, DoubleEmptyLocationHeader) {
constexpr char kRawHeaders[] =
"HTTP/1.1 200\n"
"location: \n"
"location: \n";
quiche::HttpHeaderBlock headers;
headers[spdy::kHttp2StatusHeader] = "200";
headers.AppendValueOrAddHeader("location", "");
headers.AppendValueOrAddHeader("location", "");
ASSERT_OK_AND_ASSIGN(const auto output, PerformConversion(headers));
EXPECT_EQ(kRawHeaders, ToSimpleString(output));
}
TEST_P(SpdyHeadersToHttpResponseHeadersTest,
DifferentLocationHeaderTriggersError) {
quiche::HttpHeaderBlock headers;
headers[spdy::kHttp2StatusHeader] = "200";
headers.AppendValueOrAddHeader("location", "https://same/");
headers.AppendValueOrAddHeader("location", "https://same/");
headers.AppendValueOrAddHeader("location", "https://different/");
EXPECT_THAT(PerformConversion(headers),
base::test::ErrorIs(ERR_RESPONSE_HEADERS_MULTIPLE_LOCATION));
}
// TODO(ricea): Ensure that QUICHE will never send us header values with leading
// or trailing whitespace and remove this test.
TEST_P(SpdyHeadersToHttpResponseHeadersTest,
LocationEquivalenceIgnoresSurroundingSpace) {
constexpr char kRawHeaders[] =
"HTTP/1.1 200\n"
"location: https://same/\n"
"location: https://same/\n";
quiche::HttpHeaderBlock headers;
headers[spdy::kHttp2StatusHeader] = "200";
headers.AppendValueOrAddHeader("location", " https://same/");
headers.AppendValueOrAddHeader("location", "https://same/ ");
ASSERT_OK_AND_ASSIGN(const auto output, PerformConversion(headers));
EXPECT_EQ(kRawHeaders, ToSimpleString(output));
}
INSTANTIATE_TEST_SUITE_P(
SpdyHttpUtils,
SpdyHeadersToHttpResponseHeadersTest,
Values(SpdyHeadersToHttpResponseHeadersUsingRawString,
SpdyHeadersToHttpResponseHeadersUsingBuilder),
[](const testing::TestParamInfo<
SpdyHeadersToHttpResponseHeadersTest::ParamType>& info) {
return info.param == SpdyHeadersToHttpResponseHeadersUsingRawString
? "SpdyHeadersToHttpResponseHeadersUsingRawString"
: "SpdyHeadersToHttpResponseHeadersUsingBuilder";
});
} // namespace
} // namespace net