// 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/http/http_response_headers.h" #include #include #include #include #include #include "base/pickle.h" #include "base/strings/stringprintf.h" #include "base/time/time.h" #include "base/values.h" #include "net/base/cronet_buildflags.h" #include "net/base/tracing.h" #include "net/http/http_byte_range.h" #include "net/http/http_response_headers_test_util.h" #include "net/http/http_util.h" #include "testing/gtest/include/gtest/gtest.h" #if !BUILDFLAG(CRONET_BUILD) #include "third_party/perfetto/include/perfetto/test/traced_value_test_support.h" #endif namespace net { namespace { struct TestData { const char* raw_headers; const char* expected_headers; HttpVersion expected_version; int expected_response_code; const char* expected_status_text; }; class HttpResponseHeadersTest : public testing::Test { }; // Transform "normal"-looking headers (\n-separated) to the appropriate // input format for ParseRawHeaders (\0-separated). void HeadersToRaw(std::string* headers) { std::replace(headers->begin(), headers->end(), '\n', '\0'); if (!headers->empty()) *headers += '\0'; } class HttpResponseHeadersCacheControlTest : public HttpResponseHeadersTest { protected: // Make tests less verbose. typedef base::TimeDelta TimeDelta; // Initilise the headers() value with a Cache-Control header set to // |cache_control|. |cache_control| is copied and so can safely be a // temporary. void InitializeHeadersWithCacheControl(const char* cache_control) { std::string raw_headers("HTTP/1.1 200 OK\n"); raw_headers += "Cache-Control: "; raw_headers += cache_control; raw_headers += "\n"; HeadersToRaw(&raw_headers); headers_ = base::MakeRefCounted(raw_headers); } const scoped_refptr& headers() { return headers_; } // Return a pointer to a TimeDelta object. For use when the value doesn't // matter. TimeDelta* TimeDeltaPointer() { return &delta_; } // Get the max-age value. This should only be used in tests where a valid // max-age parameter is expected to be present. TimeDelta GetMaxAgeValue() { DCHECK(headers_.get()) << "Call InitializeHeadersWithCacheControl() first"; TimeDelta max_age_value; EXPECT_TRUE(headers()->GetMaxAgeValue(&max_age_value)); return max_age_value; } // Get the stale-while-revalidate value. This should only be used in tests // where a valid max-age parameter is expected to be present. TimeDelta GetStaleWhileRevalidateValue() { DCHECK(headers_.get()) << "Call InitializeHeadersWithCacheControl() first"; TimeDelta stale_while_revalidate_value; EXPECT_TRUE( headers()->GetStaleWhileRevalidateValue(&stale_while_revalidate_value)); return stale_while_revalidate_value; } private: scoped_refptr headers_; TimeDelta delta_; }; class CommonHttpResponseHeadersTest : public HttpResponseHeadersTest, public ::testing::WithParamInterface { }; constexpr auto ToSimpleString = test::HttpResponseHeadersToSimpleString; // Transform to readable output format (so it's easier to see diffs). void EscapeForPrinting(std::string* s) { std::replace(s->begin(), s->end(), ' ', '_'); std::replace(s->begin(), s->end(), '\n', '\\'); } TEST_P(CommonHttpResponseHeadersTest, TestCommon) { const TestData test = GetParam(); std::string raw_headers(test.raw_headers); HeadersToRaw(&raw_headers); std::string expected_headers(test.expected_headers); auto parsed = base::MakeRefCounted(raw_headers); std::string headers = ToSimpleString(parsed); EscapeForPrinting(&headers); EscapeForPrinting(&expected_headers); EXPECT_EQ(expected_headers, headers); SCOPED_TRACE(test.raw_headers); EXPECT_TRUE(test.expected_version == parsed->GetHttpVersion()); EXPECT_EQ(test.expected_response_code, parsed->response_code()); EXPECT_EQ(test.expected_status_text, parsed->GetStatusText()); } TestData response_headers_tests[] = { {// Normalize whitespace. "HTTP/1.1 202 Accepted \n" "Content-TYPE : text/html; charset=utf-8 \n" "Set-Cookie: a \n" "Set-Cookie: b \n", "HTTP/1.1 202 Accepted\n" "Content-TYPE: text/html; charset=utf-8\n" "Set-Cookie: a\n" "Set-Cookie: b\n", HttpVersion(1, 1), 202, "Accepted"}, {// Normalize leading whitespace. "HTTP/1.1 202 Accepted \n" // Starts with space -- will be skipped as invalid. " Content-TYPE : text/html; charset=utf-8 \n" "Set-Cookie: a \n" "Set-Cookie: b \n", "HTTP/1.1 202 Accepted\n" "Set-Cookie: a\n" "Set-Cookie: b\n", HttpVersion(1, 1), 202, "Accepted"}, {// Keep whitespace within status text. "HTTP/1.0 404 Not found \n", "HTTP/1.0 404 Not found\n", HttpVersion(1, 0), 404, "Not found"}, {// Normalize blank headers. "HTTP/1.1 200 OK\n" "Header1 : \n" "Header2: \n" "Header3:\n" "Header4\n" "Header5 :\n", "HTTP/1.1 200 OK\n" "Header1: \n" "Header2: \n" "Header3: \n" "Header5: \n", HttpVersion(1, 1), 200, "OK"}, {// Don't believe the http/0.9 version if there are headers! "hTtP/0.9 201\n" "Content-TYPE: text/html; charset=utf-8\n", "HTTP/1.0 201\n" "Content-TYPE: text/html; charset=utf-8\n", HttpVersion(1, 0), 201, ""}, {// Accept the HTTP/0.9 version number if there are no headers. // This is how HTTP/0.9 responses get constructed from // HttpNetworkTransaction. "hTtP/0.9 200 OK\n", "HTTP/0.9 200 OK\n", HttpVersion(0, 9), 200, "OK"}, {// Do not add missing status text. "HTTP/1.1 201\n" "Content-TYPE: text/html; charset=utf-8\n", "HTTP/1.1 201\n" "Content-TYPE: text/html; charset=utf-8\n", HttpVersion(1, 1), 201, ""}, {// Normalize bad status line. "SCREWED_UP_STATUS_LINE\n" "Content-TYPE: text/html; charset=utf-8\n", "HTTP/1.0 200 OK\n" "Content-TYPE: text/html; charset=utf-8\n", HttpVersion(1, 0), 200, "OK"}, {// Normalize bad status line. "Foo bar.", "HTTP/1.0 200\n", HttpVersion(1, 0), 200, ""}, {// Normalize invalid status code. "HTTP/1.1 -1 Unknown\n", "HTTP/1.1 200\n", HttpVersion(1, 1), 200, ""}, {// Normalize empty header. "", "HTTP/1.0 200 OK\n", HttpVersion(1, 0), 200, "OK"}, {// Normalize headers that start with a colon. "HTTP/1.1 202 Accepted \n" "foo: bar\n" ": a \n" " : b\n" "baz: blat \n", "HTTP/1.1 202 Accepted\n" "foo: bar\n" "baz: blat\n", HttpVersion(1, 1), 202, "Accepted"}, {// Normalize headers that end with a colon. "HTTP/1.1 202 Accepted \n" "foo: \n" "bar:\n" "baz: blat \n" "zip:\n", "HTTP/1.1 202 Accepted\n" "foo: \n" "bar: \n" "baz: blat\n" "zip: \n", HttpVersion(1, 1), 202, "Accepted"}, {// Normalize whitespace headers. "\n \n", "HTTP/1.0 200 OK\n", HttpVersion(1, 0), 200, "OK"}, {// Has multiple Set-Cookie headers. "HTTP/1.1 200 OK\n" "Set-Cookie: x=1\n" "Set-Cookie: y=2\n", "HTTP/1.1 200 OK\n" "Set-Cookie: x=1\n" "Set-Cookie: y=2\n", HttpVersion(1, 1), 200, "OK"}, {// Has multiple cache-control headers. "HTTP/1.1 200 OK\n" "Cache-control: private\n" "cache-Control: no-store\n", "HTTP/1.1 200 OK\n" "Cache-control: private\n" "cache-Control: no-store\n", HttpVersion(1, 1), 200, "OK"}, {// Has multiple-value cache-control header. "HTTP/1.1 200 OK\n" "Cache-Control: private, no-store\n", "HTTP/1.1 200 OK\n" "Cache-Control: private, no-store\n", HttpVersion(1, 1), 200, "OK"}, {// Missing HTTP. " 200 Yes\n", "HTTP/1.0 200 Yes\n", HttpVersion(1, 0), 200, "Yes"}, {// Only HTTP. "HTTP\n", "HTTP/1.0 200 OK\n", HttpVersion(1, 0), 200, "OK"}, {// Missing HTTP version. "HTTP 404 No\n", "HTTP/1.0 404 No\n", HttpVersion(1, 0), 404, "No"}, {// Missing dot in HTTP version. "HTTP/1 304 Not Friday\n", "HTTP/1.0 304 Not Friday\n", HttpVersion(1, 0), 304, "Not Friday"}, {// Multi-digit HTTP version (our error detection is bad). "HTTP/234.01 204 Nothing here\n", "HTTP/2.0 204 Nothing here\n", HttpVersion(2, 0), 204, "Nothing here"}, {// HTTP minor version attached to response code (pretty bad parsing). "HTTP/1 302.1 Bad parse\n", "HTTP/1.1 302 .1 Bad parse\n", HttpVersion(1, 1), 302, ".1 Bad parse"}, {// HTTP minor version inside the status text (bad parsing). "HTTP/1 410 Gone in 0.1 seconds\n", "HTTP/1.1 410 Gone in 0.1 seconds\n", HttpVersion(1, 1), 410, "Gone in 0.1 seconds"}, {// Status text smushed into response code. "HTTP/1.1 426Smush\n", "HTTP/1.1 426 Smush\n", HttpVersion(1, 1), 426, "Smush"}, {// Tab not recognised as separator (this is standard compliant). "HTTP/1.1\t500 204 Bad\n", "HTTP/1.1 204 Bad\n", HttpVersion(1, 1), 204, "Bad"}, {// Junk after HTTP version is ignored. "HTTP/1.1ignored 201 Not ignored\n", "HTTP/1.1 201 Not ignored\n", HttpVersion(1, 1), 201, "Not ignored"}, {// Tab gets included in status text. "HTTP/1.1 501\tStatus\t\n", "HTTP/1.1 501 \tStatus\t\n", HttpVersion(1, 1), 501, "\tStatus\t"}, {// Zero response code. "HTTP/1.1 0 Zero\n", "HTTP/1.1 0 Zero\n", HttpVersion(1, 1), 0, "Zero"}, {// Oversize response code. "HTTP/1.1 20230904 Monday\n", "HTTP/1.1 20230904 Monday\n", HttpVersion(1, 1), 20230904, "Monday"}, {// Overflowing response code. "HTTP/1.1 9123456789 Overflow\n", "HTTP/1.1 9123456789 Overflow\n", HttpVersion(1, 1), 2147483647, "Overflow"}, }; INSTANTIATE_TEST_SUITE_P(HttpResponseHeaders, CommonHttpResponseHeadersTest, testing::ValuesIn(response_headers_tests)); struct PersistData { HttpResponseHeaders::PersistOptions options; const char* raw_headers; const char* expected_headers; }; class PersistenceTest : public HttpResponseHeadersTest, public ::testing::WithParamInterface { }; TEST_P(PersistenceTest, Persist) { const PersistData test = GetParam(); std::string headers = test.raw_headers; HeadersToRaw(&headers); auto parsed1 = base::MakeRefCounted(headers); base::Pickle pickle; parsed1->Persist(&pickle, test.options); base::PickleIterator iter(pickle); auto parsed2 = base::MakeRefCounted(&iter); EXPECT_EQ(std::string(test.expected_headers), ToSimpleString(parsed2)); } const struct PersistData persistence_tests[] = { {HttpResponseHeaders::PERSIST_ALL, "HTTP/1.1 200 OK\n" "Cache-control:private\n" "cache-Control:no-store\n", "HTTP/1.1 200 OK\n" "Cache-control: private\n" "cache-Control: no-store\n"}, {HttpResponseHeaders::PERSIST_SANS_HOP_BY_HOP, "HTTP/1.1 200 OK\n" "connection: keep-alive\n" "server: blah\n", "HTTP/1.1 200 OK\n" "server: blah\n"}, {HttpResponseHeaders::PERSIST_SANS_NON_CACHEABLE | HttpResponseHeaders::PERSIST_SANS_HOP_BY_HOP, "HTTP/1.1 200 OK\n" "fOo: 1\n" "Foo: 2\n" "Transfer-Encoding: chunked\n" "CoNnection: keep-alive\n" "cache-control: private, no-cache=\"foo\"\n", "HTTP/1.1 200 OK\n" "cache-control: private, no-cache=\"foo\"\n"}, {HttpResponseHeaders::PERSIST_SANS_NON_CACHEABLE, "HTTP/1.1 200 OK\n" "Foo: 2\n" "Cache-Control: private,no-cache=\"foo, bar\"\n" "bar", "HTTP/1.1 200 OK\n" "Cache-Control: private,no-cache=\"foo, bar\"\n"}, // Ignore bogus no-cache value. {HttpResponseHeaders::PERSIST_SANS_NON_CACHEABLE, "HTTP/1.1 200 OK\n" "Foo: 2\n" "Cache-Control: private,no-cache=foo\n", "HTTP/1.1 200 OK\n" "Foo: 2\n" "Cache-Control: private,no-cache=foo\n"}, // Ignore bogus no-cache value. {HttpResponseHeaders::PERSIST_SANS_NON_CACHEABLE, "HTTP/1.1 200 OK\n" "Foo: 2\n" "Cache-Control: private, no-cache=\n", "HTTP/1.1 200 OK\n" "Foo: 2\n" "Cache-Control: private, no-cache=\n"}, // Ignore empty no-cache value. {HttpResponseHeaders::PERSIST_SANS_NON_CACHEABLE, "HTTP/1.1 200 OK\n" "Foo: 2\n" "Cache-Control: private, no-cache=\"\"\n", "HTTP/1.1 200 OK\n" "Foo: 2\n" "Cache-Control: private, no-cache=\"\"\n"}, // Ignore wrong quotes no-cache value. {HttpResponseHeaders::PERSIST_SANS_NON_CACHEABLE, "HTTP/1.1 200 OK\n" "Foo: 2\n" "Cache-Control: private, no-cache=\'foo\'\n", "HTTP/1.1 200 OK\n" "Foo: 2\n" "Cache-Control: private, no-cache=\'foo\'\n"}, // Ignore unterminated quotes no-cache value. {HttpResponseHeaders::PERSIST_SANS_NON_CACHEABLE, "HTTP/1.1 200 OK\n" "Foo: 2\n" "Cache-Control: private, no-cache=\"foo\n", "HTTP/1.1 200 OK\n" "Foo: 2\n" "Cache-Control: private, no-cache=\"foo\n"}, // Accept sloppy LWS. {HttpResponseHeaders::PERSIST_SANS_NON_CACHEABLE, "HTTP/1.1 200 OK\n" "Foo: 2\n" "Cache-Control: private, no-cache=\" foo\t, bar\"\n", "HTTP/1.1 200 OK\n" "Cache-Control: private, no-cache=\" foo\t, bar\"\n"}, // Header name appears twice, separated by another header. {HttpResponseHeaders::PERSIST_ALL, "HTTP/1.1 200 OK\n" "Foo: 1\n" "Bar: 2\n" "Foo: 3\n", "HTTP/1.1 200 OK\n" "Foo: 1\n" "Bar: 2\n" "Foo: 3\n"}, // Header name appears twice, separated by another header (type 2). {HttpResponseHeaders::PERSIST_ALL, "HTTP/1.1 200 OK\n" "Foo: 1, 3\n" "Bar: 2\n" "Foo: 4\n", "HTTP/1.1 200 OK\n" "Foo: 1, 3\n" "Bar: 2\n" "Foo: 4\n"}, // Test filtering of cookie headers. {HttpResponseHeaders::PERSIST_SANS_COOKIES, "HTTP/1.1 200 OK\n" "Set-Cookie: foo=bar; httponly\n" "Set-Cookie: bar=foo\n" "Bar: 1\n" "Set-Cookie2: bar2=foo2\n", "HTTP/1.1 200 OK\n" "Bar: 1\n"}, {HttpResponseHeaders::PERSIST_SANS_COOKIES, "HTTP/1.1 200 OK\n" "Set-Cookie: foo=bar\n" "Foo: 2\n" "Clear-Site-Data: \"cookies\"\n" "Bar: 3\n", "HTTP/1.1 200 OK\n" "Foo: 2\n" "Bar: 3\n"}, // Test LWS at the end of a header. {HttpResponseHeaders::PERSIST_ALL, "HTTP/1.1 200 OK\n" "Content-Length: 450 \n" "Content-Encoding: gzip\n", "HTTP/1.1 200 OK\n" "Content-Length: 450\n" "Content-Encoding: gzip\n"}, // Test LWS at the end of a header. {HttpResponseHeaders::PERSIST_RAW, "HTTP/1.1 200 OK\n" "Content-Length: 450 \n" "Content-Encoding: gzip\n", "HTTP/1.1 200 OK\n" "Content-Length: 450\n" "Content-Encoding: gzip\n"}, // Test filtering of transport security state headers. {HttpResponseHeaders::PERSIST_SANS_SECURITY_STATE, "HTTP/1.1 200 OK\n" "Strict-Transport-Security: max-age=1576800\n" "Bar: 1\n", "HTTP/1.1 200 OK\n" "Bar: 1\n"}, }; INSTANTIATE_TEST_SUITE_P(HttpResponseHeaders, PersistenceTest, testing::ValuesIn(persistence_tests)); TEST(HttpResponseHeadersTest, EnumerateHeader_Coalesced) { // Ensure that commas in quoted strings are not regarded as value separators. // Ensure that whitespace following a value is trimmed properly. std::string headers = "HTTP/1.1 200 OK\n" "Cache-control:,,private , no-cache=\"set-cookie,server\",\n" "cache-Control: no-store\n" "cache-Control:\n"; HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); size_t iter = 0; std::string value; ASSERT_TRUE(parsed->EnumerateHeader(&iter, "cache-control", &value)); EXPECT_EQ("", value); ASSERT_TRUE(parsed->EnumerateHeader(&iter, "cache-control", &value)); EXPECT_EQ("", value); ASSERT_TRUE(parsed->EnumerateHeader(&iter, "cache-control", &value)); EXPECT_EQ("private", value); ASSERT_TRUE(parsed->EnumerateHeader(&iter, "cache-control", &value)); EXPECT_EQ("no-cache=\"set-cookie,server\"", value); ASSERT_TRUE(parsed->EnumerateHeader(&iter, "cache-control", &value)); EXPECT_EQ("", value); ASSERT_TRUE(parsed->EnumerateHeader(&iter, "cache-control", &value)); EXPECT_EQ("no-store", value); ASSERT_TRUE(parsed->EnumerateHeader(&iter, "cache-control", &value)); EXPECT_EQ("", value); EXPECT_FALSE(parsed->EnumerateHeader(&iter, "cache-control", &value)); } TEST(HttpResponseHeadersTest, EnumerateHeader_Challenge) { // Even though WWW-Authenticate has commas, it should not be treated as // coalesced values. std::string headers = "HTTP/1.1 401 OK\n" "WWW-Authenticate:Digest realm=foobar, nonce=x, domain=y\n" "WWW-Authenticate:Basic realm=quatar\n"; HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); size_t iter = 0; std::string value; EXPECT_TRUE(parsed->EnumerateHeader(&iter, "WWW-Authenticate", &value)); EXPECT_EQ("Digest realm=foobar, nonce=x, domain=y", value); EXPECT_TRUE(parsed->EnumerateHeader(&iter, "WWW-Authenticate", &value)); EXPECT_EQ("Basic realm=quatar", value); EXPECT_FALSE(parsed->EnumerateHeader(&iter, "WWW-Authenticate", &value)); } TEST(HttpResponseHeadersTest, EnumerateHeader_DateValued) { // The comma in a date valued header should not be treated as a // field-value separator. std::string headers = "HTTP/1.1 200 OK\n" "Date: Tue, 07 Aug 2007 23:10:55 GMT\n" "Last-Modified: Wed, 01 Aug 2007 23:23:45 GMT\n"; HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); std::string value; EXPECT_TRUE(parsed->EnumerateHeader(nullptr, "date", &value)); EXPECT_EQ("Tue, 07 Aug 2007 23:10:55 GMT", value); EXPECT_TRUE(parsed->EnumerateHeader(nullptr, "last-modified", &value)); EXPECT_EQ("Wed, 01 Aug 2007 23:23:45 GMT", value); } TEST(HttpResponseHeadersTest, DefaultDateToGMT) { // Verify we make the best interpretation when parsing dates that incorrectly // do not end in "GMT" as RFC2616 requires. std::string headers = "HTTP/1.1 200 OK\n" "Date: Tue, 07 Aug 2007 23:10:55\n" "Last-Modified: Tue, 07 Aug 2007 19:10:55 EDT\n" "Expires: Tue, 07 Aug 2007 23:10:55 UTC\n"; HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); base::Time expected_value; ASSERT_TRUE(base::Time::FromString("Tue, 07 Aug 2007 23:10:55 GMT", &expected_value)); base::Time value; // When the timezone is missing, GMT is a good guess as its what RFC2616 // requires. EXPECT_TRUE(parsed->GetDateValue(&value)); EXPECT_EQ(expected_value, value); // If GMT is missing but an RFC822-conforming one is present, use that. EXPECT_TRUE(parsed->GetLastModifiedValue(&value)); EXPECT_EQ(expected_value, value); // If an unknown timezone is present, treat like a missing timezone and // default to GMT. The only example of a web server not specifying "GMT" // used "UTC" which is equivalent to GMT. if (parsed->GetExpiresValue(&value)) EXPECT_EQ(expected_value, value); } TEST(HttpResponseHeadersTest, GetAgeValue10) { std::string headers = "HTTP/1.1 200 OK\n" "Age: 10\n"; HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); base::TimeDelta age; ASSERT_TRUE(parsed->GetAgeValue(&age)); EXPECT_EQ(10, age.InSeconds()); } TEST(HttpResponseHeadersTest, GetAgeValue0) { std::string headers = "HTTP/1.1 200 OK\n" "Age: 0\n"; HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); base::TimeDelta age; ASSERT_TRUE(parsed->GetAgeValue(&age)); EXPECT_EQ(0, age.InSeconds()); } TEST(HttpResponseHeadersTest, GetAgeValueBogus) { std::string headers = "HTTP/1.1 200 OK\n" "Age: donkey\n"; HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); base::TimeDelta age; ASSERT_FALSE(parsed->GetAgeValue(&age)); } TEST(HttpResponseHeadersTest, GetAgeValueNegative) { std::string headers = "HTTP/1.1 200 OK\n" "Age: -10\n"; HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); base::TimeDelta age; ASSERT_FALSE(parsed->GetAgeValue(&age)); } TEST(HttpResponseHeadersTest, GetAgeValueLeadingPlus) { std::string headers = "HTTP/1.1 200 OK\n" "Age: +10\n"; HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); base::TimeDelta age; ASSERT_FALSE(parsed->GetAgeValue(&age)); } TEST(HttpResponseHeadersTest, GetAgeValueOverflow) { std::string headers = "HTTP/1.1 200 OK\n" "Age: 999999999999999999999999999999999999999999\n"; HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); base::TimeDelta age; ASSERT_TRUE(parsed->GetAgeValue(&age)); // Should have saturated to 2^32 - 1. EXPECT_EQ(static_cast(0xFFFFFFFFL), age.InSeconds()); } struct ContentTypeTestData { const std::string raw_headers; const std::string mime_type; const bool has_mimetype; const std::string charset; const bool has_charset; const std::string all_content_type; }; class ContentTypeTest : public HttpResponseHeadersTest, public ::testing::WithParamInterface { }; TEST_P(ContentTypeTest, GetMimeType) { const ContentTypeTestData test = GetParam(); std::string headers(test.raw_headers); HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); std::string value; EXPECT_EQ(test.has_mimetype, parsed->GetMimeType(&value)); EXPECT_EQ(test.mime_type, value); value.clear(); EXPECT_EQ(test.has_charset, parsed->GetCharset(&value)); EXPECT_EQ(test.charset, value); EXPECT_TRUE(parsed->GetNormalizedHeader("content-type", &value)); EXPECT_EQ(test.all_content_type, value); } // clang-format off const ContentTypeTestData mimetype_tests[] = { { "HTTP/1.1 200 OK\n" "Content-type: text/html\n", "text/html", true, "", false, "text/html" }, // Multiple content-type headers should give us the last one. { "HTTP/1.1 200 OK\n" "Content-type: text/html\n" "Content-type: text/html\n", "text/html", true, "", false, "text/html, text/html" }, { "HTTP/1.1 200 OK\n" "Content-type: text/plain\n" "Content-type: text/html\n" "Content-type: text/plain\n" "Content-type: text/html\n", "text/html", true, "", false, "text/plain, text/html, text/plain, text/html" }, // Test charset parsing. { "HTTP/1.1 200 OK\n" "Content-type: text/html\n" "Content-type: text/html; charset=ISO-8859-1\n", "text/html", true, "iso-8859-1", true, "text/html, text/html; charset=ISO-8859-1" }, // Test charset in double quotes. { "HTTP/1.1 200 OK\n" "Content-type: text/html\n" "Content-type: text/html; charset=\"ISO-8859-1\"\n", "text/html", true, "iso-8859-1", true, "text/html, text/html; charset=\"ISO-8859-1\"" }, // If there are multiple matching content-type headers, we carry // over the charset value. { "HTTP/1.1 200 OK\n" "Content-type: text/html;charset=utf-8\n" "Content-type: text/html\n", "text/html", true, "utf-8", true, "text/html;charset=utf-8, text/html" }, // Regression test for https://crbug.com/772350: // Single quotes are not delimiters but must be treated as part of charset. { "HTTP/1.1 200 OK\n" "Content-type: text/html;charset='utf-8'\n" "Content-type: text/html\n", "text/html", true, "'utf-8'", true, "text/html;charset='utf-8', text/html" }, // First charset wins if matching content-type. { "HTTP/1.1 200 OK\n" "Content-type: text/html;charset=utf-8\n" "Content-type: text/html;charset=iso-8859-1\n", "text/html", true, "iso-8859-1", true, "text/html;charset=utf-8, text/html;charset=iso-8859-1" }, // Charset is ignored if the content types change. { "HTTP/1.1 200 OK\n" "Content-type: text/plain;charset=utf-8\n" "Content-type: text/html\n", "text/html", true, "", false, "text/plain;charset=utf-8, text/html" }, // Empty content-type. { "HTTP/1.1 200 OK\n" "Content-type: \n", "", false, "", false, "" }, // Emtpy charset. { "HTTP/1.1 200 OK\n" "Content-type: text/html;charset=\n", "text/html", true, "", false, "text/html;charset=" }, // Multiple charsets, first one wins. { "HTTP/1.1 200 OK\n" "Content-type: text/html;charset=utf-8; charset=iso-8859-1\n", "text/html", true, "utf-8", true, "text/html;charset=utf-8; charset=iso-8859-1" }, // Multiple params. { "HTTP/1.1 200 OK\n" "Content-type: text/html; foo=utf-8; charset=iso-8859-1\n", "text/html", true, "iso-8859-1", true, "text/html; foo=utf-8; charset=iso-8859-1" }, { "HTTP/1.1 200 OK\n" "Content-type: text/html ; charset=utf-8 ; bar=iso-8859-1\n", "text/html", true, "utf-8", true, "text/html ; charset=utf-8 ; bar=iso-8859-1" }, // Comma embeded in quotes. { "HTTP/1.1 200 OK\n" "Content-type: text/html ; charset=\"utf-8,text/plain\" ;\n", "text/html", true, "utf-8,text/plain", true, "text/html ; charset=\"utf-8,text/plain\" ;" }, // Charset with leading spaces. { "HTTP/1.1 200 OK\n" "Content-type: text/html ; charset= \"utf-8\" ;\n", "text/html", true, "utf-8", true, "text/html ; charset= \"utf-8\" ;" }, // Media type comments in mime-type. { "HTTP/1.1 200 OK\n" "Content-type: text/html (html)\n", "text/html", true, "", false, "text/html (html)" }, // Incomplete charset= param. { "HTTP/1.1 200 OK\n" "Content-type: text/html; char=\n", "text/html", true, "", false, "text/html; char=" }, // Invalid media type: no slash. { "HTTP/1.1 200 OK\n" "Content-type: texthtml\n", "", false, "", false, "texthtml" }, // Invalid media type: "*/*". { "HTTP/1.1 200 OK\n" "Content-type: */*\n", "", false, "", false, "*/*" }, }; // clang-format on INSTANTIATE_TEST_SUITE_P(HttpResponseHeaders, ContentTypeTest, testing::ValuesIn(mimetype_tests)); struct RequiresValidationTestData { const char* headers; ValidationType validation_type; }; class RequiresValidationTest : public HttpResponseHeadersTest, public ::testing::WithParamInterface { }; TEST_P(RequiresValidationTest, RequiresValidation) { const RequiresValidationTestData test = GetParam(); base::Time request_time, response_time, current_time; ASSERT_TRUE( base::Time::FromString("Wed, 28 Nov 2007 00:40:09 GMT", &request_time)); ASSERT_TRUE( base::Time::FromString("Wed, 28 Nov 2007 00:40:12 GMT", &response_time)); ASSERT_TRUE( base::Time::FromString("Wed, 28 Nov 2007 00:45:20 GMT", ¤t_time)); std::string headers(test.headers); HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); ValidationType validation_type = parsed->RequiresValidation(request_time, response_time, current_time); EXPECT_EQ(test.validation_type, validation_type); } const struct RequiresValidationTestData requires_validation_tests[] = { // No expiry info: expires immediately. {"HTTP/1.1 200 OK\n" "\n", VALIDATION_SYNCHRONOUS}, // No expiry info: expires immediately. {"HTTP/1.1 200 OK\n" "\n", VALIDATION_SYNCHRONOUS}, // Valid for a little while. {"HTTP/1.1 200 OK\n" "cache-control: max-age=10000\n" "\n", VALIDATION_NONE}, // Expires in the future. {"HTTP/1.1 200 OK\n" "date: Wed, 28 Nov 2007 00:40:11 GMT\n" "expires: Wed, 28 Nov 2007 01:00:00 GMT\n" "\n", VALIDATION_NONE}, // Already expired. {"HTTP/1.1 200 OK\n" "date: Wed, 28 Nov 2007 00:40:11 GMT\n" "expires: Wed, 28 Nov 2007 00:00:00 GMT\n" "\n", VALIDATION_SYNCHRONOUS}, // Max-age trumps expires. {"HTTP/1.1 200 OK\n" "date: Wed, 28 Nov 2007 00:40:11 GMT\n" "expires: Wed, 28 Nov 2007 00:00:00 GMT\n" "cache-control: max-age=10000\n" "\n", VALIDATION_NONE}, // Last-modified heuristic: modified a while ago. {"HTTP/1.1 200 OK\n" "date: Wed, 28 Nov 2007 00:40:11 GMT\n" "last-modified: Wed, 27 Nov 2007 08:00:00 GMT\n" "\n", VALIDATION_NONE}, {"HTTP/1.1 203 Non-Authoritative Information\n" "date: Wed, 28 Nov 2007 00:40:11 GMT\n" "last-modified: Wed, 27 Nov 2007 08:00:00 GMT\n" "\n", VALIDATION_NONE}, {"HTTP/1.1 206 Partial Content\n" "date: Wed, 28 Nov 2007 00:40:11 GMT\n" "last-modified: Wed, 27 Nov 2007 08:00:00 GMT\n" "\n", VALIDATION_NONE}, // Last-modified heuristic: modified a while ago and it's VALIDATION_NONE // (fresh) like above but VALIDATION_SYNCHRONOUS if expires header value is // "0". {"HTTP/1.1 200 OK\n" "date: Wed, 28 Nov 2007 00:40:11 GMT\n" "last-modified: Tue, 27 Nov 2007 08:00:00 GMT\n" "expires: 0\n" "\n", VALIDATION_SYNCHRONOUS}, {"HTTP/1.1 200 OK\n" "date: Wed, 28 Nov 2007 00:40:11 GMT\n" "last-modified: Tue, 27 Nov 2007 08:00:00 GMT\n" "expires: 0 \n" "\n", VALIDATION_SYNCHRONOUS}, // The cache is fresh if the expires header value is an invalid date string // except for "0" {"HTTP/1.1 200 OK\n" "date: Wed, 28 Nov 2007 00:40:11 GMT\n" "last-modified: Tue, 27 Nov 2007 08:00:00 GMT\n" "expires: banana \n" "\n", VALIDATION_NONE}, // Last-modified heuristic: modified recently. {"HTTP/1.1 200 OK\n" "date: Wed, 28 Nov 2007 00:40:11 GMT\n" "last-modified: Wed, 28 Nov 2007 00:40:10 GMT\n" "\n", VALIDATION_SYNCHRONOUS}, {"HTTP/1.1 203 Non-Authoritative Information\n" "date: Wed, 28 Nov 2007 00:40:11 GMT\n" "last-modified: Wed, 28 Nov 2007 00:40:10 GMT\n" "\n", VALIDATION_SYNCHRONOUS}, {"HTTP/1.1 206 Partial Content\n" "date: Wed, 28 Nov 2007 00:40:11 GMT\n" "last-modified: Wed, 28 Nov 2007 00:40:10 GMT\n" "\n", VALIDATION_SYNCHRONOUS}, // Cached permanent redirect. {"HTTP/1.1 301 Moved Permanently\n" "\n", VALIDATION_NONE}, // Another cached permanent redirect. {"HTTP/1.1 308 Permanent Redirect\n" "\n", VALIDATION_NONE}, // Cached redirect: not reusable even though by default it would be. {"HTTP/1.1 300 Multiple Choices\n" "Cache-Control: no-cache\n" "\n", VALIDATION_SYNCHRONOUS}, // Cached forever by default. {"HTTP/1.1 410 Gone\n" "\n", VALIDATION_NONE}, // Cached temporary redirect: not reusable. {"HTTP/1.1 302 Found\n" "\n", VALIDATION_SYNCHRONOUS}, // Cached temporary redirect: reusable. {"HTTP/1.1 302 Found\n" "cache-control: max-age=10000\n" "\n", VALIDATION_NONE}, // Cache-control: max-age=N overrides expires: date in the past. {"HTTP/1.1 200 OK\n" "date: Wed, 28 Nov 2007 00:40:11 GMT\n" "expires: Wed, 28 Nov 2007 00:20:11 GMT\n" "cache-control: max-age=10000\n" "\n", VALIDATION_NONE}, // Cache-control: no-store overrides expires: in the future. {"HTTP/1.1 200 OK\n" "date: Wed, 28 Nov 2007 00:40:11 GMT\n" "expires: Wed, 29 Nov 2007 00:40:11 GMT\n" "cache-control: no-store,private,no-cache=\"foo\"\n" "\n", VALIDATION_SYNCHRONOUS}, // Pragma: no-cache overrides last-modified heuristic. {"HTTP/1.1 200 OK\n" "date: Wed, 28 Nov 2007 00:40:11 GMT\n" "last-modified: Wed, 27 Nov 2007 08:00:00 GMT\n" "pragma: no-cache\n" "\n", VALIDATION_SYNCHRONOUS}, // max-age has expired, needs synchronous revalidation {"HTTP/1.1 200 OK\n" "date: Wed, 28 Nov 2007 00:40:11 GMT\n" "cache-control: max-age=300\n" "\n", VALIDATION_SYNCHRONOUS}, // max-age has expired, stale-while-revalidate has not, eligible for // asynchronous revalidation {"HTTP/1.1 200 OK\n" "date: Wed, 28 Nov 2007 00:40:11 GMT\n" "cache-control: max-age=300, stale-while-revalidate=3600\n" "\n", VALIDATION_ASYNCHRONOUS}, // max-age and stale-while-revalidate have expired, needs synchronous // revalidation {"HTTP/1.1 200 OK\n" "date: Wed, 28 Nov 2007 00:40:11 GMT\n" "cache-control: max-age=300, stale-while-revalidate=5\n" "\n", VALIDATION_SYNCHRONOUS}, // max-age is 0, stale-while-revalidate is large enough to permit // asynchronous revalidation {"HTTP/1.1 200 OK\n" "date: Wed, 28 Nov 2007 00:40:11 GMT\n" "cache-control: max-age=0, stale-while-revalidate=360\n" "\n", VALIDATION_ASYNCHRONOUS}, // stale-while-revalidate must not override no-cache or similar directives. {"HTTP/1.1 200 OK\n" "date: Wed, 28 Nov 2007 00:40:11 GMT\n" "cache-control: no-cache, stale-while-revalidate=360\n" "\n", VALIDATION_SYNCHRONOUS}, // max-age has not expired, so no revalidation is needed. {"HTTP/1.1 200 OK\n" "date: Wed, 28 Nov 2007 00:40:11 GMT\n" "cache-control: max-age=3600, stale-while-revalidate=3600\n" "\n", VALIDATION_NONE}, // must-revalidate overrides stale-while-revalidate, so synchronous // validation // is needed. {"HTTP/1.1 200 OK\n" "date: Wed, 28 Nov 2007 00:40:11 GMT\n" "cache-control: must-revalidate, max-age=300, " "stale-while-revalidate=3600\n" "\n", VALIDATION_SYNCHRONOUS}, // TODO(darin): Add many many more tests here. }; INSTANTIATE_TEST_SUITE_P(HttpResponseHeaders, RequiresValidationTest, testing::ValuesIn(requires_validation_tests)); struct UpdateTestData { const char* orig_headers; const char* new_headers; const char* expected_headers; }; class UpdateTest : public HttpResponseHeadersTest, public ::testing::WithParamInterface { }; TEST_P(UpdateTest, Update) { const UpdateTestData test = GetParam(); std::string orig_headers(test.orig_headers); HeadersToRaw(&orig_headers); auto parsed = base::MakeRefCounted(orig_headers); std::string new_headers(test.new_headers); HeadersToRaw(&new_headers); auto new_parsed = base::MakeRefCounted(new_headers); parsed->Update(*new_parsed.get()); EXPECT_EQ(std::string(test.expected_headers), ToSimpleString(parsed)); } const UpdateTestData update_tests[] = { {"HTTP/1.1 200 OK\n", "HTTP/1/1 304 Not Modified\n" "connection: keep-alive\n" "Cache-control: max-age=10000\n", "HTTP/1.1 200 OK\n" "Cache-control: max-age=10000\n"}, {"HTTP/1.1 200 OK\n" "Foo: 1\n" "Cache-control: private\n", "HTTP/1/1 304 Not Modified\n" "connection: keep-alive\n" "Cache-control: max-age=10000\n", "HTTP/1.1 200 OK\n" "Cache-control: max-age=10000\n" "Foo: 1\n"}, {"HTTP/1.1 200 OK\n" "Foo: 1\n" "Cache-control: private\n", "HTTP/1/1 304 Not Modified\n" "connection: keep-alive\n" "Cache-CONTROL: max-age=10000\n", "HTTP/1.1 200 OK\n" "Cache-CONTROL: max-age=10000\n" "Foo: 1\n"}, {"HTTP/1.1 200 OK\n" "Content-Length: 450\n", "HTTP/1/1 304 Not Modified\n" "connection: keep-alive\n" "Cache-control: max-age=10001 \n", "HTTP/1.1 200 OK\n" "Cache-control: max-age=10001\n" "Content-Length: 450\n"}, { "HTTP/1.1 200 OK\n" "X-Frame-Options: DENY\n", "HTTP/1/1 304 Not Modified\n" "X-Frame-Options: ALLOW\n", "HTTP/1.1 200 OK\n" "X-Frame-Options: DENY\n", }, { "HTTP/1.1 200 OK\n" "X-WebKit-CSP: default-src 'none'\n", "HTTP/1/1 304 Not Modified\n" "X-WebKit-CSP: default-src *\n", "HTTP/1.1 200 OK\n" "X-WebKit-CSP: default-src 'none'\n", }, { "HTTP/1.1 200 OK\n" "X-XSS-Protection: 1\n", "HTTP/1/1 304 Not Modified\n" "X-XSS-Protection: 0\n", "HTTP/1.1 200 OK\n" "X-XSS-Protection: 1\n", }, {"HTTP/1.1 200 OK\n", "HTTP/1/1 304 Not Modified\n" "X-Content-Type-Options: nosniff\n", "HTTP/1.1 200 OK\n"}, {"HTTP/1.1 200 OK\n" "Content-Encoding: identity\n" "Content-Length: 100\n" "Content-Type: text/html\n" "Content-Security-Policy: default-src 'none'\n", "HTTP/1/1 304 Not Modified\n" "Content-Encoding: gzip\n" "Content-Length: 200\n" "Content-Type: text/xml\n" "Content-Security-Policy: default-src 'self'\n", "HTTP/1.1 200 OK\n" "Content-Security-Policy: default-src 'self'\n" "Content-Encoding: identity\n" "Content-Length: 100\n" "Content-Type: text/html\n"}, {"HTTP/1.1 200 OK\n" "Content-Location: /example_page.html\n", "HTTP/1/1 304 Not Modified\n" "Content-Location: /not_example_page.html\n", "HTTP/1.1 200 OK\n" "Content-Location: /example_page.html\n"}, }; INSTANTIATE_TEST_SUITE_P(HttpResponseHeaders, UpdateTest, testing::ValuesIn(update_tests)); struct EnumerateHeaderTestData { const char* headers; const char* expected_lines; }; class EnumerateHeaderLinesTest : public HttpResponseHeadersTest, public ::testing::WithParamInterface { }; TEST_P(EnumerateHeaderLinesTest, EnumerateHeaderLines) { const EnumerateHeaderTestData test = GetParam(); std::string headers(test.headers); HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); std::string name, value, lines; size_t iter = 0; while (parsed->EnumerateHeaderLines(&iter, &name, &value)) { lines.append(name); lines.append(": "); lines.append(value); lines.append("\n"); } EXPECT_EQ(std::string(test.expected_lines), lines); } const EnumerateHeaderTestData enumerate_header_tests[] = { {"HTTP/1.1 200 OK\n", ""}, {"HTTP/1.1 200 OK\n" "Foo: 1\n", "Foo: 1\n"}, {"HTTP/1.1 200 OK\n" "Foo: 1\n" "Bar: 2\n" "Foo: 3\n", "Foo: 1\nBar: 2\nFoo: 3\n"}, {"HTTP/1.1 200 OK\n" "Foo: 1, 2, 3\n", "Foo: 1, 2, 3\n"}, {"HTTP/1.1 200 OK\n" "Foo: ,, 1,, 2, 3,, \n", "Foo: ,, 1,, 2, 3,,\n"}, }; INSTANTIATE_TEST_SUITE_P(HttpResponseHeaders, EnumerateHeaderLinesTest, testing::ValuesIn(enumerate_header_tests)); struct IsRedirectTestData { const char* headers; const char* location; bool is_redirect; }; class IsRedirectTest : public HttpResponseHeadersTest, public ::testing::WithParamInterface { }; TEST_P(IsRedirectTest, IsRedirect) { const IsRedirectTestData test = GetParam(); std::string headers(test.headers); HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); std::string location; EXPECT_EQ(parsed->IsRedirect(&location), test.is_redirect); EXPECT_EQ(location, test.location); } const IsRedirectTestData is_redirect_tests[] = { { "HTTP/1.1 200 OK\n", "", false }, { "HTTP/1.1 301 Moved\n" "Location: http://foopy/\n", "http://foopy/", true }, { "HTTP/1.1 301 Moved\n" "Location: \t \n", "", false }, // We use the first location header as the target of the redirect. { "HTTP/1.1 301 Moved\n" "Location: http://foo/\n" "Location: http://bar/\n", "http://foo/", true }, // We use the first _valid_ location header as the target of the redirect. { "HTTP/1.1 301 Moved\n" "Location: \n" "Location: http://bar/\n", "http://bar/", true }, // Bug 1050541 (location header with an unescaped comma). { "HTTP/1.1 301 Moved\n" "Location: http://foo/bar,baz.html\n", "http://foo/bar,baz.html", true }, // Bug 1224617 (location header with non-ASCII bytes). { "HTTP/1.1 301 Moved\n" "Location: http://foo/bar?key=\xE4\xF6\xFC\n", "http://foo/bar?key=%E4%F6%FC", true }, // Shift_JIS, Big5, and GBK contain multibyte characters with the trailing // byte falling in the ASCII range. { "HTTP/1.1 301 Moved\n" "Location: http://foo/bar?key=\x81\x5E\xD8\xBF\n", "http://foo/bar?key=%81^%D8%BF", true }, { "HTTP/1.1 301 Moved\n" "Location: http://foo/bar?key=\x82\x40\xBD\xC4\n", "http://foo/bar?key=%82@%BD%C4", true }, { "HTTP/1.1 301 Moved\n" "Location: http://foo/bar?key=\x83\x5C\x82\x5D\xCB\xD7\n", "http://foo/bar?key=%83\\%82]%CB%D7", true }, }; INSTANTIATE_TEST_SUITE_P(HttpResponseHeaders, IsRedirectTest, testing::ValuesIn(is_redirect_tests)); struct ContentLengthTestData { const char* headers; int64_t expected_len; }; class GetContentLengthTest : public HttpResponseHeadersTest, public ::testing::WithParamInterface { }; TEST_P(GetContentLengthTest, GetContentLength) { const ContentLengthTestData test = GetParam(); std::string headers(test.headers); HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); EXPECT_EQ(test.expected_len, parsed->GetContentLength()); } const ContentLengthTestData content_length_tests[] = { {"HTTP/1.1 200 OK\n", -1}, {"HTTP/1.1 200 OK\n" "Content-Length: 10\n", 10}, {"HTTP/1.1 200 OK\n" "Content-Length: \n", -1}, {"HTTP/1.1 200 OK\n" "Content-Length: abc\n", -1}, {"HTTP/1.1 200 OK\n" "Content-Length: -10\n", -1}, {"HTTP/1.1 200 OK\n" "Content-Length: +10\n", -1}, {"HTTP/1.1 200 OK\n" "Content-Length: 23xb5\n", -1}, {"HTTP/1.1 200 OK\n" "Content-Length: 0xA\n", -1}, {"HTTP/1.1 200 OK\n" "Content-Length: 010\n", 10}, // Content-Length too big, will overflow an int64_t. {"HTTP/1.1 200 OK\n" "Content-Length: 40000000000000000000\n", -1}, {"HTTP/1.1 200 OK\n" "Content-Length: 10\n", 10}, {"HTTP/1.1 200 OK\n" "Content-Length: 10 \n", 10}, {"HTTP/1.1 200 OK\n" "Content-Length: \t10\n", 10}, {"HTTP/1.1 200 OK\n" "Content-Length: \v10\n", -1}, {"HTTP/1.1 200 OK\n" "Content-Length: \f10\n", -1}, {"HTTP/1.1 200 OK\n" "cOnTeNt-LENgth: 33\n", 33}, {"HTTP/1.1 200 OK\n" "Content-Length: 34\r\n", -1}, }; INSTANTIATE_TEST_SUITE_P(HttpResponseHeaders, GetContentLengthTest, testing::ValuesIn(content_length_tests)); struct ContentRangeTestData { const char* headers; bool expected_return_value; int64_t expected_first_byte_position; int64_t expected_last_byte_position; int64_t expected_instance_size; }; class ContentRangeTest : public HttpResponseHeadersTest, public ::testing::WithParamInterface { }; TEST_P(ContentRangeTest, GetContentRangeFor206) { const ContentRangeTestData test = GetParam(); std::string headers(test.headers); HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); int64_t first_byte_position; int64_t last_byte_position; int64_t instance_size; bool return_value = parsed->GetContentRangeFor206( &first_byte_position, &last_byte_position, &instance_size); EXPECT_EQ(test.expected_return_value, return_value); EXPECT_EQ(test.expected_first_byte_position, first_byte_position); EXPECT_EQ(test.expected_last_byte_position, last_byte_position); EXPECT_EQ(test.expected_instance_size, instance_size); } const ContentRangeTestData content_range_tests[] = { {"HTTP/1.1 206 Partial Content", false, -1, -1, -1}, {"HTTP/1.1 206 Partial Content\n" "Content-Range:", false, -1, -1, -1}, {"HTTP/1.1 206 Partial Content\n" "Content-Range: bytes 0-50/51", true, 0, 50, 51}, {"HTTP/1.1 206 Partial Content\n" "Content-Range: bytes 50-0/51", false, -1, -1, -1}, {"HTTP/1.1 416 Requested range not satisfiable\n" "Content-Range: bytes */*", false, -1, -1, -1}, {"HTTP/1.1 206 Partial Content\n" "Content-Range: bytes 0-50/*", false, -1, -1, -1}, }; INSTANTIATE_TEST_SUITE_P(HttpResponseHeaders, ContentRangeTest, testing::ValuesIn(content_range_tests)); struct KeepAliveTestData { const char* headers; bool expected_keep_alive; }; // Enable GTest to print KeepAliveTestData in an intelligible way if the test // fails. void PrintTo(const KeepAliveTestData& keep_alive_test_data, std::ostream* os) { *os << "{\"" << keep_alive_test_data.headers << "\", " << std::boolalpha << keep_alive_test_data.expected_keep_alive << "}"; } class IsKeepAliveTest : public HttpResponseHeadersTest, public ::testing::WithParamInterface { }; TEST_P(IsKeepAliveTest, IsKeepAlive) { const KeepAliveTestData test = GetParam(); std::string headers(test.headers); HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); EXPECT_EQ(test.expected_keep_alive, parsed->IsKeepAlive()); } const KeepAliveTestData keepalive_tests[] = { // The status line fabricated by HttpNetworkTransaction for a 0.9 response. // Treated as 0.9. { "HTTP/0.9 200 OK", false }, // This could come from a broken server. Treated as 1.0 because it has a // header. { "HTTP/0.9 200 OK\n" "connection: keep-alive\n", true }, { "HTTP/1.1 200 OK\n", true }, { "HTTP/1.0 200 OK\n", false }, { "HTTP/1.0 200 OK\n" "connection: close\n", false }, { "HTTP/1.0 200 OK\n" "connection: keep-alive\n", true }, { "HTTP/1.0 200 OK\n" "connection: kEeP-AliVe\n", true }, { "HTTP/1.0 200 OK\n" "connection: keep-aliveX\n", false }, { "HTTP/1.1 200 OK\n" "connection: close\n", false }, { "HTTP/1.1 200 OK\n" "connection: keep-alive\n", true }, { "HTTP/1.0 200 OK\n" "proxy-connection: close\n", false }, { "HTTP/1.0 200 OK\n" "proxy-connection: keep-alive\n", true }, { "HTTP/1.1 200 OK\n" "proxy-connection: close\n", false }, { "HTTP/1.1 200 OK\n" "proxy-connection: keep-alive\n", true }, { "HTTP/1.1 200 OK\n" "Connection: Upgrade, close\n", false }, { "HTTP/1.1 200 OK\n" "Connection: Upgrade, keep-alive\n", true }, { "HTTP/1.1 200 OK\n" "Connection: Upgrade\n" "Connection: close\n", false }, { "HTTP/1.1 200 OK\n" "Connection: Upgrade\n" "Connection: keep-alive\n", true }, { "HTTP/1.1 200 OK\n" "Connection: close, Upgrade\n", false }, { "HTTP/1.1 200 OK\n" "Connection: keep-alive, Upgrade\n", true }, { "HTTP/1.1 200 OK\n" "Connection: Upgrade\n" "Proxy-Connection: close\n", false }, { "HTTP/1.1 200 OK\n" "Connection: Upgrade\n" "Proxy-Connection: keep-alive\n", true }, // In situations where the response headers conflict with themselves, use the // first one for backwards-compatibility. { "HTTP/1.1 200 OK\n" "Connection: close\n" "Connection: keep-alive\n", false }, { "HTTP/1.1 200 OK\n" "Connection: keep-alive\n" "Connection: close\n", true }, { "HTTP/1.0 200 OK\n" "Connection: close\n" "Connection: keep-alive\n", false }, { "HTTP/1.0 200 OK\n" "Connection: keep-alive\n" "Connection: close\n", true }, // Ignore the Proxy-Connection header if at all possible. { "HTTP/1.0 200 OK\n" "Proxy-Connection: keep-alive\n" "Connection: close\n", false }, { "HTTP/1.1 200 OK\n" "Proxy-Connection: close\n" "Connection: keep-alive\n", true }, // Older versions of Chrome would have ignored Proxy-Connection in this case, // but it doesn't seem safe. { "HTTP/1.1 200 OK\n" "Proxy-Connection: close\n" "Connection: Transfer-Encoding\n", false }, }; INSTANTIATE_TEST_SUITE_P(HttpResponseHeaders, IsKeepAliveTest, testing::ValuesIn(keepalive_tests)); struct HasStrongValidatorsTestData { const char* headers; bool expected_result; }; class HasStrongValidatorsTest : public HttpResponseHeadersTest, public ::testing::WithParamInterface { }; TEST_P(HasStrongValidatorsTest, HasStrongValidators) { const HasStrongValidatorsTestData test = GetParam(); std::string headers(test.headers); HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); EXPECT_EQ(test.expected_result, parsed->HasStrongValidators()); } const HasStrongValidatorsTestData strong_validators_tests[] = { { "HTTP/0.9 200 OK", false }, { "HTTP/1.0 200 OK\n" "Date: Wed, 28 Nov 2007 01:40:10 GMT\n" "Last-Modified: Wed, 28 Nov 2007 00:40:10 GMT\n" "ETag: \"foo\"\n", false }, { "HTTP/1.1 200 OK\n" "Date: Wed, 28 Nov 2007 01:40:10 GMT\n" "Last-Modified: Wed, 28 Nov 2007 00:40:10 GMT\n" "ETag: \"foo\"\n", true }, { "HTTP/1.1 200 OK\n" "Date: Wed, 28 Nov 2007 00:41:10 GMT\n" "Last-Modified: Wed, 28 Nov 2007 00:40:10 GMT\n", true }, { "HTTP/1.1 200 OK\n" "Date: Wed, 28 Nov 2007 00:41:09 GMT\n" "Last-Modified: Wed, 28 Nov 2007 00:40:10 GMT\n", false }, { "HTTP/1.1 200 OK\n" "ETag: \"foo\"\n", true }, // This is not really a weak etag: { "HTTP/1.1 200 OK\n" "etag: \"w/foo\"\n", true }, // This is a weak etag: { "HTTP/1.1 200 OK\n" "etag: w/\"foo\"\n", false }, { "HTTP/1.1 200 OK\n" "etag: W / \"foo\"\n", false } }; INSTANTIATE_TEST_SUITE_P(HttpResponseHeaders, HasStrongValidatorsTest, testing::ValuesIn(strong_validators_tests)); TEST(HttpResponseHeadersTest, HasValidatorsNone) { std::string headers("HTTP/1.1 200 OK"); HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); EXPECT_FALSE(parsed->HasValidators()); } TEST(HttpResponseHeadersTest, HasValidatorsEtag) { std::string headers( "HTTP/1.1 200 OK\n" "etag: \"anything\""); HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); EXPECT_TRUE(parsed->HasValidators()); } TEST(HttpResponseHeadersTest, HasValidatorsLastModified) { std::string headers( "HTTP/1.1 200 OK\n" "Last-Modified: Wed, 28 Nov 2007 00:40:10 GMT"); HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); EXPECT_TRUE(parsed->HasValidators()); } TEST(HttpResponseHeadersTest, HasValidatorsWeakEtag) { std::string headers( "HTTP/1.1 200 OK\n" "etag: W/\"anything\""); HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); EXPECT_TRUE(parsed->HasValidators()); } TEST(HttpResponseHeadersTest, GetNormalizedHeaderWithEmptyValues) { std::string headers( "HTTP/1.1 200 OK\n" "a:\n" "b: \n" "c:*\n" "d: *\n" "e: \n" "a: \n" "b:*\n" "c:\n" "d:*\n" "a:\n"); HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); std::string value; EXPECT_TRUE(parsed->GetNormalizedHeader("a", &value)); EXPECT_EQ(value, ", , "); EXPECT_TRUE(parsed->GetNormalizedHeader("b", &value)); EXPECT_EQ(value, ", *"); EXPECT_TRUE(parsed->GetNormalizedHeader("c", &value)); EXPECT_EQ(value, "*, "); EXPECT_TRUE(parsed->GetNormalizedHeader("d", &value)); EXPECT_EQ(value, "*, *"); EXPECT_TRUE(parsed->GetNormalizedHeader("e", &value)); EXPECT_EQ(value, ""); EXPECT_FALSE(parsed->GetNormalizedHeader("f", &value)); } TEST(HttpResponseHeadersTest, GetNormalizedHeaderWithCommas) { std::string headers( "HTTP/1.1 200 OK\n" "a: foo, bar\n" "b: , foo, bar,\n" "c: ,,,\n" "d: , , , \n" "e:\t,\t,\t,\t\n" "a: ,"); HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); std::string value; // TODO(mmenke): "Normalized" headers probably should preserve the // leading/trailing whitespace from the original headers. ASSERT_TRUE(parsed->GetNormalizedHeader("a", &value)); EXPECT_EQ("foo, bar, ,", value); ASSERT_TRUE(parsed->GetNormalizedHeader("b", &value)); EXPECT_EQ(", foo, bar,", value); ASSERT_TRUE(parsed->GetNormalizedHeader("c", &value)); EXPECT_EQ(",,,", value); ASSERT_TRUE(parsed->GetNormalizedHeader("d", &value)); EXPECT_EQ(", , ,", value); ASSERT_TRUE(parsed->GetNormalizedHeader("e", &value)); EXPECT_EQ(",\t,\t,", value); EXPECT_FALSE(parsed->GetNormalizedHeader("f", &value)); } TEST(HttpResponseHeadersTest, AddHeader) { scoped_refptr headers = HttpResponseHeaders::TryToCreate( "HTTP/1.1 200 OK\n" "connection: keep-alive\n" "Cache-control: max-age=10000\n"); ASSERT_TRUE(headers); headers->AddHeader("Content-Length", "450"); EXPECT_EQ( "HTTP/1.1 200 OK\n" "connection: keep-alive\n" "Cache-control: max-age=10000\n" "Content-Length: 450\n", ToSimpleString(headers)); // Add a second Content-Length header with extra spaces in the value. It // should be added to the end, and the extra spaces removed. headers->AddHeader("Content-Length", " 42 "); EXPECT_EQ( "HTTP/1.1 200 OK\n" "connection: keep-alive\n" "Cache-control: max-age=10000\n" "Content-Length: 450\n" "Content-Length: 42\n", ToSimpleString(headers)); } TEST(HttpResponseHeadersTest, SetHeader) { scoped_refptr headers = HttpResponseHeaders::TryToCreate( "HTTP/1.1 200 OK\n" "connection: keep-alive\n" "Cache-control: max-age=10000\n"); ASSERT_TRUE(headers); headers->SetHeader("Content-Length", "450"); EXPECT_EQ( "HTTP/1.1 200 OK\n" "connection: keep-alive\n" "Cache-control: max-age=10000\n" "Content-Length: 450\n", ToSimpleString(headers)); headers->SetHeader("Content-Length", " 42 "); EXPECT_EQ( "HTTP/1.1 200 OK\n" "connection: keep-alive\n" "Cache-control: max-age=10000\n" "Content-Length: 42\n", ToSimpleString(headers)); headers->SetHeader("connection", "close"); EXPECT_EQ( "HTTP/1.1 200 OK\n" "Cache-control: max-age=10000\n" "Content-Length: 42\n" "connection: close\n", ToSimpleString(headers)); } TEST(HttpResponseHeadersTest, TryToCreateWithNul) { static constexpr char kHeadersWithNuls[] = { "HTTP/1.1 200 OK\0" "Content-Type: application/octet-stream\0"}; // The size must be specified explicitly to include the nul characters. static constexpr std::string_view kHeadersWithNulsAsStringPiece( kHeadersWithNuls, sizeof(kHeadersWithNuls)); scoped_refptr headers = HttpResponseHeaders::TryToCreate(kHeadersWithNulsAsStringPiece); EXPECT_EQ(headers, nullptr); } #if !BUILDFLAG(CRONET_BUILD) // Cronet disables tracing so this test would fail. TEST(HttpResponseHeadersTest, TracingSupport) { scoped_refptr headers = HttpResponseHeaders::TryToCreate( "HTTP/1.1 200 OK\n" "connection: keep-alive\n"); ASSERT_TRUE(headers); EXPECT_EQ(perfetto::TracedValueToString(headers), "{response_code:200,headers:[{name:connection,value:keep-alive}]}"); } #endif struct RemoveHeaderTestData { const char* orig_headers; const char* to_remove; const char* expected_headers; }; class RemoveHeaderTest : public HttpResponseHeadersTest, public ::testing::WithParamInterface { }; TEST_P(RemoveHeaderTest, RemoveHeader) { const RemoveHeaderTestData test = GetParam(); std::string orig_headers(test.orig_headers); HeadersToRaw(&orig_headers); auto parsed = base::MakeRefCounted(orig_headers); std::string name(test.to_remove); parsed->RemoveHeader(name); EXPECT_EQ(std::string(test.expected_headers), ToSimpleString(parsed)); } const RemoveHeaderTestData remove_header_tests[] = { { "HTTP/1.1 200 OK\n" "connection: keep-alive\n" "Cache-control: max-age=10000\n" "Content-Length: 450\n", "Content-Length", "HTTP/1.1 200 OK\n" "connection: keep-alive\n" "Cache-control: max-age=10000\n" }, { "HTTP/1.1 200 OK\n" "connection: keep-alive \n" "Content-Length : 450 \n" "Cache-control: max-age=10000\n", "Content-Length", "HTTP/1.1 200 OK\n" "connection: keep-alive\n" "Cache-control: max-age=10000\n" }, }; INSTANTIATE_TEST_SUITE_P(HttpResponseHeaders, RemoveHeaderTest, testing::ValuesIn(remove_header_tests)); struct RemoveHeadersTestData { const char* orig_headers; const char* to_remove[2]; const char* expected_headers; }; class RemoveHeadersTest : public HttpResponseHeadersTest, public ::testing::WithParamInterface {}; TEST_P(RemoveHeadersTest, RemoveHeaders) { const RemoveHeadersTestData test = GetParam(); std::string orig_headers(test.orig_headers); HeadersToRaw(&orig_headers); auto parsed = base::MakeRefCounted(orig_headers); std::unordered_set to_remove; for (auto* header : test.to_remove) { if (header) to_remove.insert(header); } parsed->RemoveHeaders(to_remove); EXPECT_EQ(std::string(test.expected_headers), ToSimpleString(parsed)); } const RemoveHeadersTestData remove_headers_tests[] = { {"HTTP/1.1 200 OK\n" "connection: keep-alive\n" "Cache-control: max-age=10000\n" "Content-Length: 450\n", {"Content-Length", "CACHE-control"}, "HTTP/1.1 200 OK\n" "connection: keep-alive\n"}, {"HTTP/1.1 200 OK\n" "connection: keep-alive\n" "Content-Length: 450\n", {"foo", "bar"}, "HTTP/1.1 200 OK\n" "connection: keep-alive\n" "Content-Length: 450\n"}, {"HTTP/1.1 404 Kinda not OK\n" "connection: keep-alive \n", {}, "HTTP/1.1 404 Kinda not OK\n" "connection: keep-alive\n"}, }; INSTANTIATE_TEST_SUITE_P(HttpResponseHeaders, RemoveHeadersTest, testing::ValuesIn(remove_headers_tests)); struct RemoveIndividualHeaderTestData { const char* orig_headers; const char* to_remove_name; const char* to_remove_value; const char* expected_headers; }; class RemoveIndividualHeaderTest : public HttpResponseHeadersTest, public ::testing::WithParamInterface { }; TEST_P(RemoveIndividualHeaderTest, RemoveIndividualHeader) { const RemoveIndividualHeaderTestData test = GetParam(); std::string orig_headers(test.orig_headers); HeadersToRaw(&orig_headers); auto parsed = base::MakeRefCounted(orig_headers); std::string name(test.to_remove_name); std::string value(test.to_remove_value); parsed->RemoveHeaderLine(name, value); EXPECT_EQ(std::string(test.expected_headers), ToSimpleString(parsed)); } const RemoveIndividualHeaderTestData remove_individual_header_tests[] = { { "HTTP/1.1 200 OK\n" "connection: keep-alive\n" "Cache-control: max-age=10000\n" "Content-Length: 450\n", "Content-Length", "450", "HTTP/1.1 200 OK\n" "connection: keep-alive\n" "Cache-control: max-age=10000\n" }, { "HTTP/1.1 200 OK\n" "connection: keep-alive \n" "Content-Length : 450 \n" "Cache-control: max-age=10000\n", "Content-Length", "450", "HTTP/1.1 200 OK\n" "connection: keep-alive\n" "Cache-control: max-age=10000\n" }, { "HTTP/1.1 200 OK\n" "connection: keep-alive \n" "Content-Length: 450\n" "Cache-control: max-age=10000\n", "Content-Length", // Matching name. "999", // Mismatching value. "HTTP/1.1 200 OK\n" "connection: keep-alive\n" "Content-Length: 450\n" "Cache-control: max-age=10000\n" }, { "HTTP/1.1 200 OK\n" "connection: keep-alive \n" "Foo: bar, baz\n" "Foo: bar\n" "Cache-control: max-age=10000\n", "Foo", "bar, baz", // Space in value. "HTTP/1.1 200 OK\n" "connection: keep-alive\n" "Foo: bar\n" "Cache-control: max-age=10000\n" }, { "HTTP/1.1 200 OK\n" "connection: keep-alive \n" "Foo: bar, baz\n" "Cache-control: max-age=10000\n", "Foo", "baz", // Only partial match -> ignored. "HTTP/1.1 200 OK\n" "connection: keep-alive\n" "Foo: bar, baz\n" "Cache-control: max-age=10000\n" }, }; INSTANTIATE_TEST_SUITE_P(HttpResponseHeaders, RemoveIndividualHeaderTest, testing::ValuesIn(remove_individual_header_tests)); struct ReplaceStatusTestData { const char* orig_headers; const char* new_status; const char* expected_headers; }; class ReplaceStatusTest : public HttpResponseHeadersTest, public ::testing::WithParamInterface { }; TEST_P(ReplaceStatusTest, ReplaceStatus) { const ReplaceStatusTestData test = GetParam(); std::string orig_headers(test.orig_headers); HeadersToRaw(&orig_headers); auto parsed = base::MakeRefCounted(orig_headers); std::string name(test.new_status); parsed->ReplaceStatusLine(name); EXPECT_EQ(std::string(test.expected_headers), ToSimpleString(parsed)); } const ReplaceStatusTestData replace_status_tests[] = { { "HTTP/1.1 206 Partial Content\n" "connection: keep-alive\n" "Cache-control: max-age=10000\n" "Content-Length: 450\n", "HTTP/1.1 200 OK", "HTTP/1.1 200 OK\n" "connection: keep-alive\n" "Cache-control: max-age=10000\n" "Content-Length: 450\n" }, { "HTTP/1.1 200 OK\n" "connection: keep-alive\n", "HTTP/1.1 304 Not Modified", "HTTP/1.1 304 Not Modified\n" "connection: keep-alive\n" }, { "HTTP/1.1 200 OK\n" "connection: keep-alive \n" "Content-Length : 450 \n" "Cache-control: max-age=10000\n", "HTTP/1//1 304 Not Modified", "HTTP/1.0 304 Not Modified\n" "connection: keep-alive\n" "Content-Length: 450\n" "Cache-control: max-age=10000\n" }, }; INSTANTIATE_TEST_SUITE_P(HttpResponseHeaders, ReplaceStatusTest, testing::ValuesIn(replace_status_tests)); struct UpdateWithNewRangeTestData { const char* orig_headers; const char* expected_headers; const char* expected_headers_with_replaced_status; }; class UpdateWithNewRangeTest : public HttpResponseHeadersTest, public ::testing::WithParamInterface { }; TEST_P(UpdateWithNewRangeTest, UpdateWithNewRange) { const UpdateWithNewRangeTestData test = GetParam(); const HttpByteRange range = HttpByteRange::Bounded(3, 5); std::string orig_headers(test.orig_headers); std::replace(orig_headers.begin(), orig_headers.end(), '\n', '\0'); auto parsed = base::MakeRefCounted(orig_headers + '\0'); int64_t content_size = parsed->GetContentLength(); // Update headers without replacing status line. parsed->UpdateWithNewRange(range, content_size, false); EXPECT_EQ(std::string(test.expected_headers), ToSimpleString(parsed)); // Replace status line too. parsed->UpdateWithNewRange(range, content_size, true); EXPECT_EQ(std::string(test.expected_headers_with_replaced_status), ToSimpleString(parsed)); } const UpdateWithNewRangeTestData update_range_tests[] = { { "HTTP/1.1 200 OK\n" "Content-Length: 450\n", "HTTP/1.1 200 OK\n" "Content-Range: bytes 3-5/450\n" "Content-Length: 3\n", "HTTP/1.1 206 Partial Content\n" "Content-Range: bytes 3-5/450\n" "Content-Length: 3\n", }, { "HTTP/1.1 200 OK\n" "Content-Length: 5\n", "HTTP/1.1 200 OK\n" "Content-Range: bytes 3-5/5\n" "Content-Length: 3\n", "HTTP/1.1 206 Partial Content\n" "Content-Range: bytes 3-5/5\n" "Content-Length: 3\n", }, }; INSTANTIATE_TEST_SUITE_P(HttpResponseHeaders, UpdateWithNewRangeTest, testing::ValuesIn(update_range_tests)); TEST_F(HttpResponseHeadersCacheControlTest, AbsentMaxAgeReturnsFalse) { InitializeHeadersWithCacheControl("nocache"); EXPECT_FALSE(headers()->GetMaxAgeValue(TimeDeltaPointer())); } TEST_F(HttpResponseHeadersCacheControlTest, MaxAgeWithNoParameterRejected) { InitializeHeadersWithCacheControl("max-age=,private"); EXPECT_FALSE(headers()->GetMaxAgeValue(TimeDeltaPointer())); } TEST_F(HttpResponseHeadersCacheControlTest, MaxAgeWithSpaceParameterRejected) { InitializeHeadersWithCacheControl("max-age= ,private"); EXPECT_FALSE(headers()->GetMaxAgeValue(TimeDeltaPointer())); } TEST_F(HttpResponseHeadersCacheControlTest, MaxAgeWithInterimSpaceIsRejected) { InitializeHeadersWithCacheControl("max-age=1 2"); EXPECT_FALSE(headers()->GetMaxAgeValue(TimeDeltaPointer())); } TEST_F(HttpResponseHeadersCacheControlTest, MaxAgeWithMinusSignIsRejected) { InitializeHeadersWithCacheControl("max-age=-7"); EXPECT_FALSE(headers()->GetMaxAgeValue(TimeDeltaPointer())); } TEST_F(HttpResponseHeadersCacheControlTest, MaxAgeWithSpaceBeforeEqualsIsRejected) { InitializeHeadersWithCacheControl("max-age = 7"); EXPECT_FALSE(headers()->GetMaxAgeValue(TimeDeltaPointer())); } TEST_F(HttpResponseHeadersCacheControlTest, MaxAgeWithLeadingandTrailingSpaces) { InitializeHeadersWithCacheControl("max-age= 7 "); EXPECT_EQ(base::Seconds(7), GetMaxAgeValue()); } TEST_F(HttpResponseHeadersCacheControlTest, MaxAgeFirstMatchUsed) { InitializeHeadersWithCacheControl("max-age=10, max-age=20"); EXPECT_EQ(base::Seconds(10), GetMaxAgeValue()); } TEST_F(HttpResponseHeadersCacheControlTest, MaxAgeBogusFirstMatchUsed) { // "max-age10" isn't parsed as "max-age"; "max-age=now" is bogus and // ignored and so "max-age=20" is used. InitializeHeadersWithCacheControl( "max-age10, max-age=now, max-age=20, max-age=30"); EXPECT_EQ(base::Seconds(20), GetMaxAgeValue()); } TEST_F(HttpResponseHeadersCacheControlTest, MaxAgeCaseInsensitive) { InitializeHeadersWithCacheControl("Max-aGe=15"); EXPECT_EQ(base::Seconds(15), GetMaxAgeValue()); } TEST_F(HttpResponseHeadersCacheControlTest, MaxAgeOverflow) { InitializeHeadersWithCacheControl("max-age=99999999999999999999"); EXPECT_EQ(base::TimeDelta::FiniteMax().InSeconds(), GetMaxAgeValue().InSeconds()); } struct MaxAgeTestData { const char* max_age_string; const std::optional expected_seconds; }; class MaxAgeEdgeCasesTest : public HttpResponseHeadersCacheControlTest, public ::testing::WithParamInterface { }; TEST_P(MaxAgeEdgeCasesTest, MaxAgeEdgeCases) { const MaxAgeTestData test = GetParam(); std::string max_age = "max-age="; InitializeHeadersWithCacheControl( (max_age + test.max_age_string).c_str()); if (test.expected_seconds.has_value()) { EXPECT_EQ(test.expected_seconds.value(), GetMaxAgeValue().InSeconds()) << " for max-age=" << test.max_age_string; } else { EXPECT_FALSE(headers()->GetMaxAgeValue(TimeDeltaPointer())); } } const MaxAgeTestData max_age_tests[] = { {" 1 ", 1}, // Spaces are ignored. {"-1", std::nullopt}, {"--1", std::nullopt}, {"2s", std::nullopt}, {"3 days", std::nullopt}, {"'4'", std::nullopt}, {"\"5\"", std::nullopt}, {"0x6", std::nullopt}, // Hex not parsed as hex. {"7F", std::nullopt}, // Hex without 0x still not parsed as hex. {"010", 10}, // Octal not parsed as octal. {"9223372036853", 9223372036853}, {"9223372036854", 9223372036854}, {"9223372036855", 9223372036854}, {"9223372036854775806", 9223372036854}, {"9223372036854775807", 9223372036854}, {"20000000000000000000", 9223372036854}, // Overflow int64_t. }; INSTANTIATE_TEST_SUITE_P(HttpResponseHeadersCacheControl, MaxAgeEdgeCasesTest, testing::ValuesIn(max_age_tests)); TEST_F(HttpResponseHeadersCacheControlTest, AbsentStaleWhileRevalidateReturnsFalse) { InitializeHeadersWithCacheControl("max-age=3600"); EXPECT_FALSE(headers()->GetStaleWhileRevalidateValue(TimeDeltaPointer())); } TEST_F(HttpResponseHeadersCacheControlTest, StaleWhileRevalidateWithoutValueRejected) { InitializeHeadersWithCacheControl("max-age=3600,stale-while-revalidate="); EXPECT_FALSE(headers()->GetStaleWhileRevalidateValue(TimeDeltaPointer())); } TEST_F(HttpResponseHeadersCacheControlTest, StaleWhileRevalidateWithInvalidValueIgnored) { InitializeHeadersWithCacheControl("max-age=3600,stale-while-revalidate=true"); EXPECT_FALSE(headers()->GetStaleWhileRevalidateValue(TimeDeltaPointer())); } TEST_F(HttpResponseHeadersCacheControlTest, StaleWhileRevalidateValueReturned) { InitializeHeadersWithCacheControl("max-age=3600,stale-while-revalidate=7200"); EXPECT_EQ(base::Seconds(7200), GetStaleWhileRevalidateValue()); } TEST_F(HttpResponseHeadersCacheControlTest, FirstStaleWhileRevalidateValueUsed) { InitializeHeadersWithCacheControl( "stale-while-revalidate=1,stale-while-revalidate=7200"); EXPECT_EQ(base::Seconds(1), GetStaleWhileRevalidateValue()); } struct GetCurrentAgeTestData { const char* headers; const char* request_time; const char* response_time; const char* current_time; const int expected_age; }; class GetCurrentAgeTest : public HttpResponseHeadersTest, public ::testing::WithParamInterface { }; TEST_P(GetCurrentAgeTest, GetCurrentAge) { const GetCurrentAgeTestData test = GetParam(); base::Time request_time, response_time, current_time; ASSERT_TRUE(base::Time::FromString(test.request_time, &request_time)); ASSERT_TRUE(base::Time::FromString(test.response_time, &response_time)); ASSERT_TRUE(base::Time::FromString(test.current_time, ¤t_time)); std::string headers(test.headers); HeadersToRaw(&headers); auto parsed = base::MakeRefCounted(headers); base::TimeDelta age = parsed->GetCurrentAge(request_time, response_time, current_time); EXPECT_EQ(test.expected_age, age.InSeconds()); } const struct GetCurrentAgeTestData get_current_age_tests[] = { // Without Date header. {"HTTP/1.1 200 OK\n" "Age: 2", "Fri, 20 Jan 2011 10:40:08 GMT", "Fri, 20 Jan 2011 10:40:12 GMT", "Fri, 20 Jan 2011 10:40:14 GMT", 8}, // Without Age header. {"HTTP/1.1 200 OK\n" "Date: Fri, 20 Jan 2011 10:40:10 GMT\n", "Fri, 20 Jan 2011 10:40:08 GMT", "Fri, 20 Jan 2011 10:40:12 GMT", "Fri, 20 Jan 2011 10:40:14 GMT", 6}, // date_value > response_time with Age header. {"HTTP/1.1 200 OK\n" "Date: Fri, 20 Jan 2011 10:40:14 GMT\n" "Age: 2\n", "Fri, 20 Jan 2011 10:40:08 GMT", "Fri, 20 Jan 2011 10:40:12 GMT", "Fri, 20 Jan 2011 10:40:14 GMT", 8}, // date_value > response_time without Age header. {"HTTP/1.1 200 OK\n" "Date: Fri, 20 Jan 2011 10:40:14 GMT\n", "Fri, 20 Jan 2011 10:40:08 GMT", "Fri, 20 Jan 2011 10:40:12 GMT", "Fri, 20 Jan 2011 10:40:14 GMT", 6}, // apparent_age > corrected_age_value {"HTTP/1.1 200 OK\n" "Date: Fri, 20 Jan 2011 10:40:07 GMT\n" "Age: 0\n", "Fri, 20 Jan 2011 10:40:08 GMT", "Fri, 20 Jan 2011 10:40:12 GMT", "Fri, 20 Jan 2011 10:40:14 GMT", 7}}; INSTANTIATE_TEST_SUITE_P(HttpResponseHeaders, GetCurrentAgeTest, testing::ValuesIn(get_current_age_tests)); TEST(HttpResponseHeadersBuilderTest, Version) { for (HttpVersion version : {HttpVersion(1, 0), HttpVersion(1, 1), HttpVersion(2, 0)}) { auto headers = HttpResponseHeaders::Builder(version, "200").Build(); EXPECT_EQ(base::StringPrintf("HTTP/%d.%d 200", version.major_value(), version.minor_value()), headers->GetStatusLine()); EXPECT_EQ(version, headers->GetHttpVersion()); } } struct BuilderStatusLineTestData { const std::string_view status; const std::string_view expected_status_line; const int expected_response_code; const std::string_view expected_status_text; }; // Provide GTest with a method to print the BuilderStatusLineTestData, for ease // of debugging. void PrintTo(const BuilderStatusLineTestData& data, std::ostream* os) { *os << "\"" << data.status << "\", \"" << data.expected_status_line << "\", " << data.expected_response_code << ", \"" << data.expected_status_text << "\"}"; } class BuilderStatusLineTest : public HttpResponseHeadersTest, public ::testing::WithParamInterface {}; TEST_P(BuilderStatusLineTest, Common) { const auto& [status, expected_status_line, expected_response_code, expected_status_text] = GetParam(); auto http_response_headers = HttpResponseHeaders::Builder({1, 1}, status).Build(); EXPECT_EQ(expected_status_line, http_response_headers->GetStatusLine()); EXPECT_EQ(expected_response_code, http_response_headers->response_code()); EXPECT_EQ(expected_status_text, http_response_headers->GetStatusText()); } constexpr BuilderStatusLineTestData kBuilderStatusLineTests[] = { {// Simple case. "200 OK", "HTTP/1.1 200 OK", 200, "OK"}, {// No status text. "200", "HTTP/1.1 200", 200, ""}, {// Empty status. "", "HTTP/1.1 200", 200, ""}, {// Space status. " ", "HTTP/1.1 200", 200, ""}, {// Spaces removed from status. " 204 No content ", "HTTP/1.1 204 No content", 204, "No content"}, {// Tabs treated as terminating whitespace. "204 \t No content \t ", "HTTP/1.1 204 \t No content \t", 204, "\t No content \t"}, {// Status text smushed into response code. "426Smush", "HTTP/1.1 426 Smush", 426, "Smush"}, {// Tab gets included in status text. "501\tStatus\t", "HTTP/1.1 501 \tStatus\t", 501, "\tStatus\t"}, {// Zero response code. "0 Zero", "HTTP/1.1 0 Zero", 0, "Zero"}, {// Oversize response code. "20230904 Monday", "HTTP/1.1 20230904 Monday", 20230904, "Monday"}, {// Overflowing response code. "9123456789 Overflow", "HTTP/1.1 9123456789 Overflow", 2147483647, "Overflow"}, }; INSTANTIATE_TEST_SUITE_P(HttpResponseHeaders, BuilderStatusLineTest, testing::ValuesIn(kBuilderStatusLineTests)); struct BuilderHeadersTestData { const std::vector> headers; const std::string_view expected_headers; }; // Provide GTest with a method to print the BuilderHeadersTestData, for ease of // debugging. void PrintTo(const BuilderHeadersTestData& data, std::ostream* os) { *os << "{"; for (const auto& header : data.headers) { *os << "{\"" << header.first << "\", \"" << header.second << "\"},"; } std::string expected_headers(data.expected_headers); EscapeForPrinting(&expected_headers); *os << "}, \"" << expected_headers << "\"}"; } class BuilderHeadersTest : public HttpResponseHeadersTest, public ::testing::WithParamInterface {}; TEST_P(BuilderHeadersTest, Common) { const auto& [headers, expected_headers_const] = GetParam(); HttpResponseHeaders::Builder builder({1, 1}, "200"); for (const auto& [key, value] : headers) { builder.AddHeader(key, value); } auto http_response_headers = builder.Build(); std::string output_headers = ToSimpleString(http_response_headers); std::string expected_headers(expected_headers_const); EscapeForPrinting(&output_headers); EscapeForPrinting(&expected_headers); EXPECT_EQ(expected_headers, output_headers); } const BuilderHeadersTestData builder_headers_tests[] = { {// Single header. {{"Content-Type", "text/html"}}, "HTTP/1.1 200\n" "Content-Type: text/html\n"}, {// Multiple headers. { {"Content-Type", "text/html"}, {"Content-Length", "6"}, {"Set-Cookie", "a=1"}, }, "HTTP/1.1 200\n" "Content-Type: text/html\n" "Content-Length: 6\n" "Set-Cookie: a=1\n"}, {// Empty header value. {{"Pragma", ""}}, "HTTP/1.1 200\n" "Pragma: \n"}, {// Multiple header value. {{"Cache-Control", "no-cache, no-store"}}, "HTTP/1.1 200\n" "Cache-Control: no-cache, no-store\n"}, {// Spaces are removed around values, but when EnumerateHeaderLines() // rejoins continuations, it keeps interior spaces. . {{"X-Commas", " , , "}}, "HTTP/1.1 200\n" "X-Commas: , ,\n"}, {// Single value is trimmed. {{"Pragma", " no-cache "}}, "HTTP/1.1 200\n" "Pragma: no-cache\n"}, {// Location header is trimmed. {{"Location", " http://example.com/ "}}, "HTTP/1.1 200\n" "Location: http://example.com/\n"}, }; INSTANTIATE_TEST_SUITE_P(HttpResponseHeaders, BuilderHeadersTest, testing::ValuesIn(builder_headers_tests)); TEST(HttpResponseHeadersTest, StrictlyEqualsSuccess) { constexpr char kRawHeaders[] = "HTTP/1.1 200\n" "Content-Type:application/octet-stream\n" "Cache-Control:no-cache, no-store\n"; std::string raw_headers = kRawHeaders; HeadersToRaw(&raw_headers); const auto parsed = base::MakeRefCounted(raw_headers); const auto built = HttpResponseHeaders::Builder({1, 1}, "200") .AddHeader("Content-Type", "application/octet-stream") .AddHeader("Cache-Control", "no-cache, no-store") .Build(); EXPECT_TRUE(parsed->StrictlyEquals(*built)); EXPECT_TRUE(built->StrictlyEquals(*parsed)); } TEST(HttpResponseHeadersTest, StrictlyEqualsVersionMismatch) { const auto http10 = HttpResponseHeaders::Builder({1, 0}, "200").Build(); const auto http11 = HttpResponseHeaders::Builder({1, 1}, "200").Build(); EXPECT_FALSE(http10->StrictlyEquals(*http11)); EXPECT_FALSE(http11->StrictlyEquals(*http10)); } TEST(HttpResponseHeadersTest, StrictlyEqualsResponseCodeMismatch) { const auto response200 = HttpResponseHeaders::Builder({1, 1}, "200").Build(); const auto response404 = HttpResponseHeaders::Builder({1, 1}, "404").Build(); EXPECT_FALSE(response200->StrictlyEquals(*response404)); EXPECT_FALSE(response404->StrictlyEquals(*response200)); } TEST(HttpResponseHeadersTest, StrictlyEqualsStatusTextMismatch) { const auto ok = HttpResponseHeaders::Builder({1, 1}, "200 OK").Build(); const auto ng = HttpResponseHeaders::Builder({1, 1}, "200 NG").Build(); EXPECT_FALSE(ok->StrictlyEquals(*ng)); EXPECT_FALSE(ng->StrictlyEquals(*ok)); } TEST(HttpResponseHeadersTest, StrictlyEqualsRawMismatch) { // These are designed so that the offsets of names and values will be the // same. std::string raw1 = "HTTP/1.1 200\n" "Pragma :None\n"; std::string raw2 = "HTTP/1.1 200\n" "Pragma: None\n"; HeadersToRaw(&raw1); HeadersToRaw(&raw2); const auto parsed1 = base::MakeRefCounted(raw1); const auto parsed2 = base::MakeRefCounted(raw2); EXPECT_FALSE(parsed1->StrictlyEquals(*parsed2)); EXPECT_FALSE(parsed2->StrictlyEquals(*parsed1)); } // There's no known way to produce an HttpResponseHeaders object with the same // `raw_headers_` but different `parsed_` structures, so there's no test for // that. } // namespace } // namespace net