blob: 421cb854d6bd718f056c814863ae0ea06c4788f3 [file] [log] [blame]
// Copyright 2017 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 "services/network/restricted_cookie_manager.h"
#include <algorithm>
#include "base/bind.h"
#include "base/message_loop/message_loop.h"
#include "base/run_loop.h"
#include "base/test/bind_test_util.h"
#include "base/time/time.h"
#include "mojo/core/embedder/embedder.h"
#include "net/cookies/cookie_constants.h"
#include "net/cookies/cookie_monster.h"
#include "net/cookies/cookie_store.h"
#include "net/cookies/cookie_store_test_callbacks.h"
#include "services/network/cookie_settings.h"
#include "services/network/public/mojom/cookie_manager.mojom.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace network {
// Synchronous proxies to a wrapped RestrictedCookieManager's methods.
class RestrictedCookieManagerSync {
public:
// Caller must guarantee that |*cookie_service| outlives the
// SynchronousCookieManager.
explicit RestrictedCookieManagerSync(
mojom::RestrictedCookieManager* cookie_service)
: cookie_service_(cookie_service) {}
~RestrictedCookieManagerSync() {}
std::vector<net::CanonicalCookie> GetAllForUrl(
const GURL& url,
const GURL& site_for_cookies,
mojom::CookieManagerGetOptionsPtr options) {
base::RunLoop run_loop;
std::vector<net::CanonicalCookie> result;
cookie_service_->GetAllForUrl(
url, site_for_cookies, std::move(options),
base::BindLambdaForTesting(
[&run_loop,
&result](const std::vector<net::CanonicalCookie>& backend_result) {
result = backend_result;
run_loop.Quit();
}));
run_loop.Run();
return result;
}
bool SetCanonicalCookie(const net::CanonicalCookie& cookie,
const GURL& url,
const GURL& site_for_cookies) {
base::RunLoop run_loop;
bool result = false;
cookie_service_->SetCanonicalCookie(
cookie, url, site_for_cookies,
base::BindLambdaForTesting([&run_loop, &result](bool backend_result) {
result = backend_result;
run_loop.Quit();
}));
run_loop.Run();
return result;
}
void AddChangeListener(const GURL& url,
const GURL& site_for_cookies,
mojom::CookieChangeListenerPtr listener) {
base::RunLoop run_loop;
cookie_service_->AddChangeListener(
url, site_for_cookies, std::move(listener), run_loop.QuitClosure());
run_loop.Run();
}
private:
mojom::RestrictedCookieManager* cookie_service_;
DISALLOW_COPY_AND_ASSIGN(RestrictedCookieManagerSync);
};
class RestrictedCookieManagerTest : public testing::Test {
public:
RestrictedCookieManagerTest()
: cookie_monster_(nullptr, nullptr /* netlog */),
service_(std::make_unique<RestrictedCookieManager>(
&cookie_monster_,
&cookie_settings_,
url::Origin::Create(GURL("http://example.com")))),
binding_(service_.get(), mojo::MakeRequest(&service_ptr_)) {
sync_service_ =
std::make_unique<RestrictedCookieManagerSync>(service_ptr_.get());
}
~RestrictedCookieManagerTest() override {}
void SetUp() override {
mojo::core::SetDefaultProcessErrorCallback(base::BindRepeating(
&RestrictedCookieManagerTest::OnBadMessage, base::Unretained(this)));
}
void TearDown() override {
mojo::core::SetDefaultProcessErrorCallback(
mojo::core::ProcessErrorCallback());
}
// Set a canonical cookie directly into the store.
bool SetCanonicalCookie(const net::CanonicalCookie& cookie,
std::string source_scheme,
bool can_modify_httponly) {
net::ResultSavingCookieCallback<net::CanonicalCookie::CookieInclusionStatus>
callback;
net::CookieOptions options;
if (can_modify_httponly)
options.set_include_httponly();
cookie_monster_.SetCanonicalCookieAsync(
std::make_unique<net::CanonicalCookie>(cookie),
std::move(source_scheme), options,
base::BindOnce(&net::ResultSavingCookieCallback<
net::CanonicalCookie::CookieInclusionStatus>::Run,
base::Unretained(&callback)));
callback.WaitUntilDone();
return callback.result() ==
net::CanonicalCookie::CookieInclusionStatus::INCLUDE;
}
// Simplified helper for SetCanonicalCookie.
//
// Creates a CanonicalCookie that is not secure, not http-only,
// and not restricted to first parties. Crashes if the creation fails.
void SetSessionCookie(const char* name,
const char* value,
const char* domain,
const char* path) {
CHECK(SetCanonicalCookie(
net::CanonicalCookie(name, value, domain, path, base::Time(),
base::Time(), base::Time(), /* secure = */ false,
/* httponly = */ false,
net::CookieSameSite::NO_RESTRICTION,
net::COOKIE_PRIORITY_DEFAULT),
"https", /* can_modify_httponly = */ true));
}
void ExpectBadMessage() { expecting_bad_message_ = true; }
bool received_bad_message() { return received_bad_message_; }
protected:
void OnBadMessage(const std::string& reason) {
EXPECT_TRUE(expecting_bad_message_) << "Unexpected bad message: " << reason;
received_bad_message_ = true;
}
base::MessageLoopForIO message_loop_;
net::CookieMonster cookie_monster_;
CookieSettings cookie_settings_;
std::unique_ptr<RestrictedCookieManager> service_;
mojom::RestrictedCookieManagerPtr service_ptr_;
mojo::Binding<mojom::RestrictedCookieManager> binding_;
std::unique_ptr<RestrictedCookieManagerSync> sync_service_;
bool expecting_bad_message_ = false;
bool received_bad_message_ = false;
};
namespace {
bool CompareCanonicalCookies(const net::CanonicalCookie& c1,
const net::CanonicalCookie& c2) {
return c1.FullCompare(c2);
}
} // anonymous namespace
TEST_F(RestrictedCookieManagerTest, GetAllForUrlBlankFilter) {
SetSessionCookie("cookie-name", "cookie-value", "example.com", "/");
SetSessionCookie("cookie-name-2", "cookie-value-2", "example.com", "/");
SetSessionCookie("other-cookie-name", "other-cookie-value", "not-example.com",
"/");
auto options = mojom::CookieManagerGetOptions::New();
options->name = "";
options->match_type = mojom::CookieMatchType::STARTS_WITH;
std::vector<net::CanonicalCookie> cookies = sync_service_->GetAllForUrl(
GURL("http://example.com/test/"), GURL("http://example.com"),
std::move(options));
ASSERT_THAT(cookies, testing::SizeIs(2));
std::sort(cookies.begin(), cookies.end(), &CompareCanonicalCookies);
EXPECT_EQ("cookie-name", cookies[0].Name());
EXPECT_EQ("cookie-value", cookies[0].Value());
EXPECT_EQ("cookie-name-2", cookies[1].Name());
EXPECT_EQ("cookie-value-2", cookies[1].Value());
}
TEST_F(RestrictedCookieManagerTest, GetAllForUrlEmptyFilter) {
SetSessionCookie("cookie-name", "cookie-value", "example.com", "/");
auto options = mojom::CookieManagerGetOptions::New();
options->name = "";
options->match_type = mojom::CookieMatchType::EQUALS;
std::vector<net::CanonicalCookie> cookies = sync_service_->GetAllForUrl(
GURL("http://example.com/test/"), GURL("http://example.com"),
std::move(options));
ASSERT_THAT(cookies, testing::SizeIs(0));
}
TEST_F(RestrictedCookieManagerTest, GetAllForUrlEqualsMatch) {
SetSessionCookie("cookie-name", "cookie-value", "example.com", "/");
SetSessionCookie("cookie-name-2", "cookie-value-2", "example.com", "/");
auto options = mojom::CookieManagerGetOptions::New();
options->name = "cookie-name";
options->match_type = mojom::CookieMatchType::EQUALS;
std::vector<net::CanonicalCookie> cookies = sync_service_->GetAllForUrl(
GURL("http://example.com/test/"), GURL("http://example.com"),
std::move(options));
ASSERT_THAT(cookies, testing::SizeIs(1));
EXPECT_EQ("cookie-name", cookies[0].Name());
EXPECT_EQ("cookie-value", cookies[0].Value());
}
TEST_F(RestrictedCookieManagerTest, GetAllForUrlStartsWithMatch) {
SetSessionCookie("cookie-name", "cookie-value", "example.com", "/");
SetSessionCookie("cookie-name-2", "cookie-value-2", "example.com", "/");
SetSessionCookie("cookie-name-2b", "cookie-value-2b", "example.com", "/");
SetSessionCookie("cookie-name-3b", "cookie-value-3b", "example.com", "/");
auto options = mojom::CookieManagerGetOptions::New();
options->name = "cookie-name-2";
options->match_type = mojom::CookieMatchType::STARTS_WITH;
std::vector<net::CanonicalCookie> cookies = sync_service_->GetAllForUrl(
GURL("http://example.com/test/"), GURL("http://example.com"),
std::move(options));
ASSERT_THAT(cookies, testing::SizeIs(2));
std::sort(cookies.begin(), cookies.end(), &CompareCanonicalCookies);
EXPECT_EQ("cookie-name-2", cookies[0].Name());
EXPECT_EQ("cookie-value-2", cookies[0].Value());
EXPECT_EQ("cookie-name-2b", cookies[1].Name());
EXPECT_EQ("cookie-value-2b", cookies[1].Value());
}
TEST_F(RestrictedCookieManagerTest, GetAllForUrlFromWrongOrigin) {
SetSessionCookie("cookie-name", "cookie-value", "example.com", "/");
SetSessionCookie("cookie-name-2", "cookie-value-2", "example.com", "/");
SetSessionCookie("other-cookie-name", "other-cookie-value", "not-example.com",
"/");
auto options = mojom::CookieManagerGetOptions::New();
options->name = "";
options->match_type = mojom::CookieMatchType::STARTS_WITH;
ExpectBadMessage();
std::vector<net::CanonicalCookie> cookies = sync_service_->GetAllForUrl(
GURL("http://not-example.com/test/"), GURL("http://not-example.com"),
std::move(options));
EXPECT_TRUE(received_bad_message());
ASSERT_THAT(cookies, testing::SizeIs(0));
}
TEST_F(RestrictedCookieManagerTest, GetAllForUrlPolicy) {
SetSessionCookie("cookie-name", "cookie-value", "example.com", "/");
// With default policy, should be able to get all cookies, even third-party.
{
auto options = mojom::CookieManagerGetOptions::New();
options->name = "cookie-name";
options->match_type = mojom::CookieMatchType::STARTS_WITH;
std::vector<net::CanonicalCookie> cookies = sync_service_->GetAllForUrl(
GURL("http://example.com/test/"), GURL("http://notexample.com"),
std::move(options));
ASSERT_THAT(cookies, testing::SizeIs(1));
EXPECT_EQ("cookie-name", cookies[0].Name());
EXPECT_EQ("cookie-value", cookies[0].Value());
}
// Disabing getting third-party cookies works correctly.
cookie_settings_.set_block_third_party_cookies(true);
{
auto options = mojom::CookieManagerGetOptions::New();
options->name = "cookie-name";
options->match_type = mojom::CookieMatchType::STARTS_WITH;
std::vector<net::CanonicalCookie> cookies = sync_service_->GetAllForUrl(
GURL("http://example.com/test/"), GURL("http://notexample.com"),
std::move(options));
ASSERT_THAT(cookies, testing::SizeIs(0));
}
}
TEST_F(RestrictedCookieManagerTest, SetCanonicalCookie) {
EXPECT_TRUE(sync_service_->SetCanonicalCookie(
net::CanonicalCookie(
"new-name", "new-value", "example.com", "/", base::Time(),
base::Time(), base::Time(), /* secure = */ false,
/* httponly = */ false, net::CookieSameSite::NO_RESTRICTION,
net::COOKIE_PRIORITY_DEFAULT),
GURL("http://example.com/test/"), GURL("http://example.com")));
auto options = mojom::CookieManagerGetOptions::New();
options->name = "new-name";
options->match_type = mojom::CookieMatchType::EQUALS;
std::vector<net::CanonicalCookie> cookies = sync_service_->GetAllForUrl(
GURL("http://example.com/test/"), GURL("http://example.com"),
std::move(options));
ASSERT_THAT(cookies, testing::SizeIs(1));
EXPECT_EQ("new-name", cookies[0].Name());
EXPECT_EQ("new-value", cookies[0].Value());
}
TEST_F(RestrictedCookieManagerTest, SetCanonicalCookieFromWrongOrigin) {
ExpectBadMessage();
EXPECT_FALSE(sync_service_->SetCanonicalCookie(
net::CanonicalCookie(
"new-name", "new-value", "not-example.com", "/", base::Time(),
base::Time(), base::Time(), /* secure = */ false,
/* httponly = */ false, net::CookieSameSite::NO_RESTRICTION,
net::COOKIE_PRIORITY_DEFAULT),
GURL("http://not-example.com/test/"), GURL("http://not-example.com")));
ASSERT_TRUE(received_bad_message());
}
TEST_F(RestrictedCookieManagerTest, SetCanonicalCookiePolicy) {
{
// With default settings object, setting a third-party cookie is OK.
auto cookie =
net::CanonicalCookie::Create(GURL("http://example.com"), "A=B",
base::Time::Now(), net::CookieOptions());
EXPECT_TRUE(sync_service_->SetCanonicalCookie(
*cookie, GURL("http://example.com"), GURL("http://notexample.com")));
}
{
// Not if third-party cookies are disabled, though.
cookie_settings_.set_block_third_party_cookies(true);
auto cookie =
net::CanonicalCookie::Create(GURL("http://example.com"), "A2=B2",
base::Time::Now(), net::CookieOptions());
EXPECT_FALSE(sync_service_->SetCanonicalCookie(
*cookie, GURL("http://example.com"), GURL("http://notexample.com")));
}
// Read back, in first-part context
auto options = mojom::CookieManagerGetOptions::New();
options->name = "A";
options->match_type = mojom::CookieMatchType::STARTS_WITH;
std::vector<net::CanonicalCookie> cookies = sync_service_->GetAllForUrl(
GURL("http://example.com/test/"), GURL("http://example.com/"),
std::move(options));
ASSERT_THAT(cookies, testing::SizeIs(1));
EXPECT_EQ("A", cookies[0].Name());
EXPECT_EQ("B", cookies[0].Value());
}
namespace {
// Stashes the cookie changes it receives, for testing.
class TestCookieChangeListener : public network::mojom::CookieChangeListener {
public:
// Records a cookie change received from RestrictedCookieManager.
struct Change {
Change(const net::CanonicalCookie& cookie,
network::mojom::CookieChangeCause change_cause)
: cookie(cookie), change_cause(change_cause) {}
net::CanonicalCookie cookie;
network::mojom::CookieChangeCause change_cause;
};
TestCookieChangeListener(network::mojom::CookieChangeListenerRequest request)
: binding_(this, std::move(request)) {}
~TestCookieChangeListener() override = default;
// Spin in a run loop until a change is received.
void WaitForChange() {
base::RunLoop loop;
run_loop_ = &loop;
loop.Run();
run_loop_ = nullptr;
}
// Changes received by this listener.
const std::vector<Change>& observed_changes() const {
return observed_changes_;
}
// network::mojom::CookieChangeListener
void OnCookieChange(const net::CanonicalCookie& cookie,
network::mojom::CookieChangeCause change_cause) override {
observed_changes_.emplace_back(cookie, change_cause);
if (run_loop_) // Set in WaitForChange().
run_loop_->Quit();
}
private:
std::vector<Change> observed_changes_;
mojo::Binding<network::mojom::CookieChangeListener> binding_;
// If not null, will be stopped when a cookie change notification is received.
base::RunLoop* run_loop_ = nullptr;
};
} // anonymous namespace
TEST_F(RestrictedCookieManagerTest, ChangeDispatch) {
network::mojom::CookieChangeListenerPtr listener_ptr;
network::mojom::CookieChangeListenerRequest request =
mojo::MakeRequest(&listener_ptr);
sync_service_->AddChangeListener(GURL("http://example.com/test/"),
GURL("http://example.com"),
std::move(listener_ptr));
TestCookieChangeListener listener(std::move(request));
ASSERT_THAT(listener.observed_changes(), testing::SizeIs(0));
SetSessionCookie("cookie-name", "cookie-value", "example.com", "/");
listener.WaitForChange();
ASSERT_THAT(listener.observed_changes(), testing::SizeIs(1));
EXPECT_EQ(network::mojom::CookieChangeCause::INSERTED,
listener.observed_changes()[0].change_cause);
EXPECT_EQ("cookie-name", listener.observed_changes()[0].cookie.Name());
EXPECT_EQ("cookie-value", listener.observed_changes()[0].cookie.Value());
}
TEST_F(RestrictedCookieManagerTest, AddChangeListenerFromWrongOrigin) {
network::mojom::CookieChangeListenerPtr bad_listener_ptr;
network::mojom::CookieChangeListenerRequest bad_request =
mojo::MakeRequest(&bad_listener_ptr);
ExpectBadMessage();
sync_service_->AddChangeListener(GURL("http://not-example.com/test/"),
GURL("http://not-example.com"),
std::move(bad_listener_ptr));
EXPECT_TRUE(received_bad_message());
TestCookieChangeListener bad_listener(std::move(bad_request));
network::mojom::CookieChangeListenerPtr good_listener_ptr;
network::mojom::CookieChangeListenerRequest good_request =
mojo::MakeRequest(&good_listener_ptr);
sync_service_->AddChangeListener(GURL("http://example.com/test/"),
GURL("http://example.com"),
std::move(good_listener_ptr));
TestCookieChangeListener good_listener(std::move(good_request));
ASSERT_THAT(bad_listener.observed_changes(), testing::SizeIs(0));
ASSERT_THAT(good_listener.observed_changes(), testing::SizeIs(0));
SetSessionCookie("other-cookie-name", "other-cookie-value", "not-example.com",
"/");
SetSessionCookie("cookie-name", "cookie-value", "example.com", "/");
good_listener.WaitForChange();
EXPECT_THAT(bad_listener.observed_changes(), testing::SizeIs(0));
ASSERT_THAT(good_listener.observed_changes(), testing::SizeIs(1));
EXPECT_EQ(network::mojom::CookieChangeCause::INSERTED,
good_listener.observed_changes()[0].change_cause);
EXPECT_EQ("cookie-name", good_listener.observed_changes()[0].cookie.Name());
EXPECT_EQ("cookie-value", good_listener.observed_changes()[0].cookie.Value());
}
} // namespace network