// Copyright (c) 2006-2009 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include #include "base/basictypes.h" #include "base/pickle.h" #include "base/time.h" #include "net/http/http_response_headers.h" #include "testing/gtest/include/gtest/gtest.h" using namespace std; using base::Time; using net::HttpResponseHeaders; using net::HttpVersion; namespace { struct TestData { const char* raw_headers; const char* expected_headers; int expected_response_code; HttpVersion expected_parsed_version; HttpVersion expected_version; }; struct ContentTypeTestData { const string raw_headers; const string mime_type; const bool has_mimetype; const string charset; const bool has_charset; const string all_content_type; }; 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) { replace(headers->begin(), headers->end(), '\n', '\0'); if (!headers->empty()) *headers += '\0'; } void TestCommon(const TestData& test) { string raw_headers(test.raw_headers); HeadersToRaw(&raw_headers); string expected_headers(test.expected_headers); string headers; scoped_refptr parsed = new HttpResponseHeaders(raw_headers); parsed->GetNormalizedHeaders(&headers); // Transform to readable output format (so it's easier to see diffs). replace(headers.begin(), headers.end(), ' ', '_'); replace(headers.begin(), headers.end(), '\n', '\\'); replace(expected_headers.begin(), expected_headers.end(), ' ', '_'); replace(expected_headers.begin(), expected_headers.end(), '\n', '\\'); EXPECT_EQ(expected_headers, headers); EXPECT_EQ(test.expected_response_code, parsed->response_code()); EXPECT_TRUE(test.expected_parsed_version == parsed->GetParsedHttpVersion()); EXPECT_TRUE(test.expected_version == parsed->GetHttpVersion()); } } // end namespace // Check that we normalize headers properly. TEST(HttpResponseHeadersTest, NormalizeHeadersWhitespace) { TestData test = { "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, b\n", 202, HttpVersion(1,1), HttpVersion(1,1) }; TestCommon(test); } // Check that we normalize headers properly (header name is invalid if starts // with LWS). TEST(HttpResponseHeadersTest, NormalizeHeadersLeadingWhitespace) { TestData test = { "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, b\n", 202, HttpVersion(1,1), HttpVersion(1,1) }; TestCommon(test); } TEST(HttpResponseHeadersTest, BlankHeaders) { TestData test = { "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", 200, HttpVersion(1,1), HttpVersion(1,1) }; TestCommon(test); } TEST(HttpResponseHeadersTest, NormalizeHeadersVersion) { // Don't believe the http/0.9 version if there are headers! TestData test = { "hTtP/0.9 201\n" "Content-TYPE: text/html; charset=utf-8\n", "HTTP/1.0 201 OK\n" "Content-TYPE: text/html; charset=utf-8\n", 201, HttpVersion(0,9), HttpVersion(1,0) }; TestCommon(test); } TEST(HttpResponseHeadersTest, PreserveHttp09) { // Accept the HTTP/0.9 version number if there are no headers. // This is how HTTP/0.9 responses get constructed from HttpNetworkTransaction. TestData test = { "hTtP/0.9 200 OK\n", "HTTP/0.9 200 OK\n", 200, HttpVersion(0,9), HttpVersion(0,9) }; TestCommon(test); } TEST(HttpResponseHeadersTest, NormalizeHeadersMissingOK) { TestData test = { "HTTP/1.1 201\n" "Content-TYPE: text/html; charset=utf-8\n", "HTTP/1.1 201 OK\n" "Content-TYPE: text/html; charset=utf-8\n", 201, HttpVersion(1,1), HttpVersion(1,1) }; TestCommon(test); } TEST(HttpResponseHeadersTest, NormalizeHeadersBadStatus) { TestData test = { "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", 200, HttpVersion(0,0), // Parse error HttpVersion(1,0) }; TestCommon(test); } TEST(HttpResponseHeadersTest, NormalizeHeadersEmpty) { TestData test = { "", "HTTP/1.0 200 OK\n", 200, HttpVersion(0,0), // Parse Error HttpVersion(1,0) }; TestCommon(test); } TEST(HttpResponseHeadersTest, NormalizeHeadersStartWithColon) { TestData test = { "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", 202, HttpVersion(1,1), HttpVersion(1,1) }; TestCommon(test); } TEST(HttpResponseHeadersTest, NormalizeHeadersStartWithColonAtEOL) { TestData test = { "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", 202, HttpVersion(1,1), HttpVersion(1,1) }; TestCommon(test); } TEST(HttpResponseHeadersTest, NormalizeHeadersOfWhitepace) { TestData test = { "\n \n", "HTTP/1.0 200 OK\n", 200, HttpVersion(0,0), // Parse error HttpVersion(1,0) }; TestCommon(test); } TEST(HttpResponseHeadersTest, RepeatedSetCookie) { TestData test = { "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, y=2\n", 200, HttpVersion(1,1), HttpVersion(1,1) }; TestCommon(test); } TEST(HttpResponseHeadersTest, GetNormalizedHeader) { std::string headers = "HTTP/1.1 200 OK\n" "Cache-control: private\n" "cache-Control: no-store\n"; HeadersToRaw(&headers); scoped_refptr parsed = new HttpResponseHeaders(headers); std::string value; EXPECT_TRUE(parsed->GetNormalizedHeader("cache-control", &value)); EXPECT_EQ("private, no-store", value); } TEST(HttpResponseHeadersTest, Persist) { const struct { net::HttpResponseHeaders::PersistOptions options; const char* raw_headers; const char* expected_headers; } tests[] = { { net::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, no-store\n" }, { net::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" }, { net::HttpResponseHeaders::PERSIST_SANS_NON_CACHEABLE | net::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" }, { net::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 { net::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 { net::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 { net::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 { net::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 { net::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 { net::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 { net::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, 3\n" "Bar: 2\n" }, // header name appears twice, separated by another header (type 2) { net::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, 4\n" "Bar: 2\n" }, // Test filtering of cookie headers. { net::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" }, // Test LWS at the end of a header. { net::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. { net::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" }, }; for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { std::string headers = tests[i].raw_headers; HeadersToRaw(&headers); scoped_refptr parsed1 = new HttpResponseHeaders(headers); Pickle pickle; parsed1->Persist(&pickle, tests[i].options); void* iter = NULL; scoped_refptr parsed2 = new HttpResponseHeaders(pickle, &iter); std::string h2; parsed2->GetNormalizedHeaders(&h2); EXPECT_EQ(string(tests[i].expected_headers), h2); } } 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"; HeadersToRaw(&headers); scoped_refptr parsed = new HttpResponseHeaders(headers); void* iter = NULL; std::string value; EXPECT_TRUE(parsed->EnumerateHeader(&iter, "cache-control", &value)); EXPECT_EQ("private", value); EXPECT_TRUE(parsed->EnumerateHeader(&iter, "cache-control", &value)); EXPECT_EQ("no-cache=\"set-cookie,server\"", value); EXPECT_TRUE(parsed->EnumerateHeader(&iter, "cache-control", &value)); EXPECT_EQ("no-store", 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); scoped_refptr parsed = new HttpResponseHeaders(headers); void* iter = NULL; 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); scoped_refptr parsed = new HttpResponseHeaders(headers); std::string value; EXPECT_TRUE(parsed->EnumerateHeader(NULL, "date", &value)); EXPECT_EQ("Tue, 07 Aug 2007 23:10:55 GMT", value); EXPECT_TRUE(parsed->EnumerateHeader(NULL, "last-modified", &value)); EXPECT_EQ("Wed, 01 Aug 2007 23:23:45 GMT", value); } TEST(HttpResponseHeadersTest, GetMimeType) { const ContentTypeTestData 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" }, // Test single quotes. { "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" }, // Last 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, last one wins. { "HTTP/1.1 200 OK\n" "Content-type: text/html;charset=utf-8; charset=iso-8859-1\n", "text/html", true, "iso-8859-1", 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, "*/*" }, }; for (size_t i = 0; i < arraysize(tests); ++i) { string headers(tests[i].raw_headers); HeadersToRaw(&headers); scoped_refptr parsed = new HttpResponseHeaders(headers); std::string value; EXPECT_EQ(tests[i].has_mimetype, parsed->GetMimeType(&value)); EXPECT_EQ(tests[i].mime_type, value); value.clear(); EXPECT_EQ(tests[i].has_charset, parsed->GetCharset(&value)); EXPECT_EQ(tests[i].charset, value); EXPECT_TRUE(parsed->GetNormalizedHeader("content-type", &value)); EXPECT_EQ(tests[i].all_content_type, value); } } TEST(HttpResponseHeadersTest, RequiresValidation) { const struct { const char* headers; bool requires_validation; } tests[] = { // no expiry info: expires immediately { "HTTP/1.1 200 OK\n" "\n", true }, // valid for a little while { "HTTP/1.1 200 OK\n" "cache-control: max-age=10000\n" "\n", false }, // 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", false }, // expired already { "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", true }, // 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", false }, // 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", false }, { "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", false }, { "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", false }, // 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", true }, { "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", true }, { "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", true }, // cached permanent redirect { "HTTP/1.1 301 Moved Permanently\n" "\n", false }, // cached redirect: not reusable even though by default it would be { "HTTP/1.1 300 Multiple Choices\n" "Cache-Control: no-cache\n" "\n", true }, // cached forever by default { "HTTP/1.1 410 Gone\n" "\n", false }, // cached temporary redirect: not reusable { "HTTP/1.1 302 Found\n" "\n", true }, // cached temporary redirect: reusable { "HTTP/1.1 302 Found\n" "cache-control: max-age=10000\n" "\n", false }, // 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", false }, // 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", true }, // 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", true }, // TODO(darin): add many many more tests here }; Time request_time, response_time, current_time; Time::FromString(L"Wed, 28 Nov 2007 00:40:09 GMT", &request_time); Time::FromString(L"Wed, 28 Nov 2007 00:40:12 GMT", &response_time); Time::FromString(L"Wed, 28 Nov 2007 00:45:20 GMT", ¤t_time); for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { string headers(tests[i].headers); HeadersToRaw(&headers); scoped_refptr parsed = new HttpResponseHeaders(headers); bool requires_validation = parsed->RequiresValidation(request_time, response_time, current_time); EXPECT_EQ(tests[i].requires_validation, requires_validation); } } TEST(HttpResponseHeadersTest, Update) { const struct { const char* orig_headers; const char* new_headers; const char* expected_headers; } 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" }, }; for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { string orig_headers(tests[i].orig_headers); HeadersToRaw(&orig_headers); scoped_refptr parsed = new HttpResponseHeaders(orig_headers); string new_headers(tests[i].new_headers); HeadersToRaw(&new_headers); scoped_refptr new_parsed = new HttpResponseHeaders(new_headers); parsed->Update(*new_parsed); string resulting_headers; parsed->GetNormalizedHeaders(&resulting_headers); EXPECT_EQ(string(tests[i].expected_headers), resulting_headers); } } TEST(HttpResponseHeadersTest, EnumerateHeaderLines) { const struct { const char* headers; const char* expected_lines; } 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" }, }; for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { string headers(tests[i].headers); HeadersToRaw(&headers); scoped_refptr parsed = new HttpResponseHeaders(headers); string name, value, lines; void* iter = NULL; while (parsed->EnumerateHeaderLines(&iter, &name, &value)) { lines.append(name); lines.append(": "); lines.append(value); lines.append("\n"); } EXPECT_EQ(string(tests[i].expected_lines), lines); } } TEST(HttpResponseHeadersTest, IsRedirect) { const struct { const char* headers; const char* location; bool 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 w/ 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 w/ 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 }, }; for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { string headers(tests[i].headers); HeadersToRaw(&headers); scoped_refptr parsed = new HttpResponseHeaders(headers); std::string location; EXPECT_EQ(parsed->IsRedirect(&location), tests[i].is_redirect); EXPECT_EQ(location, tests[i].location); } } TEST(HttpResponseHeadersTest, GetContentLength) { const struct { const char* headers; int64 expected_len; } 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 { "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 }, }; for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { string headers(tests[i].headers); HeadersToRaw(&headers); scoped_refptr parsed = new HttpResponseHeaders(headers); EXPECT_EQ(tests[i].expected_len, parsed->GetContentLength()); } } TEST(HttpResponseHeaders, GetContentRange) { const struct { const char* headers; bool expected_return_value; int64 expected_first_byte_position; int64 expected_last_byte_position; int64 expected_instance_size; } 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: megabytes 0-10/50", false, -1, -1, -1 }, { "HTTP/1.1 206 Partial Content\n" "Content-Range: 0-10/50", 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 0-50/51", true, 0, 50, 51 }, { "HTTP/1.1 206 Partial Content\n" "Content-Range: bytes\t0-50/51", 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 0 - 50 \t / \t51", true, 0, 50, 51 }, { "HTTP/1.1 206 Partial Content\n" "Content-Range: bytes 0\t-\t50\t/\t51\t", true, 0, 50, 51 }, { "HTTP/1.1 206 Partial Content\n" "Content-Range: \tbytes\t\t\t 0\t-\t50\t/\t51\t", true, 0, 50, 51 }, { "HTTP/1.1 206 Partial Content\n" "Content-Range: \t bytes \t 0 - 50 / 5 1", false, 0, 50, -1 }, { "HTTP/1.1 206 Partial Content\n" "Content-Range: \t bytes \t 0 - 5 0 / 51", false, -1, -1, -1 }, { "HTTP/1.1 206 Partial Content\n" "Content-Range: bytes 50-0/51", false, 50, 0, -1 }, { "HTTP/1.1 416 Requested range not satisfiable\n" "Content-Range: bytes * /*", 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, 0, 50, -1 }, { "HTTP/1.1 206 Partial Content\n" "Content-Range: bytes 0-50 / * ", false, 0, 50, -1 }, { "HTTP/1.1 206 Partial Content\n" "Content-Range: bytes 0-10000000000/10000000001", true, 0, 10000000000ll, 10000000001ll }, { "HTTP/1.1 206 Partial Content\n" "Content-Range: bytes 0-10000000000/10000000000", false, 0, 10000000000ll, 10000000000ll }, // 64 bits wraparound. { "HTTP/1.1 206 Partial Content\n" "Content-Range: bytes 0 - 9223372036854775807 / 100", false, 0, kint64max, 100 }, // 64 bits wraparound. { "HTTP/1.1 206 Partial Content\n" "Content-Range: bytes 0 - 100 / -9223372036854775808", false, 0, 100, kint64min }, { "HTTP/1.1 206 Partial Content\n" "Content-Range: bytes */50", false, -1, -1, 50 }, { "HTTP/1.1 206 Partial Content\n" "Content-Range: bytes 0-50/10", false, 0, 50, 10 }, { "HTTP/1.1 206 Partial Content\n" "Content-Range: bytes 40-50/45", false, 40, 50, 45 }, { "HTTP/1.1 206 Partial Content\n" "Content-Range: bytes 0-50/-10", false, 0, 50, -10 }, { "HTTP/1.1 206 Partial Content\n" "Content-Range: bytes 0-0/1", true, 0, 0, 1 }, { "HTTP/1.1 206 Partial Content\n" "Content-Range: bytes 0-40000000000000000000/40000000000000000001", false, -1, -1, -1 }, { "HTTP/1.1 206 Partial Content\n" "Content-Range: bytes 1-/100", false, -1, -1, -1 }, { "HTTP/1.1 206 Partial Content\n" "Content-Range: bytes -/100", false, -1, -1, -1 }, { "HTTP/1.1 206 Partial Content\n" "Content-Range: bytes -1/100", false, -1, -1, -1 }, { "HTTP/1.1 206 Partial Content\n" "Content-Range: bytes 0-1233/*", false, 0, 1233, -1 }, { "HTTP/1.1 206 Partial Content\n" "Content-Range: bytes -123 - -1/100", false, -1, -1, -1 }, }; for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { string headers(tests[i].headers); HeadersToRaw(&headers); scoped_refptr parsed = new HttpResponseHeaders(headers); int64 first_byte_position; int64 last_byte_position; int64 instance_size; bool return_value = parsed->GetContentRange(&first_byte_position, &last_byte_position, &instance_size); EXPECT_EQ(tests[i].expected_return_value, return_value); EXPECT_EQ(tests[i].expected_first_byte_position, first_byte_position); EXPECT_EQ(tests[i].expected_last_byte_position, last_byte_position); EXPECT_EQ(tests[i].expected_instance_size, instance_size); } } TEST(HttpResponseHeadersTest, IsKeepAlive) { const struct { const char* headers; bool expected_keep_alive; } 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 }, }; for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { string headers(tests[i].headers); HeadersToRaw(&headers); scoped_refptr parsed = new HttpResponseHeaders(headers); EXPECT_EQ(tests[i].expected_keep_alive, parsed->IsKeepAlive()); } } TEST(HttpResponseHeadersTest, HasStrongValidators) { const struct { const char* headers; bool expected_result; } tests[] = { { "HTTP/0.9 200 OK", false }, { "HTTP/0.9 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 } }; for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { string headers(tests[i].headers); HeadersToRaw(&headers); scoped_refptr parsed = new HttpResponseHeaders(headers); EXPECT_EQ(tests[i].expected_result, parsed->HasStrongValidators()) << "Failed test case " << i; } } TEST(HttpResponseHeadersTest, GetStatusText) { std::string headers("HTTP/1.1 404 Not Found"); HeadersToRaw(&headers); scoped_refptr parsed = new HttpResponseHeaders(headers); EXPECT_EQ(std::string("Not Found"), parsed->GetStatusText()); } TEST(HttpResponseHeadersTest, GetStatusTextMissing) { std::string headers("HTTP/1.1 404"); HeadersToRaw(&headers); scoped_refptr parsed = new HttpResponseHeaders(headers); // Since the status line gets normalized, we have OK EXPECT_EQ(std::string("OK"), parsed->GetStatusText()); } TEST(HttpResponseHeadersTest, GetStatusTextMultiSpace) { std::string headers("HTTP/1.0 404 Not Found"); HeadersToRaw(&headers); scoped_refptr parsed = new HttpResponseHeaders(headers); EXPECT_EQ(std::string("Not Found"), parsed->GetStatusText()); } TEST(HttpResponseHeadersTest, GetStatusBadStatusLine) { std::string headers("Foo bar."); HeadersToRaw(&headers); scoped_refptr parsed = new HttpResponseHeaders(headers); // The bad status line would have gotten rewritten as // HTTP/1.0 200 OK. EXPECT_EQ(std::string("OK"), parsed->GetStatusText()); } TEST(HttpResponseHeadersTest, AddHeader) { const struct { const char* orig_headers; const char* new_header; const char* expected_headers; } tests[] = { { "HTTP/1.1 200 OK\n" "connection: keep-alive\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" "Content-Length: 450\n" }, { "HTTP/1.1 200 OK\n" "connection: keep-alive\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" "Content-Length: 450\n" }, }; for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { string orig_headers(tests[i].orig_headers); HeadersToRaw(&orig_headers); scoped_refptr parsed = new HttpResponseHeaders(orig_headers); string new_header(tests[i].new_header); parsed->AddHeader(new_header); string resulting_headers; parsed->GetNormalizedHeaders(&resulting_headers); EXPECT_EQ(string(tests[i].expected_headers), resulting_headers); } } TEST(HttpResponseHeadersTest, RemoveHeader) { const struct { const char* orig_headers; const char* to_remove; const char* expected_headers; } 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" }, }; for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { string orig_headers(tests[i].orig_headers); HeadersToRaw(&orig_headers); scoped_refptr parsed = new HttpResponseHeaders(orig_headers); string name(tests[i].to_remove); parsed->RemoveHeader(name); string resulting_headers; parsed->GetNormalizedHeaders(&resulting_headers); EXPECT_EQ(string(tests[i].expected_headers), resulting_headers); } } TEST(HttpResponseHeadersTest, ReplaceStatus) { const struct { const char* orig_headers; const char* new_status; const char* expected_headers; } 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" }, }; for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { string orig_headers(tests[i].orig_headers); HeadersToRaw(&orig_headers); scoped_refptr parsed = new HttpResponseHeaders(orig_headers); string name(tests[i].new_status); parsed->ReplaceStatusLine(name); string resulting_headers; parsed->GetNormalizedHeaders(&resulting_headers); EXPECT_EQ(string(tests[i].expected_headers), resulting_headers); } }