2446 lines
94 KiB
C++
2446 lines
94 KiB
C++
|
|
// Copyright 2012 The Chromium Authors
|
||
|
|
// Use of this source code is governed by a BSD-style license that can be
|
||
|
|
// found in the LICENSE file.
|
||
|
|
|
||
|
|
// Portions of this code based on Mozilla:
|
||
|
|
// (netwerk/cookie/src/nsCookieService.cpp)
|
||
|
|
/* ***** BEGIN LICENSE BLOCK *****
|
||
|
|
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
|
||
|
|
*
|
||
|
|
* The contents of this file are subject to the Mozilla Public License Version
|
||
|
|
* 1.1 (the "License"); you may not use this file except in compliance with
|
||
|
|
* the License. You may obtain a copy of the License at
|
||
|
|
* http://www.mozilla.org/MPL/
|
||
|
|
*
|
||
|
|
* Software distributed under the License is distributed on an "AS IS" basis,
|
||
|
|
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
|
||
|
|
* for the specific language governing rights and limitations under the
|
||
|
|
* License.
|
||
|
|
*
|
||
|
|
* The Original Code is mozilla.org code.
|
||
|
|
*
|
||
|
|
* The Initial Developer of the Original Code is
|
||
|
|
* Netscape Communications Corporation.
|
||
|
|
* Portions created by the Initial Developer are Copyright (C) 2003
|
||
|
|
* the Initial Developer. All Rights Reserved.
|
||
|
|
*
|
||
|
|
* Contributor(s):
|
||
|
|
* Daniel Witte (dwitte@stanford.edu)
|
||
|
|
* Michiel van Leeuwen (mvl@exedo.nl)
|
||
|
|
*
|
||
|
|
* Alternatively, the contents of this file may be used under the terms of
|
||
|
|
* either the GNU General Public License Version 2 or later (the "GPL"), or
|
||
|
|
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
|
||
|
|
* in which case the provisions of the GPL or the LGPL are applicable instead
|
||
|
|
* of those above. If you wish to allow use of your version of this file only
|
||
|
|
* under the terms of either the GPL or the LGPL, and not to allow others to
|
||
|
|
* use your version of this file under the terms of the MPL, indicate your
|
||
|
|
* decision by deleting the provisions above and replace them with the notice
|
||
|
|
* and other provisions required by the GPL or the LGPL. If you do not delete
|
||
|
|
* the provisions above, a recipient may use your version of this file under
|
||
|
|
* the terms of any one of the MPL, the GPL or the LGPL.
|
||
|
|
*
|
||
|
|
* ***** END LICENSE BLOCK ***** */
|
||
|
|
|
||
|
|
#include "net/cookies/cookie_monster.h"
|
||
|
|
|
||
|
|
#include <functional>
|
||
|
|
#include <numeric>
|
||
|
|
#include <set>
|
||
|
|
#include <utility>
|
||
|
|
|
||
|
|
#include "base/containers/flat_map.h"
|
||
|
|
#include "base/feature_list.h"
|
||
|
|
#include "base/functional/bind.h"
|
||
|
|
#include "base/functional/callback.h"
|
||
|
|
#include "base/location.h"
|
||
|
|
#include "base/logging.h"
|
||
|
|
#include "base/metrics/field_trial.h"
|
||
|
|
#include "base/metrics/histogram_functions.h"
|
||
|
|
#include "base/metrics/histogram_macros.h"
|
||
|
|
#include "base/ranges/algorithm.h"
|
||
|
|
#include "base/strings/strcat.h"
|
||
|
|
#include "base/strings/string_piece.h"
|
||
|
|
#include "base/strings/string_util.h"
|
||
|
|
#include "base/strings/stringprintf.h"
|
||
|
|
#include "base/task/single_thread_task_runner.h"
|
||
|
|
#include "base/threading/thread_checker.h"
|
||
|
|
#include "net/base/features.h"
|
||
|
|
#include "net/base/isolation_info.h"
|
||
|
|
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
|
||
|
|
#include "net/base/schemeful_site.h"
|
||
|
|
#include "net/base/url_util.h"
|
||
|
|
#include "net/cookies/canonical_cookie.h"
|
||
|
|
#include "net/cookies/cookie_constants.h"
|
||
|
|
#include "net/cookies/cookie_monster_change_dispatcher.h"
|
||
|
|
#include "net/cookies/cookie_monster_netlog_params.h"
|
||
|
|
#include "net/cookies/cookie_partition_key.h"
|
||
|
|
#include "net/cookies/cookie_partition_key_collection.h"
|
||
|
|
#include "net/cookies/cookie_util.h"
|
||
|
|
#include "net/cookies/parsed_cookie.h"
|
||
|
|
#include "net/http/http_util.h"
|
||
|
|
#include "net/log/net_log.h"
|
||
|
|
#include "net/log/net_log_values.h"
|
||
|
|
#include "third_party/abseil-cpp/absl/types/optional.h"
|
||
|
|
#include "url/origin.h"
|
||
|
|
#include "url/third_party/mozilla/url_parse.h"
|
||
|
|
#include "url/url_canon.h"
|
||
|
|
#include "url/url_constants.h"
|
||
|
|
|
||
|
|
using base::Time;
|
||
|
|
using base::TimeTicks;
|
||
|
|
using TimeRange = net::CookieDeletionInfo::TimeRange;
|
||
|
|
|
||
|
|
// In steady state, most cookie requests can be satisfied by the in memory
|
||
|
|
// cookie monster store. If the cookie request cannot be satisfied by the in
|
||
|
|
// memory store, the relevant cookies must be fetched from the persistent
|
||
|
|
// store. The task is queued in CookieMonster::tasks_pending_ if it requires
|
||
|
|
// all cookies to be loaded from the backend, or tasks_pending_for_key_ if it
|
||
|
|
// only requires all cookies associated with an eTLD+1.
|
||
|
|
//
|
||
|
|
// On the browser critical paths (e.g. for loading initial web pages in a
|
||
|
|
// session restore) it may take too long to wait for the full load. If a cookie
|
||
|
|
// request is for a specific URL, DoCookieCallbackForURL is called, which
|
||
|
|
// triggers a priority load if the key is not loaded yet by calling
|
||
|
|
// PersistentCookieStore::LoadCookiesForKey. The request is queued in
|
||
|
|
// CookieMonster::tasks_pending_for_key_ and executed upon receiving
|
||
|
|
// notification of key load completion via CookieMonster::OnKeyLoaded(). If
|
||
|
|
// multiple requests for the same eTLD+1 are received before key load
|
||
|
|
// completion, only the first request calls
|
||
|
|
// PersistentCookieStore::LoadCookiesForKey, all subsequent requests are queued
|
||
|
|
// in CookieMonster::tasks_pending_for_key_ and executed upon receiving
|
||
|
|
// notification of key load completion triggered by the first request for the
|
||
|
|
// same eTLD+1.
|
||
|
|
|
||
|
|
static const int kDaysInTenYears = 10 * 365;
|
||
|
|
static const int kMinutesInTenYears = kDaysInTenYears * 24 * 60;
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
void MaybeRunDeleteCallback(base::WeakPtr<net::CookieMonster> cookie_monster,
|
||
|
|
base::OnceClosure callback) {
|
||
|
|
if (cookie_monster && callback)
|
||
|
|
std::move(callback).Run();
|
||
|
|
}
|
||
|
|
|
||
|
|
template <typename CB, typename... R>
|
||
|
|
void MaybeRunCookieCallback(base::OnceCallback<CB> callback, R&&... result) {
|
||
|
|
if (callback) {
|
||
|
|
std::move(callback).Run(std::forward<R>(result)...);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Anonymous and Fenced Frame uses a CookiePartitionKey with a nonce. In these
|
||
|
|
// contexts, access to unpartitioned cookie is not granted.
|
||
|
|
//
|
||
|
|
// This returns true if the |list| of key should include unpartitioned cookie in
|
||
|
|
// GetCookie...().
|
||
|
|
bool IncludeUnpartitionedCookies(
|
||
|
|
const net::CookiePartitionKeyCollection& list) {
|
||
|
|
if (list.IsEmpty() || list.ContainsAllKeys())
|
||
|
|
return true;
|
||
|
|
|
||
|
|
for (const net::CookiePartitionKey& key : list.PartitionKeys()) {
|
||
|
|
if (!key.nonce())
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
size_t NameValueSizeBytes(const net::CanonicalCookie& cc) {
|
||
|
|
base::CheckedNumeric<size_t> name_value_pair_size = cc.Name().size();
|
||
|
|
name_value_pair_size += cc.Value().size();
|
||
|
|
DCHECK(name_value_pair_size.IsValid());
|
||
|
|
return name_value_pair_size.ValueOrDie();
|
||
|
|
}
|
||
|
|
|
||
|
|
size_t NumBytesInCookieMapForKey(
|
||
|
|
const net::CookieMonster::CookieMap& cookie_map,
|
||
|
|
const std::string& key) {
|
||
|
|
size_t result = 0;
|
||
|
|
auto range = cookie_map.equal_range(key);
|
||
|
|
for (auto it = range.first; it != range.second; ++it) {
|
||
|
|
result += NameValueSizeBytes(*it->second);
|
||
|
|
}
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
size_t NumBytesInCookieItVector(
|
||
|
|
const net::CookieMonster::CookieItVector& cookie_its) {
|
||
|
|
size_t result = 0;
|
||
|
|
for (const auto& it : cookie_its) {
|
||
|
|
result += NameValueSizeBytes(*it->second);
|
||
|
|
}
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
namespace net {
|
||
|
|
|
||
|
|
// See comments at declaration of these variables in cookie_monster.h
|
||
|
|
// for details.
|
||
|
|
const size_t CookieMonster::kDomainMaxCookies = 180;
|
||
|
|
const size_t CookieMonster::kDomainPurgeCookies = 30;
|
||
|
|
const size_t CookieMonster::kMaxCookies = 3300;
|
||
|
|
const size_t CookieMonster::kPurgeCookies = 300;
|
||
|
|
|
||
|
|
const size_t CookieMonster::kMaxDomainPurgedKeys = 100;
|
||
|
|
|
||
|
|
const size_t CookieMonster::kPerPartitionDomainMaxCookieBytes = 10240;
|
||
|
|
const size_t CookieMonster::kPerPartitionDomainMaxCookies = 180;
|
||
|
|
|
||
|
|
const size_t CookieMonster::kDomainCookiesQuotaLow = 30;
|
||
|
|
const size_t CookieMonster::kDomainCookiesQuotaMedium = 50;
|
||
|
|
const size_t CookieMonster::kDomainCookiesQuotaHigh =
|
||
|
|
kDomainMaxCookies - kDomainPurgeCookies - kDomainCookiesQuotaLow -
|
||
|
|
kDomainCookiesQuotaMedium;
|
||
|
|
|
||
|
|
const int CookieMonster::kSafeFromGlobalPurgeDays = 30;
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
bool ContainsControlCharacter(const std::string& s) {
|
||
|
|
return base::ranges::any_of(s, &HttpUtil::IsControlChar);
|
||
|
|
}
|
||
|
|
|
||
|
|
typedef std::vector<CanonicalCookie*> CanonicalCookieVector;
|
||
|
|
|
||
|
|
// Default minimum delay after updating a cookie's LastAccessDate before we
|
||
|
|
// will update it again.
|
||
|
|
const int kDefaultAccessUpdateThresholdSeconds = 60;
|
||
|
|
|
||
|
|
// Comparator to sort cookies from highest creation date to lowest
|
||
|
|
// creation date.
|
||
|
|
struct OrderByCreationTimeDesc {
|
||
|
|
bool operator()(const CookieMonster::CookieMap::iterator& a,
|
||
|
|
const CookieMonster::CookieMap::iterator& b) const {
|
||
|
|
return a->second->CreationDate() > b->second->CreationDate();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
bool LRACookieSorter(const CookieMonster::CookieMap::iterator& it1,
|
||
|
|
const CookieMonster::CookieMap::iterator& it2) {
|
||
|
|
if (it1->second->LastAccessDate() != it2->second->LastAccessDate())
|
||
|
|
return it1->second->LastAccessDate() < it2->second->LastAccessDate();
|
||
|
|
|
||
|
|
// Ensure stability for == last access times by falling back to creation.
|
||
|
|
return it1->second->CreationDate() < it2->second->CreationDate();
|
||
|
|
}
|
||
|
|
|
||
|
|
// For a CookieItVector iterator range [|it_begin|, |it_end|),
|
||
|
|
// sorts the first |num_sort| elements by LastAccessDate().
|
||
|
|
void SortLeastRecentlyAccessed(CookieMonster::CookieItVector::iterator it_begin,
|
||
|
|
CookieMonster::CookieItVector::iterator it_end,
|
||
|
|
size_t num_sort) {
|
||
|
|
DCHECK_LE(static_cast<int>(num_sort), it_end - it_begin);
|
||
|
|
std::partial_sort(it_begin, it_begin + num_sort, it_end, LRACookieSorter);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Given a single cookie vector |cookie_its|, pushs all of the secure cookies in
|
||
|
|
// |cookie_its| into |secure_cookie_its| and all of the non-secure cookies into
|
||
|
|
// |non_secure_cookie_its|. Both |secure_cookie_its| and |non_secure_cookie_its|
|
||
|
|
// must be non-NULL.
|
||
|
|
void SplitCookieVectorIntoSecureAndNonSecure(
|
||
|
|
const CookieMonster::CookieItVector& cookie_its,
|
||
|
|
CookieMonster::CookieItVector* secure_cookie_its,
|
||
|
|
CookieMonster::CookieItVector* non_secure_cookie_its) {
|
||
|
|
DCHECK(secure_cookie_its && non_secure_cookie_its);
|
||
|
|
for (const auto& curit : cookie_its) {
|
||
|
|
if (curit->second->IsSecure())
|
||
|
|
secure_cookie_its->push_back(curit);
|
||
|
|
else
|
||
|
|
non_secure_cookie_its->push_back(curit);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
bool LowerBoundAccessDateComparator(const CookieMonster::CookieMap::iterator it,
|
||
|
|
const Time& access_date) {
|
||
|
|
return it->second->LastAccessDate() < access_date;
|
||
|
|
}
|
||
|
|
|
||
|
|
// For a CookieItVector iterator range [|it_begin|, |it_end|)
|
||
|
|
// from a CookieItVector sorted by LastAccessDate(), returns the
|
||
|
|
// first iterator with access date >= |access_date|, or cookie_its_end if this
|
||
|
|
// holds for all.
|
||
|
|
CookieMonster::CookieItVector::iterator LowerBoundAccessDate(
|
||
|
|
const CookieMonster::CookieItVector::iterator its_begin,
|
||
|
|
const CookieMonster::CookieItVector::iterator its_end,
|
||
|
|
const Time& access_date) {
|
||
|
|
return std::lower_bound(its_begin, its_end, access_date,
|
||
|
|
LowerBoundAccessDateComparator);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Mapping between DeletionCause and CookieChangeCause; the
|
||
|
|
// mapping also provides a boolean that specifies whether or not an
|
||
|
|
// OnCookieChange notification ought to be generated.
|
||
|
|
typedef struct ChangeCausePair_struct {
|
||
|
|
CookieChangeCause cause;
|
||
|
|
bool notify;
|
||
|
|
} ChangeCausePair;
|
||
|
|
const ChangeCausePair kChangeCauseMapping[] = {
|
||
|
|
// DELETE_COOKIE_EXPLICIT
|
||
|
|
{CookieChangeCause::EXPLICIT, true},
|
||
|
|
// DELETE_COOKIE_OVERWRITE
|
||
|
|
{CookieChangeCause::OVERWRITE, true},
|
||
|
|
// DELETE_COOKIE_EXPIRED
|
||
|
|
{CookieChangeCause::EXPIRED, true},
|
||
|
|
// DELETE_COOKIE_EVICTED
|
||
|
|
{CookieChangeCause::EVICTED, true},
|
||
|
|
// DELETE_COOKIE_DUPLICATE_IN_BACKING_STORE
|
||
|
|
{CookieChangeCause::EXPLICIT, false},
|
||
|
|
// DELETE_COOKIE_DONT_RECORD
|
||
|
|
{CookieChangeCause::EXPLICIT, false},
|
||
|
|
// DELETE_COOKIE_EVICTED_DOMAIN
|
||
|
|
{CookieChangeCause::EVICTED, true},
|
||
|
|
// DELETE_COOKIE_EVICTED_GLOBAL
|
||
|
|
{CookieChangeCause::EVICTED, true},
|
||
|
|
// DELETE_COOKIE_EVICTED_DOMAIN_PRE_SAFE
|
||
|
|
{CookieChangeCause::EVICTED, true},
|
||
|
|
// DELETE_COOKIE_EVICTED_DOMAIN_POST_SAFE
|
||
|
|
{CookieChangeCause::EVICTED, true},
|
||
|
|
// DELETE_COOKIE_EXPIRED_OVERWRITE
|
||
|
|
{CookieChangeCause::EXPIRED_OVERWRITE, true},
|
||
|
|
// DELETE_COOKIE_CONTROL_CHAR
|
||
|
|
{CookieChangeCause::EVICTED, true},
|
||
|
|
// DELETE_COOKIE_NON_SECURE
|
||
|
|
{CookieChangeCause::EVICTED, true},
|
||
|
|
// DELETE_COOKIE_EVICTED_PER_PARTITION_DOMAIN
|
||
|
|
{CookieChangeCause::EVICTED, true},
|
||
|
|
// DELETE_COOKIE_LAST_ENTRY
|
||
|
|
{CookieChangeCause::EXPLICIT, false}};
|
||
|
|
|
||
|
|
bool IsCookieEligibleForEviction(CookiePriority current_priority_level,
|
||
|
|
bool protect_secure_cookies,
|
||
|
|
const CanonicalCookie* cookie) {
|
||
|
|
if (cookie->Priority() == current_priority_level && protect_secure_cookies)
|
||
|
|
return !cookie->IsSecure();
|
||
|
|
|
||
|
|
return cookie->Priority() == current_priority_level;
|
||
|
|
}
|
||
|
|
|
||
|
|
size_t CountCookiesForPossibleDeletion(
|
||
|
|
CookiePriority priority,
|
||
|
|
const CookieMonster::CookieItVector* cookies,
|
||
|
|
bool protect_secure_cookies) {
|
||
|
|
size_t cookies_count = 0U;
|
||
|
|
for (const auto& cookie : *cookies) {
|
||
|
|
if (cookie->second->Priority() == priority) {
|
||
|
|
if (!protect_secure_cookies || cookie->second->IsSecure())
|
||
|
|
cookies_count++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return cookies_count;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Records minutes until the expiration date of a cookie to the appropriate
|
||
|
|
// histogram. Only histograms cookies that have an expiration date (i.e. are
|
||
|
|
// persistent).
|
||
|
|
void HistogramExpirationDuration(const CanonicalCookie& cookie,
|
||
|
|
base::Time creation_time) {
|
||
|
|
if (!cookie.IsPersistent())
|
||
|
|
return;
|
||
|
|
|
||
|
|
int expiration_duration_minutes =
|
||
|
|
(cookie.ExpiryDate() - creation_time).InMinutes();
|
||
|
|
if (cookie.IsSecure()) {
|
||
|
|
UMA_HISTOGRAM_CUSTOM_COUNTS("Cookie.ExpirationDurationMinutesSecure",
|
||
|
|
expiration_duration_minutes, 1,
|
||
|
|
kMinutesInTenYears, 50);
|
||
|
|
} else {
|
||
|
|
UMA_HISTOGRAM_CUSTOM_COUNTS("Cookie.ExpirationDurationMinutesNonSecure",
|
||
|
|
expiration_duration_minutes, 1,
|
||
|
|
kMinutesInTenYears, 50);
|
||
|
|
}
|
||
|
|
// The proposed rfc6265bis sets an upper limit on Expires/Max-Age attribute
|
||
|
|
// values of 400 days. We need to study the impact this change would have:
|
||
|
|
// https://httpwg.org/http-extensions/draft-ietf-httpbis-rfc6265bis.html
|
||
|
|
int expiration_duration_days = (cookie.ExpiryDate() - creation_time).InDays();
|
||
|
|
if (expiration_duration_days > 400) {
|
||
|
|
UMA_HISTOGRAM_CUSTOM_COUNTS("Cookie.ExpirationDuration400DaysGT",
|
||
|
|
expiration_duration_days, 401, kDaysInTenYears,
|
||
|
|
100);
|
||
|
|
} else {
|
||
|
|
UMA_HISTOGRAM_CUSTOM_COUNTS("Cookie.ExpirationDuration400DaysLTE",
|
||
|
|
expiration_duration_days, 1, 400, 50);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
CookieMonster::CookieMonster(scoped_refptr<PersistentCookieStore> store,
|
||
|
|
NetLog* net_log)
|
||
|
|
: CookieMonster(std::move(store),
|
||
|
|
base::Seconds(kDefaultAccessUpdateThresholdSeconds),
|
||
|
|
net_log) {}
|
||
|
|
|
||
|
|
CookieMonster::CookieMonster(scoped_refptr<PersistentCookieStore> store,
|
||
|
|
base::TimeDelta last_access_threshold,
|
||
|
|
NetLog* net_log)
|
||
|
|
: same_party_attribute_enabled_(base::FeatureList::IsEnabled(
|
||
|
|
net::features::kSamePartyAttributeEnabled)),
|
||
|
|
change_dispatcher_(this, same_party_attribute_enabled_),
|
||
|
|
net_log_(NetLogWithSource::Make(net_log, NetLogSourceType::COOKIE_STORE)),
|
||
|
|
store_(std::move(store)),
|
||
|
|
last_access_threshold_(last_access_threshold),
|
||
|
|
last_statistic_record_time_(base::Time::Now()) {
|
||
|
|
cookieable_schemes_.insert(
|
||
|
|
cookieable_schemes_.begin(), kDefaultCookieableSchemes,
|
||
|
|
kDefaultCookieableSchemes + kDefaultCookieableSchemesCount);
|
||
|
|
net_log_.BeginEvent(NetLogEventType::COOKIE_STORE_ALIVE, [&] {
|
||
|
|
return NetLogCookieMonsterConstructorParams(store_ != nullptr);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Asynchronous CookieMonster API
|
||
|
|
|
||
|
|
void CookieMonster::FlushStore(base::OnceClosure callback) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
if (initialized_ && store_.get()) {
|
||
|
|
store_->Flush(std::move(callback));
|
||
|
|
} else if (callback) {
|
||
|
|
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
|
||
|
|
FROM_HERE, std::move(callback));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::SetForceKeepSessionState() {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
if (store_)
|
||
|
|
store_->SetForceKeepSessionState();
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::SetAllCookiesAsync(const CookieList& list,
|
||
|
|
SetCookiesCallback callback) {
|
||
|
|
DoCookieCallback(base::BindOnce(
|
||
|
|
// base::Unretained is safe as DoCookieCallback stores
|
||
|
|
// the callback on |*this|, so the callback will not outlive
|
||
|
|
// the object.
|
||
|
|
&CookieMonster::SetAllCookies, base::Unretained(this), list,
|
||
|
|
std::move(callback)));
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::SetCanonicalCookieAsync(
|
||
|
|
std::unique_ptr<CanonicalCookie> cookie,
|
||
|
|
const GURL& source_url,
|
||
|
|
const CookieOptions& options,
|
||
|
|
SetCookiesCallback callback,
|
||
|
|
absl::optional<CookieAccessResult> cookie_access_result) {
|
||
|
|
DCHECK(cookie->IsCanonical());
|
||
|
|
|
||
|
|
std::string domain = cookie->Domain();
|
||
|
|
DoCookieCallbackForHostOrDomain(
|
||
|
|
base::BindOnce(
|
||
|
|
// base::Unretained is safe as DoCookieCallbackForHostOrDomain stores
|
||
|
|
// the callback on |*this|, so the callback will not outlive
|
||
|
|
// the object.
|
||
|
|
&CookieMonster::SetCanonicalCookie, base::Unretained(this),
|
||
|
|
std::move(cookie), source_url, options, std::move(callback),
|
||
|
|
std::move(cookie_access_result)),
|
||
|
|
domain);
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::GetCookieListWithOptionsAsync(
|
||
|
|
const GURL& url,
|
||
|
|
const CookieOptions& options,
|
||
|
|
const CookiePartitionKeyCollection& cookie_partition_key_collection,
|
||
|
|
GetCookieListCallback callback) {
|
||
|
|
DoCookieCallbackForURL(
|
||
|
|
base::BindOnce(
|
||
|
|
// base::Unretained is safe as DoCookieCallbackForURL stores
|
||
|
|
// the callback on |*this|, so the callback will not outlive
|
||
|
|
// the object.
|
||
|
|
&CookieMonster::GetCookieListWithOptions, base::Unretained(this), url,
|
||
|
|
options, cookie_partition_key_collection, std::move(callback)),
|
||
|
|
url);
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::GetAllCookiesAsync(GetAllCookiesCallback callback) {
|
||
|
|
DoCookieCallback(base::BindOnce(
|
||
|
|
// base::Unretained is safe as DoCookieCallback stores
|
||
|
|
// the callback on |*this|, so the callback will not outlive
|
||
|
|
// the object.
|
||
|
|
&CookieMonster::GetAllCookies, base::Unretained(this),
|
||
|
|
std::move(callback)));
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::GetAllCookiesWithAccessSemanticsAsync(
|
||
|
|
GetAllCookiesWithAccessSemanticsCallback callback) {
|
||
|
|
DoCookieCallback(base::BindOnce(
|
||
|
|
// base::Unretained is safe as DoCookieCallback stores
|
||
|
|
// the callback on |*this|, so the callback will not outlive
|
||
|
|
// the object.
|
||
|
|
&CookieMonster::GetAllCookies, base::Unretained(this),
|
||
|
|
base::BindOnce(&CookieMonster::AttachAccessSemanticsListForCookieList,
|
||
|
|
base::Unretained(this), std::move(callback))));
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::DeleteCanonicalCookieAsync(const CanonicalCookie& cookie,
|
||
|
|
DeleteCallback callback) {
|
||
|
|
DoCookieCallback(base::BindOnce(
|
||
|
|
// base::Unretained is safe as DoCookieCallback stores
|
||
|
|
// the callback on |*this|, so the callback will not outlive
|
||
|
|
// the object.
|
||
|
|
&CookieMonster::DeleteCanonicalCookie, base::Unretained(this), cookie,
|
||
|
|
std::move(callback)));
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::DeleteAllCreatedInTimeRangeAsync(
|
||
|
|
const TimeRange& creation_range,
|
||
|
|
DeleteCallback callback) {
|
||
|
|
DoCookieCallback(base::BindOnce(
|
||
|
|
// base::Unretained is safe as DoCookieCallback stores
|
||
|
|
// the callback on |*this|, so the callback will not outlive
|
||
|
|
// the object.
|
||
|
|
&CookieMonster::DeleteAllCreatedInTimeRange, base::Unretained(this),
|
||
|
|
creation_range, std::move(callback)));
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::DeleteAllMatchingInfoAsync(CookieDeletionInfo delete_info,
|
||
|
|
DeleteCallback callback) {
|
||
|
|
auto cookie_matcher =
|
||
|
|
base::BindRepeating(&CookieMonster::MatchCookieDeletionInfo,
|
||
|
|
base::Unretained(this), std::move(delete_info));
|
||
|
|
|
||
|
|
DoCookieCallback(base::BindOnce(
|
||
|
|
// base::Unretained is safe as DoCookieCallback stores
|
||
|
|
// the callback on |*this|, so the callback will not outlive
|
||
|
|
// the object.
|
||
|
|
&CookieMonster::DeleteMatchingCookies, base::Unretained(this),
|
||
|
|
std::move(cookie_matcher), DELETE_COOKIE_EXPLICIT, std::move(callback)));
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::DeleteSessionCookiesAsync(
|
||
|
|
CookieStore::DeleteCallback callback) {
|
||
|
|
auto session_cookie_matcher =
|
||
|
|
base::BindRepeating([](const net::CanonicalCookie& cookie) {
|
||
|
|
return !cookie.IsPersistent();
|
||
|
|
});
|
||
|
|
DoCookieCallback(base::BindOnce(
|
||
|
|
// base::Unretained is safe as DoCookieCallback stores
|
||
|
|
// the callback on |*this|, so the callback will not outlive
|
||
|
|
// the object.
|
||
|
|
&CookieMonster::DeleteMatchingCookies, base::Unretained(this),
|
||
|
|
std::move(session_cookie_matcher), DELETE_COOKIE_EXPIRED,
|
||
|
|
std::move(callback)));
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::DeleteMatchingCookiesAsync(
|
||
|
|
CookieStore::DeletePredicate predicate,
|
||
|
|
CookieStore::DeleteCallback callback) {
|
||
|
|
DoCookieCallback(base::BindOnce(
|
||
|
|
// base::Unretained is safe as DoCookieCallback stores
|
||
|
|
// the callback on |*this|, so the callback will not outlive
|
||
|
|
// the object.
|
||
|
|
&CookieMonster::DeleteMatchingCookies, base::Unretained(this),
|
||
|
|
std::move(predicate), DELETE_COOKIE_EXPLICIT, std::move(callback)));
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::SetCookieableSchemes(
|
||
|
|
const std::vector<std::string>& schemes,
|
||
|
|
SetCookieableSchemesCallback callback) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
// Calls to this method will have no effect if made after a WebView or
|
||
|
|
// CookieManager instance has been created.
|
||
|
|
if (initialized_) {
|
||
|
|
MaybeRunCookieCallback(std::move(callback), false);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
cookieable_schemes_ = schemes;
|
||
|
|
MaybeRunCookieCallback(std::move(callback), true);
|
||
|
|
}
|
||
|
|
|
||
|
|
// This function must be called before the CookieMonster is used.
|
||
|
|
void CookieMonster::SetPersistSessionCookies(bool persist_session_cookies) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
DCHECK(!initialized_);
|
||
|
|
net_log_.AddEntryWithBoolParams(
|
||
|
|
NetLogEventType::COOKIE_STORE_SESSION_PERSISTENCE, NetLogEventPhase::NONE,
|
||
|
|
"persistence", persist_session_cookies);
|
||
|
|
persist_session_cookies_ = persist_session_cookies;
|
||
|
|
}
|
||
|
|
|
||
|
|
const char* const CookieMonster::kDefaultCookieableSchemes[] = {"http", "https",
|
||
|
|
"ws", "wss"};
|
||
|
|
const int CookieMonster::kDefaultCookieableSchemesCount =
|
||
|
|
std::size(kDefaultCookieableSchemes);
|
||
|
|
|
||
|
|
CookieChangeDispatcher& CookieMonster::GetChangeDispatcher() {
|
||
|
|
return change_dispatcher_;
|
||
|
|
}
|
||
|
|
|
||
|
|
CookieMonster::~CookieMonster() {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
net_log_.EndEvent(NetLogEventType::COOKIE_STORE_ALIVE);
|
||
|
|
}
|
||
|
|
|
||
|
|
// static
|
||
|
|
bool CookieMonster::CookieSorter(const CanonicalCookie* cc1,
|
||
|
|
const CanonicalCookie* cc2) {
|
||
|
|
// Mozilla sorts on the path length (longest first), and then it sorts by
|
||
|
|
// creation time (oldest first). The RFC says the sort order for the domain
|
||
|
|
// attribute is undefined.
|
||
|
|
if (cc1->Path().length() == cc2->Path().length())
|
||
|
|
return cc1->CreationDate() < cc2->CreationDate();
|
||
|
|
return cc1->Path().length() > cc2->Path().length();
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::GetAllCookies(GetAllCookiesCallback callback) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
// This function is being called to scrape the cookie list for management UI
|
||
|
|
// or similar. We shouldn't show expired cookies in this list since it will
|
||
|
|
// just be confusing to users, and this function is called rarely enough (and
|
||
|
|
// is already slow enough) that it's OK to take the time to garbage collect
|
||
|
|
// the expired cookies now.
|
||
|
|
//
|
||
|
|
// Note that this does not prune cookies to be below our limits (if we've
|
||
|
|
// exceeded them) the way that calling GarbageCollect() would.
|
||
|
|
GarbageCollectExpired(
|
||
|
|
Time::Now(), CookieMapItPair(cookies_.begin(), cookies_.end()), nullptr);
|
||
|
|
GarbageCollectAllExpiredPartitionedCookies(Time::Now());
|
||
|
|
|
||
|
|
// Copy the CanonicalCookie pointers from the map so that we can use the same
|
||
|
|
// sorter as elsewhere, then copy the result out.
|
||
|
|
std::vector<CanonicalCookie*> cookie_ptrs;
|
||
|
|
cookie_ptrs.reserve(cookies_.size());
|
||
|
|
for (const auto& cookie : cookies_)
|
||
|
|
cookie_ptrs.push_back(cookie.second.get());
|
||
|
|
|
||
|
|
for (const auto& cookie_partition : partitioned_cookies_) {
|
||
|
|
for (const auto& cookie : *cookie_partition.second.get())
|
||
|
|
cookie_ptrs.push_back(cookie.second.get());
|
||
|
|
}
|
||
|
|
|
||
|
|
std::sort(cookie_ptrs.begin(), cookie_ptrs.end(), CookieSorter);
|
||
|
|
|
||
|
|
CookieList cookie_list;
|
||
|
|
cookie_list.reserve(cookie_ptrs.size());
|
||
|
|
for (auto* cookie_ptr : cookie_ptrs)
|
||
|
|
cookie_list.push_back(*cookie_ptr);
|
||
|
|
|
||
|
|
MaybeRunCookieCallback(std::move(callback), cookie_list);
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::AttachAccessSemanticsListForCookieList(
|
||
|
|
GetAllCookiesWithAccessSemanticsCallback callback,
|
||
|
|
const CookieList& cookie_list) {
|
||
|
|
std::vector<CookieAccessSemantics> access_semantics_list;
|
||
|
|
for (const CanonicalCookie& cookie : cookie_list) {
|
||
|
|
access_semantics_list.push_back(GetAccessSemanticsForCookie(cookie));
|
||
|
|
}
|
||
|
|
MaybeRunCookieCallback(std::move(callback), cookie_list,
|
||
|
|
access_semantics_list);
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::GetCookieListWithOptions(
|
||
|
|
const GURL& url,
|
||
|
|
const CookieOptions& options,
|
||
|
|
const CookiePartitionKeyCollection& cookie_partition_key_collection,
|
||
|
|
GetCookieListCallback callback) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
CookieAccessResultList included_cookies;
|
||
|
|
CookieAccessResultList excluded_cookies;
|
||
|
|
if (HasCookieableScheme(url)) {
|
||
|
|
std::vector<CanonicalCookie*> cookie_ptrs;
|
||
|
|
if (IncludeUnpartitionedCookies(cookie_partition_key_collection)) {
|
||
|
|
cookie_ptrs = FindCookiesForRegistryControlledHost(url);
|
||
|
|
} else {
|
||
|
|
DCHECK(!cookie_partition_key_collection.IsEmpty());
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!cookie_partition_key_collection.IsEmpty()) {
|
||
|
|
if (cookie_partition_key_collection.ContainsAllKeys()) {
|
||
|
|
for (const auto& it : partitioned_cookies_) {
|
||
|
|
std::vector<CanonicalCookie*> partitioned_cookie_ptrs =
|
||
|
|
FindPartitionedCookiesForRegistryControlledHost(it.first, url);
|
||
|
|
cookie_ptrs.insert(cookie_ptrs.end(), partitioned_cookie_ptrs.begin(),
|
||
|
|
partitioned_cookie_ptrs.end());
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
for (const CookiePartitionKey& key :
|
||
|
|
cookie_partition_key_collection.PartitionKeys()) {
|
||
|
|
std::vector<CanonicalCookie*> partitioned_cookie_ptrs =
|
||
|
|
FindPartitionedCookiesForRegistryControlledHost(key, url);
|
||
|
|
cookie_ptrs.insert(cookie_ptrs.end(), partitioned_cookie_ptrs.begin(),
|
||
|
|
partitioned_cookie_ptrs.end());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
std::sort(cookie_ptrs.begin(), cookie_ptrs.end(), CookieSorter);
|
||
|
|
|
||
|
|
included_cookies.reserve(cookie_ptrs.size());
|
||
|
|
FilterCookiesWithOptions(url, options, &cookie_ptrs, &included_cookies,
|
||
|
|
&excluded_cookies);
|
||
|
|
}
|
||
|
|
|
||
|
|
MaybeRunCookieCallback(std::move(callback), included_cookies,
|
||
|
|
excluded_cookies);
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::DeleteAllCreatedInTimeRange(const TimeRange& creation_range,
|
||
|
|
DeleteCallback callback) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
uint32_t num_deleted = 0;
|
||
|
|
for (auto it = cookies_.begin(); it != cookies_.end();) {
|
||
|
|
auto curit = it;
|
||
|
|
CanonicalCookie* cc = curit->second.get();
|
||
|
|
++it;
|
||
|
|
|
||
|
|
if (creation_range.Contains(cc->CreationDate())) {
|
||
|
|
InternalDeleteCookie(curit, true, /*sync_to_store*/
|
||
|
|
DELETE_COOKIE_EXPLICIT);
|
||
|
|
++num_deleted;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
for (PartitionedCookieMap::iterator partition_it =
|
||
|
|
partitioned_cookies_.begin();
|
||
|
|
partition_it != partitioned_cookies_.end();) {
|
||
|
|
auto cur_partition_it = partition_it;
|
||
|
|
CookieMap::iterator cookie_it = cur_partition_it->second->begin();
|
||
|
|
CookieMap::iterator cookie_end = cur_partition_it->second->end();
|
||
|
|
// InternalDeletePartitionedCookie may delete this cookie partition if it
|
||
|
|
// only has one cookie, so we need to increment the iterator beforehand.
|
||
|
|
++partition_it;
|
||
|
|
|
||
|
|
while (cookie_it != cookie_end) {
|
||
|
|
auto cur_cookie_it = cookie_it;
|
||
|
|
CanonicalCookie* cc = cur_cookie_it->second.get();
|
||
|
|
++cookie_it;
|
||
|
|
|
||
|
|
if (creation_range.Contains(cc->CreationDate())) {
|
||
|
|
InternalDeletePartitionedCookie(cur_partition_it, cur_cookie_it,
|
||
|
|
true /*sync_to_store*/,
|
||
|
|
DELETE_COOKIE_EXPLICIT);
|
||
|
|
++num_deleted;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
FlushStore(
|
||
|
|
base::BindOnce(&MaybeRunDeleteCallback, weak_ptr_factory_.GetWeakPtr(),
|
||
|
|
callback ? base::BindOnce(std::move(callback), num_deleted)
|
||
|
|
: base::OnceClosure()));
|
||
|
|
}
|
||
|
|
|
||
|
|
bool CookieMonster::MatchCookieDeletionInfo(
|
||
|
|
const CookieDeletionInfo& delete_info,
|
||
|
|
const net::CanonicalCookie& cookie) {
|
||
|
|
bool delegate_treats_url_as_trustworthy = false; // irrelevant if no URL.
|
||
|
|
if (delete_info.url.has_value()) {
|
||
|
|
delegate_treats_url_as_trustworthy =
|
||
|
|
cookie_access_delegate() &&
|
||
|
|
cookie_access_delegate()->ShouldTreatUrlAsTrustworthy(
|
||
|
|
delete_info.url.value());
|
||
|
|
}
|
||
|
|
|
||
|
|
// Deletion uses all inclusive options, so it's ok to get the
|
||
|
|
// `CookieSamePartyStatus` wrong here.
|
||
|
|
return delete_info.Matches(
|
||
|
|
cookie,
|
||
|
|
CookieAccessParams{GetAccessSemanticsForCookie(cookie),
|
||
|
|
delegate_treats_url_as_trustworthy,
|
||
|
|
CookieSamePartyStatus::kNoSamePartyEnforcement});
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::DeleteCanonicalCookie(const CanonicalCookie& cookie,
|
||
|
|
DeleteCallback callback) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
uint32_t result = 0u;
|
||
|
|
CookieMap* cookie_map = nullptr;
|
||
|
|
PartitionedCookieMap::iterator cookie_partition_it;
|
||
|
|
|
||
|
|
if (cookie.IsPartitioned()) {
|
||
|
|
cookie_partition_it =
|
||
|
|
partitioned_cookies_.find(cookie.PartitionKey().value());
|
||
|
|
if (cookie_partition_it != partitioned_cookies_.end())
|
||
|
|
cookie_map = cookie_partition_it->second.get();
|
||
|
|
} else {
|
||
|
|
cookie_map = &cookies_;
|
||
|
|
}
|
||
|
|
if (cookie_map) {
|
||
|
|
for (CookieMapItPair its = cookie_map->equal_range(GetKey(cookie.Domain()));
|
||
|
|
its.first != its.second; ++its.first) {
|
||
|
|
const std::unique_ptr<CanonicalCookie>& candidate = its.first->second;
|
||
|
|
// Historically, this has refused modification if the cookie has changed
|
||
|
|
// value in between the CanonicalCookie object was returned by a getter
|
||
|
|
// and when this ran. The later parts of the conditional (everything but
|
||
|
|
// the equivalence check) attempt to preserve this behavior.
|
||
|
|
if (candidate->IsEquivalent(cookie) &&
|
||
|
|
candidate->Value() == cookie.Value()) {
|
||
|
|
if (cookie.IsPartitioned()) {
|
||
|
|
InternalDeletePartitionedCookie(cookie_partition_it, its.first, true,
|
||
|
|
DELETE_COOKIE_EXPLICIT);
|
||
|
|
} else {
|
||
|
|
InternalDeleteCookie(its.first, true, DELETE_COOKIE_EXPLICIT);
|
||
|
|
}
|
||
|
|
result = 1u;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
FlushStore(
|
||
|
|
base::BindOnce(&MaybeRunDeleteCallback, weak_ptr_factory_.GetWeakPtr(),
|
||
|
|
callback ? base::BindOnce(std::move(callback), result)
|
||
|
|
: base::OnceClosure()));
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::DeleteMatchingCookies(DeletePredicate predicate,
|
||
|
|
DeletionCause cause,
|
||
|
|
DeleteCallback callback) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
DCHECK(predicate);
|
||
|
|
|
||
|
|
uint32_t num_deleted = 0;
|
||
|
|
for (auto it = cookies_.begin(); it != cookies_.end();) {
|
||
|
|
auto curit = it;
|
||
|
|
CanonicalCookie* cc = curit->second.get();
|
||
|
|
++it;
|
||
|
|
if (predicate.Run(*cc)) {
|
||
|
|
InternalDeleteCookie(curit, true /*sync_to_store*/, cause);
|
||
|
|
++num_deleted;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for (auto partition_it = partitioned_cookies_.begin();
|
||
|
|
partition_it != partitioned_cookies_.end();) {
|
||
|
|
// InternalDeletePartitionedCookie may invalidate |partition_it| if that
|
||
|
|
// cookie partition only has one cookie.
|
||
|
|
auto cur_partition_it = partition_it;
|
||
|
|
CookieMap::iterator cookie_it = cur_partition_it->second->begin();
|
||
|
|
CookieMap::iterator cookie_end = cur_partition_it->second->end();
|
||
|
|
++partition_it;
|
||
|
|
|
||
|
|
while (cookie_it != cookie_end) {
|
||
|
|
auto cur_cookie_it = cookie_it;
|
||
|
|
CanonicalCookie* cc = cur_cookie_it->second.get();
|
||
|
|
++cookie_it;
|
||
|
|
|
||
|
|
if (predicate.Run(*cc)) {
|
||
|
|
InternalDeletePartitionedCookie(cur_partition_it, cur_cookie_it, true,
|
||
|
|
cause);
|
||
|
|
++num_deleted;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
FlushStore(
|
||
|
|
base::BindOnce(&MaybeRunDeleteCallback, weak_ptr_factory_.GetWeakPtr(),
|
||
|
|
callback ? base::BindOnce(std::move(callback), num_deleted)
|
||
|
|
: base::OnceClosure()));
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::MarkCookieStoreAsInitialized() {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
initialized_ = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::FetchAllCookiesIfNecessary() {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
if (store_.get() && !started_fetching_all_cookies_) {
|
||
|
|
started_fetching_all_cookies_ = true;
|
||
|
|
FetchAllCookies();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::FetchAllCookies() {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
DCHECK(store_.get()) << "Store must exist to initialize";
|
||
|
|
DCHECK(!finished_fetching_all_cookies_)
|
||
|
|
<< "All cookies have already been fetched.";
|
||
|
|
|
||
|
|
// We bind in the current time so that we can report the wall-clock time for
|
||
|
|
// loading cookies.
|
||
|
|
store_->Load(base::BindOnce(&CookieMonster::OnLoaded,
|
||
|
|
weak_ptr_factory_.GetWeakPtr(), TimeTicks::Now()),
|
||
|
|
net_log_);
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::OnLoaded(
|
||
|
|
TimeTicks beginning_time,
|
||
|
|
std::vector<std::unique_ptr<CanonicalCookie>> cookies) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
StoreLoadedCookies(std::move(cookies));
|
||
|
|
base::UmaHistogramCustomTimes("Cookie.TimeBlockedOnLoad",
|
||
|
|
base::TimeTicks::Now() - beginning_time,
|
||
|
|
base::Milliseconds(1), base::Minutes(1), 50);
|
||
|
|
|
||
|
|
// Invoke the task queue of cookie request.
|
||
|
|
InvokeQueue();
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::OnKeyLoaded(
|
||
|
|
const std::string& key,
|
||
|
|
std::vector<std::unique_ptr<CanonicalCookie>> cookies) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
StoreLoadedCookies(std::move(cookies));
|
||
|
|
|
||
|
|
auto tasks_pending_for_key = tasks_pending_for_key_.find(key);
|
||
|
|
|
||
|
|
// TODO(mmenke): Can this be turned into a DCHECK?
|
||
|
|
if (tasks_pending_for_key == tasks_pending_for_key_.end())
|
||
|
|
return;
|
||
|
|
|
||
|
|
// Run all tasks for the key. Note that running a task can result in multiple
|
||
|
|
// tasks being added to the back of the deque.
|
||
|
|
while (!tasks_pending_for_key->second.empty()) {
|
||
|
|
base::OnceClosure task = std::move(tasks_pending_for_key->second.front());
|
||
|
|
tasks_pending_for_key->second.pop_front();
|
||
|
|
std::move(task).Run();
|
||
|
|
}
|
||
|
|
|
||
|
|
tasks_pending_for_key_.erase(tasks_pending_for_key);
|
||
|
|
|
||
|
|
// This has to be done last, in case running a task queues a new task for the
|
||
|
|
// key, to ensure tasks are run in the correct order.
|
||
|
|
keys_loaded_.insert(key);
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::StoreLoadedCookies(
|
||
|
|
std::vector<std::unique_ptr<CanonicalCookie>> cookies) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
// Even if a key is expired, insert it so it can be garbage collected,
|
||
|
|
// removed, and sync'd.
|
||
|
|
CookieItVector cookies_with_control_chars;
|
||
|
|
std::vector<PartitionedCookieMapIterators>
|
||
|
|
partitioned_cookies_with_control_chars;
|
||
|
|
|
||
|
|
for (auto& cookie : cookies) {
|
||
|
|
CanonicalCookie* cookie_ptr = cookie.get();
|
||
|
|
CookieAccessResult access_result;
|
||
|
|
access_result.access_semantics = CookieAccessSemantics::UNKNOWN;
|
||
|
|
|
||
|
|
if (cookie_ptr->IsPartitioned()) {
|
||
|
|
auto inserted = InternalInsertPartitionedCookie(
|
||
|
|
GetKey(cookie_ptr->Domain()), std::move(cookie),
|
||
|
|
false /* sync_to_store */, access_result,
|
||
|
|
false /* dispatch_change */);
|
||
|
|
if (ContainsControlCharacter(cookie_ptr->Name()) ||
|
||
|
|
ContainsControlCharacter(cookie_ptr->Value())) {
|
||
|
|
partitioned_cookies_with_control_chars.push_back(inserted);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
auto inserted =
|
||
|
|
InternalInsertCookie(GetKey(cookie_ptr->Domain()), std::move(cookie),
|
||
|
|
false /* sync_to_store */, access_result,
|
||
|
|
false /* dispatch_change */);
|
||
|
|
|
||
|
|
if (ContainsControlCharacter(cookie_ptr->Name()) ||
|
||
|
|
ContainsControlCharacter(cookie_ptr->Value())) {
|
||
|
|
cookies_with_control_chars.push_back(inserted);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const Time cookie_access_time(cookie_ptr->LastAccessDate());
|
||
|
|
if (earliest_access_time_.is_null() ||
|
||
|
|
cookie_access_time < earliest_access_time_) {
|
||
|
|
earliest_access_time_ = cookie_access_time;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Any cookies that contain control characters that we have loaded from the
|
||
|
|
// persistent store should be deleted. See http://crbug.com/238041.
|
||
|
|
for (auto it = cookies_with_control_chars.begin();
|
||
|
|
it != cookies_with_control_chars.end();) {
|
||
|
|
auto curit = it;
|
||
|
|
++it;
|
||
|
|
InternalDeleteCookie(*curit, true, DELETE_COOKIE_CONTROL_CHAR);
|
||
|
|
}
|
||
|
|
for (auto it = partitioned_cookies_with_control_chars.begin();
|
||
|
|
it != partitioned_cookies_with_control_chars.end();) {
|
||
|
|
// InternalDeletePartitionedCookie may invalidate the current iterator, so
|
||
|
|
// we increment the iterator in the loop before calling the function.
|
||
|
|
auto curit = it;
|
||
|
|
++it;
|
||
|
|
InternalDeletePartitionedCookie(curit->first, curit->second, true,
|
||
|
|
DELETE_COOKIE_CONTROL_CHAR);
|
||
|
|
}
|
||
|
|
|
||
|
|
// After importing cookies from the PersistentCookieStore, verify that
|
||
|
|
// none of our other constraints are violated.
|
||
|
|
// In particular, the backing store might have given us duplicate cookies.
|
||
|
|
|
||
|
|
// This method could be called multiple times due to priority loading, thus
|
||
|
|
// cookies loaded in previous runs will be validated again, but this is OK
|
||
|
|
// since they are expected to be much fewer than total DB.
|
||
|
|
EnsureCookiesMapIsValid();
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::InvokeQueue() {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
// Move all per-key tasks into the global queue, if there are any. This is
|
||
|
|
// protection about a race where the store learns about all cookies loading
|
||
|
|
// before it learned about the cookies for a key loading.
|
||
|
|
|
||
|
|
// Needed to prevent any recursively queued tasks from going back into the
|
||
|
|
// per-key queues.
|
||
|
|
seen_global_task_ = true;
|
||
|
|
for (auto& tasks_for_key : tasks_pending_for_key_) {
|
||
|
|
tasks_pending_.insert(tasks_pending_.begin(),
|
||
|
|
std::make_move_iterator(tasks_for_key.second.begin()),
|
||
|
|
std::make_move_iterator(tasks_for_key.second.end()));
|
||
|
|
}
|
||
|
|
tasks_pending_for_key_.clear();
|
||
|
|
|
||
|
|
while (!tasks_pending_.empty()) {
|
||
|
|
base::OnceClosure request_task = std::move(tasks_pending_.front());
|
||
|
|
tasks_pending_.pop_front();
|
||
|
|
std::move(request_task).Run();
|
||
|
|
}
|
||
|
|
|
||
|
|
DCHECK(tasks_pending_for_key_.empty());
|
||
|
|
|
||
|
|
finished_fetching_all_cookies_ = true;
|
||
|
|
keys_loaded_.clear();
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::EnsureCookiesMapIsValid() {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
// Iterate through all the of the cookies, grouped by host.
|
||
|
|
for (auto next = cookies_.begin(); next != cookies_.end();) {
|
||
|
|
auto cur_range_begin = next;
|
||
|
|
const std::string key = cur_range_begin->first; // Keep a copy.
|
||
|
|
auto cur_range_end = cookies_.upper_bound(key);
|
||
|
|
next = cur_range_end;
|
||
|
|
|
||
|
|
// Ensure no equivalent cookies for this host.
|
||
|
|
TrimDuplicateCookiesForKey(key, cur_range_begin, cur_range_end,
|
||
|
|
absl::nullopt);
|
||
|
|
}
|
||
|
|
|
||
|
|
for (auto cookie_partition_it = partitioned_cookies_.begin();
|
||
|
|
cookie_partition_it != partitioned_cookies_.end();) {
|
||
|
|
auto cur_cookie_partition_it = cookie_partition_it;
|
||
|
|
++cookie_partition_it;
|
||
|
|
|
||
|
|
// Iterate through the cookies in this partition, grouped by host.
|
||
|
|
CookieMap* cookie_partition = cur_cookie_partition_it->second.get();
|
||
|
|
auto prev_range_end = cookie_partition->begin();
|
||
|
|
while (prev_range_end != cookie_partition->end()) {
|
||
|
|
auto cur_range_begin = prev_range_end;
|
||
|
|
const std::string key = cur_range_begin->first; // Keep a copy.
|
||
|
|
auto cur_range_end = cookie_partition->upper_bound(key);
|
||
|
|
prev_range_end = cur_range_end;
|
||
|
|
|
||
|
|
// Ensure no equivalent cookies for this host and cookie partition key.
|
||
|
|
TrimDuplicateCookiesForKey(key, cur_range_begin, cur_range_end,
|
||
|
|
absl::make_optional(cur_cookie_partition_it));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Our strategy to find duplicates is:
|
||
|
|
// (1) Build a map from cookie unique key to
|
||
|
|
// {list of cookies with this signature, sorted by creation time}.
|
||
|
|
// (2) For each list with more than 1 entry, keep the cookie having the
|
||
|
|
// most recent creation time, and delete the others.
|
||
|
|
//
|
||
|
|
void CookieMonster::TrimDuplicateCookiesForKey(
|
||
|
|
const std::string& key,
|
||
|
|
CookieMap::iterator begin,
|
||
|
|
CookieMap::iterator end,
|
||
|
|
absl::optional<PartitionedCookieMap::iterator> cookie_partition_it) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
// Set of cookies ordered by creation time.
|
||
|
|
typedef std::multiset<CookieMap::iterator, OrderByCreationTimeDesc> CookieSet;
|
||
|
|
|
||
|
|
// Helper map we populate to find the duplicates.
|
||
|
|
typedef std::map<CanonicalCookie::UniqueCookieKey, CookieSet> EquivalenceMap;
|
||
|
|
EquivalenceMap equivalent_cookies;
|
||
|
|
|
||
|
|
// The number of duplicate cookies that have been found.
|
||
|
|
int num_duplicates = 0;
|
||
|
|
|
||
|
|
// Iterate through all of the cookies in our range, and insert them into
|
||
|
|
// the equivalence map.
|
||
|
|
for (auto it = begin; it != end; ++it) {
|
||
|
|
DCHECK_EQ(key, it->first);
|
||
|
|
CanonicalCookie* cookie = it->second.get();
|
||
|
|
|
||
|
|
CanonicalCookie::UniqueCookieKey signature(cookie->UniqueKey());
|
||
|
|
CookieSet& set = equivalent_cookies[signature];
|
||
|
|
|
||
|
|
// We found a duplicate!
|
||
|
|
if (!set.empty())
|
||
|
|
num_duplicates++;
|
||
|
|
|
||
|
|
// We save the iterator into |cookies_| rather than the actual cookie
|
||
|
|
// pointer, since we may need to delete it later.
|
||
|
|
set.insert(it);
|
||
|
|
}
|
||
|
|
|
||
|
|
// If there were no duplicates, we are done!
|
||
|
|
if (num_duplicates == 0)
|
||
|
|
return;
|
||
|
|
|
||
|
|
// Make sure we find everything below that we did above.
|
||
|
|
int num_duplicates_found = 0;
|
||
|
|
|
||
|
|
// Otherwise, delete all the duplicate cookies, both from our in-memory store
|
||
|
|
// and from the backing store.
|
||
|
|
for (std::pair<const CanonicalCookie::UniqueCookieKey, CookieSet>&
|
||
|
|
equivalent_cookie : equivalent_cookies) {
|
||
|
|
const CanonicalCookie::UniqueCookieKey& signature = equivalent_cookie.first;
|
||
|
|
CookieSet& dupes = equivalent_cookie.second;
|
||
|
|
|
||
|
|
if (dupes.size() <= 1)
|
||
|
|
continue; // This cookiename/path has no duplicates.
|
||
|
|
num_duplicates_found += dupes.size() - 1;
|
||
|
|
|
||
|
|
// Since |dupes| is sorted by creation time (descending), the first cookie
|
||
|
|
// is the most recent one (or tied for it), so we will keep it. The rest are
|
||
|
|
// duplicates.
|
||
|
|
dupes.erase(dupes.begin());
|
||
|
|
|
||
|
|
// TODO(crbug.com/1225444) Include cookie partition key in this log
|
||
|
|
// statement as well if needed.
|
||
|
|
LOG(ERROR) << base::StringPrintf(
|
||
|
|
"Found %d duplicate cookies for key='%s', "
|
||
|
|
"with {name='%s', domain='%s', path='%s'}",
|
||
|
|
static_cast<int>(dupes.size()), key.c_str(),
|
||
|
|
std::get<1>(signature).c_str(), std::get<2>(signature).c_str(),
|
||
|
|
std::get<3>(signature).c_str());
|
||
|
|
|
||
|
|
// Remove all the cookies identified by |dupes|. It is valid to delete our
|
||
|
|
// list of iterators one at a time, since |cookies_| is a multimap (they
|
||
|
|
// don't invalidate existing iterators following deletion).
|
||
|
|
for (const CookieMap::iterator& dupe : dupes) {
|
||
|
|
if (cookie_partition_it) {
|
||
|
|
InternalDeletePartitionedCookie(
|
||
|
|
cookie_partition_it.value(), dupe, true,
|
||
|
|
DELETE_COOKIE_DUPLICATE_IN_BACKING_STORE);
|
||
|
|
} else {
|
||
|
|
InternalDeleteCookie(dupe, true,
|
||
|
|
DELETE_COOKIE_DUPLICATE_IN_BACKING_STORE);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
DCHECK_EQ(num_duplicates, num_duplicates_found);
|
||
|
|
}
|
||
|
|
|
||
|
|
std::vector<CanonicalCookie*>
|
||
|
|
CookieMonster::FindCookiesForRegistryControlledHost(
|
||
|
|
const GURL& url,
|
||
|
|
CookieMap* cookie_map,
|
||
|
|
CookieMonster::PartitionedCookieMap::iterator* partition_it) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
if (!cookie_map)
|
||
|
|
cookie_map = &cookies_;
|
||
|
|
|
||
|
|
Time current_time = Time::Now();
|
||
|
|
|
||
|
|
// Retrieve all cookies for a given key
|
||
|
|
const std::string key(GetKey(url.host_piece()));
|
||
|
|
|
||
|
|
std::vector<CanonicalCookie*> cookies;
|
||
|
|
for (CookieMapItPair its = cookie_map->equal_range(key);
|
||
|
|
its.first != its.second;) {
|
||
|
|
auto curit = its.first;
|
||
|
|
CanonicalCookie* cc = curit->second.get();
|
||
|
|
++its.first;
|
||
|
|
|
||
|
|
// If the cookie is expired, delete it.
|
||
|
|
if (cc->IsExpired(current_time)) {
|
||
|
|
if (cc->IsPartitioned()) {
|
||
|
|
DCHECK(partition_it);
|
||
|
|
DCHECK_EQ((*partition_it)->second.get(), cookie_map);
|
||
|
|
InternalDeletePartitionedCookie(*partition_it, curit, true,
|
||
|
|
DELETE_COOKIE_EXPIRED);
|
||
|
|
} else {
|
||
|
|
InternalDeleteCookie(curit, true, DELETE_COOKIE_EXPIRED);
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
cookies.push_back(cc);
|
||
|
|
}
|
||
|
|
return cookies;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::vector<CanonicalCookie*>
|
||
|
|
CookieMonster::FindPartitionedCookiesForRegistryControlledHost(
|
||
|
|
const CookiePartitionKey& cookie_partition_key,
|
||
|
|
const GURL& url) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
PartitionedCookieMap::iterator it =
|
||
|
|
partitioned_cookies_.find(cookie_partition_key);
|
||
|
|
if (it == partitioned_cookies_.end())
|
||
|
|
return std::vector<CanonicalCookie*>();
|
||
|
|
|
||
|
|
return FindCookiesForRegistryControlledHost(url, it->second.get(), &it);
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::FilterCookiesWithOptions(
|
||
|
|
const GURL url,
|
||
|
|
const CookieOptions options,
|
||
|
|
std::vector<CanonicalCookie*>* cookie_ptrs,
|
||
|
|
CookieAccessResultList* included_cookies,
|
||
|
|
CookieAccessResultList* excluded_cookies) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
// Probe to save statistics relatively frequently. We do it here rather
|
||
|
|
// than in the set path as many websites won't set cookies, and we
|
||
|
|
// want to collect statistics whenever the browser's being used.
|
||
|
|
Time current_time = Time::Now();
|
||
|
|
RecordPeriodicStats(current_time);
|
||
|
|
|
||
|
|
bool delegate_treats_url_as_trustworthy =
|
||
|
|
cookie_access_delegate() &&
|
||
|
|
cookie_access_delegate()->ShouldTreatUrlAsTrustworthy(url);
|
||
|
|
|
||
|
|
for (CanonicalCookie* cookie_ptr : *cookie_ptrs) {
|
||
|
|
// Filter out cookies that should not be included for a request to the
|
||
|
|
// given |url|. HTTP only cookies are filtered depending on the passed
|
||
|
|
// cookie |options|.
|
||
|
|
CookieAccessResult access_result = cookie_ptr->IncludeForRequestURL(
|
||
|
|
url, options,
|
||
|
|
CookieAccessParams{
|
||
|
|
GetAccessSemanticsForCookie(*cookie_ptr),
|
||
|
|
delegate_treats_url_as_trustworthy,
|
||
|
|
cookie_util::GetSamePartyStatus(*cookie_ptr, options,
|
||
|
|
same_party_attribute_enabled_)});
|
||
|
|
|
||
|
|
if (!access_result.status.IsInclude()) {
|
||
|
|
if (options.return_excluded_cookies())
|
||
|
|
excluded_cookies->push_back({*cookie_ptr, access_result});
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (options.update_access_time())
|
||
|
|
InternalUpdateCookieAccessTime(cookie_ptr, current_time);
|
||
|
|
|
||
|
|
int destination_port = url.EffectiveIntPort();
|
||
|
|
|
||
|
|
if (IsLocalhost(url)) {
|
||
|
|
UMA_HISTOGRAM_ENUMERATION(
|
||
|
|
"Cookie.Port.Read.Localhost",
|
||
|
|
ReducePortRangeForCookieHistogram(destination_port));
|
||
|
|
UMA_HISTOGRAM_ENUMERATION(
|
||
|
|
"Cookie.Port.ReadDiffersFromSet.Localhost",
|
||
|
|
IsCookieSentToSamePortThatSetIt(url, cookie_ptr->SourcePort(),
|
||
|
|
cookie_ptr->SourceScheme()));
|
||
|
|
} else {
|
||
|
|
UMA_HISTOGRAM_ENUMERATION(
|
||
|
|
"Cookie.Port.Read.RemoteHost",
|
||
|
|
ReducePortRangeForCookieHistogram(destination_port));
|
||
|
|
UMA_HISTOGRAM_ENUMERATION(
|
||
|
|
"Cookie.Port.ReadDiffersFromSet.RemoteHost",
|
||
|
|
IsCookieSentToSamePortThatSetIt(url, cookie_ptr->SourcePort(),
|
||
|
|
cookie_ptr->SourceScheme()));
|
||
|
|
}
|
||
|
|
|
||
|
|
if (cookie_ptr->IsDomainCookie()) {
|
||
|
|
UMA_HISTOGRAM_ENUMERATION(
|
||
|
|
"Cookie.Port.ReadDiffersFromSet.DomainSet",
|
||
|
|
IsCookieSentToSamePortThatSetIt(url, cookie_ptr->SourcePort(),
|
||
|
|
cookie_ptr->SourceScheme()));
|
||
|
|
}
|
||
|
|
|
||
|
|
included_cookies->push_back({*cookie_ptr, access_result});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::MaybeDeleteEquivalentCookieAndUpdateStatus(
|
||
|
|
const std::string& key,
|
||
|
|
const CanonicalCookie& cookie_being_set,
|
||
|
|
bool allowed_to_set_secure_cookie,
|
||
|
|
bool skip_httponly,
|
||
|
|
bool already_expired,
|
||
|
|
base::Time* creation_date_to_inherit,
|
||
|
|
CookieInclusionStatus* status,
|
||
|
|
absl::optional<PartitionedCookieMap::iterator> cookie_partition_it) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
DCHECK(!status->HasExclusionReason(
|
||
|
|
CookieInclusionStatus::EXCLUDE_OVERWRITE_SECURE));
|
||
|
|
DCHECK(!status->HasExclusionReason(
|
||
|
|
CookieInclusionStatus::EXCLUDE_OVERWRITE_HTTP_ONLY));
|
||
|
|
|
||
|
|
CookieMap* cookie_map = &cookies_;
|
||
|
|
if (cookie_partition_it) {
|
||
|
|
cookie_map = cookie_partition_it.value()->second.get();
|
||
|
|
}
|
||
|
|
|
||
|
|
bool found_equivalent_cookie = false;
|
||
|
|
CookieMap::iterator deletion_candidate_it = cookie_map->end();
|
||
|
|
CanonicalCookie* skipped_secure_cookie = nullptr;
|
||
|
|
|
||
|
|
// Check every cookie matching this domain key for equivalence.
|
||
|
|
CookieMapItPair range_its = cookie_map->equal_range(key);
|
||
|
|
for (auto cur_it = range_its.first; cur_it != range_its.second; ++cur_it) {
|
||
|
|
CanonicalCookie* cur_existing_cookie = cur_it->second.get();
|
||
|
|
|
||
|
|
// Evaluate "Leave Secure Cookies Alone":
|
||
|
|
// If the cookie is being set from an insecure source, then if an
|
||
|
|
// "equivalent" Secure cookie already exists, then the cookie should *not*
|
||
|
|
// be updated.
|
||
|
|
//
|
||
|
|
// "Equivalent" means they are the same by
|
||
|
|
// IsEquivalentForSecureCookieMatching(). See the comment there for
|
||
|
|
// details. (Note this is not a symmetric comparison.) This notion of
|
||
|
|
// equivalence is slightly more inclusive than the usual IsEquivalent() one.
|
||
|
|
//
|
||
|
|
// See: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-alone
|
||
|
|
if (cur_existing_cookie->IsSecure() && !allowed_to_set_secure_cookie &&
|
||
|
|
cookie_being_set.IsEquivalentForSecureCookieMatching(
|
||
|
|
*cur_existing_cookie)) {
|
||
|
|
// Hold onto this for additional Netlogging later if we end up preserving
|
||
|
|
// a would-have-been-deleted cookie because of this.
|
||
|
|
skipped_secure_cookie = cur_existing_cookie;
|
||
|
|
net_log_.AddEvent(NetLogEventType::COOKIE_STORE_COOKIE_REJECTED_SECURE,
|
||
|
|
[&](NetLogCaptureMode capture_mode) {
|
||
|
|
return NetLogCookieMonsterCookieRejectedSecure(
|
||
|
|
skipped_secure_cookie, &cookie_being_set,
|
||
|
|
capture_mode);
|
||
|
|
});
|
||
|
|
status->AddExclusionReason(
|
||
|
|
CookieInclusionStatus::EXCLUDE_OVERWRITE_SECURE);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (cookie_being_set.IsEquivalent(*cur_existing_cookie)) {
|
||
|
|
// We should never have more than one equivalent cookie, since they should
|
||
|
|
// overwrite each other.
|
||
|
|
CHECK(!found_equivalent_cookie)
|
||
|
|
<< "Duplicate equivalent cookies found, cookie store is corrupted.";
|
||
|
|
DCHECK(deletion_candidate_it == cookie_map->end());
|
||
|
|
found_equivalent_cookie = true;
|
||
|
|
|
||
|
|
// The |cookie_being_set| is rejected for trying to overwrite an httponly
|
||
|
|
// cookie when it should not be able to.
|
||
|
|
if (skip_httponly && cur_existing_cookie->IsHttpOnly()) {
|
||
|
|
net_log_.AddEvent(
|
||
|
|
NetLogEventType::COOKIE_STORE_COOKIE_REJECTED_HTTPONLY,
|
||
|
|
[&](NetLogCaptureMode capture_mode) {
|
||
|
|
return NetLogCookieMonsterCookieRejectedHttponly(
|
||
|
|
cur_existing_cookie, &cookie_being_set, capture_mode);
|
||
|
|
});
|
||
|
|
status->AddExclusionReason(
|
||
|
|
CookieInclusionStatus::EXCLUDE_OVERWRITE_HTTP_ONLY);
|
||
|
|
} else {
|
||
|
|
deletion_candidate_it = cur_it;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (deletion_candidate_it != cookie_map->end()) {
|
||
|
|
CanonicalCookie* deletion_candidate = deletion_candidate_it->second.get();
|
||
|
|
if (deletion_candidate->Value() == cookie_being_set.Value())
|
||
|
|
*creation_date_to_inherit = deletion_candidate->CreationDate();
|
||
|
|
if (status->IsInclude()) {
|
||
|
|
if (cookie_being_set.IsPartitioned()) {
|
||
|
|
InternalDeletePartitionedCookie(
|
||
|
|
cookie_partition_it.value(), deletion_candidate_it,
|
||
|
|
true /* sync_to_store */,
|
||
|
|
already_expired ? DELETE_COOKIE_EXPIRED_OVERWRITE
|
||
|
|
: DELETE_COOKIE_OVERWRITE);
|
||
|
|
} else {
|
||
|
|
InternalDeleteCookie(deletion_candidate_it, true /* sync_to_store */,
|
||
|
|
already_expired ? DELETE_COOKIE_EXPIRED_OVERWRITE
|
||
|
|
: DELETE_COOKIE_OVERWRITE);
|
||
|
|
}
|
||
|
|
} else if (status->HasExclusionReason(
|
||
|
|
CookieInclusionStatus::EXCLUDE_OVERWRITE_SECURE)) {
|
||
|
|
// Log that we preserved a cookie that would have been deleted due to
|
||
|
|
// Leave Secure Cookies Alone. This arbitrarily only logs the last
|
||
|
|
// |skipped_secure_cookie| that we were left with after the for loop, even
|
||
|
|
// if there were multiple matching Secure cookies that were left alone.
|
||
|
|
DCHECK(skipped_secure_cookie);
|
||
|
|
net_log_.AddEvent(
|
||
|
|
NetLogEventType::COOKIE_STORE_COOKIE_PRESERVED_SKIPPED_SECURE,
|
||
|
|
[&](NetLogCaptureMode capture_mode) {
|
||
|
|
return NetLogCookieMonsterCookiePreservedSkippedSecure(
|
||
|
|
skipped_secure_cookie, deletion_candidate, &cookie_being_set,
|
||
|
|
capture_mode);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
CookieMonster::CookieMap::iterator CookieMonster::InternalInsertCookie(
|
||
|
|
const std::string& key,
|
||
|
|
std::unique_ptr<CanonicalCookie> cc,
|
||
|
|
bool sync_to_store,
|
||
|
|
const CookieAccessResult& access_result,
|
||
|
|
bool dispatch_change) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
CanonicalCookie* cc_ptr = cc.get();
|
||
|
|
|
||
|
|
net_log_.AddEvent(NetLogEventType::COOKIE_STORE_COOKIE_ADDED,
|
||
|
|
[&](NetLogCaptureMode capture_mode) {
|
||
|
|
return NetLogCookieMonsterCookieAdded(
|
||
|
|
cc.get(), sync_to_store, capture_mode);
|
||
|
|
});
|
||
|
|
if (ShouldUpdatePersistentStore(cc_ptr) && sync_to_store)
|
||
|
|
store_->AddCookie(*cc_ptr);
|
||
|
|
auto inserted = cookies_.insert(CookieMap::value_type(key, std::move(cc)));
|
||
|
|
|
||
|
|
LogCookieTypeToUMA(cc_ptr, access_result);
|
||
|
|
|
||
|
|
DCHECK(access_result.status.IsInclude());
|
||
|
|
if (dispatch_change) {
|
||
|
|
change_dispatcher_.DispatchChange(
|
||
|
|
CookieChangeInfo(*cc_ptr, access_result, CookieChangeCause::INSERTED),
|
||
|
|
true);
|
||
|
|
}
|
||
|
|
|
||
|
|
// If this is the first cookie in |cookies_| with this key, increment the
|
||
|
|
// |num_keys_| counter.
|
||
|
|
bool different_prev =
|
||
|
|
inserted == cookies_.begin() || std::prev(inserted)->first != key;
|
||
|
|
// According to std::multiqueue documentation:
|
||
|
|
// "If the container has elements with equivalent key, inserts at the upper
|
||
|
|
// bound of that range. (since C++11)"
|
||
|
|
// This means that "inserted" iterator either points to the last element in
|
||
|
|
// the map, or the element succeeding it has to have different key.
|
||
|
|
DCHECK(std::next(inserted) == cookies_.end() ||
|
||
|
|
std::next(inserted)->first != key);
|
||
|
|
if (different_prev)
|
||
|
|
++num_keys_;
|
||
|
|
|
||
|
|
return inserted;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool CookieMonster::ShouldUpdatePersistentStore(CanonicalCookie* cc) {
|
||
|
|
return (cc->IsPersistent() || persist_session_cookies_) && store_.get();
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::LogCookieTypeToUMA(
|
||
|
|
CanonicalCookie* cc,
|
||
|
|
const CookieAccessResult& access_result) {
|
||
|
|
int32_t type_sample =
|
||
|
|
!cc->IsEffectivelySameSiteNone(access_result.access_semantics)
|
||
|
|
? 1 << COOKIE_TYPE_SAME_SITE
|
||
|
|
: 0;
|
||
|
|
type_sample |= cc->IsHttpOnly() ? 1 << COOKIE_TYPE_HTTPONLY : 0;
|
||
|
|
type_sample |= cc->IsSecure() ? 1 << COOKIE_TYPE_SECURE : 0;
|
||
|
|
UMA_HISTOGRAM_EXACT_LINEAR("Cookie.Type", type_sample,
|
||
|
|
(1 << COOKIE_TYPE_LAST_ENTRY));
|
||
|
|
}
|
||
|
|
|
||
|
|
CookieMonster::PartitionedCookieMapIterators
|
||
|
|
CookieMonster::InternalInsertPartitionedCookie(
|
||
|
|
std::string key,
|
||
|
|
std::unique_ptr<CanonicalCookie> cc,
|
||
|
|
bool sync_to_store,
|
||
|
|
const CookieAccessResult& access_result,
|
||
|
|
bool dispatch_change) {
|
||
|
|
DCHECK(cc->IsPartitioned());
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
CanonicalCookie* cc_ptr = cc.get();
|
||
|
|
|
||
|
|
net_log_.AddEvent(NetLogEventType::COOKIE_STORE_COOKIE_ADDED,
|
||
|
|
[&](NetLogCaptureMode capture_mode) {
|
||
|
|
return NetLogCookieMonsterCookieAdded(
|
||
|
|
cc.get(), sync_to_store, capture_mode);
|
||
|
|
});
|
||
|
|
if (ShouldUpdatePersistentStore(cc_ptr) && sync_to_store)
|
||
|
|
store_->AddCookie(*cc_ptr);
|
||
|
|
|
||
|
|
CookiePartitionKey partition_key(cc->PartitionKey().value());
|
||
|
|
PartitionedCookieMap::iterator partition_it =
|
||
|
|
partitioned_cookies_.find(partition_key);
|
||
|
|
if (partition_it == partitioned_cookies_.end()) {
|
||
|
|
partition_it =
|
||
|
|
partitioned_cookies_
|
||
|
|
.insert(PartitionedCookieMap::value_type(
|
||
|
|
std::move(partition_key), std::make_unique<CookieMap>()))
|
||
|
|
.first;
|
||
|
|
}
|
||
|
|
|
||
|
|
CookieMap::iterator cookie_it = partition_it->second->insert(
|
||
|
|
CookieMap::value_type(std::move(key), std::move(cc)));
|
||
|
|
++num_partitioned_cookies_;
|
||
|
|
|
||
|
|
LogCookieTypeToUMA(cc_ptr, access_result);
|
||
|
|
|
||
|
|
DCHECK(access_result.status.IsInclude());
|
||
|
|
if (dispatch_change) {
|
||
|
|
change_dispatcher_.DispatchChange(
|
||
|
|
CookieChangeInfo(*cc_ptr, access_result, CookieChangeCause::INSERTED),
|
||
|
|
true);
|
||
|
|
}
|
||
|
|
|
||
|
|
return std::make_pair(partition_it, cookie_it);
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::SetCanonicalCookie(
|
||
|
|
std::unique_ptr<CanonicalCookie> cc,
|
||
|
|
const GURL& source_url,
|
||
|
|
const CookieOptions& options,
|
||
|
|
SetCookiesCallback callback,
|
||
|
|
absl::optional<CookieAccessResult> cookie_access_result) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
bool delegate_treats_url_as_trustworthy =
|
||
|
|
cookie_access_delegate() &&
|
||
|
|
cookie_access_delegate()->ShouldTreatUrlAsTrustworthy(source_url);
|
||
|
|
|
||
|
|
CookieAccessResult access_result = cc->IsSetPermittedInContext(
|
||
|
|
source_url, options,
|
||
|
|
CookieAccessParams(GetAccessSemanticsForCookie(*cc),
|
||
|
|
delegate_treats_url_as_trustworthy,
|
||
|
|
cookie_util::GetSamePartyStatus(
|
||
|
|
*cc, options, same_party_attribute_enabled_)),
|
||
|
|
cookieable_schemes_, cookie_access_result);
|
||
|
|
|
||
|
|
const std::string key(GetKey(cc->Domain()));
|
||
|
|
|
||
|
|
base::Time creation_date = cc->CreationDate();
|
||
|
|
if (creation_date.is_null()) {
|
||
|
|
creation_date = Time::Now();
|
||
|
|
cc->SetCreationDate(creation_date);
|
||
|
|
}
|
||
|
|
bool already_expired = cc->IsExpired(creation_date);
|
||
|
|
|
||
|
|
base::Time creation_date_to_inherit;
|
||
|
|
|
||
|
|
absl::optional<PartitionedCookieMap::iterator> cookie_partition_it;
|
||
|
|
bool should_try_to_delete_duplicates = true;
|
||
|
|
|
||
|
|
if (cc->IsPartitioned()) {
|
||
|
|
auto it = partitioned_cookies_.find(cc->PartitionKey().value());
|
||
|
|
if (it == partitioned_cookies_.end()) {
|
||
|
|
// This is the first cookie in its partition, so it won't have any
|
||
|
|
// duplicates.
|
||
|
|
should_try_to_delete_duplicates = false;
|
||
|
|
} else {
|
||
|
|
cookie_partition_it = absl::make_optional(it);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Iterates through existing cookies for the same eTLD+1, and potentially
|
||
|
|
// deletes an existing cookie, so any ExclusionReasons in |status| that would
|
||
|
|
// prevent such deletion should be finalized beforehand.
|
||
|
|
if (should_try_to_delete_duplicates) {
|
||
|
|
MaybeDeleteEquivalentCookieAndUpdateStatus(
|
||
|
|
key, *cc, access_result.is_allowed_to_access_secure_cookies,
|
||
|
|
options.exclude_httponly(), already_expired, &creation_date_to_inherit,
|
||
|
|
&access_result.status, cookie_partition_it);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (access_result.status.HasExclusionReason(
|
||
|
|
CookieInclusionStatus::EXCLUDE_OVERWRITE_SECURE) ||
|
||
|
|
access_result.status.HasExclusionReason(
|
||
|
|
CookieInclusionStatus::EXCLUDE_OVERWRITE_HTTP_ONLY)) {
|
||
|
|
DVLOG(net::cookie_util::kVlogSetCookies)
|
||
|
|
<< "SetCookie() not clobbering httponly cookie or secure cookie for "
|
||
|
|
"insecure scheme";
|
||
|
|
}
|
||
|
|
|
||
|
|
if (access_result.status.IsInclude()) {
|
||
|
|
DVLOG(net::cookie_util::kVlogSetCookies)
|
||
|
|
<< "SetCookie() key: " << key << " cc: " << cc->DebugString();
|
||
|
|
|
||
|
|
if (cc->IsEffectivelySameSiteNone()) {
|
||
|
|
UMA_HISTOGRAM_COUNTS_10000("Cookie.SameSiteNoneSizeBytes",
|
||
|
|
NameValueSizeBytes(*cc));
|
||
|
|
}
|
||
|
|
|
||
|
|
bool is_partitioned_cookie = cc->IsPartitioned();
|
||
|
|
CookiePartitionKey cookie_partition_key;
|
||
|
|
if (is_partitioned_cookie)
|
||
|
|
cookie_partition_key = cc->PartitionKey().value();
|
||
|
|
|
||
|
|
// Realize that we might be setting an expired cookie, and the only point
|
||
|
|
// was to delete the cookie which we've already done.
|
||
|
|
if (!already_expired) {
|
||
|
|
HistogramExpirationDuration(*cc, creation_date);
|
||
|
|
|
||
|
|
// Histogram the type of scheme used on URLs that set cookies. This
|
||
|
|
// intentionally includes cookies that are set or overwritten by
|
||
|
|
// http:// URLs, but not cookies that are cleared by http:// URLs, to
|
||
|
|
// understand if the former behavior can be deprecated for Secure
|
||
|
|
// cookies.
|
||
|
|
// TODO(crbug.com/993120): Consider removing this histogram. The decision
|
||
|
|
// it was added to evaluate has been implemented and standardized.
|
||
|
|
CookieSource cookie_source_sample =
|
||
|
|
(source_url.SchemeIsCryptographic()
|
||
|
|
? (cc->IsSecure()
|
||
|
|
? CookieSource::kSecureCookieCryptographicScheme
|
||
|
|
: CookieSource::kNonsecureCookieCryptographicScheme)
|
||
|
|
: (cc->IsSecure()
|
||
|
|
? CookieSource::kSecureCookieNoncryptographicScheme
|
||
|
|
: CookieSource::kNonsecureCookieNoncryptographicScheme));
|
||
|
|
UMA_HISTOGRAM_ENUMERATION("Cookie.CookieSourceScheme",
|
||
|
|
cookie_source_sample);
|
||
|
|
|
||
|
|
UMA_HISTOGRAM_BOOLEAN("Cookie.DomainSet", cc->IsDomainCookie());
|
||
|
|
|
||
|
|
if (!creation_date_to_inherit.is_null()) {
|
||
|
|
cc->SetCreationDate(creation_date_to_inherit);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (is_partitioned_cookie) {
|
||
|
|
InternalInsertPartitionedCookie(key, std::move(cc), true,
|
||
|
|
access_result);
|
||
|
|
} else {
|
||
|
|
InternalInsertCookie(key, std::move(cc), true, access_result);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
DVLOG(net::cookie_util::kVlogSetCookies)
|
||
|
|
<< "SetCookie() not storing already expired cookie.";
|
||
|
|
}
|
||
|
|
|
||
|
|
// We assume that hopefully setting a cookie will be less common than
|
||
|
|
// querying a cookie. Since setting a cookie can put us over our limits,
|
||
|
|
// make sure that we garbage collect... We can also make the assumption
|
||
|
|
// that if a cookie was set, in the common case it will be used soon after,
|
||
|
|
// and we will purge the expired cookies in GetCookies().
|
||
|
|
if (is_partitioned_cookie) {
|
||
|
|
GarbageCollectPartitionedCookies(creation_date, cookie_partition_key,
|
||
|
|
key);
|
||
|
|
} else {
|
||
|
|
GarbageCollect(creation_date, key);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (IsLocalhost(source_url)) {
|
||
|
|
UMA_HISTOGRAM_ENUMERATION(
|
||
|
|
"Cookie.Port.Set.Localhost",
|
||
|
|
ReducePortRangeForCookieHistogram(source_url.EffectiveIntPort()));
|
||
|
|
} else {
|
||
|
|
UMA_HISTOGRAM_ENUMERATION(
|
||
|
|
"Cookie.Port.Set.RemoteHost",
|
||
|
|
ReducePortRangeForCookieHistogram(source_url.EffectiveIntPort()));
|
||
|
|
}
|
||
|
|
|
||
|
|
UMA_HISTOGRAM_ENUMERATION("Cookie.CookieSourceSchemeName",
|
||
|
|
GetSchemeNameEnum(source_url));
|
||
|
|
}
|
||
|
|
|
||
|
|
// TODO(chlily): Log metrics.
|
||
|
|
MaybeRunCookieCallback(std::move(callback), access_result);
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::SetAllCookies(CookieList list,
|
||
|
|
SetCookiesCallback callback) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
// Nuke the existing store.
|
||
|
|
while (!cookies_.empty()) {
|
||
|
|
// TODO(rdsmith): The CANONICAL is a lie.
|
||
|
|
InternalDeleteCookie(cookies_.begin(), true, DELETE_COOKIE_EXPLICIT);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Set all passed in cookies.
|
||
|
|
for (const auto& cookie : list) {
|
||
|
|
const std::string key(GetKey(cookie.Domain()));
|
||
|
|
Time creation_time = cookie.CreationDate();
|
||
|
|
if (cookie.IsExpired(creation_time))
|
||
|
|
continue;
|
||
|
|
|
||
|
|
HistogramExpirationDuration(cookie, creation_time);
|
||
|
|
|
||
|
|
CookieAccessResult access_result;
|
||
|
|
access_result.access_semantics = GetAccessSemanticsForCookie(cookie);
|
||
|
|
|
||
|
|
if (cookie.IsPartitioned()) {
|
||
|
|
InternalInsertPartitionedCookie(
|
||
|
|
key, std::make_unique<CanonicalCookie>(cookie), true, access_result);
|
||
|
|
GarbageCollectPartitionedCookies(creation_time,
|
||
|
|
cookie.PartitionKey().value(), key);
|
||
|
|
} else {
|
||
|
|
InternalInsertCookie(key, std::make_unique<CanonicalCookie>(cookie), true,
|
||
|
|
access_result);
|
||
|
|
GarbageCollect(creation_time, key);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TODO(rdsmith): If this function always returns the same value, it
|
||
|
|
// shouldn't have a return value. But it should also be deleted (see
|
||
|
|
// https://codereview.chromium.org/2882063002/#msg64), which would
|
||
|
|
// solve the return value problem.
|
||
|
|
MaybeRunCookieCallback(std::move(callback), CookieAccessResult());
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::InternalUpdateCookieAccessTime(CanonicalCookie* cc,
|
||
|
|
const Time& current) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
// Based off the Mozilla code. When a cookie has been accessed recently,
|
||
|
|
// don't bother updating its access time again. This reduces the number of
|
||
|
|
// updates we do during pageload, which in turn reduces the chance our storage
|
||
|
|
// backend will hit its batch thresholds and be forced to update.
|
||
|
|
if ((current - cc->LastAccessDate()) < last_access_threshold_)
|
||
|
|
return;
|
||
|
|
|
||
|
|
cc->SetLastAccessDate(current);
|
||
|
|
if (ShouldUpdatePersistentStore(cc))
|
||
|
|
store_->UpdateCookieAccessTime(*cc);
|
||
|
|
}
|
||
|
|
|
||
|
|
// InternalDeleteCookies must not invalidate iterators other than the one being
|
||
|
|
// deleted.
|
||
|
|
void CookieMonster::InternalDeleteCookie(CookieMap::iterator it,
|
||
|
|
bool sync_to_store,
|
||
|
|
DeletionCause deletion_cause) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
// Ideally, this would be asserted up where we define kChangeCauseMapping,
|
||
|
|
// but DeletionCause's visibility (or lack thereof) forces us to make
|
||
|
|
// this check here.
|
||
|
|
static_assert(std::size(kChangeCauseMapping) == DELETE_COOKIE_LAST_ENTRY + 1,
|
||
|
|
"kChangeCauseMapping size should match DeletionCause size");
|
||
|
|
|
||
|
|
CanonicalCookie* cc = it->second.get();
|
||
|
|
DVLOG(net::cookie_util::kVlogSetCookies)
|
||
|
|
<< "InternalDeleteCookie()"
|
||
|
|
<< ", cause:" << deletion_cause << ", cc: " << cc->DebugString();
|
||
|
|
|
||
|
|
ChangeCausePair mapping = kChangeCauseMapping[deletion_cause];
|
||
|
|
if (deletion_cause != DELETE_COOKIE_DONT_RECORD) {
|
||
|
|
net_log_.AddEvent(NetLogEventType::COOKIE_STORE_COOKIE_DELETED,
|
||
|
|
[&](NetLogCaptureMode capture_mode) {
|
||
|
|
return NetLogCookieMonsterCookieDeleted(
|
||
|
|
cc, mapping.cause, sync_to_store, capture_mode);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (ShouldUpdatePersistentStore(cc) && sync_to_store)
|
||
|
|
store_->DeleteCookie(*cc);
|
||
|
|
|
||
|
|
change_dispatcher_.DispatchChange(
|
||
|
|
CookieChangeInfo(
|
||
|
|
*cc,
|
||
|
|
CookieAccessResult(CookieEffectiveSameSite::UNDEFINED,
|
||
|
|
CookieInclusionStatus(),
|
||
|
|
GetAccessSemanticsForCookie(*cc),
|
||
|
|
true /* is_allowed_to_access_secure_cookies */),
|
||
|
|
mapping.cause),
|
||
|
|
mapping.notify);
|
||
|
|
|
||
|
|
// If this is the last cookie in |cookies_| with this key, decrement the
|
||
|
|
// |num_keys_| counter.
|
||
|
|
bool different_prev =
|
||
|
|
it == cookies_.begin() || std::prev(it)->first != it->first;
|
||
|
|
bool different_next =
|
||
|
|
std::next(it) == cookies_.end() || std::next(it)->first != it->first;
|
||
|
|
if (different_prev && different_next)
|
||
|
|
--num_keys_;
|
||
|
|
|
||
|
|
DCHECK(cookies_.find(it->first) != cookies_.end())
|
||
|
|
<< "Called erase with an iterator not in the cookie map";
|
||
|
|
cookies_.erase(it);
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::InternalDeletePartitionedCookie(
|
||
|
|
PartitionedCookieMap::iterator partition_it,
|
||
|
|
CookieMap::iterator cookie_it,
|
||
|
|
bool sync_to_store,
|
||
|
|
DeletionCause deletion_cause) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
// Ideally, this would be asserted up where we define kChangeCauseMapping,
|
||
|
|
// but DeletionCause's visibility (or lack thereof) forces us to make
|
||
|
|
// this check here.
|
||
|
|
static_assert(std::size(kChangeCauseMapping) == DELETE_COOKIE_LAST_ENTRY + 1,
|
||
|
|
"kChangeCauseMapping size should match DeletionCause size");
|
||
|
|
|
||
|
|
CanonicalCookie* cc = cookie_it->second.get();
|
||
|
|
DCHECK(cc->IsPartitioned());
|
||
|
|
DVLOG(net::cookie_util::kVlogSetCookies)
|
||
|
|
<< "InternalDeletePartitionedCookie()"
|
||
|
|
<< ", cause:" << deletion_cause << ", cc: " << cc->DebugString();
|
||
|
|
|
||
|
|
ChangeCausePair mapping = kChangeCauseMapping[deletion_cause];
|
||
|
|
if (deletion_cause != DELETE_COOKIE_DONT_RECORD) {
|
||
|
|
net_log_.AddEvent(NetLogEventType::COOKIE_STORE_COOKIE_DELETED,
|
||
|
|
[&](NetLogCaptureMode capture_mode) {
|
||
|
|
return NetLogCookieMonsterCookieDeleted(
|
||
|
|
cc, mapping.cause, sync_to_store, capture_mode);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (ShouldUpdatePersistentStore(cc) && sync_to_store)
|
||
|
|
store_->DeleteCookie(*cc);
|
||
|
|
|
||
|
|
change_dispatcher_.DispatchChange(
|
||
|
|
CookieChangeInfo(
|
||
|
|
*cc,
|
||
|
|
CookieAccessResult(CookieEffectiveSameSite::UNDEFINED,
|
||
|
|
CookieInclusionStatus(),
|
||
|
|
GetAccessSemanticsForCookie(*cc),
|
||
|
|
true /* is_allowed_to_access_secure_cookies */),
|
||
|
|
mapping.cause),
|
||
|
|
mapping.notify);
|
||
|
|
|
||
|
|
DCHECK(partition_it->second->find(cookie_it->first) !=
|
||
|
|
partition_it->second->end())
|
||
|
|
<< "Called erase with an iterator not in this partitioned cookie map";
|
||
|
|
partition_it->second->erase(cookie_it);
|
||
|
|
--num_partitioned_cookies_;
|
||
|
|
|
||
|
|
if (partition_it->second->empty())
|
||
|
|
partitioned_cookies_.erase(partition_it);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Domain expiry behavior is unchanged by key/expiry scheme (the
|
||
|
|
// meaning of the key is different, but that's not visible to this routine).
|
||
|
|
size_t CookieMonster::GarbageCollect(const Time& current,
|
||
|
|
const std::string& key) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
size_t num_deleted = 0;
|
||
|
|
Time safe_date(Time::Now() - base::Days(kSafeFromGlobalPurgeDays));
|
||
|
|
|
||
|
|
// Collect garbage for this key, minding cookie priorities.
|
||
|
|
if (cookies_.count(key) > kDomainMaxCookies) {
|
||
|
|
DVLOG(net::cookie_util::kVlogGarbageCollection)
|
||
|
|
<< "GarbageCollect() key: " << key;
|
||
|
|
|
||
|
|
CookieItVector* cookie_its;
|
||
|
|
|
||
|
|
CookieItVector non_expired_cookie_its;
|
||
|
|
cookie_its = &non_expired_cookie_its;
|
||
|
|
num_deleted +=
|
||
|
|
GarbageCollectExpired(current, cookies_.equal_range(key), cookie_its);
|
||
|
|
|
||
|
|
if (cookie_its->size() > kDomainMaxCookies) {
|
||
|
|
DVLOG(net::cookie_util::kVlogGarbageCollection)
|
||
|
|
<< "Deep Garbage Collect domain.";
|
||
|
|
|
||
|
|
if (domain_purged_keys_.size() < kMaxDomainPurgedKeys)
|
||
|
|
domain_purged_keys_.insert(key);
|
||
|
|
|
||
|
|
size_t purge_goal =
|
||
|
|
cookie_its->size() - (kDomainMaxCookies - kDomainPurgeCookies);
|
||
|
|
DCHECK(purge_goal > kDomainPurgeCookies);
|
||
|
|
|
||
|
|
// Sort the cookies by access date, from least-recent to most-recent.
|
||
|
|
std::sort(cookie_its->begin(), cookie_its->end(), LRACookieSorter);
|
||
|
|
|
||
|
|
// Remove all but the kDomainCookiesQuotaLow most-recently accessed
|
||
|
|
// cookies with low-priority. Then, if cookies still need to be removed,
|
||
|
|
// bump the quota and remove low- and medium-priority. Then, if cookies
|
||
|
|
// _still_ need to be removed, bump the quota and remove cookies with
|
||
|
|
// any priority.
|
||
|
|
//
|
||
|
|
// 1. Low-priority non-secure cookies.
|
||
|
|
// 2. Low-priority secure cookies.
|
||
|
|
// 3. Medium-priority non-secure cookies.
|
||
|
|
// 4. High-priority non-secure cookies.
|
||
|
|
// 5. Medium-priority secure cookies.
|
||
|
|
// 6. High-priority secure cookies.
|
||
|
|
constexpr struct {
|
||
|
|
CookiePriority priority;
|
||
|
|
bool protect_secure_cookies;
|
||
|
|
} kPurgeRounds[] = {
|
||
|
|
// 1. Low-priority non-secure cookies.
|
||
|
|
{COOKIE_PRIORITY_LOW, true},
|
||
|
|
// 2. Low-priority secure cookies.
|
||
|
|
{COOKIE_PRIORITY_LOW, false},
|
||
|
|
// 3. Medium-priority non-secure cookies.
|
||
|
|
{COOKIE_PRIORITY_MEDIUM, true},
|
||
|
|
// 4. High-priority non-secure cookies.
|
||
|
|
{COOKIE_PRIORITY_HIGH, true},
|
||
|
|
// 5. Medium-priority secure cookies.
|
||
|
|
{COOKIE_PRIORITY_MEDIUM, false},
|
||
|
|
// 6. High-priority secure cookies.
|
||
|
|
{COOKIE_PRIORITY_HIGH, false},
|
||
|
|
};
|
||
|
|
|
||
|
|
size_t quota = 0;
|
||
|
|
for (const auto& purge_round : kPurgeRounds) {
|
||
|
|
// Adjust quota according to the priority of cookies. Each round should
|
||
|
|
// protect certain number of cookies in order to avoid starvation.
|
||
|
|
// For example, when each round starts to remove cookies, the number of
|
||
|
|
// cookies of that priority are counted and a decision whether they
|
||
|
|
// should be deleted or not is made. If yes, some number of cookies of
|
||
|
|
// that priority are deleted considering the quota.
|
||
|
|
switch (purge_round.priority) {
|
||
|
|
case COOKIE_PRIORITY_LOW:
|
||
|
|
quota = kDomainCookiesQuotaLow;
|
||
|
|
break;
|
||
|
|
case COOKIE_PRIORITY_MEDIUM:
|
||
|
|
quota = kDomainCookiesQuotaMedium;
|
||
|
|
break;
|
||
|
|
case COOKIE_PRIORITY_HIGH:
|
||
|
|
quota = kDomainCookiesQuotaHigh;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
size_t just_deleted = 0u;
|
||
|
|
// Purge up to |purge_goal| for all cookies at the given priority. This
|
||
|
|
// path will be taken only if the initial non-secure purge did not evict
|
||
|
|
// enough cookies.
|
||
|
|
if (purge_goal > 0) {
|
||
|
|
just_deleted = PurgeLeastRecentMatches(
|
||
|
|
cookie_its, purge_round.priority, quota, purge_goal,
|
||
|
|
purge_round.protect_secure_cookies);
|
||
|
|
DCHECK_LE(just_deleted, purge_goal);
|
||
|
|
purge_goal -= just_deleted;
|
||
|
|
num_deleted += just_deleted;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
DCHECK_EQ(0u, purge_goal);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Collect garbage for everything. With firefox style we want to preserve
|
||
|
|
// cookies accessed in kSafeFromGlobalPurgeDays, otherwise evict.
|
||
|
|
if (cookies_.size() > kMaxCookies && earliest_access_time_ < safe_date) {
|
||
|
|
DVLOG(net::cookie_util::kVlogGarbageCollection)
|
||
|
|
<< "GarbageCollect() everything";
|
||
|
|
CookieItVector cookie_its;
|
||
|
|
|
||
|
|
num_deleted += GarbageCollectExpired(
|
||
|
|
current, CookieMapItPair(cookies_.begin(), cookies_.end()),
|
||
|
|
&cookie_its);
|
||
|
|
|
||
|
|
if (cookie_its.size() > kMaxCookies) {
|
||
|
|
DVLOG(net::cookie_util::kVlogGarbageCollection)
|
||
|
|
<< "Deep Garbage Collect everything.";
|
||
|
|
size_t purge_goal = cookie_its.size() - (kMaxCookies - kPurgeCookies);
|
||
|
|
DCHECK(purge_goal > kPurgeCookies);
|
||
|
|
|
||
|
|
CookieItVector secure_cookie_its;
|
||
|
|
CookieItVector non_secure_cookie_its;
|
||
|
|
SplitCookieVectorIntoSecureAndNonSecure(cookie_its, &secure_cookie_its,
|
||
|
|
&non_secure_cookie_its);
|
||
|
|
size_t non_secure_purge_goal =
|
||
|
|
std::min<size_t>(purge_goal, non_secure_cookie_its.size());
|
||
|
|
|
||
|
|
base::Time earliest_non_secure_access_time;
|
||
|
|
size_t just_deleted = GarbageCollectLeastRecentlyAccessed(
|
||
|
|
current, safe_date, non_secure_purge_goal, non_secure_cookie_its,
|
||
|
|
&earliest_non_secure_access_time);
|
||
|
|
num_deleted += just_deleted;
|
||
|
|
|
||
|
|
if (secure_cookie_its.size() == 0) {
|
||
|
|
// This case is unlikely, but should still update
|
||
|
|
// |earliest_access_time_| if only have non-secure cookies.
|
||
|
|
earliest_access_time_ = earliest_non_secure_access_time;
|
||
|
|
// Garbage collection can't delete all cookies.
|
||
|
|
DCHECK(!earliest_access_time_.is_null());
|
||
|
|
} else if (just_deleted < purge_goal) {
|
||
|
|
size_t secure_purge_goal = std::min<size_t>(purge_goal - just_deleted,
|
||
|
|
secure_cookie_its.size());
|
||
|
|
base::Time earliest_secure_access_time;
|
||
|
|
num_deleted += GarbageCollectLeastRecentlyAccessed(
|
||
|
|
current, safe_date, secure_purge_goal, secure_cookie_its,
|
||
|
|
&earliest_secure_access_time);
|
||
|
|
|
||
|
|
if (!earliest_non_secure_access_time.is_null() &&
|
||
|
|
earliest_non_secure_access_time < earliest_secure_access_time) {
|
||
|
|
earliest_access_time_ = earliest_non_secure_access_time;
|
||
|
|
} else {
|
||
|
|
earliest_access_time_ = earliest_secure_access_time;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Garbage collection can't delete all cookies.
|
||
|
|
DCHECK(!earliest_access_time_.is_null());
|
||
|
|
}
|
||
|
|
|
||
|
|
// If there are secure cookies, but deleting non-secure cookies was enough
|
||
|
|
// to meet the purge goal, secure cookies are never examined, so
|
||
|
|
// |earliest_access_time_| can't be determined. Leaving it alone will mean
|
||
|
|
// it's no later than the real earliest last access time, so this won't
|
||
|
|
// lead to any problems.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return num_deleted;
|
||
|
|
}
|
||
|
|
|
||
|
|
size_t CookieMonster::GarbageCollectPartitionedCookies(
|
||
|
|
const base::Time& current,
|
||
|
|
const CookiePartitionKey& cookie_partition_key,
|
||
|
|
const std::string& key) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
size_t num_deleted = 0;
|
||
|
|
PartitionedCookieMap::iterator cookie_partition_it =
|
||
|
|
partitioned_cookies_.find(cookie_partition_key);
|
||
|
|
|
||
|
|
if (cookie_partition_it == partitioned_cookies_.end())
|
||
|
|
return num_deleted;
|
||
|
|
|
||
|
|
if (NumBytesInCookieMapForKey(*cookie_partition_it->second.get(), key) >
|
||
|
|
kPerPartitionDomainMaxCookieBytes ||
|
||
|
|
cookie_partition_it->second->count(key) > kPerPartitionDomainMaxCookies) {
|
||
|
|
// TODO(crbug.com/1225444): Log garbage collection for partitioned cookies.
|
||
|
|
|
||
|
|
CookieItVector non_expired_cookie_its;
|
||
|
|
num_deleted += GarbageCollectExpiredPartitionedCookies(
|
||
|
|
current, cookie_partition_it,
|
||
|
|
cookie_partition_it->second->equal_range(key), &non_expired_cookie_its);
|
||
|
|
|
||
|
|
size_t bytes_used = NumBytesInCookieItVector(non_expired_cookie_its);
|
||
|
|
|
||
|
|
if (bytes_used > kPerPartitionDomainMaxCookieBytes ||
|
||
|
|
non_expired_cookie_its.size() > kPerPartitionDomainMaxCookies) {
|
||
|
|
// TODO(crbug.com/1225444): Log deep garbage collection for partitioned
|
||
|
|
// cookies.
|
||
|
|
std::sort(non_expired_cookie_its.begin(), non_expired_cookie_its.end(),
|
||
|
|
LRACookieSorter);
|
||
|
|
|
||
|
|
for (size_t i = 0;
|
||
|
|
bytes_used > kPerPartitionDomainMaxCookieBytes ||
|
||
|
|
non_expired_cookie_its.size() - i > kPerPartitionDomainMaxCookies;
|
||
|
|
++i) {
|
||
|
|
bytes_used -= NameValueSizeBytes(*non_expired_cookie_its[i]->second);
|
||
|
|
InternalDeletePartitionedCookie(
|
||
|
|
cookie_partition_it, non_expired_cookie_its[i], true,
|
||
|
|
DELETE_COOKIE_EVICTED_PER_PARTITION_DOMAIN);
|
||
|
|
++num_deleted;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TODO(crbug.com/1225444): Enforce global limit on partitioned cookies.
|
||
|
|
|
||
|
|
return num_deleted;
|
||
|
|
}
|
||
|
|
|
||
|
|
size_t CookieMonster::PurgeLeastRecentMatches(CookieItVector* cookies,
|
||
|
|
CookiePriority priority,
|
||
|
|
size_t to_protect,
|
||
|
|
size_t purge_goal,
|
||
|
|
bool protect_secure_cookies) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
// 1. Count number of the cookies at |priority|
|
||
|
|
size_t cookies_count_possibly_to_be_deleted = CountCookiesForPossibleDeletion(
|
||
|
|
priority, cookies, false /* count all cookies */);
|
||
|
|
|
||
|
|
// 2. If |cookies_count_possibly_to_be_deleted| at |priority| is less than or
|
||
|
|
// equal |to_protect|, skip round in order to preserve the quota. This
|
||
|
|
// involves secure and non-secure cookies at |priority|.
|
||
|
|
if (cookies_count_possibly_to_be_deleted <= to_protect)
|
||
|
|
return 0u;
|
||
|
|
|
||
|
|
// 3. Calculate number of secure cookies at |priority|
|
||
|
|
// and number of cookies at |priority| that can possibly be deleted.
|
||
|
|
// It is guaranteed we do not delete more than |purge_goal| even if
|
||
|
|
// |cookies_count_possibly_to_be_deleted| is higher.
|
||
|
|
size_t secure_cookies = 0u;
|
||
|
|
if (protect_secure_cookies) {
|
||
|
|
secure_cookies = CountCookiesForPossibleDeletion(
|
||
|
|
priority, cookies, protect_secure_cookies /* count secure cookies */);
|
||
|
|
cookies_count_possibly_to_be_deleted -=
|
||
|
|
std::max(secure_cookies, to_protect);
|
||
|
|
} else {
|
||
|
|
cookies_count_possibly_to_be_deleted -= to_protect;
|
||
|
|
}
|
||
|
|
|
||
|
|
size_t removed = 0u;
|
||
|
|
size_t current = 0u;
|
||
|
|
while ((removed < purge_goal && current < cookies->size()) &&
|
||
|
|
cookies_count_possibly_to_be_deleted > 0) {
|
||
|
|
const CanonicalCookie* current_cookie = cookies->at(current)->second.get();
|
||
|
|
// Only delete the current cookie if the priority is equal to
|
||
|
|
// the current level.
|
||
|
|
if (IsCookieEligibleForEviction(priority, protect_secure_cookies,
|
||
|
|
current_cookie)) {
|
||
|
|
InternalDeleteCookie(cookies->at(current), true,
|
||
|
|
DELETE_COOKIE_EVICTED_DOMAIN);
|
||
|
|
cookies->erase(cookies->begin() + current);
|
||
|
|
removed++;
|
||
|
|
cookies_count_possibly_to_be_deleted--;
|
||
|
|
} else {
|
||
|
|
current++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return removed;
|
||
|
|
}
|
||
|
|
|
||
|
|
size_t CookieMonster::GarbageCollectExpired(const Time& current,
|
||
|
|
const CookieMapItPair& itpair,
|
||
|
|
CookieItVector* cookie_its) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
int num_deleted = 0;
|
||
|
|
for (CookieMap::iterator it = itpair.first, end = itpair.second; it != end;) {
|
||
|
|
auto curit = it;
|
||
|
|
++it;
|
||
|
|
|
||
|
|
if (curit->second->IsExpired(current)) {
|
||
|
|
InternalDeleteCookie(curit, true, DELETE_COOKIE_EXPIRED);
|
||
|
|
++num_deleted;
|
||
|
|
} else if (cookie_its) {
|
||
|
|
cookie_its->push_back(curit);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return num_deleted;
|
||
|
|
}
|
||
|
|
|
||
|
|
size_t CookieMonster::GarbageCollectExpiredPartitionedCookies(
|
||
|
|
const Time& current,
|
||
|
|
const PartitionedCookieMap::iterator& cookie_partition_it,
|
||
|
|
const CookieMapItPair& itpair,
|
||
|
|
CookieItVector* cookie_its) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
int num_deleted = 0;
|
||
|
|
for (CookieMap::iterator it = itpair.first, end = itpair.second; it != end;) {
|
||
|
|
auto curit = it;
|
||
|
|
++it;
|
||
|
|
|
||
|
|
if (curit->second->IsExpired(current)) {
|
||
|
|
InternalDeletePartitionedCookie(cookie_partition_it, curit, true,
|
||
|
|
DELETE_COOKIE_EXPIRED);
|
||
|
|
++num_deleted;
|
||
|
|
} else if (cookie_its) {
|
||
|
|
cookie_its->push_back(curit);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return num_deleted;
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::GarbageCollectAllExpiredPartitionedCookies(
|
||
|
|
const Time& current) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
for (auto it = partitioned_cookies_.begin();
|
||
|
|
it != partitioned_cookies_.end();) {
|
||
|
|
// GarbageCollectExpiredPartitionedCookies calls
|
||
|
|
// InternalDeletePartitionedCookie which may invalidate
|
||
|
|
// |cur_cookie_partition_it|.
|
||
|
|
auto cur_cookie_partition_it = it;
|
||
|
|
++it;
|
||
|
|
GarbageCollectExpiredPartitionedCookies(
|
||
|
|
current, cur_cookie_partition_it,
|
||
|
|
CookieMapItPair(cur_cookie_partition_it->second->begin(),
|
||
|
|
cur_cookie_partition_it->second->end()),
|
||
|
|
nullptr /*cookie_its*/);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
size_t CookieMonster::GarbageCollectDeleteRange(
|
||
|
|
const Time& current,
|
||
|
|
DeletionCause cause,
|
||
|
|
CookieItVector::iterator it_begin,
|
||
|
|
CookieItVector::iterator it_end) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
for (auto it = it_begin; it != it_end; it++) {
|
||
|
|
InternalDeleteCookie((*it), true, cause);
|
||
|
|
}
|
||
|
|
return it_end - it_begin;
|
||
|
|
}
|
||
|
|
|
||
|
|
size_t CookieMonster::GarbageCollectLeastRecentlyAccessed(
|
||
|
|
const base::Time& current,
|
||
|
|
const base::Time& safe_date,
|
||
|
|
size_t purge_goal,
|
||
|
|
CookieItVector cookie_its,
|
||
|
|
base::Time* earliest_time) {
|
||
|
|
DCHECK_LE(purge_goal, cookie_its.size());
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
// Sorts up to *and including* |cookie_its[purge_goal]| (if it exists), so
|
||
|
|
// |earliest_time| will be properly assigned even if
|
||
|
|
// |global_purge_it| == |cookie_its.begin() + purge_goal|.
|
||
|
|
SortLeastRecentlyAccessed(
|
||
|
|
cookie_its.begin(), cookie_its.end(),
|
||
|
|
cookie_its.size() < purge_goal ? purge_goal + 1 : purge_goal);
|
||
|
|
// Find boundary to cookies older than safe_date.
|
||
|
|
auto global_purge_it = LowerBoundAccessDate(
|
||
|
|
cookie_its.begin(), cookie_its.begin() + purge_goal, safe_date);
|
||
|
|
// Only delete the old cookies and delete non-secure ones first.
|
||
|
|
size_t num_deleted =
|
||
|
|
GarbageCollectDeleteRange(current, DELETE_COOKIE_EVICTED_GLOBAL,
|
||
|
|
cookie_its.begin(), global_purge_it);
|
||
|
|
if (global_purge_it != cookie_its.end())
|
||
|
|
*earliest_time = (*global_purge_it)->second->LastAccessDate();
|
||
|
|
return num_deleted;
|
||
|
|
}
|
||
|
|
|
||
|
|
// A wrapper around registry_controlled_domains::GetDomainAndRegistry
|
||
|
|
// to make clear we're creating a key for our local map or for the persistent
|
||
|
|
// store's use. Here and in FindCookiesForRegistryControlledHost() are the only
|
||
|
|
// two places where we need to conditionalize based on key type.
|
||
|
|
//
|
||
|
|
// Note that this key algorithm explicitly ignores the scheme. This is
|
||
|
|
// because when we're entering cookies into the map from the backing store,
|
||
|
|
// we in general won't have the scheme at that point.
|
||
|
|
// In practical terms, this means that file cookies will be stored
|
||
|
|
// in the map either by an empty string or by UNC name (and will be
|
||
|
|
// limited by kMaxCookiesPerHost), and extension cookies will be stored
|
||
|
|
// based on the single extension id, as the extension id won't have the
|
||
|
|
// form of a DNS host and hence GetKey() will return it unchanged.
|
||
|
|
//
|
||
|
|
// Arguably the right thing to do here is to make the key
|
||
|
|
// algorithm dependent on the scheme, and make sure that the scheme is
|
||
|
|
// available everywhere the key must be obtained (specfically at backing
|
||
|
|
// store load time). This would require either changing the backing store
|
||
|
|
// database schema to include the scheme (far more trouble than it's worth), or
|
||
|
|
// separating out file cookies into their own CookieMonster instance and
|
||
|
|
// thus restricting each scheme to a single cookie monster (which might
|
||
|
|
// be worth it, but is still too much trouble to solve what is currently a
|
||
|
|
// non-problem).
|
||
|
|
//
|
||
|
|
// static
|
||
|
|
std::string CookieMonster::GetKey(base::StringPiece domain) {
|
||
|
|
std::string effective_domain(
|
||
|
|
registry_controlled_domains::GetDomainAndRegistry(
|
||
|
|
domain, registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES));
|
||
|
|
if (effective_domain.empty())
|
||
|
|
effective_domain = std::string(domain);
|
||
|
|
|
||
|
|
return cookie_util::CookieDomainAsHost(effective_domain);
|
||
|
|
}
|
||
|
|
|
||
|
|
bool CookieMonster::HasCookieableScheme(const GURL& url) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
// Make sure the request is on a cookie-able url scheme.
|
||
|
|
bool is_cookieable = base::ranges::any_of(
|
||
|
|
cookieable_schemes_, [&url](const std::string& cookieable_scheme) {
|
||
|
|
return url.SchemeIs(cookieable_scheme.c_str());
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!is_cookieable) {
|
||
|
|
// The scheme didn't match any in our allowed list.
|
||
|
|
DVLOG(net::cookie_util::kVlogPerCookieMonster)
|
||
|
|
<< "WARNING: Unsupported cookie scheme: " << url.scheme();
|
||
|
|
}
|
||
|
|
return is_cookieable;
|
||
|
|
}
|
||
|
|
|
||
|
|
CookieAccessSemantics CookieMonster::GetAccessSemanticsForCookie(
|
||
|
|
const CanonicalCookie& cookie) const {
|
||
|
|
if (cookie_access_delegate())
|
||
|
|
return cookie_access_delegate()->GetAccessSemantics(cookie);
|
||
|
|
return CookieAccessSemantics::UNKNOWN;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test to see if stats should be recorded, and record them if so.
|
||
|
|
// The goal here is to get sampling for the average browser-hour of
|
||
|
|
// activity. We won't take samples when the web isn't being surfed,
|
||
|
|
// and when the web is being surfed, we'll take samples about every
|
||
|
|
// kRecordStatisticsIntervalSeconds.
|
||
|
|
// last_statistic_record_time_ is initialized to Now() rather than null
|
||
|
|
// in the constructor so that we won't take statistics right after
|
||
|
|
// startup, to avoid bias from browsers that are started but not used.
|
||
|
|
void CookieMonster::RecordPeriodicStats(const base::Time& current_time) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
const base::TimeDelta kRecordStatisticsIntervalTime(
|
||
|
|
base::Seconds(kRecordStatisticsIntervalSeconds));
|
||
|
|
|
||
|
|
// If we've taken statistics recently, return.
|
||
|
|
if (current_time - last_statistic_record_time_ <=
|
||
|
|
kRecordStatisticsIntervalTime) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (DoRecordPeriodicStats())
|
||
|
|
last_statistic_record_time_ = current_time;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool CookieMonster::DoRecordPeriodicStats() {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
// These values are all bogus if we have only partially loaded the cookies.
|
||
|
|
if (started_fetching_all_cookies_ && !finished_fetching_all_cookies_)
|
||
|
|
return false;
|
||
|
|
|
||
|
|
base::UmaHistogramCounts100000("Cookie.Count2", cookies_.size());
|
||
|
|
|
||
|
|
if (cookie_access_delegate()) {
|
||
|
|
std::vector<SchemefulSite> sites;
|
||
|
|
for (const auto& entry : cookies_) {
|
||
|
|
sites.emplace_back(
|
||
|
|
GURL(base::StrCat({url::kHttpsScheme, "://", entry.first})));
|
||
|
|
}
|
||
|
|
for (const auto& [partition_key, cookie_map] : partitioned_cookies_) {
|
||
|
|
for (const auto& [domain, unused_cookie] : *cookie_map) {
|
||
|
|
sites.emplace_back(
|
||
|
|
GURL(base::StrCat({url::kHttpsScheme, "://", domain})));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
absl::optional<base::flat_map<SchemefulSite, FirstPartySetEntry>>
|
||
|
|
maybe_sets = cookie_access_delegate()->FindFirstPartySetEntries(
|
||
|
|
sites,
|
||
|
|
base::BindOnce(&CookieMonster::RecordPeriodicFirstPartySetsStats,
|
||
|
|
weak_ptr_factory_.GetWeakPtr()));
|
||
|
|
if (maybe_sets.has_value())
|
||
|
|
RecordPeriodicFirstPartySetsStats(maybe_sets.value());
|
||
|
|
}
|
||
|
|
|
||
|
|
// Can be up to kMaxCookies.
|
||
|
|
UMA_HISTOGRAM_COUNTS_10000("Cookie.NumKeys", num_keys_);
|
||
|
|
|
||
|
|
std::map<std::string, size_t> n_same_site_none_cookies;
|
||
|
|
for (const auto& [host_key, host_cookie] : cookies_) {
|
||
|
|
if (!host_cookie || !host_cookie->IsEffectivelySameSiteNone())
|
||
|
|
continue;
|
||
|
|
n_same_site_none_cookies[host_key]++;
|
||
|
|
}
|
||
|
|
size_t max_n_cookies = 0;
|
||
|
|
for (const auto& entry : n_same_site_none_cookies) {
|
||
|
|
max_n_cookies = std::max(max_n_cookies, entry.second);
|
||
|
|
}
|
||
|
|
// Can be up to 180 cookies, the max per-domain.
|
||
|
|
base::UmaHistogramCounts1000("Cookie.MaxSameSiteNoneCookiesPerKey",
|
||
|
|
max_n_cookies);
|
||
|
|
|
||
|
|
// Collect stats for partitioned cookies if they are enabled.
|
||
|
|
if (base::FeatureList::IsEnabled(features::kPartitionedCookies)) {
|
||
|
|
base::UmaHistogramCounts1000("Cookie.PartitionCount",
|
||
|
|
partitioned_cookies_.size());
|
||
|
|
base::UmaHistogramCounts100000("Cookie.PartitionedCookieCount",
|
||
|
|
num_partitioned_cookies_);
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::RecordPeriodicFirstPartySetsStats(
|
||
|
|
base::flat_map<SchemefulSite, FirstPartySetEntry> sets) const {
|
||
|
|
base::flat_map<SchemefulSite, std::set<SchemefulSite>> grouped_by_owner;
|
||
|
|
for (const auto& [site, entry] : sets) {
|
||
|
|
grouped_by_owner[entry.primary()].insert(site);
|
||
|
|
}
|
||
|
|
for (const auto& set : grouped_by_owner) {
|
||
|
|
int sample = std::accumulate(
|
||
|
|
set.second.begin(), set.second.end(), 0,
|
||
|
|
[this](int acc, const net::SchemefulSite& site) -> int {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
if (!site.has_registrable_domain_or_host())
|
||
|
|
return acc;
|
||
|
|
return acc + cookies_.count(site.registrable_domain_or_host());
|
||
|
|
});
|
||
|
|
base::UmaHistogramCustomCounts("Cookie.PerFirstPartySetCount", sample, 0,
|
||
|
|
4000, 50);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::DoCookieCallback(base::OnceClosure callback) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
|
||
|
|
MarkCookieStoreAsInitialized();
|
||
|
|
FetchAllCookiesIfNecessary();
|
||
|
|
seen_global_task_ = true;
|
||
|
|
|
||
|
|
if (!finished_fetching_all_cookies_ && store_.get()) {
|
||
|
|
tasks_pending_.push_back(std::move(callback));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::move(callback).Run();
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::DoCookieCallbackForURL(base::OnceClosure callback,
|
||
|
|
const GURL& url) {
|
||
|
|
DoCookieCallbackForHostOrDomain(std::move(callback), url.host_piece());
|
||
|
|
}
|
||
|
|
|
||
|
|
void CookieMonster::DoCookieCallbackForHostOrDomain(
|
||
|
|
base::OnceClosure callback,
|
||
|
|
base::StringPiece host_or_domain) {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
MarkCookieStoreAsInitialized();
|
||
|
|
FetchAllCookiesIfNecessary();
|
||
|
|
|
||
|
|
// If cookies for the requested domain key (eTLD+1) have been loaded from DB
|
||
|
|
// then run the task, otherwise load from DB.
|
||
|
|
if (!finished_fetching_all_cookies_ && store_.get()) {
|
||
|
|
// If a global task has been previously seen, queue the task as a global
|
||
|
|
// task. Note that the CookieMonster may be in the middle of executing
|
||
|
|
// the global queue, |tasks_pending_| may be empty, which is why another
|
||
|
|
// bool is needed.
|
||
|
|
if (seen_global_task_) {
|
||
|
|
tasks_pending_.push_back(std::move(callback));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Checks if the domain key has been loaded.
|
||
|
|
std::string key = GetKey(host_or_domain);
|
||
|
|
if (keys_loaded_.find(key) == keys_loaded_.end()) {
|
||
|
|
auto it = tasks_pending_for_key_.find(key);
|
||
|
|
if (it == tasks_pending_for_key_.end()) {
|
||
|
|
store_->LoadCookiesForKey(
|
||
|
|
key, base::BindOnce(&CookieMonster::OnKeyLoaded,
|
||
|
|
weak_ptr_factory_.GetWeakPtr(), key));
|
||
|
|
it = tasks_pending_for_key_
|
||
|
|
.insert(std::make_pair(
|
||
|
|
key, base::circular_deque<base::OnceClosure>()))
|
||
|
|
.first;
|
||
|
|
}
|
||
|
|
it->second.push_back(std::move(callback));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
std::move(callback).Run();
|
||
|
|
}
|
||
|
|
|
||
|
|
CookieMonster::CookieSentToSamePort
|
||
|
|
CookieMonster::IsCookieSentToSamePortThatSetIt(
|
||
|
|
const GURL& destination,
|
||
|
|
int source_port,
|
||
|
|
CookieSourceScheme source_scheme) {
|
||
|
|
if (source_port == url::PORT_UNSPECIFIED)
|
||
|
|
return CookieSentToSamePort::kSourcePortUnspecified;
|
||
|
|
|
||
|
|
if (source_port == url::PORT_INVALID)
|
||
|
|
return CookieSentToSamePort::kInvalid;
|
||
|
|
|
||
|
|
int destination_port = destination.EffectiveIntPort();
|
||
|
|
if (source_port == destination_port)
|
||
|
|
return CookieSentToSamePort::kYes;
|
||
|
|
|
||
|
|
const std::string& destination_scheme = destination.scheme();
|
||
|
|
bool destination_port_is_default =
|
||
|
|
url::DefaultPortForScheme(destination_scheme.c_str(),
|
||
|
|
destination_scheme.length()) ==
|
||
|
|
destination_port;
|
||
|
|
|
||
|
|
// Since the source port has to be specified if we got to this point, that
|
||
|
|
// means this is a newer cookie that therefore has its scheme set as well.
|
||
|
|
DCHECK(source_scheme != CookieSourceScheme::kUnset);
|
||
|
|
std::string source_scheme_string =
|
||
|
|
source_scheme == CookieSourceScheme::kSecure
|
||
|
|
? url::kHttpsScheme
|
||
|
|
: url::kHttpScheme; // wss/ws have the same default port values as
|
||
|
|
// https/http, so it's ok that we use these.
|
||
|
|
|
||
|
|
bool source_port_is_default =
|
||
|
|
url::DefaultPortForScheme(source_scheme_string.c_str(),
|
||
|
|
source_scheme_string.length()) == source_port;
|
||
|
|
|
||
|
|
if (destination_port_is_default && source_port_is_default)
|
||
|
|
return CookieSentToSamePort::kNoButDefault;
|
||
|
|
|
||
|
|
return CookieSentToSamePort::kNo;
|
||
|
|
}
|
||
|
|
|
||
|
|
absl::optional<bool> CookieMonster::SiteHasCookieInOtherPartition(
|
||
|
|
const net::SchemefulSite& site,
|
||
|
|
const absl::optional<CookiePartitionKey>& partition_key) const {
|
||
|
|
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
|
||
|
|
// If the partition key is null, it implies the partitioned cookies feature is
|
||
|
|
// not enabled.
|
||
|
|
if (!partition_key)
|
||
|
|
return absl::nullopt;
|
||
|
|
|
||
|
|
std::string domain = site.GetURL().host();
|
||
|
|
if (store_ && !finished_fetching_all_cookies_ &&
|
||
|
|
!keys_loaded_.count(domain)) {
|
||
|
|
return absl::nullopt;
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const auto& it : partitioned_cookies_) {
|
||
|
|
if (it.first == partition_key || CookiePartitionKey::HasNonce(it.first))
|
||
|
|
continue;
|
||
|
|
if (it.second->find(domain) != it.second->end()) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace net
|