blob: 024f3392a3812dc5d7e5606428afd6d56bf8c9a8 [file] [log] [blame]
// Copyright 2016 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 "content/browser/browsing_data/clear_site_data_throttle.h"
#include <memory>
#include "base/memory/ref_counted.h"
#include "base/run_loop.h"
#include "base/stl_util.h"
#include "base/strings/stringprintf.h"
#include "base/task/post_task.h"
#include "base/test/scoped_command_line.h"
#include "base/test/scoped_task_environment.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/resource_request_info.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/test_browser_thread.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "net/base/load_flags.h"
#include "net/http/http_util.h"
#include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
#include "net/url_request/redirect_info.h"
#include "net/url_request/url_request_job.h"
#include "net/url_request/url_request_test_util.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using ::testing::_;
namespace content {
using ConsoleMessagesDelegate = ClearSiteDataThrottle::ConsoleMessagesDelegate;
namespace {
const char kClearSiteDataHeaderPrefix[] = "Clear-Site-Data: ";
const char kClearCookiesHeader[] = "Clear-Site-Data: \"cookies\"";
void WaitForUIThread() {
base::RunLoop run_loop;
base::PostTaskWithTraits(FROM_HERE, {BrowserThread::UI},
run_loop.QuitClosure());
run_loop.Run();
}
// Used to verify that resource throttle delegate calls are made.
class MockResourceThrottleDelegate : public ResourceThrottle::Delegate {
public:
MOCK_METHOD0(Cancel, void());
MOCK_METHOD0(CancelAndIgnore, void());
MOCK_METHOD1(CancelWithError, void(int));
MOCK_METHOD0(Resume, void());
};
// A slightly modified ClearSiteDataThrottle for testing with unconditional
// construction, injectable response headers, and dummy clearing functionality.
class TestThrottle : public ClearSiteDataThrottle {
public:
TestThrottle(net::URLRequest* request,
std::unique_ptr<ConsoleMessagesDelegate> delegate)
: ClearSiteDataThrottle(request, std::move(delegate)) {}
~TestThrottle() override {}
void SetResponseHeaders(const std::string& headers) {
std::string headers_with_status_code = "HTTP/1.1 200\n" + headers;
headers_ = new net::HttpResponseHeaders(net::HttpUtil::AssembleRawHeaders(
headers_with_status_code.c_str(), headers_with_status_code.size()));
}
MOCK_METHOD4(ClearSiteData,
void(const url::Origin& origin,
bool clear_cookies,
bool clear_storage,
bool clear_cache));
protected:
const net::HttpResponseHeaders* GetResponseHeaders() const override {
return headers_.get();
}
void ExecuteClearingTask(const url::Origin& origin,
bool clear_cookies,
bool clear_storage,
bool clear_cache,
base::OnceClosure callback) override {
ClearSiteData(origin, clear_cookies, clear_storage, clear_cache);
// NOTE: ResourceThrottle expects Resume() to be called asynchronously.
// For the purposes of this test, synchronous call works correctly, and
// is preferable for simplicity, so that we don't have to synchronize
// between triggering Clear-Site-Data and verifying test expectations.
std::move(callback).Run();
}
private:
scoped_refptr<net::HttpResponseHeaders> headers_;
};
// A TestThrottle with modifiable current url.
class RedirectableTestThrottle : public TestThrottle {
public:
RedirectableTestThrottle(net::URLRequest* request,
std::unique_ptr<ConsoleMessagesDelegate> delegate)
: TestThrottle(request, std::move(delegate)) {}
const GURL& GetCurrentURL() const override {
return current_url_.is_valid() ? current_url_
: TestThrottle::GetCurrentURL();
}
void SetCurrentURLForTesting(const GURL& url) { current_url_ = url; }
private:
GURL current_url_;
};
// A ConsoleDelegate that outputs messages to a string |output_buffer| owned
// by the caller instead of to the console (losing the level information).
class StringConsoleMessagesDelegate : public ConsoleMessagesDelegate {
public:
StringConsoleMessagesDelegate(std::string* output_buffer) {
SetOutputFormattedMessageFunctionForTesting(
base::Bind(&StringConsoleMessagesDelegate::OutputFormattedMessage,
base::Unretained(output_buffer)));
}
~StringConsoleMessagesDelegate() override {}
private:
static void OutputFormattedMessage(std::string* output_buffer,
WebContents* web_contents,
ConsoleMessageLevel level,
const std::string& formatted_text) {
*output_buffer += formatted_text + "\n";
}
};
} // namespace
class ClearSiteDataThrottleTest : public testing::Test {
public:
ClearSiteDataThrottleTest()
: thread_bundle_(TestBrowserThreadBundle::IO_MAINLOOP) {}
private:
TestBrowserThreadBundle thread_bundle_;
DISALLOW_COPY_AND_ASSIGN(ClearSiteDataThrottleTest);
};
TEST_F(ClearSiteDataThrottleTest, MaybeCreateThrottleForRequest) {
// Create a URL request.
GURL url("https://www.example.com");
net::TestURLRequestContext context;
std::unique_ptr<net::URLRequest> request(context.CreateRequest(
url, net::DEFAULT_PRIORITY, nullptr, TRAFFIC_ANNOTATION_FOR_TESTS));
// We will not create the throttle for an empty ResourceRequestInfo.
EXPECT_FALSE(
ClearSiteDataThrottle::MaybeCreateThrottleForRequest(request.get()));
// We can create the throttle for a valid ResourceRequestInfo.
ResourceRequestInfo::AllocateForTesting(request.get(), RESOURCE_TYPE_IMAGE,
nullptr, 0, 0, 0, false, true, true,
false, nullptr);
EXPECT_TRUE(
ClearSiteDataThrottle::MaybeCreateThrottleForRequest(request.get()));
}
TEST_F(ClearSiteDataThrottleTest, ParseHeaderAndExecuteClearingTask) {
struct TestCase {
const char* header;
bool cookies;
bool storage;
bool cache;
};
std::vector<TestCase> standard_test_cases = {
// One data type.
{"\"cookies\"", true, false, false},
{"\"storage\"", false, true, false},
{"\"cache\"", false, false, true},
// Two data types.
{"\"cookies\", \"storage\"", true, true, false},
{"\"cookies\", \"cache\"", true, false, true},
{"\"storage\", \"cache\"", false, true, true},
// Three data types.
{"\"storage\", \"cache\", \"cookies\"", true, true, true},
{"\"cache\", \"cookies\", \"storage\"", true, true, true},
{"\"cookies\", \"storage\", \"cache\"", true, true, true},
// The wildcard datatype is not yet shipped.
{"\"*\", \"storage\"", false, true, false},
{"\"cookies\", \"*\", \"storage\"", true, true, false},
{"\"*\", \"cookies\", \"*\"", true, false, false},
// Different formatting.
{"\"cookies\"", true, false, false},
// Duplicates.
{"\"cookies\", \"cookies\"", true, false, false},
// Other JSON-formatted items in the list.
{"\"storage\", { \"other_params\": {} }", false, true, false},
// Unknown types are ignored, but we still proceed with the deletion for
// those that we recognize.
{"\"cache\", \"foo\"", false, false, true},
};
std::vector<TestCase> experimental_test_cases = {
// Wildcard.
{"\"*\"", true, true, true},
{"\"*\", \"storage\"", true, true, true},
{"\"cache\", \"*\", \"storage\"", true, true, true},
{"\"*\", \"cookies\", \"*\"", true, true, true},
};
const std::vector<TestCase>* test_case_sets[] = {&standard_test_cases,
&experimental_test_cases};
for (const std::vector<TestCase>* test_cases : test_case_sets) {
base::test::ScopedCommandLine scoped_command_line;
if (test_cases == &experimental_test_cases) {
scoped_command_line.GetProcessCommandLine()->AppendSwitch(
switches::kEnableExperimentalWebPlatformFeatures);
}
for (const TestCase& test_case : *test_cases) {
SCOPED_TRACE(test_case.header);
// Test that ParseHeader works correctly.
bool actual_cookies;
bool actual_storage;
bool actual_cache;
GURL url("https://example.com");
ConsoleMessagesDelegate console_delegate;
EXPECT_TRUE(ClearSiteDataThrottle::ParseHeaderForTesting(
test_case.header, &actual_cookies, &actual_storage, &actual_cache,
&console_delegate, url));
EXPECT_EQ(test_case.cookies, actual_cookies);
EXPECT_EQ(test_case.storage, actual_storage);
EXPECT_EQ(test_case.cache, actual_cache);
// Test that a call with the above parameters actually reaches
// ExecuteClearingTask().
net::TestURLRequestContext context;
std::unique_ptr<net::URLRequest> request(context.CreateRequest(
url, net::DEFAULT_PRIORITY, nullptr, TRAFFIC_ANNOTATION_FOR_TESTS));
TestThrottle throttle(request.get(),
std::make_unique<ConsoleMessagesDelegate>());
MockResourceThrottleDelegate delegate;
throttle.set_delegate_for_testing(&delegate);
throttle.SetResponseHeaders(std::string(kClearSiteDataHeaderPrefix) +
test_case.header);
EXPECT_CALL(throttle,
ClearSiteData(url::Origin::Create(url), test_case.cookies,
test_case.storage, test_case.cache));
bool defer;
throttle.WillProcessResponse(&defer);
EXPECT_TRUE(defer);
testing::Mock::VerifyAndClearExpectations(&throttle);
}
}
}
TEST_F(ClearSiteDataThrottleTest, InvalidHeader) {
struct TestCase {
const char* header;
const char* console_message;
} test_cases[] = {{"", "No recognized types specified.\n"},
{"\"unclosed",
"Unrecognized type: \"unclosed.\n"
"No recognized types specified.\n"},
{"\"passwords\"",
"Unrecognized type: \"passwords\".\n"
"No recognized types specified.\n"},
// The wildcard datatype is not yet shipped.
{"[ \"*\" ]",
"Unrecognized type: [ \"*\" ].\n"
"No recognized types specified.\n"},
{"[ \"list\" ]",
"Unrecognized type: [ \"list\" ].\n"
"No recognized types specified.\n"},
{"{ \"cookies\": [ \"a\" ] }",
"Unrecognized type: { \"cookies\": [ \"a\" ] }.\n"
"No recognized types specified.\n"},
{"\"кукис\", \"сторидж\", \"кэш\"",
"Must only contain ASCII characters.\n"}};
for (const TestCase& test_case : test_cases) {
SCOPED_TRACE(test_case.header);
bool actual_cookies;
bool actual_storage;
bool actual_cache;
ConsoleMessagesDelegate console_delegate;
EXPECT_FALSE(ClearSiteDataThrottle::ParseHeaderForTesting(
test_case.header, &actual_cookies, &actual_storage, &actual_cache,
&console_delegate, GURL()));
std::string multiline_message;
for (const auto& message : console_delegate.messages()) {
EXPECT_EQ(CONSOLE_MESSAGE_LEVEL_ERROR, message.level);
multiline_message += message.text + "\n";
}
EXPECT_EQ(test_case.console_message, multiline_message);
}
}
TEST_F(ClearSiteDataThrottleTest, LoadDoNotSaveCookies) {
net::TestURLRequestContext context;
std::unique_ptr<net::URLRequest> request(context.CreateRequest(
GURL("https://www.example.com"), net::DEFAULT_PRIORITY, nullptr,
TRAFFIC_ANNOTATION_FOR_TESTS));
std::unique_ptr<ConsoleMessagesDelegate> scoped_console_delegate(
new ConsoleMessagesDelegate());
const ConsoleMessagesDelegate* console_delegate =
scoped_console_delegate.get();
TestThrottle throttle(request.get(), std::move(scoped_console_delegate));
MockResourceThrottleDelegate delegate;
throttle.set_delegate_for_testing(&delegate);
throttle.SetResponseHeaders(kClearCookiesHeader);
EXPECT_CALL(throttle, ClearSiteData(_, _, _, _));
bool defer;
throttle.WillProcessResponse(&defer);
EXPECT_TRUE(defer);
EXPECT_EQ(1u, console_delegate->messages().size());
EXPECT_EQ(
"Cleared data types: \"cookies\". "
"Clearing channel IDs and HTTP authentication cache is currently "
"not supported, as it breaks active network connections.",
console_delegate->messages().front().text);
EXPECT_EQ(console_delegate->messages().front().level,
CONSOLE_MESSAGE_LEVEL_INFO);
testing::Mock::VerifyAndClearExpectations(&throttle);
request->SetLoadFlags(net::LOAD_DO_NOT_SAVE_COOKIES);
EXPECT_CALL(throttle, ClearSiteData(_, _, _, _)).Times(0);
throttle.WillProcessResponse(&defer);
EXPECT_FALSE(defer);
EXPECT_EQ(2u, console_delegate->messages().size());
EXPECT_EQ(
"The request's credentials mode prohibits modifying cookies "
"and other local data.",
console_delegate->messages().rbegin()->text);
EXPECT_EQ(CONSOLE_MESSAGE_LEVEL_ERROR,
console_delegate->messages().rbegin()->level);
testing::Mock::VerifyAndClearExpectations(&throttle);
}
TEST_F(ClearSiteDataThrottleTest, InvalidOrigin) {
struct TestCase {
const char* origin;
bool expect_success;
std::string error_message; // Tested only if |expect_success| = false.
} kTestCases[] = {
// The throttle only works on secure origins.
{"https://secure-origin.com", true, ""},
{"filesystem:https://secure-origin.com/temporary/", true, ""},
// That includes localhost.
{"http://localhost", true, ""},
// Not on insecure origins.
{"http://insecure-origin.com", false,
"Not supported for insecure origins."},
{"filesystem:http://insecure-origin.com/temporary/", false,
"Not supported for insecure origins."},
// Not on unique origins.
{"data:unique-origin;", false, "Not supported for unique origins."},
};
net::TestURLRequestContext context;
for (const TestCase& test_case : kTestCases) {
std::unique_ptr<net::URLRequest> request(
context.CreateRequest(GURL(test_case.origin), net::DEFAULT_PRIORITY,
nullptr, TRAFFIC_ANNOTATION_FOR_TESTS));
std::unique_ptr<ConsoleMessagesDelegate> scoped_console_delegate(
new ConsoleMessagesDelegate());
const ConsoleMessagesDelegate* console_delegate =
scoped_console_delegate.get();
TestThrottle throttle(request.get(), std::move(scoped_console_delegate));
MockResourceThrottleDelegate delegate;
throttle.set_delegate_for_testing(&delegate);
throttle.SetResponseHeaders(kClearCookiesHeader);
EXPECT_CALL(throttle, ClearSiteData(_, _, _, _))
.Times(test_case.expect_success ? 1 : 0);
bool defer;
throttle.WillProcessResponse(&defer);
EXPECT_EQ(defer, test_case.expect_success);
EXPECT_EQ(console_delegate->messages().size(), 1u);
EXPECT_EQ(test_case.expect_success ? CONSOLE_MESSAGE_LEVEL_INFO
: CONSOLE_MESSAGE_LEVEL_ERROR,
console_delegate->messages().front().level);
if (!test_case.expect_success) {
EXPECT_EQ(test_case.error_message,
console_delegate->messages().front().text);
}
testing::Mock::VerifyAndClearExpectations(&throttle);
}
}
TEST_F(ClearSiteDataThrottleTest, DeferAndResume) {
enum Stage { START, REDIRECT, RESPONSE };
struct TestCase {
Stage stage;
std::string response_headers;
bool should_defer;
} kTestCases[] = {
// The throttle never interferes while the request is starting. Response
// headers are ignored, because URLRequest is not supposed to have any
// at this stage in the first place.
{START, "", false},
{START, kClearCookiesHeader, false},
// The throttle does not defer redirects if there are no interesting
// response headers.
{REDIRECT, "", false},
{REDIRECT, "Set-Cookie: abc=123;", false},
{REDIRECT, "Content-Type: image/png;", false},
// That includes malformed Clear-Site-Data headers or header values
// that do not lead to deletion.
{REDIRECT, "Clear-Site-Data: cookies", false},
{REDIRECT, "Clear-Site-Data: \"unknown type\"", false},
// However, redirects are deferred for valid Clear-Site-Data headers.
{REDIRECT, "Clear-Site-Data: \"cookies\", \"unknown type\"", true},
{REDIRECT,
base::StringPrintf("Content-Type: image/png;\n%s", kClearCookiesHeader),
true},
{REDIRECT,
base::StringPrintf("%s\nContent-Type: image/png;", kClearCookiesHeader),
true},
// Multiple instances of the header will be parsed correctly.
{REDIRECT,
base::StringPrintf("%s\n%s", kClearCookiesHeader, kClearCookiesHeader),
true},
// Final response headers are treated the same way as in the case
// of redirect.
{REDIRECT, "Set-Cookie: abc=123;", false},
{REDIRECT, "Clear-Site-Data: cookies", false},
{REDIRECT, kClearCookiesHeader, true},
};
struct TestOrigin {
const char* origin;
bool valid;
} kTestOrigins[] = {
// The throttle only works on secure origins.
{"https://secure-origin.com", true},
{"filesystem:https://secure-origin.com/temporary/", true},
// That includes localhost.
{"http://localhost", true},
// Not on insecure origins.
{"http://insecure-origin.com", false},
{"filesystem:http://insecure-origin.com/temporary/", false},
// Not on unique origins.
{"data:unique-origin;", false},
};
net::TestURLRequestContext context;
for (const TestOrigin& test_origin : kTestOrigins) {
for (const TestCase& test_case : kTestCases) {
SCOPED_TRACE(base::StringPrintf("Origin=%s\nStage=%d\nHeaders:\n%s",
test_origin.origin, test_case.stage,
test_case.response_headers.c_str()));
std::unique_ptr<net::URLRequest> request(
context.CreateRequest(GURL(test_origin.origin), net::DEFAULT_PRIORITY,
nullptr, TRAFFIC_ANNOTATION_FOR_TESTS));
TestThrottle throttle(request.get(),
std::make_unique<ConsoleMessagesDelegate>());
throttle.SetResponseHeaders(test_case.response_headers);
MockResourceThrottleDelegate delegate;
throttle.set_delegate_for_testing(&delegate);
// Whether we should defer is always conditional on the origin
// being valid.
bool expected_defer = test_case.should_defer && test_origin.valid;
// If we expect loading to be deferred, then we also expect data to be
// cleared and the load to eventually resume.
if (expected_defer) {
testing::Expectation expectation = EXPECT_CALL(
throttle,
ClearSiteData(url::Origin::Create(GURL(test_origin.origin)), _, _,
_));
EXPECT_CALL(delegate, Resume()).After(expectation);
} else {
EXPECT_CALL(throttle, ClearSiteData(_, _, _, _)).Times(0);
EXPECT_CALL(delegate, Resume()).Times(0);
}
bool actual_defer = false;
switch (test_case.stage) {
case START: {
throttle.WillStartRequest(&actual_defer);
break;
}
case REDIRECT: {
net::RedirectInfo redirect_info;
throttle.WillRedirectRequest(redirect_info, &actual_defer);
break;
}
case RESPONSE: {
throttle.WillProcessResponse(&actual_defer);
break;
}
}
EXPECT_EQ(expected_defer, actual_defer);
testing::Mock::VerifyAndClearExpectations(&delegate);
}
}
}
// Verifies that console outputs from various actions on different URLs
// are correctly pretty-printed to the console.
TEST_F(ClearSiteDataThrottleTest, FormattedConsoleOutput) {
struct TestCase {
const char* header;
const char* url;
const char* output;
} kTestCases[] = {
// Successful deletion outputs one line, and in case of cookies, also
// a disclaimer about omitted data (crbug.com/798760).
{"\"cookies\"", "https://origin1.com/foo",
"Clear-Site-Data header on 'https://origin1.com/foo': "
"Cleared data types: \"cookies\". "
"Clearing channel IDs and HTTP authentication cache is currently "
"not supported, as it breaks active network connections.\n"},
// Another successful deletion.
{"\"storage\"", "https://origin2.com/foo",
"Clear-Site-Data header on 'https://origin2.com/foo': "
"Cleared data types: \"storage\".\n"},
// Redirect to the same URL. Unsuccessful deletion outputs two lines.
{"\"foo\"", "https://origin2.com/foo",
"Clear-Site-Data header on 'https://origin2.com/foo': "
"Unrecognized type: \"foo\".\n"
"Clear-Site-Data header on 'https://origin2.com/foo': "
"No recognized types specified.\n"},
// Redirect to another URL. Another unsuccessful deletion.
{"\"some text\"", "https://origin3.com/bar",
"Clear-Site-Data header on 'https://origin3.com/bar': "
"Unrecognized type: \"some text\".\n"
"Clear-Site-Data header on 'https://origin3.com/bar': "
"No recognized types specified.\n"},
// Yet another on the same URL.
{"\"passwords\"", "https://origin3.com/bar",
"Clear-Site-Data header on 'https://origin3.com/bar': "
"Unrecognized type: \"passwords\".\n"
"Clear-Site-Data header on 'https://origin3.com/bar': "
"No recognized types specified.\n"},
// Successful deletion on the same URL.
{"\"cache\"", "https://origin3.com/bar",
"Clear-Site-Data header on 'https://origin3.com/bar': "
"Cleared data types: \"cache\".\n"},
// Redirect to the original URL.
// Successful deletion outputs one line.
{"", "https://origin1.com/foo",
"Clear-Site-Data header on 'https://origin1.com/foo': "
"No recognized types specified.\n"}};
bool kThrottleTypeIsNavigation[] = {true, false};
for (bool navigation : kThrottleTypeIsNavigation) {
SCOPED_TRACE(navigation ? "Navigation test." : "Subresource test.");
net::TestURLRequestContext context;
std::unique_ptr<net::URLRequest> request(
context.CreateRequest(GURL(kTestCases[0].url), net::DEFAULT_PRIORITY,
nullptr, TRAFFIC_ANNOTATION_FOR_TESTS));
ResourceRequestInfo::AllocateForTesting(
request.get(),
navigation ? RESOURCE_TYPE_SUB_FRAME : RESOURCE_TYPE_IMAGE, nullptr, 0,
0, 0, false, true, true, false, nullptr);
std::string output_buffer;
std::unique_ptr<RedirectableTestThrottle> throttle =
std::make_unique<RedirectableTestThrottle>(
request.get(),
std::make_unique<StringConsoleMessagesDelegate>(&output_buffer));
MockResourceThrottleDelegate delegate;
throttle->set_delegate_for_testing(&delegate);
std::string last_seen_console_output;
// Simulate redirecting the throttle through the above origins with the
// corresponding response headers.
bool defer;
throttle->WillStartRequest(&defer);
for (size_t i = 0; i < base::size(kTestCases); i++) {
throttle->SetResponseHeaders(std::string(kClearSiteDataHeaderPrefix) +
kTestCases[i].header);
// TODO(msramek): There is probably a better way to do this inside
// URLRequest.
throttle->SetCurrentURLForTesting(GURL(kTestCases[i].url));
net::RedirectInfo redirect_info;
if (i < base::size(kTestCases) - 1)
throttle->WillRedirectRequest(redirect_info, &defer);
else
throttle->WillProcessResponse(&defer);
// Wait for any messages to be output.
WaitForUIThread();
// For navigations, the console should be still empty. For subresource
// requests, messages should be added progressively.
if (navigation) {
EXPECT_TRUE(output_buffer.empty());
} else {
EXPECT_EQ(last_seen_console_output + kTestCases[i].output,
output_buffer);
}
last_seen_console_output = output_buffer;
}
throttle.reset();
WaitForUIThread();
// At the end, the console must contain all messages regardless of whether
// it was a navigation or a subresource request.
std::string expected_output;
for (struct TestCase& test_case : kTestCases)
expected_output += test_case.output;
EXPECT_EQ(expected_output, output_buffer);
}
}
} // namespace content