blob: c7e21ac690db625fd189313552b3c308af822916 [file] [log] [blame]
// Copyright 2018 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/cookie_store/cookie_store_manager.h"
#include <algorithm>
#include <utility>
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/optional.h"
#include "content/browser/cookie_store/cookie_change_subscriptions.pb.h"
#include "content/browser/service_worker/embedded_worker_status.h"
#include "content/browser/service_worker/service_worker_context_wrapper.h"
#include "content/browser/service_worker/service_worker_metrics.h"
#include "content/browser/service_worker/service_worker_registration.h"
#include "content/browser/service_worker/service_worker_version.h"
#include "content/public/browser/browser_context.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "third_party/blink/public/common/service_worker/service_worker_scope_match.h"
#include "third_party/blink/public/common/service_worker/service_worker_status_code.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_event_status.mojom.h"
#include "url/gurl.h"
namespace content {
namespace {
// ServiceWorkerStorage user data key for cookie change subscriptions.
const char kSubscriptionsUserKey[] = "cookie_store_subscriptions";
} // namespace
CookieStoreManager::CookieStoreManager(
scoped_refptr<ServiceWorkerContextWrapper> service_worker_context)
: service_worker_context_(std::move(service_worker_context)),
registration_user_data_key_(kSubscriptionsUserKey) {
service_worker_context_->AddObserver(this);
}
CookieStoreManager::~CookieStoreManager() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
service_worker_context_->RemoveObserver(this);
}
void CookieStoreManager::CreateService(
mojo::PendingReceiver<blink::mojom::CookieStore> receiver,
const url::Origin& origin) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
receivers_.Add(std::make_unique<CookieStoreHost>(this, origin),
std::move(receiver));
}
void CookieStoreManager::LoadAllSubscriptions(
base::OnceCallback<void(bool)> callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!done_loading_subscriptions_) << __func__ << " already called";
service_worker_context_->GetUserDataForAllRegistrations(
registration_user_data_key_,
base::BindOnce(&CookieStoreManager::ProcessOnDiskSubscriptions,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void CookieStoreManager::ListenToCookieChanges(
mojo::PendingRemote<::network::mojom::CookieManager> cookie_manager,
base::OnceCallback<void(bool)> callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!cookie_manager_) << __func__ << " already called";
cookie_manager_.Bind(std::move(cookie_manager));
DCHECK(!cookie_change_listener_receiver_.is_bound());
// TODO(pwnall): Switch to an API with subscription confirmation.
cookie_manager_->AddGlobalChangeListener(
cookie_change_listener_receiver_.BindNewPipeAndPassRemote());
std::move(callback).Run(true);
}
void CookieStoreManager::ProcessOnDiskSubscriptions(
base::OnceCallback<void(bool)> load_callback,
const std::vector<std::pair<int64_t, std::string>>& user_data,
blink::ServiceWorkerStatusCode status) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!done_loading_subscriptions_) << __func__ << " already called";
done_loading_subscriptions_ = true;
if (status != blink::ServiceWorkerStatusCode::kOk) {
DidLoadAllSubscriptions(false, std::move(load_callback));
return;
}
DCHECK(subscriptions_by_registration_.empty());
subscriptions_by_registration_.reserve(user_data.size());
bool load_success = true;
for (const auto& pair : user_data) {
int64_t service_worker_registration_id = pair.first;
const std::string& proto_string = pair.second;
std::vector<std::unique_ptr<CookieChangeSubscription>> subscriptions =
CookieChangeSubscription::DeserializeVector(
proto_string, service_worker_registration_id);
if (subscriptions.empty()) {
load_success = false;
continue;
}
ActivateSubscriptions(subscriptions);
DCHECK(
!subscriptions_by_registration_.count(service_worker_registration_id));
subscriptions_by_registration_.emplace(
std::move(service_worker_registration_id), std::move(subscriptions));
}
DidLoadAllSubscriptions(load_success, std::move(load_callback));
}
void CookieStoreManager::DidLoadAllSubscriptions(
bool succeeded,
base::OnceCallback<void(bool)> load_callback) {
DCHECK(done_loading_subscriptions_);
succeeded_loading_subscriptions_ = succeeded;
for (auto& callback : subscriptions_loaded_callbacks_)
std::move(callback).Run();
subscriptions_loaded_callbacks_.clear();
std::move(load_callback).Run(succeeded);
}
void CookieStoreManager::AddSubscriptions(
int64_t service_worker_registration_id,
const url::Origin& origin,
std::vector<blink::mojom::CookieChangeSubscriptionPtr> mojo_subscriptions,
mojo::ReportBadMessageCallback bad_message_callback,
blink::mojom::CookieStore::AddSubscriptionsCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!done_loading_subscriptions_) {
subscriptions_loaded_callbacks_.push_back(base::BindOnce(
&CookieStoreManager::AddSubscriptions, weak_factory_.GetWeakPtr(),
service_worker_registration_id, origin, std::move(mojo_subscriptions),
std::move(bad_message_callback), std::move(callback)));
return;
}
if (!succeeded_loading_subscriptions_) {
std::move(callback).Run(false);
return;
}
// GetLiveRegistration() is sufficient here, as opposed to a flavor of
// FindRegistration(), because we know the registration must be alive.
//
// blink::CookieStoreManager calls AddSubscription() and stays alive until the
// async call completes. blink::CookieStoreManager hangs onto the Blink side
// of the Service Worker's registration. So, the registration will be live if
// the call's result is received.
ServiceWorkerRegistration* service_worker_registration =
service_worker_context_->GetLiveRegistration(
service_worker_registration_id);
// If the calling blink::CookieStoreManager instance goes away (for example,
// it had to wait for the database load to complete, and that took too long),
// the result of this call won't be received, so it's acceptable to fail it.
if (!service_worker_registration ||
!service_worker_registration->active_version()) {
std::move(callback).Run(false);
return;
}
if (!origin.IsSameOriginWith(
url::Origin::Create(service_worker_registration->scope()))) {
std::move(bad_message_callback).Run("Invalid service worker");
std::move(callback).Run(false);
return;
}
// The empty set is special-cased because the code below assumes that the
// registration's list of subscriptions will end up non-empty.
if (mojo_subscriptions.empty()) {
std::move(callback).Run(true);
return;
}
for (const auto& mojo_subscription : mojo_subscriptions) {
if (!blink::ServiceWorkerScopeMatches(service_worker_registration->scope(),
mojo_subscription->url)) {
// Blink should have validated subscription URLs against the service
// worker registration scope. A mismatch here means that the renderer was
// compromised.
std::move(bad_message_callback).Run("Invalid subscription URL");
std::move(callback).Run(false);
return;
}
}
// If the registration does not exist in the map, the default std::vector()
// constructor is used to create a new entry. The constructor produces an
// empty vector, which is exactly what is needed here.
std::vector<std::unique_ptr<CookieChangeSubscription>>& subscriptions =
subscriptions_by_registration_[service_worker_registration_id];
// New subscriptions will be appended past the current vector end.
size_t old_subscriptions_size = subscriptions.size();
// The loop consumes the mojo subscriptions, so it can build
// CookieChangeSubscriptions more efficiently.
for (auto& mojo_subscription : mojo_subscriptions) {
auto new_subscription = std::make_unique<CookieChangeSubscription>(
std::move(mojo_subscription), service_worker_registration->id());
auto existing_subscription_it = std::find_if(
subscriptions.begin(), subscriptions.end(),
[&](const std::unique_ptr<CookieChangeSubscription>& other) -> bool {
return *new_subscription == *other;
});
if (existing_subscription_it == subscriptions.end())
subscriptions.push_back(std::move(new_subscription));
}
ActivateSubscriptions(
base::make_span(subscriptions).subspan(old_subscriptions_size));
StoreSubscriptions(service_worker_registration_id, origin.GetURL(),
subscriptions, std::move(callback));
}
void CookieStoreManager::RemoveSubscriptions(
int64_t service_worker_registration_id,
const url::Origin& origin,
std::vector<blink::mojom::CookieChangeSubscriptionPtr> mojo_subscriptions,
mojo::ReportBadMessageCallback bad_message_callback,
blink::mojom::CookieStore::RemoveSubscriptionsCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!done_loading_subscriptions_) {
subscriptions_loaded_callbacks_.push_back(base::BindOnce(
&CookieStoreManager::RemoveSubscriptions, weak_factory_.GetWeakPtr(),
service_worker_registration_id, origin, std::move(mojo_subscriptions),
std::move(bad_message_callback), std::move(callback)));
return;
}
if (!succeeded_loading_subscriptions_) {
std::move(callback).Run(false);
return;
}
// GetLiveRegistration() is sufficient here, as opposed to a flavor of
// FindRegistration(), because we know the registration must be alive.
//
// blink::CookieStoreManager calls AddSubscription() and stays alive until the
// async call completes. blink::CookieStoreManager hangs onto the Blink side
// of the Service Worker's registration. So, the registration will be live if
// the call's result is received.
ServiceWorkerRegistration* service_worker_registration =
service_worker_context_->GetLiveRegistration(
service_worker_registration_id);
// If the calling blink::CookieStoreManager instance goes away (for example,
// it had to wait for the database load to complete, and that took too long),
// the result of this call won't be received, so it's acceptable to fail it.
if (!service_worker_registration ||
!service_worker_registration->active_version()) {
std::move(callback).Run(false);
return;
}
if (!origin.IsSameOriginWith(
url::Origin::Create(service_worker_registration->scope()))) {
std::move(bad_message_callback).Run("Invalid service worker");
std::move(callback).Run(false);
return;
}
std::vector<std::unique_ptr<CookieChangeSubscription>> target_subscriptions;
target_subscriptions.reserve(mojo_subscriptions.size());
// The loop consumes the mojo subscriptions, so it can build
// CookieChangeSubscriptions more efficiently.
for (auto& mojo_subscription : mojo_subscriptions) {
// This method does not need to check the subscription's URL against the
// service worker registration's scope. AddSubscription() checks
// subscription URLs, so the registration does not have any subscriptions
// with invalid URLs. If a compromised renderer attempts to remove a
// subscription with an invalid URL, no such subscription will be found, and
// this method will be a noop.
target_subscriptions.push_back(std::make_unique<CookieChangeSubscription>(
std::move(mojo_subscription), service_worker_registration->id()));
}
auto all_subscriptions_it =
subscriptions_by_registration_.find(service_worker_registration_id);
if (all_subscriptions_it == subscriptions_by_registration_.end()) {
// Nothing to remove.
std::move(callback).Run(true);
return;
}
std::vector<std::unique_ptr<CookieChangeSubscription>>& all_subscriptions =
all_subscriptions_it->second;
std::vector<std::unique_ptr<CookieChangeSubscription>> removed_subscriptions;
removed_subscriptions.reserve(target_subscriptions.size());
std::vector<std::unique_ptr<CookieChangeSubscription>> live_subscriptions;
// Assume that the application is tracking its subscriptions carefully and
// each removal will succeed. If the assumption holds, no vector reallocation
// will be needed.
if (all_subscriptions.size() > target_subscriptions.size()) {
live_subscriptions.reserve(all_subscriptions.size() -
target_subscriptions.size());
}
for (auto& subscription : all_subscriptions) {
auto target_subscription_it = std::find_if(
target_subscriptions.begin(), target_subscriptions.end(),
[&](const std::unique_ptr<CookieChangeSubscription>& other) -> bool {
return *subscription == *other;
});
if (target_subscription_it == target_subscriptions.end()) {
// The subscription is not marked for deletion.
live_subscriptions.push_back(std::move(subscription));
} else {
DCHECK(**target_subscription_it == *subscription);
removed_subscriptions.push_back(std::move(subscription));
}
}
DeactivateSubscriptions(removed_subscriptions);
// StoreSubscriptions() needs to be called before updating
// |subscriptions_by_registration_|, because the update may delete the vector
// holding the subscriptions.
StoreSubscriptions(service_worker_registration_id, origin.GetURL(),
live_subscriptions, std::move(callback));
if (live_subscriptions.empty()) {
subscriptions_by_registration_.erase(all_subscriptions_it);
} else {
all_subscriptions = std::move(live_subscriptions);
}
}
void CookieStoreManager::GetSubscriptions(
int64_t service_worker_registration_id,
const url::Origin& origin,
mojo::ReportBadMessageCallback bad_message_callback,
blink::mojom::CookieStore::GetSubscriptionsCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!done_loading_subscriptions_) {
subscriptions_loaded_callbacks_.push_back(base::BindOnce(
&CookieStoreManager::GetSubscriptions, weak_factory_.GetWeakPtr(),
service_worker_registration_id, origin, std::move(bad_message_callback),
std::move(callback)));
return;
}
if (!succeeded_loading_subscriptions_) {
std::move(callback).Run(
std::vector<blink::mojom::CookieChangeSubscriptionPtr>(), false);
return;
}
auto it = subscriptions_by_registration_.find(service_worker_registration_id);
if (it == subscriptions_by_registration_.end() || it->second.empty()) {
std::move(callback).Run(
std::vector<blink::mojom::CookieChangeSubscriptionPtr>(), true);
return;
}
const url::Origin first_origin = url::Origin::Create(it->second[0]->url());
#if DCHECK_IS_ON()
for (const auto& subscription : it->second) {
DCHECK(
first_origin.IsSameOriginWith(url::Origin::Create(subscription->url())))
<< "Service worker's change subscriptions don't have the same origin";
}
#endif // DCHECK_IS_ON()
if (!origin.IsSameOriginWith(first_origin)) {
std::move(bad_message_callback).Run("Invalid service worker");
std::move(callback).Run(
std::vector<blink::mojom::CookieChangeSubscriptionPtr>(), false);
return;
}
std::move(callback).Run(CookieChangeSubscription::ToMojoVector(it->second),
true);
}
void CookieStoreManager::StoreSubscriptions(
int64_t service_worker_registration_id,
const GURL& service_worker_origin,
const std::vector<std::unique_ptr<CookieChangeSubscription>>& subscriptions,
base::OnceCallback<void(bool)> callback) {
if (subscriptions.empty()) {
service_worker_context_->ClearRegistrationUserData(
service_worker_registration_id, {registration_user_data_key_},
base::BindOnce(
[](base::OnceCallback<void(bool)> callback,
blink::ServiceWorkerStatusCode status) {
std::move(callback).Run(status ==
blink::ServiceWorkerStatusCode::kOk);
},
std::move(callback)));
return;
}
std::string subscriptions_data =
CookieChangeSubscription::SerializeVector(subscriptions);
DCHECK(!subscriptions_data.empty())
<< "Failed to create cookie change subscriptions protobuf";
service_worker_context_->StoreRegistrationUserData(
service_worker_registration_id,
url::Origin::Create(service_worker_origin),
std::vector<std::pair<std::string, std::string>>(
{{registration_user_data_key_, subscriptions_data}}),
base::BindOnce(
[](base::OnceCallback<void(bool)> callback,
blink::ServiceWorkerStatusCode status) {
std::move(callback).Run(status ==
blink::ServiceWorkerStatusCode::kOk);
},
std::move(callback)));
}
void CookieStoreManager::OnRegistrationDeleted(
int64_t service_worker_registration_id,
const GURL& pattern) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Waiting for the on-disk subscriptions to be loaded ensures that the
// registration's subscriptions are removed. Without waiting, there's a risk
// that a registration's subscriptions will finish loading (and thus remain
// active) right after this function runs.
if (!done_loading_subscriptions_) {
subscriptions_loaded_callbacks_.push_back(base::BindOnce(
&CookieStoreManager::OnRegistrationDeleted, weak_factory_.GetWeakPtr(),
service_worker_registration_id, pattern));
return;
}
auto it = subscriptions_by_registration_.find(service_worker_registration_id);
if (it == subscriptions_by_registration_.end())
return;
DeactivateSubscriptions(it->second);
subscriptions_by_registration_.erase(it);
}
void CookieStoreManager::ActivateSubscriptions(
base::span<const std::unique_ptr<CookieChangeSubscription>> subscriptions) {
if (subscriptions.empty())
return;
// Service workers can only observe changes to cookies for URLs under their
// scope. This means all the URLs that the worker is observing must map to the
// same domain key (eTLD+1).
//
// TODO(pwnall): This is the same as implementation as
// net::CookieMonsterChangeDispatcher::DomainKey. Extract that
// implementation into net/cookies.cookie_util.h and call it.
std::string url_key = net::registry_controlled_domains::GetDomainAndRegistry(
subscriptions[0]->url(),
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
base::LinkedList<CookieChangeSubscription>& url_key_subscriptions_list =
subscriptions_by_url_key_[url_key];
for (auto& subscription : subscriptions) {
DCHECK(!subscription->next() && !subscription->previous())
<< "Subscription passed to " << __func__ << " already activated";
DCHECK_EQ(url_key,
net::registry_controlled_domains::GetDomainAndRegistry(
subscription->url(),
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES))
<< __func__ << " subscriptions belong to different registrations";
url_key_subscriptions_list.Append(subscription.get());
}
}
void CookieStoreManager::DeactivateSubscriptions(
base::span<const std::unique_ptr<CookieChangeSubscription>> subscriptions) {
if (subscriptions.empty())
return;
// Service workers can only observe changes to cookies for URLs under their
// scope. This means all the URLs that the worker is observing must map to the
// same domain key (eTLD+1).
//
// TODO(pwnall): This has the same implementation as
// net::CookieMonsterChangeDispatcher::DomainKey. Extract that
// implementation into net/cookies.cookie_util.h and call it.
std::string url_key = net::registry_controlled_domains::GetDomainAndRegistry(
subscriptions[0]->url(),
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
for (auto& subscription : subscriptions) {
DCHECK(subscription->next() && subscription->previous())
<< "Subscription passed to " << __func__ << " not previously activated";
DCHECK_EQ(url_key,
net::registry_controlled_domains::GetDomainAndRegistry(
subscription->url(),
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES))
<< __func__ << " subscriptions belong to different registrations";
subscription->RemoveFromList();
}
auto it = subscriptions_by_url_key_.find(url_key);
DCHECK(it != subscriptions_by_url_key_.end());
if (it->second.empty())
subscriptions_by_url_key_.erase(it);
}
void CookieStoreManager::OnStorageWiped() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Waiting for the on-disk subscriptions to be loaded ensures that all
// subscriptions are removed. Without waiting, there's a risk that some
// subscriptions will finish loading (and thus remain active) after this
// function runs.
if (!done_loading_subscriptions_) {
subscriptions_loaded_callbacks_.push_back(base::BindOnce(
&CookieStoreManager::OnStorageWiped, weak_factory_.GetWeakPtr()));
return;
}
subscriptions_by_url_key_.clear();
subscriptions_by_registration_.clear();
}
void CookieStoreManager::OnCookieChange(const net::CookieChangeInfo& change) {
// Waiting for on-disk subscriptions to be loaded ensures that changes are
// delivered to all service workers that subscribed to them in previous
// browser sessions. Without waiting, workers might miss cookie changes.
if (!done_loading_subscriptions_) {
subscriptions_loaded_callbacks_.push_back(
base::BindOnce(&CookieStoreManager::OnCookieChange,
weak_factory_.GetWeakPtr(), change));
return;
}
if (change.cause == net::CookieChangeCause::OVERWRITE) {
// Cookie overwrites generate an OVERWRITE event with the old cookie data
// and an INSERTED event with the new cookie data. The Cookie Store API
// only reports new cookie information, so OVERWRITE events doesn't need to
// be dispatched to service workers.
return;
}
// Compute the list of service workers interested in this change. A worker
// might have multiple subscriptions that cover this change, but should still
// receive a single change event.
// TODO(pwnall): This has same as implementation as
// net::CookieMonsterChangeDispatcher::DomainKey. Extract that
// implementation into net/cookies.cookie_util.h and call it.
std::string url_key = net::registry_controlled_domains::GetDomainAndRegistry(
change.cookie.Domain(),
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
auto it = subscriptions_by_url_key_.find(url_key);
if (it == subscriptions_by_url_key_.end())
return;
std::set<int64_t> interested_registration_ids;
const base::LinkedList<CookieChangeSubscription>& subscriptions = it->second;
for (const base::LinkNode<CookieChangeSubscription>* node =
subscriptions.head();
node != subscriptions.end(); node = node->next()) {
const CookieChangeSubscription* subscription = node->value();
if (subscription->ShouldObserveChangeTo(
change.cookie, change.access_result.access_semantics)) {
interested_registration_ids.insert(
subscription->service_worker_registration_id());
}
}
// Dispatch the change to interested workers.
for (int64_t registration_id : interested_registration_ids) {
service_worker_context_->FindReadyRegistrationForIdOnly(
registration_id,
base::BindOnce(
[](base::WeakPtr<CookieStoreManager> manager,
const net::CookieChangeInfo& change,
blink::ServiceWorkerStatusCode find_status,
scoped_refptr<ServiceWorkerRegistration> registration) {
if (find_status != blink::ServiceWorkerStatusCode::kOk)
return;
DCHECK(registration);
if (!manager)
return;
manager->DispatchChangeEvent(std::move(registration), change);
},
weak_factory_.GetWeakPtr(), change));
}
}
void CookieStoreManager::DispatchChangeEvent(
scoped_refptr<ServiceWorkerRegistration> registration,
const net::CookieChangeInfo& change) {
scoped_refptr<ServiceWorkerVersion> active_version =
registration->active_version();
if (active_version->running_status() != EmbeddedWorkerStatus::RUNNING) {
active_version->RunAfterStartWorker(
ServiceWorkerMetrics::EventType::COOKIE_CHANGE,
base::BindOnce(&CookieStoreManager::DidStartWorkerForChangeEvent,
weak_factory_.GetWeakPtr(), std::move(registration),
change));
return;
}
int request_id = active_version->StartRequest(
ServiceWorkerMetrics::EventType::COOKIE_CHANGE, base::DoNothing());
active_version->endpoint()->DispatchCookieChangeEvent(
change, active_version->CreateSimpleEventCallback(request_id));
}
void CookieStoreManager::DidStartWorkerForChangeEvent(
scoped_refptr<ServiceWorkerRegistration> registration,
const net::CookieChangeInfo& change,
blink::ServiceWorkerStatusCode start_worker_status) {
if (start_worker_status != blink::ServiceWorkerStatusCode::kOk)
return;
DispatchChangeEvent(std::move(registration), change);
}
} // namespace content