diff options
Diffstat (limited to 'chromium/content/browser/cookie_store/cookie_store_manager.cc')
-rw-r--r-- | chromium/content/browser/cookie_store/cookie_store_manager.cc | 530 |
1 files changed, 530 insertions, 0 deletions
diff --git a/chromium/content/browser/cookie_store/cookie_store_manager.cc b/chromium/content/browser/cookie_store/cookie_store_manager.cc new file mode 100644 index 00000000000..8e08a32ed55 --- /dev/null +++ b/chromium/content/browser/cookie_store/cookie_store_manager.cc @@ -0,0 +1,530 @@ +// 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 <utility> + +#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/common/service_worker/service_worker_status_code.h" +#include "content/public/browser/browser_context.h" +#include "net/base/registry_controlled_domains/registry_controlled_domain.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"; + +// Handles the result of ServiceWorkerContextWrapper::StoreRegistrationUserData. +void HandleStoreRegistrationUserDataStatus(ServiceWorkerStatusCode status) { + // The current implementation does not have a good way to handle errors in + // StoreRegistrationUserData. Cookie change subscriptions have been added to + // the registration during the install event, so it's too late to surface the + // error to the renderer. The registration has already been persisted, and the + // Service Worker is likely active by now. + DLOG_IF(ERROR, status != SERVICE_WORKER_OK) + << "StoreRegistrationUserData failed"; +} + +} // namespace + +CookieStoreManager::CookieStoreManager( + scoped_refptr<ServiceWorkerContextWrapper> service_worker_context) + : service_worker_context_(std::move(service_worker_context)), + cookie_change_listener_binding_(this), + registration_user_data_key_(kSubscriptionsUserKey), + weak_factory_(this) { + service_worker_context_->AddObserver(this); +} + +CookieStoreManager::~CookieStoreManager() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + service_worker_context_->RemoveObserver(this); +} + +void CookieStoreManager::CreateService(blink::mojom::CookieStoreRequest request, + const url::Origin& origin) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + bindings_.AddBinding(std::make_unique<CookieStoreHost>(this, origin), + std::move(request)); +} + +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( + ::network::mojom::CookieManagerPtr cookie_manager, + base::OnceCallback<void(bool)> callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + DCHECK(!cookie_manager_) << __func__ << " already called"; + cookie_manager_ = std::move(cookie_manager); + + DCHECK(!cookie_change_listener_binding_.is_bound()); + ::network::mojom::CookieChangeListenerPtr cookie_change_listener; + cookie_change_listener_binding_.Bind( + mojo::MakeRequest(&cookie_change_listener)); + + // TODO(pwnall): Switch to an API with subscription confirmation. + cookie_manager_->AddGlobalChangeListener(std::move(cookie_change_listener)); + 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, + ServiceWorkerStatusCode status) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + DCHECK(!done_loading_subscriptions_) << __func__ << " already called"; + done_loading_subscriptions_ = true; + + if (status != SERVICE_WORKER_OK) { + 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; + + base::Optional<std::vector<CookieChangeSubscription>> subscriptions_opt = + CookieChangeSubscription::DeserializeVector( + proto_string, service_worker_registration_id); + if (!subscriptions_opt.has_value()) { + load_success = false; + continue; + } + + ActivateSubscriptions(&subscriptions_opt.value()); + DCHECK( + !subscriptions_by_registration_.count(service_worker_registration_id)); + subscriptions_by_registration_.emplace( + std::move(service_worker_registration_id), + std::move(subscriptions_opt).value()); + } + + 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::AppendSubscriptions( + int64_t service_worker_registration_id, + const url::Origin& origin, + std::vector<blink::mojom::CookieChangeSubscriptionPtr> mojo_subscriptions, + blink::mojom::CookieStore::AppendSubscriptionsCallback callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!done_loading_subscriptions_) { + subscriptions_loaded_callbacks_.emplace_back(base::BindOnce( + &CookieStoreManager::AppendSubscriptions, weak_factory_.GetWeakPtr(), + service_worker_registration_id, origin, std::move(mojo_subscriptions), + 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 AppendSubscriptions is only called from the + // implementation of the Cookie Store API, which is exposed to + // ServiceWorkerGlobalScope. ServiceWorkerGlobalScope references the + // service worker's registration via a ServiceWorkerRegistration JavaScript + // object, so the registration is guaranteed to be live while the service + // worker is executing. + // + // It is possible for the service worker to get killed while this API call is + // in progress, for example, if the service worker code exceeds an event + // handling time limit. In that case, the return value will not be observed, + // so a false negative is acceptable. + ServiceWorkerRegistration* service_worker_registration = + service_worker_context_->GetLiveRegistration( + service_worker_registration_id); + if (!service_worker_registration || + !origin.IsSameOriginWith( + url::Origin::Create(service_worker_registration->pattern()))) { + // This error case is a good fit for mojo::ReportBadMessage(), because the + // renderer has passed an invalid registration ID. However, the code here + // might run without a mojo call context, if the original call was delayed + // while loading on-disk subscription data. + // + // While it would be possible to have two code paths for the two situations, + // the extra complexity doesn't seem warranted for the limited debuggig + // benefits provided by mojo::ReportBadMessage. + std::move(callback).Run(false); + return; + } + + // TODO(crbug.com/843079): This check incorrectly allows an active service + // worker version to call the API, if another version + // is installing at the same time. + if (!service_worker_registration->installing_version()) { + // A service worker's cookie change subscriptions can only be modified while + // the service worker's install event is handled. + std::move(callback).Run(false); + return; + } + + if (mojo_subscriptions.empty()) { + // Empty subscriptions are special-cased so we never have to serialize an + // empty array of subscriptions. This is advantageous because the protobuf + // serialization of an empty array is the empty string, which is also used + // by the convenience protobuf serialization API to signal serialization + // failure. So, supporting serializing an empty array would mean we can't + // use the convenience serialization API. + std::move(callback).Run(true); + return; + } + + std::vector<CookieChangeSubscription> new_subscriptions = + CookieChangeSubscription::FromMojoVector( + std::move(mojo_subscriptions), service_worker_registration->id()); + DCHECK(!new_subscriptions.empty()); + + auto old_subscriptions_it = + subscriptions_by_registration_.find(service_worker_registration_id); + if (old_subscriptions_it == subscriptions_by_registration_.end()) { + subscriptions_by_registration_.emplace(service_worker_registration_id, + std::move(new_subscriptions)); + std::move(callback).Run(true); + return; + } + + std::vector<CookieChangeSubscription>& old_subscriptions = + old_subscriptions_it->second; + old_subscriptions.reserve(old_subscriptions.size() + + new_subscriptions.size()); + for (auto& new_subscription : new_subscriptions) + old_subscriptions.emplace_back(std::move(new_subscription)); + + std::move(callback).Run(true); +} + +void CookieStoreManager::GetSubscriptions( + int64_t service_worker_registration_id, + const url::Origin& origin, + blink::mojom::CookieStore::GetSubscriptionsCallback callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!done_loading_subscriptions_) { + subscriptions_loaded_callbacks_.emplace_back(base::BindOnce( + &CookieStoreManager::GetSubscriptions, weak_factory_.GetWeakPtr(), + service_worker_registration_id, origin, 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)) { + // This error case is a good fit for mojo::ReportBadMessage(), because the + // renderer has passed an invalid registration ID. However, the code here + // might run without a mojo call context, if the original call was delayed + // while loading on-disk subscription data. + // + // While it would be possible to have two code paths for the two situations, + // the extra complexity doesn't seem warranted for the limited debuggig + // benefits provided by mojo::ReportBadMessage. + std::move(callback).Run( + std::vector<blink::mojom::CookieChangeSubscriptionPtr>(), false); + return; + } + + std::move(callback).Run(CookieChangeSubscription::ToMojoVector(it->second), + true); +} + +void CookieStoreManager::OnNewLiveRegistration( + int64_t service_worker_registration_id, + const GURL& pattern) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); +} + +void CookieStoreManager::OnRegistrationStored( + 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 aren't activated twice. Without waiting, + // there's a risk that LoadAllSubscriptions() sees the result of the + // StoreRegistrationUserData() call below. + if (!done_loading_subscriptions_) { + subscriptions_loaded_callbacks_.emplace_back(base::BindOnce( + &CookieStoreManager::OnRegistrationStored, 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; + + ActivateSubscriptions(&it->second); + + std::string subscriptions_data = + CookieChangeSubscription::SerializeVector(it->second); + DCHECK(!subscriptions_data.empty()) + << "Failed to create cookie change subscriptions protobuf"; + + service_worker_context_->StoreRegistrationUserData( + service_worker_registration_id, pattern.GetOrigin(), + std::vector<std::pair<std::string, std::string>>( + {{registration_user_data_key_, subscriptions_data}}), + base::BindOnce(&HandleStoreRegistrationUserDataStatus)); +} + +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_.emplace_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( + std::vector<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); + } +} + +void CookieStoreManager::DeactivateSubscriptions( + std::vector<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_.emplace_back(base::BindOnce( + &CookieStoreManager::OnStorageWiped, weak_factory_.GetWeakPtr())); + return; + } + + subscriptions_by_url_key_.clear(); + subscriptions_by_registration_.clear(); +} + +void CookieStoreManager::OnCookieChange( + const net::CanonicalCookie& cookie, + ::network::mojom::CookieChangeCause cause) { + // 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_.emplace_back( + base::BindOnce(&CookieStoreManager::OnCookieChange, + weak_factory_.GetWeakPtr(), cookie, cause)); + 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( + 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(cookie)) { + 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::CanonicalCookie& cookie, + ::network::mojom::CookieChangeCause cause, + ServiceWorkerStatusCode find_status, + scoped_refptr<ServiceWorkerRegistration> registration) { + if (find_status != SERVICE_WORKER_OK) + return; + + DCHECK(registration); + if (!manager) + return; + manager->DispatchChangeEvent(std::move(registration), cookie, + cause); + }, + weak_factory_.GetWeakPtr(), cookie, cause)); + } +} + +void CookieStoreManager::DispatchChangeEvent( + scoped_refptr<ServiceWorkerRegistration> registration, + const net::CanonicalCookie& cookie, + ::network::mojom::CookieChangeCause cause) { + 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), + cookie, cause)); + return; + } + + int request_id = active_version->StartRequest( + ServiceWorkerMetrics::EventType::COOKIE_CHANGE, base::DoNothing()); + + active_version->event_dispatcher()->DispatchCookieChangeEvent( + cookie, cause, active_version->CreateSimpleEventCallback(request_id)); +} + +void CookieStoreManager::DidStartWorkerForChangeEvent( + scoped_refptr<ServiceWorkerRegistration> registration, + const net::CanonicalCookie& cookie, + ::network::mojom::CookieChangeCause cause, + ServiceWorkerStatusCode start_worker_status) { + if (start_worker_status != SERVICE_WORKER_OK) + return; + DispatchChangeEvent(std::move(registration), cookie, cause); +} + +} // namespace content |