diff options
author | erikwright@chromium.org <erikwright@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-03-15 09:29:58 +0000 |
---|---|---|
committer | erikwright@chromium.org <erikwright@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2012-03-15 09:29:58 +0000 |
commit | 63ee33bde2ec8471a70f0f0ec6a1962dd07fc8ab (patch) | |
tree | 7c38150e4b53fa60235d4d3845100d805ba85d35 /net/cookies | |
parent | ca2fd075cf4eaeb2ee650affff606bbb3120772c (diff) | |
download | chromium_src-63ee33bde2ec8471a70f0f0ec6a1962dd07fc8ab.zip chromium_src-63ee33bde2ec8471a70f0f0ec6a1962dd07fc8ab.tar.gz chromium_src-63ee33bde2ec8471a70f0f0ec6a1962dd07fc8ab.tar.bz2 |
Move the cookie store implementation into its own directory.
In the initial step, forwarding headers are left in net/base/cookie_*h . After all clients are updated these will be removed and erikwright will be removed from net/base/OWNERS
BUG=70818
TEST=compilation
Review URL: http://codereview.chromium.org/9703011
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@126871 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'net/cookies')
-rw-r--r-- | net/cookies/OWNERS | 1 | ||||
-rw-r--r-- | net/cookies/cookie_monster.cc | 2740 | ||||
-rw-r--r-- | net/cookies/cookie_monster.h | 971 | ||||
-rw-r--r-- | net/cookies/cookie_monster_perftest.cc | 430 | ||||
-rw-r--r-- | net/cookies/cookie_monster_store_test.cc | 231 | ||||
-rw-r--r-- | net/cookies/cookie_monster_store_test.h | 206 | ||||
-rw-r--r-- | net/cookies/cookie_monster_unittest.cc | 2687 | ||||
-rw-r--r-- | net/cookies/cookie_options.h | 40 | ||||
-rw-r--r-- | net/cookies/cookie_store.cc | 19 | ||||
-rw-r--r-- | net/cookies/cookie_store.h | 113 | ||||
-rw-r--r-- | net/cookies/cookie_store_test_callbacks.cc | 65 | ||||
-rw-r--r-- | net/cookies/cookie_store_test_callbacks.h | 137 | ||||
-rw-r--r-- | net/cookies/cookie_store_test_helpers.cc | 155 | ||||
-rw-r--r-- | net/cookies/cookie_store_test_helpers.h | 105 | ||||
-rw-r--r-- | net/cookies/cookie_store_unittest.h | 1055 | ||||
-rw-r--r-- | net/cookies/cookie_util.cc | 79 | ||||
-rw-r--r-- | net/cookies/cookie_util.h | 40 | ||||
-rw-r--r-- | net/cookies/cookie_util_unittest.cc | 23 |
18 files changed, 9097 insertions, 0 deletions
diff --git a/net/cookies/OWNERS b/net/cookies/OWNERS new file mode 100644 index 0000000..82e44d8 --- /dev/null +++ b/net/cookies/OWNERS @@ -0,0 +1 @@ +erikwright@chromium.org diff --git a/net/cookies/cookie_monster.cc b/net/cookies/cookie_monster.cc new file mode 100644 index 0000000..8a609d8 --- /dev/null +++ b/net/cookies/cookie_monster.cc @@ -0,0 +1,2740 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// 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 <algorithm> +#include <set> + +#include "base/basictypes.h" +#include "base/bind.h" +#include "base/callback.h" +#include "base/format_macros.h" +#include "base/logging.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop.h" +#include "base/message_loop_proxy.h" +#include "base/metrics/histogram.h" +#include "base/string_tokenizer.h" +#include "base/string_util.h" +#include "base/stringprintf.h" +#include "googleurl/src/gurl.h" +#include "googleurl/src/url_canon.h" +#include "net/cookies/cookie_util.h" +#include "net/base/registry_controlled_domain.h" + +using base::Time; +using base::TimeDelta; +using base::TimeTicks; + +// In steady state, most cookie requests can be satisfied by the in memory +// cookie monster store. However, if a request comes in during the initial +// cookie load, it must be delayed until that load completes. That is done by +// queueing it on CookieMonster::queue_ and running it when notification of +// cookie load completion is received via CookieMonster::OnLoaded. This callback +// is passed to the persistent store from CookieMonster::InitStore(), which is +// called on the first operation invoked on the CookieMonster. +// +// 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, DoCookieTaskForURL 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_queued +// 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_queued and executed upon receiving notification of +// key load completion triggered by the first request for the same eTLD+1. + +static const int kMinutesInTenYears = 10 * 365 * 24 * 60; + +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 int CookieMonster::kSafeFromGlobalPurgeDays = 30; + +namespace { + +typedef std::vector<CookieMonster::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(); + } +}; + +// Constants for use in VLOG +const int kVlogPerCookieMonster = 1; +const int kVlogPeriodic = 3; +const int kVlogGarbageCollection = 5; +const int kVlogSetCookies = 7; +const int kVlogGetCookies = 9; + +#if defined(ENABLE_PERSISTENT_SESSION_COOKIES) +const int kPersistentSessionCookieExpiryInDays = 14; +#endif + +// 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. +bool CookieSorter(CookieMonster::CanonicalCookie* cc1, + CookieMonster::CanonicalCookie* cc2) { + if (cc1->Path().length() == cc2->Path().length()) + return cc1->CreationDate() < cc2->CreationDate(); + return cc1->Path().length() > cc2->Path().length(); +} + +bool LRUCookieSorter(const CookieMonster::CookieMap::iterator& it1, + const CookieMonster::CookieMap::iterator& it2) { + // Cookies accessed less recently should be deleted first. + if (it1->second->LastAccessDate() != it2->second->LastAccessDate()) + return it1->second->LastAccessDate() < it2->second->LastAccessDate(); + + // In rare cases we might have two cookies with identical last access times. + // To preserve the stability of the sort, in these cases prefer to delete + // older cookies over newer ones. CreationDate() is guaranteed to be unique. + return it1->second->CreationDate() < it2->second->CreationDate(); +} + +// Our strategy to find duplicates is: +// (1) Build a map from (cookiename, cookiepath) 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. +// +// Two cookies are considered equivalent if they have the same domain, +// name, and path. +struct CookieSignature { + public: + CookieSignature(const std::string& name, const std::string& domain, + const std::string& path) + : name(name), + domain(domain), + path(path) {} + + // To be a key for a map this class needs to be assignable, copyable, + // and have an operator<. The default assignment operator + // and copy constructor are exactly what we want. + + bool operator<(const CookieSignature& cs) const { + // Name compare dominates, then domain, then path. + int diff = name.compare(cs.name); + if (diff != 0) + return diff < 0; + + diff = domain.compare(cs.domain); + if (diff != 0) + return diff < 0; + + return path.compare(cs.path) < 0; + } + + std::string name; + std::string domain; + std::string path; +}; + +// Determine the cookie domain to use for setting the specified cookie. +bool GetCookieDomain(const GURL& url, + const CookieMonster::ParsedCookie& pc, + std::string* result) { + std::string domain_string; + if (pc.HasDomain()) + domain_string = pc.Domain(); + return cookie_util::GetCookieDomainWithString(url, domain_string, result); +} + +std::string CanonPathWithString(const GURL& url, + const std::string& path_string) { + // The RFC says the path should be a prefix of the current URL path. + // However, Mozilla allows you to set any path for compatibility with + // broken websites. We unfortunately will mimic this behavior. We try + // to be generous and accept cookies with an invalid path attribute, and + // default the path to something reasonable. + + // The path was supplied in the cookie, we'll take it. + if (!path_string.empty() && path_string[0] == '/') + return path_string; + + // The path was not supplied in the cookie or invalid, we will default + // to the current URL path. + // """Defaults to the path of the request URL that generated the + // Set-Cookie response, up to, but not including, the + // right-most /.""" + // How would this work for a cookie on /? We will include it then. + const std::string& url_path = url.path(); + + size_t idx = url_path.find_last_of('/'); + + // The cookie path was invalid or a single '/'. + if (idx == 0 || idx == std::string::npos) + return std::string("/"); + + // Return up to the rightmost '/'. + return url_path.substr(0, idx); +} + +std::string CanonPath(const GURL& url, + const CookieMonster::ParsedCookie& pc) { + std::string path_string; + if (pc.HasPath()) + path_string = pc.Path(); + return CanonPathWithString(url, path_string); +} + +Time CanonExpiration(const CookieMonster::ParsedCookie& pc, + const Time& current) { + // First, try the Max-Age attribute. + uint64 max_age = 0; + if (pc.HasMaxAge() && +#ifdef COMPILER_MSVC + sscanf_s( +#else + sscanf( +#endif + pc.MaxAge().c_str(), " %" PRIu64, &max_age) == 1) { + return current + TimeDelta::FromSeconds(max_age); + } + + // Try the Expires attribute. + if (pc.HasExpires()) + return CookieMonster::ParseCookieTime(pc.Expires()); + + // Invalid or no expiration, persistent cookie. + return Time(); +} + +// Helper for GarbageCollection. If |cookie_its->size() > num_max|, remove the +// |num_max - num_purge| most recently accessed cookies from cookie_its. +// (In other words, leave the entries that are candidates for +// eviction in cookie_its.) The cookies returned will be in order sorted by +// access time, least recently accessed first. The access time of the least +// recently accessed entry not returned will be placed in +// |*lra_removed| if that pointer is set. FindLeastRecentlyAccessed +// returns false if no manipulation is done (because the list size is less +// than num_max), true otherwise. +bool FindLeastRecentlyAccessed( + size_t num_max, + size_t num_purge, + Time* lra_removed, + std::vector<CookieMonster::CookieMap::iterator>* cookie_its) { + DCHECK_LE(num_purge, num_max); + if (cookie_its->size() > num_max) { + VLOG(kVlogGarbageCollection) + << "FindLeastRecentlyAccessed() Deep Garbage Collect."; + num_purge += cookie_its->size() - num_max; + DCHECK_GT(cookie_its->size(), num_purge); + + // Add 1 so that we can get the last time left in the store. + std::partial_sort(cookie_its->begin(), cookie_its->begin() + num_purge + 1, + cookie_its->end(), LRUCookieSorter); + *lra_removed = + (*(cookie_its->begin() + num_purge))->second->LastAccessDate(); + cookie_its->erase(cookie_its->begin() + num_purge, cookie_its->end()); + return true; + } + return false; +} + +// Mapping between DeletionCause and Delegate::ChangeCause; the mapping also +// provides a boolean that specifies whether or not an OnCookieChanged +// notification ought to be generated. +typedef struct ChangeCausePair_struct { + CookieMonster::Delegate::ChangeCause cause; + bool notify; +} ChangeCausePair; +ChangeCausePair ChangeCauseMapping[] = { + // DELETE_COOKIE_EXPLICIT + { CookieMonster::Delegate::CHANGE_COOKIE_EXPLICIT, true }, + // DELETE_COOKIE_OVERWRITE + { CookieMonster::Delegate::CHANGE_COOKIE_OVERWRITE, true }, + // DELETE_COOKIE_EXPIRED + { CookieMonster::Delegate::CHANGE_COOKIE_EXPIRED, true }, + // DELETE_COOKIE_EVICTED + { CookieMonster::Delegate::CHANGE_COOKIE_EVICTED, true }, + // DELETE_COOKIE_DUPLICATE_IN_BACKING_STORE + { CookieMonster::Delegate::CHANGE_COOKIE_EXPLICIT, false }, + // DELETE_COOKIE_DONT_RECORD + { CookieMonster::Delegate::CHANGE_COOKIE_EXPLICIT, false }, + // DELETE_COOKIE_EVICTED_DOMAIN + { CookieMonster::Delegate::CHANGE_COOKIE_EVICTED, true }, + // DELETE_COOKIE_EVICTED_GLOBAL + { CookieMonster::Delegate::CHANGE_COOKIE_EVICTED, true }, + // DELETE_COOKIE_EVICTED_DOMAIN_PRE_SAFE + { CookieMonster::Delegate::CHANGE_COOKIE_EVICTED, true }, + // DELETE_COOKIE_EVICTED_DOMAIN_POST_SAFE + { CookieMonster::Delegate::CHANGE_COOKIE_EVICTED, true }, + // DELETE_COOKIE_EXPIRED_OVERWRITE + { CookieMonster::Delegate::CHANGE_COOKIE_EXPIRED_OVERWRITE, true }, + // DELETE_COOKIE_LAST_ENTRY + { CookieMonster::Delegate::CHANGE_COOKIE_EXPLICIT, false } +}; + +std::string BuildCookieLine(const CanonicalCookieVector& cookies) { + std::string cookie_line; + for (CanonicalCookieVector::const_iterator it = cookies.begin(); + it != cookies.end(); ++it) { + if (it != cookies.begin()) + cookie_line += "; "; + // In Mozilla if you set a cookie like AAAA, it will have an empty token + // and a value of AAAA. When it sends the cookie back, it will send AAAA, + // so we need to avoid sending =AAAA for a blank token value. + if (!(*it)->Name().empty()) + cookie_line += (*it)->Name() + "="; + cookie_line += (*it)->Value(); + } + return cookie_line; +} + +void BuildCookieInfoList(const CanonicalCookieVector& cookies, + std::vector<CookieStore::CookieInfo>* cookie_infos) { + for (CanonicalCookieVector::const_iterator it = cookies.begin(); + it != cookies.end(); ++it) { + const CookieMonster::CanonicalCookie* cookie = *it; + CookieStore::CookieInfo cookie_info; + + cookie_info.name = cookie->Name(); + cookie_info.creation_date = cookie->CreationDate(); + cookie_info.mac_key = cookie->MACKey(); + cookie_info.mac_algorithm = cookie->MACAlgorithm(); + + cookie_infos->push_back(cookie_info); + } +} + +} // namespace + +// static +bool CookieMonster::enable_file_scheme_ = false; + +CookieMonster::CookieMonster(PersistentCookieStore* store, Delegate* delegate) + : initialized_(false), + loaded_(false), + store_(store), + last_access_threshold_( + TimeDelta::FromSeconds(kDefaultAccessUpdateThresholdSeconds)), + delegate_(delegate), + last_statistic_record_time_(Time::Now()), + keep_expired_cookies_(false), + persist_session_cookies_(false) { + InitializeHistograms(); + SetDefaultCookieableSchemes(); +} + +CookieMonster::CookieMonster(PersistentCookieStore* store, + Delegate* delegate, + int last_access_threshold_milliseconds) + : initialized_(false), + loaded_(false), + store_(store), + last_access_threshold_(base::TimeDelta::FromMilliseconds( + last_access_threshold_milliseconds)), + delegate_(delegate), + last_statistic_record_time_(base::Time::Now()), + keep_expired_cookies_(false), + persist_session_cookies_(false) { + InitializeHistograms(); + SetDefaultCookieableSchemes(); +} + +// Parse a cookie expiration time. We try to be lenient, but we need to +// assume some order to distinguish the fields. The basic rules: +// - The month name must be present and prefix the first 3 letters of the +// full month name (jan for January, jun for June). +// - If the year is <= 2 digits, it must occur after the day of month. +// - The time must be of the format hh:mm:ss. +// An average cookie expiration will look something like this: +// Sat, 15-Apr-17 21:01:22 GMT +Time CookieMonster::ParseCookieTime(const std::string& time_string) { + static const char* kMonths[] = { "jan", "feb", "mar", "apr", "may", "jun", + "jul", "aug", "sep", "oct", "nov", "dec" }; + static const int kMonthsLen = arraysize(kMonths); + // We want to be pretty liberal, and support most non-ascii and non-digit + // characters as a delimiter. We can't treat : as a delimiter, because it + // is the delimiter for hh:mm:ss, and we want to keep this field together. + // We make sure to include - and +, since they could prefix numbers. + // If the cookie attribute came in in quotes (ex expires="XXX"), the quotes + // will be preserved, and we will get them here. So we make sure to include + // quote characters, and also \ for anything that was internally escaped. + static const char* kDelimiters = "\t !\"#$%&'()*+,-./;<=>?@[\\]^_`{|}~"; + + Time::Exploded exploded = {0}; + + StringTokenizer tokenizer(time_string, kDelimiters); + + bool found_day_of_month = false; + bool found_month = false; + bool found_time = false; + bool found_year = false; + + while (tokenizer.GetNext()) { + const std::string token = tokenizer.token(); + DCHECK(!token.empty()); + bool numerical = IsAsciiDigit(token[0]); + + // String field + if (!numerical) { + if (!found_month) { + for (int i = 0; i < kMonthsLen; ++i) { + // Match prefix, so we could match January, etc + if (base::strncasecmp(token.c_str(), kMonths[i], 3) == 0) { + exploded.month = i + 1; + found_month = true; + break; + } + } + } else { + // If we've gotten here, it means we've already found and parsed our + // month, and we have another string, which we would expect to be the + // the time zone name. According to the RFC and my experiments with + // how sites format their expirations, we don't have much of a reason + // to support timezones. We don't want to ever barf on user input, + // but this DCHECK should pass for well-formed data. + // DCHECK(token == "GMT"); + } + // Numeric field w/ a colon + } else if (token.find(':') != std::string::npos) { + if (!found_time && +#ifdef COMPILER_MSVC + sscanf_s( +#else + sscanf( +#endif + token.c_str(), "%2u:%2u:%2u", &exploded.hour, + &exploded.minute, &exploded.second) == 3) { + found_time = true; + } else { + // We should only ever encounter one time-like thing. If we're here, + // it means we've found a second, which shouldn't happen. We keep + // the first. This check should be ok for well-formed input: + // NOTREACHED(); + } + // Numeric field + } else { + // Overflow with atoi() is unspecified, so we enforce a max length. + if (!found_day_of_month && token.length() <= 2) { + exploded.day_of_month = atoi(token.c_str()); + found_day_of_month = true; + } else if (!found_year && token.length() <= 5) { + exploded.year = atoi(token.c_str()); + found_year = true; + } else { + // If we're here, it means we've either found an extra numeric field, + // or a numeric field which was too long. For well-formed input, the + // following check would be reasonable: + // NOTREACHED(); + } + } + } + + if (!found_day_of_month || !found_month || !found_time || !found_year) { + // We didn't find all of the fields we need. For well-formed input, the + // following check would be reasonable: + // NOTREACHED() << "Cookie parse expiration failed: " << time_string; + return Time(); + } + + // Normalize the year to expand abbreviated years to the full year. + if (exploded.year >= 69 && exploded.year <= 99) + exploded.year += 1900; + if (exploded.year >= 0 && exploded.year <= 68) + exploded.year += 2000; + + // If our values are within their correct ranges, we got our time. + if (exploded.day_of_month >= 1 && exploded.day_of_month <= 31 && + exploded.month >= 1 && exploded.month <= 12 && + exploded.year >= 1601 && exploded.year <= 30827 && + exploded.hour <= 23 && exploded.minute <= 59 && exploded.second <= 59) { + return Time::FromUTCExploded(exploded); + } + + // One of our values was out of expected range. For well-formed input, + // the following check would be reasonable: + // NOTREACHED() << "Cookie exploded expiration failed: " << time_string; + + return Time(); +} + +// Task classes for queueing the coming request. + +class CookieMonster::CookieMonsterTask + : public base::RefCountedThreadSafe<CookieMonsterTask> { + public: + // Runs the task and invokes the client callback on the thread that + // originally constructed the task. + virtual void Run() = 0; + + protected: + explicit CookieMonsterTask(CookieMonster* cookie_monster); + virtual ~CookieMonsterTask(); + + // Invokes the callback immediately, if the current thread is the one + // that originated the task, or queues the callback for execution on the + // appropriate thread. Maintains a reference to this CookieMonsterTask + // instance until the callback completes. + void InvokeCallback(base::Closure callback); + + CookieMonster* cookie_monster() { + return cookie_monster_; + } + + friend class base::RefCountedThreadSafe<CookieMonsterTask>; + + private: + CookieMonster* cookie_monster_; + scoped_refptr<base::MessageLoopProxy> thread_; + + DISALLOW_COPY_AND_ASSIGN(CookieMonsterTask); +}; + +CookieMonster::CookieMonsterTask::CookieMonsterTask( + CookieMonster* cookie_monster) + : cookie_monster_(cookie_monster), + thread_(base::MessageLoopProxy::current()) { } + +CookieMonster::CookieMonsterTask::~CookieMonsterTask() { } + +// Unfortunately, one cannot re-bind a Callback with parameters into a closure. +// Therefore, the closure passed to InvokeCallback is a clumsy binding of +// Callback::Run on a wrapped Callback instance. Since Callback is not +// reference counted, we bind to an instance that is a member of the +// CookieMonsterTask subclass. Then, we cannot simply post the callback to a +// message loop because the underlying instance may be destroyed (along with the +// CookieMonsterTask instance) in the interim. Therefore, we post a callback +// bound to the CookieMonsterTask, which *is* reference counted (thus preventing +// destruction of the original callback), and which invokes the closure (which +// invokes the original callback with the returned data). +void CookieMonster::CookieMonsterTask::InvokeCallback(base::Closure callback) { + if (thread_->BelongsToCurrentThread()) { + callback.Run(); + } else { + thread_->PostTask(FROM_HERE, base::Bind( + &CookieMonster::CookieMonsterTask::InvokeCallback, this, callback)); + } +} + +// Task class for SetCookieWithDetails call. +class CookieMonster::SetCookieWithDetailsTask + : public CookieMonster::CookieMonsterTask { + public: + SetCookieWithDetailsTask( + CookieMonster* cookie_monster, + const GURL& url, const std::string& name, const std::string& value, + const std::string& domain, const std::string& path, + const base::Time& expiration_time, bool secure, bool http_only, + const CookieMonster::SetCookiesCallback& callback) + : CookieMonsterTask(cookie_monster), + url_(url), + name_(name), + value_(value), + domain_(domain), + path_(path), + expiration_time_(expiration_time), + secure_(secure), + http_only_(http_only), + callback_(callback) { } + + virtual void Run() OVERRIDE; + + private: + GURL url_; + std::string name_; + std::string value_; + std::string domain_; + std::string path_; + base::Time expiration_time_; + bool secure_; + bool http_only_; + CookieMonster::SetCookiesCallback callback_; + + DISALLOW_COPY_AND_ASSIGN(SetCookieWithDetailsTask); +}; + +void CookieMonster::SetCookieWithDetailsTask::Run() { + bool success = this->cookie_monster()-> + SetCookieWithDetails(url_, name_, value_, domain_, path_, + expiration_time_, secure_, http_only_); + if (!callback_.is_null()) { + this->InvokeCallback(base::Bind(&CookieMonster::SetCookiesCallback::Run, + base::Unretained(&callback_), success)); + } +} + +// Task class for GetAllCookies call. +class CookieMonster::GetAllCookiesTask + : public CookieMonster::CookieMonsterTask { + public: + GetAllCookiesTask(CookieMonster* cookie_monster, + const CookieMonster::GetCookieListCallback& callback) + : CookieMonsterTask(cookie_monster), + callback_(callback) { } + + virtual void Run() OVERRIDE; + + private: + CookieMonster::GetCookieListCallback callback_; + + DISALLOW_COPY_AND_ASSIGN(GetAllCookiesTask); +}; + +void CookieMonster::GetAllCookiesTask::Run() { + if (!callback_.is_null()) { + CookieList cookies = this->cookie_monster()->GetAllCookies(); + this->InvokeCallback(base::Bind(&CookieMonster::GetCookieListCallback::Run, + base::Unretained(&callback_), cookies)); + } +} + +// Task class for GetAllCookiesForURLWithOptions call. +class CookieMonster::GetAllCookiesForURLWithOptionsTask + : public CookieMonster::CookieMonsterTask { + public: + GetAllCookiesForURLWithOptionsTask( + CookieMonster* cookie_monster, + const GURL& url, + const CookieOptions& options, + const CookieMonster::GetCookieListCallback& callback) + : CookieMonsterTask(cookie_monster), + url_(url), + options_(options), + callback_(callback) { } + + virtual void Run() OVERRIDE; + + private: + GURL url_; + CookieOptions options_; + CookieMonster::GetCookieListCallback callback_; + + DISALLOW_COPY_AND_ASSIGN(GetAllCookiesForURLWithOptionsTask); +}; + +void CookieMonster::GetAllCookiesForURLWithOptionsTask::Run() { + if (!callback_.is_null()) { + CookieList cookies = this->cookie_monster()-> + GetAllCookiesForURLWithOptions(url_, options_); + this->InvokeCallback(base::Bind(&CookieMonster::GetCookieListCallback::Run, + base::Unretained(&callback_), cookies)); + } +} + +// Task class for DeleteAll call. +class CookieMonster::DeleteAllTask : public CookieMonster::CookieMonsterTask { + public: + DeleteAllTask(CookieMonster* cookie_monster, + const CookieMonster::DeleteCallback& callback) + : CookieMonsterTask(cookie_monster), + callback_(callback) { } + + virtual void Run() OVERRIDE; + + private: + CookieMonster::DeleteCallback callback_; + + DISALLOW_COPY_AND_ASSIGN(DeleteAllTask); +}; + +void CookieMonster::DeleteAllTask::Run() { + int num_deleted = this->cookie_monster()->DeleteAll(true); + if (!callback_.is_null()) { + this->InvokeCallback(base::Bind(&CookieMonster::DeleteCallback::Run, + base::Unretained(&callback_), num_deleted)); + } +} + +// Task class for DeleteAllCreatedBetween call. +class CookieMonster::DeleteAllCreatedBetweenTask + : public CookieMonster::CookieMonsterTask { + public: + DeleteAllCreatedBetweenTask( + CookieMonster* cookie_monster, + const Time& delete_begin, + const Time& delete_end, + const CookieMonster::DeleteCallback& callback) + : CookieMonsterTask(cookie_monster), + delete_begin_(delete_begin), + delete_end_(delete_end), + callback_(callback) { } + + virtual void Run() OVERRIDE; + + private: + Time delete_begin_; + Time delete_end_; + CookieMonster::DeleteCallback callback_; + + DISALLOW_COPY_AND_ASSIGN(DeleteAllCreatedBetweenTask); +}; + +void CookieMonster::DeleteAllCreatedBetweenTask::Run() { + int num_deleted = this->cookie_monster()-> + DeleteAllCreatedBetween(delete_begin_, delete_end_); + if (!callback_.is_null()) { + this->InvokeCallback(base::Bind(&CookieMonster::DeleteCallback::Run, + base::Unretained(&callback_), num_deleted)); + } +} + +// Task class for DeleteAllForHost call. +class CookieMonster::DeleteAllForHostTask + : public CookieMonster::CookieMonsterTask { + public: + DeleteAllForHostTask(CookieMonster* cookie_monster, + const GURL& url, + const CookieMonster::DeleteCallback& callback) + : CookieMonsterTask(cookie_monster), + url_(url), + callback_(callback) { } + + virtual void Run() OVERRIDE; + + private: + GURL url_; + CookieMonster::DeleteCallback callback_; + + DISALLOW_COPY_AND_ASSIGN(DeleteAllForHostTask); +}; + +void CookieMonster::DeleteAllForHostTask::Run() { + int num_deleted = this->cookie_monster()->DeleteAllForHost(url_); + if (!callback_.is_null()) { + this->InvokeCallback(base::Bind(&CookieMonster::DeleteCallback::Run, + base::Unretained(&callback_), num_deleted)); + } +} + +// Task class for DeleteCanonicalCookie call. +class CookieMonster::DeleteCanonicalCookieTask + : public CookieMonster::CookieMonsterTask { + public: + DeleteCanonicalCookieTask( + CookieMonster* cookie_monster, + const CookieMonster::CanonicalCookie& cookie, + const CookieMonster::DeleteCookieCallback& callback) + : CookieMonsterTask(cookie_monster), + cookie_(cookie), + callback_(callback) { } + + virtual void Run() OVERRIDE; + + private: + CookieMonster::CanonicalCookie cookie_; + CookieMonster::DeleteCookieCallback callback_; + + DISALLOW_COPY_AND_ASSIGN(DeleteCanonicalCookieTask); +}; + +void CookieMonster::DeleteCanonicalCookieTask::Run() { + bool result = this->cookie_monster()->DeleteCanonicalCookie(cookie_); + if (!callback_.is_null()) { + this->InvokeCallback(base::Bind(&CookieMonster::DeleteCookieCallback::Run, + base::Unretained(&callback_), result)); + } +} + +// Task class for SetCookieWithOptions call. +class CookieMonster::SetCookieWithOptionsTask + : public CookieMonster::CookieMonsterTask { + public: + SetCookieWithOptionsTask(CookieMonster* cookie_monster, + const GURL& url, + const std::string& cookie_line, + const CookieOptions& options, + const CookieMonster::SetCookiesCallback& callback) + : CookieMonsterTask(cookie_monster), + url_(url), + cookie_line_(cookie_line), + options_(options), + callback_(callback) { } + + virtual void Run() OVERRIDE; + + private: + GURL url_; + std::string cookie_line_; + CookieOptions options_; + CookieMonster::SetCookiesCallback callback_; + + DISALLOW_COPY_AND_ASSIGN(SetCookieWithOptionsTask); +}; + +void CookieMonster::SetCookieWithOptionsTask::Run() { + bool result = this->cookie_monster()-> + SetCookieWithOptions(url_, cookie_line_, options_); + if (!callback_.is_null()) { + this->InvokeCallback(base::Bind(&CookieMonster::SetCookiesCallback::Run, + base::Unretained(&callback_), result)); + } +} + +// Task class for GetCookiesWithOptions call. +class CookieMonster::GetCookiesWithOptionsTask + : public CookieMonster::CookieMonsterTask { + public: + GetCookiesWithOptionsTask(CookieMonster* cookie_monster, + const GURL& url, + const CookieOptions& options, + const CookieMonster::GetCookiesCallback& callback) + : CookieMonsterTask(cookie_monster), + url_(url), + options_(options), + callback_(callback) { } + + virtual void Run() OVERRIDE; + + private: + GURL url_; + CookieOptions options_; + CookieMonster::GetCookiesCallback callback_; + + DISALLOW_COPY_AND_ASSIGN(GetCookiesWithOptionsTask); +}; + +void CookieMonster::GetCookiesWithOptionsTask::Run() { + std::string cookie = this->cookie_monster()-> + GetCookiesWithOptions(url_, options_); + if (!callback_.is_null()) { + this->InvokeCallback(base::Bind(&CookieMonster::GetCookiesCallback::Run, + base::Unretained(&callback_), cookie)); + } +} + +// Task class for GetCookiesWithInfo call. +class CookieMonster::GetCookiesWithInfoTask + : public CookieMonster::CookieMonsterTask { + public: + GetCookiesWithInfoTask(CookieMonster* cookie_monster, + const GURL& url, + const CookieOptions& options, + const CookieMonster::GetCookieInfoCallback& callback) + : CookieMonsterTask(cookie_monster), + url_(url), + options_(options), + callback_(callback) { } + + virtual void Run() OVERRIDE; + + private: + GURL url_; + CookieOptions options_; + CookieMonster::GetCookieInfoCallback callback_; + + DISALLOW_COPY_AND_ASSIGN(GetCookiesWithInfoTask); +}; + +void CookieMonster::GetCookiesWithInfoTask::Run() { + if (!callback_.is_null()) { + std::string cookie_line; + std::vector<CookieMonster::CookieInfo> cookie_infos; + this->cookie_monster()-> + GetCookiesWithInfo(url_, options_, &cookie_line, &cookie_infos); + this->InvokeCallback(base::Bind(&CookieMonster::GetCookieInfoCallback::Run, + base::Unretained(&callback_), + cookie_line, cookie_infos)); + } +} + +// Task class for DeleteCookie call. +class CookieMonster::DeleteCookieTask + : public CookieMonster::CookieMonsterTask { + public: + DeleteCookieTask(CookieMonster* cookie_monster, + const GURL& url, + const std::string& cookie_name, + const base::Closure& callback) + : CookieMonsterTask(cookie_monster), + url_(url), + cookie_name_(cookie_name), + callback_(callback) { } + + virtual void Run() OVERRIDE; + + private: + GURL url_; + std::string cookie_name_; + base::Closure callback_; + + DISALLOW_COPY_AND_ASSIGN(DeleteCookieTask); +}; + +void CookieMonster::DeleteCookieTask::Run() { + this->cookie_monster()->DeleteCookie(url_, cookie_name_); + if (!callback_.is_null()) { + this->InvokeCallback(callback_); + } +} + +// Asynchronous CookieMonster API + +void CookieMonster::SetCookieWithDetailsAsync( + const GURL& url, const std::string& name, const std::string& value, + const std::string& domain, const std::string& path, + const base::Time& expiration_time, bool secure, bool http_only, + const SetCookiesCallback& callback) { + scoped_refptr<SetCookieWithDetailsTask> task = + new SetCookieWithDetailsTask(this, url, name, value, domain, path, + expiration_time, secure, http_only, + callback); + + DoCookieTaskForURL(task, url); +} + +void CookieMonster::GetAllCookiesAsync(const GetCookieListCallback& callback) { + scoped_refptr<GetAllCookiesTask> task = + new GetAllCookiesTask(this, callback); + + DoCookieTask(task); +} + + +void CookieMonster::GetAllCookiesForURLWithOptionsAsync( + const GURL& url, + const CookieOptions& options, + const GetCookieListCallback& callback) { + scoped_refptr<GetAllCookiesForURLWithOptionsTask> task = + new GetAllCookiesForURLWithOptionsTask(this, url, options, callback); + + DoCookieTaskForURL(task, url); +} + +void CookieMonster::GetAllCookiesForURLAsync( + const GURL& url, const GetCookieListCallback& callback) { + CookieOptions options; + options.set_include_httponly(); + scoped_refptr<GetAllCookiesForURLWithOptionsTask> task = + new GetAllCookiesForURLWithOptionsTask(this, url, options, callback); + + DoCookieTaskForURL(task, url); +} + +void CookieMonster::DeleteAllAsync(const DeleteCallback& callback) { + scoped_refptr<DeleteAllTask> task = + new DeleteAllTask(this, callback); + + DoCookieTask(task); +} + +void CookieMonster::DeleteAllCreatedBetweenAsync( + const Time& delete_begin, const Time& delete_end, + const DeleteCallback& callback) { + scoped_refptr<DeleteAllCreatedBetweenTask> task = + new DeleteAllCreatedBetweenTask(this, delete_begin, delete_end, + callback); + + DoCookieTask(task); +} + +void CookieMonster::DeleteAllForHostAsync( + const GURL& url, const DeleteCallback& callback) { + scoped_refptr<DeleteAllForHostTask> task = + new DeleteAllForHostTask(this, url, callback); + + DoCookieTaskForURL(task, url); +} + +void CookieMonster::DeleteCanonicalCookieAsync( + const CanonicalCookie& cookie, + const DeleteCookieCallback& callback) { + scoped_refptr<DeleteCanonicalCookieTask> task = + new DeleteCanonicalCookieTask(this, cookie, callback); + + DoCookieTask(task); +} + +void CookieMonster::SetCookieWithOptionsAsync( + const GURL& url, + const std::string& cookie_line, + const CookieOptions& options, + const SetCookiesCallback& callback) { + scoped_refptr<SetCookieWithOptionsTask> task = + new SetCookieWithOptionsTask(this, url, cookie_line, options, callback); + + DoCookieTaskForURL(task, url); +} + +void CookieMonster::GetCookiesWithOptionsAsync( + const GURL& url, + const CookieOptions& options, + const GetCookiesCallback& callback) { + scoped_refptr<GetCookiesWithOptionsTask> task = + new GetCookiesWithOptionsTask(this, url, options, callback); + + DoCookieTaskForURL(task, url); +} + +void CookieMonster::GetCookiesWithInfoAsync( + const GURL& url, + const CookieOptions& options, + const GetCookieInfoCallback& callback) { + scoped_refptr<GetCookiesWithInfoTask> task = + new GetCookiesWithInfoTask(this, url, options, callback); + + DoCookieTaskForURL(task, url); +} + +void CookieMonster::DeleteCookieAsync(const GURL& url, + const std::string& cookie_name, + const base::Closure& callback) { + scoped_refptr<DeleteCookieTask> task = + new DeleteCookieTask(this, url, cookie_name, callback); + + DoCookieTaskForURL(task, url); +} + +void CookieMonster::DoCookieTask( + const scoped_refptr<CookieMonsterTask>& task_item) { + { + base::AutoLock autolock(lock_); + InitIfNecessary(); + if (!loaded_) { + queue_.push(task_item); + return; + } + } + + task_item->Run(); +} + +void CookieMonster::DoCookieTaskForURL( + const scoped_refptr<CookieMonsterTask>& task_item, + const GURL& url) { + { + base::AutoLock autolock(lock_); + InitIfNecessary(); + // If cookies for the requested domain key (eTLD+1) have been loaded from DB + // then run the task, otherwise load from DB. + if (!loaded_) { + // Checks if the domain key has been loaded. + std::string key(cookie_util::GetEffectiveDomain(url.scheme(), + url.host())); + if (keys_loaded_.find(key) == keys_loaded_.end()) { + std::map<std::string, std::deque<scoped_refptr<CookieMonsterTask> > > + ::iterator it = tasks_queued_.find(key); + if (it == tasks_queued_.end()) { + store_->LoadCookiesForKey(key, + base::Bind(&CookieMonster::OnKeyLoaded, this, key)); + it = tasks_queued_.insert(std::make_pair(key, + std::deque<scoped_refptr<CookieMonsterTask> >())).first; + } + it->second.push_back(task_item); + return; + } + } + } + task_item->Run(); +} + +bool CookieMonster::SetCookieWithDetails( + const GURL& url, const std::string& name, const std::string& value, + const std::string& domain, const std::string& path, + const base::Time& expiration_time, bool secure, bool http_only) { + base::AutoLock autolock(lock_); + + if (!HasCookieableScheme(url)) + return false; + + Time creation_time = CurrentTime(); + last_time_seen_ = creation_time; + + // TODO(abarth): Take these values as parameters. + std::string mac_key; + std::string mac_algorithm; + + scoped_ptr<CanonicalCookie> cc; + cc.reset(CanonicalCookie::Create( + url, name, value, domain, path, + mac_key, mac_algorithm, + creation_time, expiration_time, + secure, http_only, !expiration_time.is_null())); + + if (!cc.get()) + return false; + + CookieOptions options; + options.set_include_httponly(); + return SetCanonicalCookie(&cc, creation_time, options); +} + +bool CookieMonster::InitializeFrom(const CookieList& list) { + base::AutoLock autolock(lock_); + InitIfNecessary(); + for (net::CookieList::const_iterator iter = list.begin(); + iter != list.end(); ++iter) { + scoped_ptr<net::CookieMonster::CanonicalCookie> cookie; + cookie.reset(new net::CookieMonster::CanonicalCookie(*iter)); + net::CookieOptions options; + options.set_include_httponly(); + if (!SetCanonicalCookie(&cookie, cookie->CreationDate(), + options)) { + return false; + } + } + return true; +} + +CookieList CookieMonster::GetAllCookies() { + base::AutoLock autolock(lock_); + + // 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()), + NULL); + + // 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 (CookieMap::iterator it = cookies_.begin(); it != cookies_.end(); ++it) + cookie_ptrs.push_back(it->second); + std::sort(cookie_ptrs.begin(), cookie_ptrs.end(), CookieSorter); + + CookieList cookie_list; + cookie_list.reserve(cookie_ptrs.size()); + for (std::vector<CanonicalCookie*>::const_iterator it = cookie_ptrs.begin(); + it != cookie_ptrs.end(); ++it) + cookie_list.push_back(**it); + + return cookie_list; +} + +CookieList CookieMonster::GetAllCookiesForURLWithOptions( + const GURL& url, + const CookieOptions& options) { + base::AutoLock autolock(lock_); + + std::vector<CanonicalCookie*> cookie_ptrs; + FindCookiesForHostAndDomain(url, options, false, &cookie_ptrs); + std::sort(cookie_ptrs.begin(), cookie_ptrs.end(), CookieSorter); + + CookieList cookies; + for (std::vector<CanonicalCookie*>::const_iterator it = cookie_ptrs.begin(); + it != cookie_ptrs.end(); it++) + cookies.push_back(**it); + + return cookies; +} + +CookieList CookieMonster::GetAllCookiesForURL(const GURL& url) { + CookieOptions options; + options.set_include_httponly(); + + return GetAllCookiesForURLWithOptions(url, options); +} + +int CookieMonster::DeleteAll(bool sync_to_store) { + base::AutoLock autolock(lock_); + + int num_deleted = 0; + for (CookieMap::iterator it = cookies_.begin(); it != cookies_.end();) { + CookieMap::iterator curit = it; + ++it; + InternalDeleteCookie(curit, sync_to_store, + sync_to_store ? DELETE_COOKIE_EXPLICIT : + DELETE_COOKIE_DONT_RECORD /* Destruction. */); + ++num_deleted; + } + + return num_deleted; +} + +int CookieMonster::DeleteAllCreatedBetween(const Time& delete_begin, + const Time& delete_end) { + base::AutoLock autolock(lock_); + + int num_deleted = 0; + for (CookieMap::iterator it = cookies_.begin(); it != cookies_.end();) { + CookieMap::iterator curit = it; + CanonicalCookie* cc = curit->second; + ++it; + + if (cc->CreationDate() >= delete_begin && + (delete_end.is_null() || cc->CreationDate() < delete_end)) { + InternalDeleteCookie(curit, + true, /*sync_to_store*/ + DELETE_COOKIE_EXPLICIT); + ++num_deleted; + } + } + + return num_deleted; +} + +int CookieMonster::DeleteAllForHost(const GURL& url) { + base::AutoLock autolock(lock_); + + if (!HasCookieableScheme(url)) + return 0; + + const std::string scheme(url.scheme()); + const std::string host(url.host()); + + // We store host cookies in the store by their canonical host name; + // domain cookies are stored with a leading ".". So this is a pretty + // simple lookup and per-cookie delete. + int num_deleted = 0; + for (CookieMapItPair its = cookies_.equal_range(GetKey(host)); + its.first != its.second;) { + CookieMap::iterator curit = its.first; + ++its.first; + + const CanonicalCookie* const cc = curit->second; + + // Delete only on a match as a host cookie. + if (cc->IsHostCookie() && cc->IsDomainMatch(scheme, host)) { + num_deleted++; + + InternalDeleteCookie(curit, true, DELETE_COOKIE_EXPLICIT); + } + } + return num_deleted; +} + +bool CookieMonster::DeleteCanonicalCookie(const CanonicalCookie& cookie) { + base::AutoLock autolock(lock_); + + for (CookieMapItPair its = cookies_.equal_range(GetKey(cookie.Domain())); + its.first != its.second; ++its.first) { + // The creation date acts as our unique index... + if (its.first->second->CreationDate() == cookie.CreationDate()) { + InternalDeleteCookie(its.first, true, DELETE_COOKIE_EXPLICIT); + return true; + } + } + return false; +} + +void CookieMonster::SetCookieableSchemes( + const char* schemes[], size_t num_schemes) { + base::AutoLock autolock(lock_); + + // Cookieable Schemes must be set before first use of function. + DCHECK(!initialized_); + + cookieable_schemes_.clear(); + cookieable_schemes_.insert(cookieable_schemes_.end(), + schemes, schemes + num_schemes); +} + +void CookieMonster::SetKeepExpiredCookies() { + keep_expired_cookies_ = true; +} + +void CookieMonster::SetClearPersistentStoreOnExit(bool clear_local_store) { + if (store_) + store_->SetClearLocalStateOnExit(clear_local_store); +} + +// static +void CookieMonster::EnableFileScheme() { + enable_file_scheme_ = true; +} + +void CookieMonster::FlushStore(const base::Closure& callback) { + base::AutoLock autolock(lock_); + if (initialized_ && store_) + store_->Flush(callback); + else if (!callback.is_null()) + MessageLoop::current()->PostTask(FROM_HERE, callback); +} + +bool CookieMonster::SetCookieWithOptions(const GURL& url, + const std::string& cookie_line, + const CookieOptions& options) { + base::AutoLock autolock(lock_); + + if (!HasCookieableScheme(url)) { + return false; + } + + return SetCookieWithCreationTimeAndOptions(url, cookie_line, Time(), options); +} + +std::string CookieMonster::GetCookiesWithOptions(const GURL& url, + const CookieOptions& options) { + base::AutoLock autolock(lock_); + + if (!HasCookieableScheme(url)) + return std::string(); + + TimeTicks start_time(TimeTicks::Now()); + + std::vector<CanonicalCookie*> cookies; + FindCookiesForHostAndDomain(url, options, true, &cookies); + std::sort(cookies.begin(), cookies.end(), CookieSorter); + + std::string cookie_line = BuildCookieLine(cookies); + + histogram_time_get_->AddTime(TimeTicks::Now() - start_time); + + VLOG(kVlogGetCookies) << "GetCookies() result: " << cookie_line; + + return cookie_line; +} + +void CookieMonster::GetCookiesWithInfo(const GURL& url, + const CookieOptions& options, + std::string* cookie_line, + std::vector<CookieInfo>* cookie_infos) { + DCHECK(cookie_line->empty()); + DCHECK(cookie_infos->empty()); + + base::AutoLock autolock(lock_); + + if (!HasCookieableScheme(url)) + return; + + TimeTicks start_time(TimeTicks::Now()); + + std::vector<CanonicalCookie*> cookies; + FindCookiesForHostAndDomain(url, options, true, &cookies); + std::sort(cookies.begin(), cookies.end(), CookieSorter); + *cookie_line = BuildCookieLine(cookies); + + histogram_time_get_->AddTime(TimeTicks::Now() - start_time); + + TimeTicks mac_start_time = TimeTicks::Now(); + BuildCookieInfoList(cookies, cookie_infos); + histogram_time_mac_->AddTime(TimeTicks::Now() - mac_start_time); +} + +void CookieMonster::DeleteCookie(const GURL& url, + const std::string& cookie_name) { + base::AutoLock autolock(lock_); + + if (!HasCookieableScheme(url)) + return; + + CookieOptions options; + options.set_include_httponly(); + // Get the cookies for this host and its domain(s). + std::vector<CanonicalCookie*> cookies; + FindCookiesForHostAndDomain(url, options, true, &cookies); + std::set<CanonicalCookie*> matching_cookies; + + for (std::vector<CanonicalCookie*>::const_iterator it = cookies.begin(); + it != cookies.end(); ++it) { + if ((*it)->Name() != cookie_name) + continue; + if (url.path().find((*it)->Path())) + continue; + matching_cookies.insert(*it); + } + + for (CookieMap::iterator it = cookies_.begin(); it != cookies_.end();) { + CookieMap::iterator curit = it; + ++it; + if (matching_cookies.find(curit->second) != matching_cookies.end()) { + InternalDeleteCookie(curit, true, DELETE_COOKIE_EXPLICIT); + } + } +} + +CookieMonster* CookieMonster::GetCookieMonster() { + return this; +} + +void CookieMonster::SetPersistSessionCookies(bool persist_session_cookies) { + // This function must be called before the CookieMonster is used. + DCHECK(!initialized_); + persist_session_cookies_ = persist_session_cookies; +} + +void CookieMonster::SaveSessionCookies() { + if (store_) { + store_->SetClearLocalStateOnExit(false); + } +} + +CookieMonster::~CookieMonster() { + DeleteAll(false); +} + +bool CookieMonster::SetCookieWithCreationTime(const GURL& url, + const std::string& cookie_line, + const base::Time& creation_time) { + DCHECK(!store_) << "This method is only to be used by unit-tests."; + base::AutoLock autolock(lock_); + + if (!HasCookieableScheme(url)) { + return false; + } + + InitIfNecessary(); + return SetCookieWithCreationTimeAndOptions(url, cookie_line, creation_time, + CookieOptions()); +} + +void CookieMonster::InitStore() { + DCHECK(store_) << "Store must exist to initialize"; + + // We bind in the current time so that we can report the wall-clock time for + // loading cookies. + store_->Load(base::Bind(&CookieMonster::OnLoaded, this, TimeTicks::Now())); +} + +void CookieMonster::OnLoaded(TimeTicks beginning_time, + const std::vector<CanonicalCookie*>& cookies) { + StoreLoadedCookies(cookies); + histogram_time_blocked_on_load_->AddTime(TimeTicks::Now() - beginning_time); + + // Invoke the task queue of cookie request. + InvokeQueue(); +} + +void CookieMonster::OnKeyLoaded(const std::string& key, + const std::vector<CanonicalCookie*>& cookies) { + // This function does its own separate locking. + StoreLoadedCookies(cookies); + + std::deque<scoped_refptr<CookieMonsterTask> > tasks_queued; + { + base::AutoLock autolock(lock_); + keys_loaded_.insert(key); + std::map<std::string, std::deque<scoped_refptr<CookieMonsterTask> > > + ::iterator it = tasks_queued_.find(key); + if (it == tasks_queued_.end()) + return; + it->second.swap(tasks_queued); + tasks_queued_.erase(it); + } + + while (!tasks_queued.empty()) { + scoped_refptr<CookieMonsterTask> task = tasks_queued.front(); + task->Run(); + tasks_queued.pop_front(); + } +} + +void CookieMonster::StoreLoadedCookies( + const std::vector<CanonicalCookie*>& cookies) { + // Initialize the store and sync in any saved persistent cookies. We don't + // care if it's expired, insert it so it can be garbage collected, removed, + // and sync'd. + base::AutoLock autolock(lock_); + + for (std::vector<CanonicalCookie*>::const_iterator it = cookies.begin(); + it != cookies.end(); ++it) { + int64 cookie_creation_time = (*it)->CreationDate().ToInternalValue(); + + if (creation_times_.insert(cookie_creation_time).second) { + InternalInsertCookie(GetKey((*it)->Domain()), *it, false); + const Time cookie_access_time((*it)->LastAccessDate()); + if (earliest_access_time_.is_null() || + cookie_access_time < earliest_access_time_) + earliest_access_time_ = cookie_access_time; + } else { + LOG(ERROR) << base::StringPrintf("Found cookies with duplicate creation " + "times in backing store: " + "{name='%s', domain='%s', path='%s'}", + (*it)->Name().c_str(), + (*it)->Domain().c_str(), + (*it)->Path().c_str()); + // We've been given ownership of the cookie and are throwing it + // away; reclaim the space. + delete (*it); + } + } + + // 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() { + while (true) { + scoped_refptr<CookieMonsterTask> request_task; + { + base::AutoLock autolock(lock_); + if (queue_.empty()) { + loaded_ = true; + creation_times_.clear(); + keys_loaded_.clear(); + break; + } + request_task = queue_.front(); + queue_.pop(); + } + request_task->Run(); + } +} + +void CookieMonster::EnsureCookiesMapIsValid() { + lock_.AssertAcquired(); + + int num_duplicates_trimmed = 0; + + // Iterate through all the of the cookies, grouped by host. + CookieMap::iterator prev_range_end = cookies_.begin(); + while (prev_range_end != cookies_.end()) { + CookieMap::iterator cur_range_begin = prev_range_end; + const std::string key = cur_range_begin->first; // Keep a copy. + CookieMap::iterator cur_range_end = cookies_.upper_bound(key); + prev_range_end = cur_range_end; + + // Ensure no equivalent cookies for this host. + num_duplicates_trimmed += + TrimDuplicateCookiesForKey(key, cur_range_begin, cur_range_end); + } + + // Record how many duplicates were found in the database. + // See InitializeHistograms() for details. + histogram_cookie_deletion_cause_->Add(num_duplicates_trimmed); +} + +int CookieMonster::TrimDuplicateCookiesForKey( + const std::string& key, + CookieMap::iterator begin, + CookieMap::iterator end) { + lock_.AssertAcquired(); + + // Set of cookies ordered by creation time. + typedef std::set<CookieMap::iterator, OrderByCreationTimeDesc> CookieSet; + + // Helper map we populate to find the duplicates. + typedef std::map<CookieSignature, 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 (CookieMap::iterator it = begin; it != end; ++it) { + DCHECK_EQ(key, it->first); + CanonicalCookie* cookie = it->second; + + CookieSignature signature(cookie->Name(), cookie->Domain(), + cookie->Path()); + 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. + bool insert_success = set.insert(it).second; + DCHECK(insert_success) << + "Duplicate creation times found in duplicate cookie name scan."; + } + + // If there were no duplicates, we are done! + if (num_duplicates == 0) + return 0; + + // 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 (EquivalenceMap::iterator it = equivalent_cookies.begin(); + it != equivalent_cookies.end(); + ++it) { + const CookieSignature& signature = it->first; + CookieSet& dupes = it->second; + + if (dupes.size() <= 1) + continue; // This cookiename/path has no duplicates. + num_duplicates_found += dupes.size() - 1; + + // Since |dups| is sorted by creation time (descending), the first cookie + // is the most recent one, so we will keep it. The rest are duplicates. + dupes.erase(dupes.begin()); + + LOG(ERROR) << base::StringPrintf( + "Found %d duplicate cookies for host='%s', " + "with {name='%s', domain='%s', path='%s'}", + static_cast<int>(dupes.size()), + key.c_str(), + signature.name.c_str(), + signature.domain.c_str(), + signature.path.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 (CookieSet::iterator dupes_it = dupes.begin(); + dupes_it != dupes.end(); + ++dupes_it) { + InternalDeleteCookie(*dupes_it, true, + DELETE_COOKIE_DUPLICATE_IN_BACKING_STORE); + } + } + DCHECK_EQ(num_duplicates, num_duplicates_found); + + return num_duplicates; +} + +// Note: file must be the last scheme. +const char* CookieMonster::kDefaultCookieableSchemes[] = + { "http", "https", "file" }; +const int CookieMonster::kDefaultCookieableSchemesCount = + arraysize(CookieMonster::kDefaultCookieableSchemes); + +void CookieMonster::SetDefaultCookieableSchemes() { + int num_schemes = enable_file_scheme_ ? + kDefaultCookieableSchemesCount : kDefaultCookieableSchemesCount - 1; + SetCookieableSchemes(kDefaultCookieableSchemes, num_schemes); +} + + +void CookieMonster::FindCookiesForHostAndDomain( + const GURL& url, + const CookieOptions& options, + bool update_access_time, + std::vector<CanonicalCookie*>* cookies) { + lock_.AssertAcquired(); + + const Time current_time(CurrentTime()); + + // 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. + RecordPeriodicStats(current_time); + + // Can just dispatch to FindCookiesForKey + const std::string key(GetKey(url.host())); + FindCookiesForKey(key, url, options, current_time, + update_access_time, cookies); +} + +void CookieMonster::FindCookiesForKey( + const std::string& key, + const GURL& url, + const CookieOptions& options, + const Time& current, + bool update_access_time, + std::vector<CanonicalCookie*>* cookies) { + lock_.AssertAcquired(); + + const std::string scheme(url.scheme()); + const std::string host(url.host()); + bool secure = url.SchemeIsSecure(); + + for (CookieMapItPair its = cookies_.equal_range(key); + its.first != its.second; ) { + CookieMap::iterator curit = its.first; + CanonicalCookie* cc = curit->second; + ++its.first; + + // If the cookie is expired, delete it. + if (cc->IsExpired(current) && !keep_expired_cookies_) { + InternalDeleteCookie(curit, true, DELETE_COOKIE_EXPIRED); + continue; + } + + // Filter out HttpOnly cookies, per options. + if (options.exclude_httponly() && cc->IsHttpOnly()) + continue; + + // Filter out secure cookies unless we're https. + if (!secure && cc->IsSecure()) + continue; + + // Filter out cookies that don't apply to this domain. + if (!cc->IsDomainMatch(scheme, host)) + continue; + + if (!cc->IsOnPath(url.path())) + continue; + + // Add this cookie to the set of matching cookies. Update the access + // time if we've been requested to do so. + if (update_access_time) { + InternalUpdateCookieAccessTime(cc, current); + } + cookies->push_back(cc); + } +} + +bool CookieMonster::DeleteAnyEquivalentCookie(const std::string& key, + const CanonicalCookie& ecc, + bool skip_httponly, + bool already_expired) { + lock_.AssertAcquired(); + + bool found_equivalent_cookie = false; + bool skipped_httponly = false; + for (CookieMapItPair its = cookies_.equal_range(key); + its.first != its.second; ) { + CookieMap::iterator curit = its.first; + CanonicalCookie* cc = curit->second; + ++its.first; + + if (ecc.IsEquivalent(*cc)) { + // 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."; + if (skip_httponly && cc->IsHttpOnly()) { + skipped_httponly = true; + } else { + InternalDeleteCookie(curit, true, already_expired ? + DELETE_COOKIE_EXPIRED_OVERWRITE : DELETE_COOKIE_OVERWRITE); + } + found_equivalent_cookie = true; + } + } + return skipped_httponly; +} + +void CookieMonster::InternalInsertCookie(const std::string& key, + CanonicalCookie* cc, + bool sync_to_store) { + lock_.AssertAcquired(); + + if ((cc->IsPersistent() || persist_session_cookies_) && + store_ && sync_to_store) + store_->AddCookie(*cc); + cookies_.insert(CookieMap::value_type(key, cc)); + if (delegate_.get()) { + delegate_->OnCookieChanged( + *cc, false, CookieMonster::Delegate::CHANGE_COOKIE_EXPLICIT); + } +} + +bool CookieMonster::SetCookieWithCreationTimeAndOptions( + const GURL& url, + const std::string& cookie_line, + const Time& creation_time_or_null, + const CookieOptions& options) { + lock_.AssertAcquired(); + + VLOG(kVlogSetCookies) << "SetCookie() line: " << cookie_line; + + Time creation_time = creation_time_or_null; + if (creation_time.is_null()) { + creation_time = CurrentTime(); + last_time_seen_ = creation_time; + } + + // Parse the cookie. + ParsedCookie pc(cookie_line); + + if (!pc.IsValid()) { + VLOG(kVlogSetCookies) << "WARNING: Couldn't parse cookie"; + return false; + } + + if (options.exclude_httponly() && pc.IsHttpOnly()) { + VLOG(kVlogSetCookies) << "SetCookie() not setting httponly cookie"; + return false; + } + + std::string cookie_domain; + if (!GetCookieDomain(url, pc, &cookie_domain)) { + return false; + } + + std::string cookie_path = CanonPath(url, pc); + std::string mac_key = pc.HasMACKey() ? pc.MACKey() : std::string(); + std::string mac_algorithm = pc.HasMACAlgorithm() ? + pc.MACAlgorithm() : std::string(); + + scoped_ptr<CanonicalCookie> cc; + Time cookie_expires = CanonExpiration(pc, creation_time); + + bool session_only = options.force_session() || cookie_expires.is_null(); + cc.reset(new CanonicalCookie(url, pc.Name(), pc.Value(), cookie_domain, + cookie_path, mac_key, mac_algorithm, + creation_time, cookie_expires, + creation_time, pc.IsSecure(), pc.IsHttpOnly(), + !cookie_expires.is_null(), + !session_only)); + + if (!cc.get()) { + VLOG(kVlogSetCookies) << "WARNING: Failed to allocate CanonicalCookie"; + return false; + } + return SetCanonicalCookie(&cc, creation_time, options); +} + +bool CookieMonster::SetCanonicalCookie(scoped_ptr<CanonicalCookie>* cc, + const Time& creation_time, + const CookieOptions& options) { + const std::string key(GetKey((*cc)->Domain())); + bool already_expired = (*cc)->IsExpired(creation_time); + if (DeleteAnyEquivalentCookie(key, **cc, options.exclude_httponly(), + already_expired)) { + VLOG(kVlogSetCookies) << "SetCookie() not clobbering httponly cookie"; + return false; + } + + VLOG(kVlogSetCookies) << "SetCookie() key: " << key << " cc: " + << (*cc)->DebugString(); + + // 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 || keep_expired_cookies_) { + // See InitializeHistograms() for details. + if ((*cc)->DoesExpire()) { + histogram_expiration_duration_minutes_->Add( + ((*cc)->ExpiryDate() - creation_time).InMinutes()); + } + + InternalInsertCookie(key, cc->release(), true); + } + + // 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(). + GarbageCollect(creation_time, key); + + return true; +} + +void CookieMonster::InternalUpdateCookieAccessTime(CanonicalCookie* cc, + const Time& current) { + lock_.AssertAcquired(); + + // 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; + + // See InitializeHistograms() for details. + histogram_between_access_interval_minutes_->Add( + (current - cc->LastAccessDate()).InMinutes()); + + cc->SetLastAccessDate(current); + if ((cc->IsPersistent() || persist_session_cookies_) && store_) + store_->UpdateCookieAccessTime(*cc); +} + +void CookieMonster::InternalDeleteCookie(CookieMap::iterator it, + bool sync_to_store, + DeletionCause deletion_cause) { + lock_.AssertAcquired(); + + // Ideally, this would be asserted up where we define ChangeCauseMapping, + // but DeletionCause's visibility (or lack thereof) forces us to make + // this check here. + COMPILE_ASSERT(arraysize(ChangeCauseMapping) == DELETE_COOKIE_LAST_ENTRY + 1, + ChangeCauseMapping_size_not_eq_DeletionCause_enum_size); + + // See InitializeHistograms() for details. + if (deletion_cause != DELETE_COOKIE_DONT_RECORD) + histogram_cookie_deletion_cause_->Add(deletion_cause); + + CanonicalCookie* cc = it->second; + VLOG(kVlogSetCookies) << "InternalDeleteCookie() cc: " << cc->DebugString(); + + if ((cc->IsPersistent() || persist_session_cookies_) + && store_ && sync_to_store) + store_->DeleteCookie(*cc); + if (delegate_.get()) { + ChangeCausePair mapping = ChangeCauseMapping[deletion_cause]; + + if (mapping.notify) + delegate_->OnCookieChanged(*cc, true, mapping.cause); + } + cookies_.erase(it); + delete cc; +} + +// Domain expiry behavior is unchanged by key/expiry scheme (the +// meaning of the key is different, but that's not visible to this +// routine). +int CookieMonster::GarbageCollect(const Time& current, + const std::string& key) { + lock_.AssertAcquired(); + + int num_deleted = 0; + + // Collect garbage for this key. + if (cookies_.count(key) > kDomainMaxCookies) { + VLOG(kVlogGarbageCollection) << "GarbageCollect() key: " << key; + + std::vector<CookieMap::iterator> cookie_its; + num_deleted += GarbageCollectExpired( + current, cookies_.equal_range(key), &cookie_its); + base::Time oldest_removed; + if (FindLeastRecentlyAccessed(kDomainMaxCookies, kDomainPurgeCookies, + &oldest_removed, &cookie_its)) { + // Delete in two passes so we can figure out what we're nuking + // that would be kept at the global level. + int num_subject_to_global_purge = + GarbageCollectDeleteList( + current, + Time::Now() - TimeDelta::FromDays(kSafeFromGlobalPurgeDays), + DELETE_COOKIE_EVICTED_DOMAIN_PRE_SAFE, + cookie_its); + num_deleted += num_subject_to_global_purge; + // Correct because FindLeastRecentlyAccessed returns a sorted list. + cookie_its.erase(cookie_its.begin(), + cookie_its.begin() + num_subject_to_global_purge); + num_deleted += + GarbageCollectDeleteList( + current, + Time(), + DELETE_COOKIE_EVICTED_DOMAIN_POST_SAFE, + cookie_its); + } + } + + // Collect garbage for everything. With firefox style we want to + // preserve cookies touched in kSafeFromGlobalPurgeDays, otherwise + // not. + if (cookies_.size() > kMaxCookies && + (earliest_access_time_ < + Time::Now() - TimeDelta::FromDays(kSafeFromGlobalPurgeDays))) { + VLOG(kVlogGarbageCollection) << "GarbageCollect() everything"; + std::vector<CookieMap::iterator> cookie_its; + base::Time oldest_left; + num_deleted += GarbageCollectExpired( + current, CookieMapItPair(cookies_.begin(), cookies_.end()), + &cookie_its); + if (FindLeastRecentlyAccessed(kMaxCookies, kPurgeCookies, + &oldest_left, &cookie_its)) { + Time oldest_safe_cookie( + (Time::Now() - TimeDelta::FromDays(kSafeFromGlobalPurgeDays))); + int num_evicted = GarbageCollectDeleteList( + current, + oldest_safe_cookie, + DELETE_COOKIE_EVICTED_GLOBAL, + cookie_its); + + // If no cookies were preserved by the time limit, the global last + // access is set to the value returned from FindLeastRecentlyAccessed. + // If the time limit preserved some cookies, we use the last access of + // the oldest preserved cookie. + if (num_evicted == static_cast<int>(cookie_its.size())) { + earliest_access_time_ = oldest_left; + } else { + earliest_access_time_ = + (*(cookie_its.begin() + num_evicted))->second->LastAccessDate(); + } + num_deleted += num_evicted; + } + } + + return num_deleted; +} + +int CookieMonster::GarbageCollectExpired( + const Time& current, + const CookieMapItPair& itpair, + std::vector<CookieMap::iterator>* cookie_its) { + if (keep_expired_cookies_) + return 0; + + lock_.AssertAcquired(); + + int num_deleted = 0; + for (CookieMap::iterator it = itpair.first, end = itpair.second; it != end;) { + CookieMap::iterator 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; +} + +int CookieMonster::GarbageCollectDeleteList( + const Time& current, + const Time& keep_accessed_after, + DeletionCause cause, + std::vector<CookieMap::iterator>& cookie_its) { + int num_deleted = 0; + for (std::vector<CookieMap::iterator>::iterator it = cookie_its.begin(); + it != cookie_its.end(); it++) { + if (keep_accessed_after.is_null() || + (*it)->second->LastAccessDate() < keep_accessed_after) { + histogram_evicted_last_access_minutes_->Add( + (current - (*it)->second->LastAccessDate()).InMinutes()); + InternalDeleteCookie((*it), true, cause); + num_deleted++; + } + } + return num_deleted; +} + +// A wrapper around RegistryControlledDomainService::GetDomainAndRegistry +// to make clear we're creating a key for our local map. Here and +// in FindCookiesForHostAndDomain() 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). +std::string CookieMonster::GetKey(const std::string& domain) const { + std::string effective_domain( + RegistryControlledDomainService::GetDomainAndRegistry(domain)); + if (effective_domain.empty()) + effective_domain = domain; + + if (!effective_domain.empty() && effective_domain[0] == '.') + return effective_domain.substr(1); + return effective_domain; +} + +bool CookieMonster::HasCookieableScheme(const GURL& url) { + lock_.AssertAcquired(); + + // Make sure the request is on a cookie-able url scheme. + for (size_t i = 0; i < cookieable_schemes_.size(); ++i) { + // We matched a scheme. + if (url.SchemeIs(cookieable_schemes_[i].c_str())) { + // We've matched a supported scheme. + return true; + } + } + + // The scheme didn't match any in our whitelist. + VLOG(kVlogPerCookieMonster) << "WARNING: Unsupported cookie scheme: " + << url.scheme(); + return false; +} + +// 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) { + const base::TimeDelta kRecordStatisticsIntervalTime( + base::TimeDelta::FromSeconds(kRecordStatisticsIntervalSeconds)); + + // If we've taken statistics recently, return. + if (current_time - last_statistic_record_time_ <= + kRecordStatisticsIntervalTime) { + return; + } + + // See InitializeHistograms() for details. + histogram_count_->Add(cookies_.size()); + + // More detailed statistics on cookie counts at different granularities. + TimeTicks beginning_of_time(TimeTicks::Now()); + + for (CookieMap::const_iterator it_key = cookies_.begin(); + it_key != cookies_.end(); ) { + const std::string& key(it_key->first); + + int key_count = 0; + typedef std::map<std::string, unsigned int> DomainMap; + DomainMap domain_map; + CookieMapItPair its_cookies = cookies_.equal_range(key); + while (its_cookies.first != its_cookies.second) { + key_count++; + const std::string& cookie_domain(its_cookies.first->second->Domain()); + domain_map[cookie_domain]++; + + its_cookies.first++; + } + histogram_etldp1_count_->Add(key_count); + histogram_domain_per_etldp1_count_->Add(domain_map.size()); + for (DomainMap::const_iterator domain_map_it = domain_map.begin(); + domain_map_it != domain_map.end(); domain_map_it++) + histogram_domain_count_->Add(domain_map_it->second); + + it_key = its_cookies.second; + } + + VLOG(kVlogPeriodic) + << "Time for recording cookie stats (us): " + << (TimeTicks::Now() - beginning_of_time).InMicroseconds(); + + last_statistic_record_time_ = current_time; +} + +// Initialize all histogram counter variables used in this class. +// +// Normal histogram usage involves using the macros defined in +// histogram.h, which automatically takes care of declaring these +// variables (as statics), initializing them, and accumulating into +// them, all from a single entry point. Unfortunately, that solution +// doesn't work for the CookieMonster, as it's vulnerable to races between +// separate threads executing the same functions and hence initializing the +// same static variables. There isn't a race danger in the histogram +// accumulation calls; they are written to be resilient to simultaneous +// calls from multiple threads. +// +// The solution taken here is to have per-CookieMonster instance +// variables that are constructed during CookieMonster construction. +// Note that these variables refer to the same underlying histogram, +// so we still race (but safely) with other CookieMonster instances +// for accumulation. +// +// To do this we've expanded out the individual histogram macros calls, +// with declarations of the variables in the class decl, initialization here +// (done from the class constructor) and direct calls to the accumulation +// methods where needed. The specific histogram macro calls on which the +// initialization is based are included in comments below. +void CookieMonster::InitializeHistograms() { + // From UMA_HISTOGRAM_CUSTOM_COUNTS + histogram_expiration_duration_minutes_ = base::Histogram::FactoryGet( + "Cookie.ExpirationDurationMinutes", + 1, kMinutesInTenYears, 50, + base::Histogram::kUmaTargetedHistogramFlag); + histogram_between_access_interval_minutes_ = base::Histogram::FactoryGet( + "Cookie.BetweenAccessIntervalMinutes", + 1, kMinutesInTenYears, 50, + base::Histogram::kUmaTargetedHistogramFlag); + histogram_evicted_last_access_minutes_ = base::Histogram::FactoryGet( + "Cookie.EvictedLastAccessMinutes", + 1, kMinutesInTenYears, 50, + base::Histogram::kUmaTargetedHistogramFlag); + histogram_count_ = base::Histogram::FactoryGet( + "Cookie.Count", 1, 4000, 50, + base::Histogram::kUmaTargetedHistogramFlag); + histogram_domain_count_ = base::Histogram::FactoryGet( + "Cookie.DomainCount", 1, 4000, 50, + base::Histogram::kUmaTargetedHistogramFlag); + histogram_etldp1_count_ = base::Histogram::FactoryGet( + "Cookie.Etldp1Count", 1, 4000, 50, + base::Histogram::kUmaTargetedHistogramFlag); + histogram_domain_per_etldp1_count_ = base::Histogram::FactoryGet( + "Cookie.DomainPerEtldp1Count", 1, 4000, 50, + base::Histogram::kUmaTargetedHistogramFlag); + + // From UMA_HISTOGRAM_COUNTS_10000 & UMA_HISTOGRAM_CUSTOM_COUNTS + histogram_number_duplicate_db_cookies_ = base::Histogram::FactoryGet( + "Net.NumDuplicateCookiesInDb", 1, 10000, 50, + base::Histogram::kUmaTargetedHistogramFlag); + + // From UMA_HISTOGRAM_ENUMERATION + histogram_cookie_deletion_cause_ = base::LinearHistogram::FactoryGet( + "Cookie.DeletionCause", 1, + DELETE_COOKIE_LAST_ENTRY - 1, DELETE_COOKIE_LAST_ENTRY, + base::Histogram::kUmaTargetedHistogramFlag); + + // From UMA_HISTOGRAM_{CUSTOM_,}TIMES + histogram_time_get_ = base::Histogram::FactoryTimeGet("Cookie.TimeGet", + base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromMinutes(1), + 50, base::Histogram::kUmaTargetedHistogramFlag); + histogram_time_mac_ = base::Histogram::FactoryTimeGet("Cookie.TimeGetMac", + base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromMinutes(1), + 50, base::Histogram::kUmaTargetedHistogramFlag); + histogram_time_blocked_on_load_ = base::Histogram::FactoryTimeGet( + "Cookie.TimeBlockedOnLoad", + base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromMinutes(1), + 50, base::Histogram::kUmaTargetedHistogramFlag); +} + + +// The system resolution is not high enough, so we can have multiple +// set cookies that result in the same system time. When this happens, we +// increment by one Time unit. Let's hope computers don't get too fast. +Time CookieMonster::CurrentTime() { + return std::max(Time::Now(), + Time::FromInternalValue(last_time_seen_.ToInternalValue() + 1)); +} + +CookieMonster::ParsedCookie::ParsedCookie(const std::string& cookie_line) + : is_valid_(false), + path_index_(0), + domain_index_(0), + mac_key_index_(0), + mac_algorithm_index_(0), + expires_index_(0), + maxage_index_(0), + secure_index_(0), + httponly_index_(0) { + + if (cookie_line.size() > kMaxCookieSize) { + VLOG(1) << "Not parsing cookie, too large: " << cookie_line.size(); + return; + } + + ParseTokenValuePairs(cookie_line); + if (!pairs_.empty()) { + is_valid_ = true; + SetupAttributes(); + } +} + +CookieMonster::ParsedCookie::~ParsedCookie() { +} + +// Returns true if |c| occurs in |chars| +// TODO(erikwright): maybe make this take an iterator, could check for end also? +static inline bool CharIsA(const char c, const char* chars) { + return strchr(chars, c) != NULL; +} +// Seek the iterator to the first occurrence of a character in |chars|. +// Returns true if it hit the end, false otherwise. +static inline bool SeekTo(std::string::const_iterator* it, + const std::string::const_iterator& end, + const char* chars) { + for (; *it != end && !CharIsA(**it, chars); ++(*it)) {} + return *it == end; +} +// Seek the iterator to the first occurrence of a character not in |chars|. +// Returns true if it hit the end, false otherwise. +static inline bool SeekPast(std::string::const_iterator* it, + const std::string::const_iterator& end, + const char* chars) { + for (; *it != end && CharIsA(**it, chars); ++(*it)) {} + return *it == end; +} +static inline bool SeekBackPast(std::string::const_iterator* it, + const std::string::const_iterator& end, + const char* chars) { + for (; *it != end && CharIsA(**it, chars); --(*it)) {} + return *it == end; +} + +const char CookieMonster::ParsedCookie::kTerminator[] = "\n\r\0"; +const int CookieMonster::ParsedCookie::kTerminatorLen = + sizeof(kTerminator) - 1; +const char CookieMonster::ParsedCookie::kWhitespace[] = " \t"; +const char CookieMonster::ParsedCookie::kValueSeparator[] = ";"; +const char CookieMonster::ParsedCookie::kTokenSeparator[] = ";="; + +// Create a cookie-line for the cookie. For debugging only! +// If we want to use this for something more than debugging, we +// should rewrite it better... +std::string CookieMonster::ParsedCookie::DebugString() const { + std::string out; + for (PairList::const_iterator it = pairs_.begin(); + it != pairs_.end(); ++it) { + out.append(it->first); + out.append("="); + out.append(it->second); + out.append("; "); + } + return out; +} + +std::string::const_iterator CookieMonster::ParsedCookie::FindFirstTerminator( + const std::string& s) { + std::string::const_iterator end = s.end(); + size_t term_pos = + s.find_first_of(std::string(kTerminator, kTerminatorLen)); + if (term_pos != std::string::npos) { + // We found a character we should treat as an end of string. + end = s.begin() + term_pos; + } + return end; +} + +bool CookieMonster::ParsedCookie::ParseToken( + std::string::const_iterator* it, + const std::string::const_iterator& end, + std::string::const_iterator* token_start, + std::string::const_iterator* token_end) { + DCHECK(it && token_start && token_end); + std::string::const_iterator token_real_end; + + // Seek past any whitespace before the "token" (the name). + // token_start should point at the first character in the token + if (SeekPast(it, end, kWhitespace)) + return false; // No token, whitespace or empty. + *token_start = *it; + + // Seek over the token, to the token separator. + // token_real_end should point at the token separator, i.e. '='. + // If it == end after the seek, we probably have a token-value. + SeekTo(it, end, kTokenSeparator); + token_real_end = *it; + + // Ignore any whitespace between the token and the token separator. + // token_end should point after the last interesting token character, + // pointing at either whitespace, or at '=' (and equal to token_real_end). + if (*it != *token_start) { // We could have an empty token name. + --(*it); // Go back before the token separator. + // Skip over any whitespace to the first non-whitespace character. + SeekBackPast(it, *token_start, kWhitespace); + // Point after it. + ++(*it); + } + *token_end = *it; + + // Seek us back to the end of the token. + *it = token_real_end; + return true; +} + +void CookieMonster::ParsedCookie::ParseValue( + std::string::const_iterator* it, + const std::string::const_iterator& end, + std::string::const_iterator* value_start, + std::string::const_iterator* value_end) { + DCHECK(it && value_start && value_end); + + // Seek past any whitespace that might in-between the token and value. + SeekPast(it, end, kWhitespace); + // value_start should point at the first character of the value. + *value_start = *it; + + // Just look for ';' to terminate ('=' allowed). + // We can hit the end, maybe they didn't terminate. + SeekTo(it, end, kValueSeparator); + + // Will be pointed at the ; seperator or the end. + *value_end = *it; + + // Ignore any unwanted whitespace after the value. + if (*value_end != *value_start) { // Could have an empty value + --(*value_end); + SeekBackPast(value_end, *value_start, kWhitespace); + ++(*value_end); + } +} + +std::string CookieMonster::ParsedCookie::ParseTokenString( + const std::string& token) { + std::string::const_iterator it = token.begin(); + std::string::const_iterator end = FindFirstTerminator(token); + + std::string::const_iterator token_start, token_end; + if (ParseToken(&it, end, &token_start, &token_end)) + return std::string(token_start, token_end); + return std::string(); +} + +std::string CookieMonster::ParsedCookie::ParseValueString( + const std::string& value) { + std::string::const_iterator it = value.begin(); + std::string::const_iterator end = FindFirstTerminator(value); + + std::string::const_iterator value_start, value_end; + ParseValue(&it, end, &value_start, &value_end); + return std::string(value_start, value_end); +} + +// Parse all token/value pairs and populate pairs_. +void CookieMonster::ParsedCookie::ParseTokenValuePairs( + const std::string& cookie_line) { + pairs_.clear(); + + // Ok, here we go. We should be expecting to be starting somewhere + // before the cookie line, not including any header name... + std::string::const_iterator start = cookie_line.begin(); + std::string::const_iterator it = start; + + // TODO(erikwright): Make sure we're stripping \r\n in the network code. + // Then we can log any unexpected terminators. + std::string::const_iterator end = FindFirstTerminator(cookie_line); + + for (int pair_num = 0; pair_num < kMaxPairs && it != end; ++pair_num) { + TokenValuePair pair; + + std::string::const_iterator token_start, token_end; + if (!ParseToken(&it, end, &token_start, &token_end)) + break; + + if (it == end || *it != '=') { + // We have a token-value, we didn't have any token name. + if (pair_num == 0) { + // For the first time around, we want to treat single values + // as a value with an empty name. (Mozilla bug 169091). + // IE seems to also have this behavior, ex "AAA", and "AAA=10" will + // set 2 different cookies, and setting "BBB" will then replace "AAA". + pair.first = ""; + // Rewind to the beginning of what we thought was the token name, + // and let it get parsed as a value. + it = token_start; + } else { + // Any not-first attribute we want to treat a value as a + // name with an empty value... This is so something like + // "secure;" will get parsed as a Token name, and not a value. + pair.first = std::string(token_start, token_end); + } + } else { + // We have a TOKEN=VALUE. + pair.first = std::string(token_start, token_end); + ++it; // Skip past the '='. + } + + // OK, now try to parse a value. + std::string::const_iterator value_start, value_end; + ParseValue(&it, end, &value_start, &value_end); + // OK, we're finished with a Token/Value. + pair.second = std::string(value_start, value_end); + + // From RFC2109: "Attributes (names) (attr) are case-insensitive." + if (pair_num != 0) + StringToLowerASCII(&pair.first); + pairs_.push_back(pair); + + // We've processed a token/value pair, we're either at the end of + // the string or a ValueSeparator like ';', which we want to skip. + if (it != end) + ++it; + } +} + +void CookieMonster::ParsedCookie::SetupAttributes() { + static const char kPathTokenName[] = "path"; + static const char kDomainTokenName[] = "domain"; + static const char kMACKeyTokenName[] = "mac-key"; + static const char kMACAlgorithmTokenName[] = "mac-algorithm"; + static const char kExpiresTokenName[] = "expires"; + static const char kMaxAgeTokenName[] = "max-age"; + static const char kSecureTokenName[] = "secure"; + static const char kHttpOnlyTokenName[] = "httponly"; + + // We skip over the first token/value, the user supplied one. + for (size_t i = 1; i < pairs_.size(); ++i) { + if (pairs_[i].first == kPathTokenName) { + path_index_ = i; + } else if (pairs_[i].first == kDomainTokenName) { + domain_index_ = i; + } else if (pairs_[i].first == kMACKeyTokenName) { + mac_key_index_ = i; + } else if (pairs_[i].first == kMACAlgorithmTokenName) { + mac_algorithm_index_ = i; + } else if (pairs_[i].first == kExpiresTokenName) { + expires_index_ = i; + } else if (pairs_[i].first == kMaxAgeTokenName) { + maxage_index_ = i; + } else if (pairs_[i].first == kSecureTokenName) { + secure_index_ = i; + } else if (pairs_[i].first == kHttpOnlyTokenName) { + httponly_index_ = i; + } else { + /* some attribute we don't know or don't care about. */ + } + } +} + +CookieMonster::CanonicalCookie::CanonicalCookie() + : secure_(false), + httponly_(false), + has_expires_(false), + is_persistent_(false) { + SetSessionCookieExpiryTime(); +} + +CookieMonster::CanonicalCookie::CanonicalCookie( + const GURL& url, const std::string& name, const std::string& value, + const std::string& domain, const std::string& path, + const std::string& mac_key, const std::string& mac_algorithm, + const base::Time& creation, const base::Time& expiration, + const base::Time& last_access, bool secure, bool httponly, bool has_expires, + bool is_persistent) + : source_(GetCookieSourceFromURL(url)), + name_(name), + value_(value), + domain_(domain), + path_(path), + mac_key_(mac_key), + mac_algorithm_(mac_algorithm), + creation_date_(creation), + expiry_date_(expiration), + last_access_date_(last_access), + secure_(secure), + httponly_(httponly), + has_expires_(has_expires), + is_persistent_(is_persistent) { + if (!has_expires_) { + DCHECK(!is_persistent_); + SetSessionCookieExpiryTime(); + } +} + +CookieMonster::CanonicalCookie::CanonicalCookie(const GURL& url, + const ParsedCookie& pc) + : source_(GetCookieSourceFromURL(url)), + name_(pc.Name()), + value_(pc.Value()), + path_(CanonPath(url, pc)), + mac_key_(pc.MACKey()), + mac_algorithm_(pc.MACAlgorithm()), + creation_date_(Time::Now()), + last_access_date_(Time()), + secure_(pc.IsSecure()), + httponly_(pc.IsHttpOnly()), + has_expires_(pc.HasExpires()), + is_persistent_(pc.HasExpires()) { + if (has_expires_) + expiry_date_ = CanonExpiration(pc, creation_date_); + else + SetSessionCookieExpiryTime(); + + // Do the best we can with the domain. + std::string cookie_domain; + std::string domain_string; + if (pc.HasDomain()) { + domain_string = pc.Domain(); + } + bool result + = cookie_util::GetCookieDomainWithString(url, domain_string, + &cookie_domain); + // Caller is responsible for passing in good arguments. + DCHECK(result); + domain_ = cookie_domain; +} + +CookieMonster::CanonicalCookie::~CanonicalCookie() { +} + +std::string CookieMonster::CanonicalCookie::GetCookieSourceFromURL( + const GURL& url) { + if (url.SchemeIsFile()) + return url.spec(); + + url_canon::Replacements<char> replacements; + replacements.ClearPort(); + if (url.SchemeIsSecure()) + replacements.SetScheme("http", url_parse::Component(0, 4)); + + return url.GetOrigin().ReplaceComponents(replacements).spec(); +} + +void CookieMonster::CanonicalCookie::SetSessionCookieExpiryTime() { +#if defined(ENABLE_PERSISTENT_SESSION_COOKIES) + // Mobile apps can sometimes be shut down without any warning, so the session + // cookie has to be persistent and given a default expiration time. + expiry_date_ = base::Time::Now() + + base::TimeDelta::FromDays(kPersistentSessionCookieExpiryInDays); + has_expires_ = true; +#endif +} + +CookieMonster::CanonicalCookie* CookieMonster::CanonicalCookie::Create( + const GURL& url, + const ParsedCookie& pc) { + if (!pc.IsValid()) { + return NULL; + } + + std::string domain_string; + if (!GetCookieDomain(url, pc, &domain_string)) { + return NULL; + } + std::string path_string = CanonPath(url, pc); + std::string mac_key = pc.HasMACKey() ? pc.MACKey() : std::string(); + std::string mac_algorithm = pc.HasMACAlgorithm() ? + pc.MACAlgorithm() : std::string(); + Time creation_time = Time::Now(); + Time expiration_time; + if (pc.HasExpires()) + expiration_time = net::CookieMonster::ParseCookieTime(pc.Expires()); + + return (Create(url, pc.Name(), pc.Value(), domain_string, path_string, + mac_key, mac_algorithm, creation_time, expiration_time, + pc.IsSecure(), pc.IsHttpOnly(), !expiration_time.is_null())); +} + +CookieMonster::CanonicalCookie* CookieMonster::CanonicalCookie::Create( + const GURL& url, + const std::string& name, + const std::string& value, + const std::string& domain, + const std::string& path, + const std::string& mac_key, + const std::string& mac_algorithm, + const base::Time& creation, + const base::Time& expiration, + bool secure, + bool http_only, + bool is_persistent) { + // Expect valid attribute tokens and values, as defined by the ParsedCookie + // logic, otherwise don't create the cookie. + std::string parsed_name = ParsedCookie::ParseTokenString(name); + if (parsed_name != name) + return NULL; + std::string parsed_value = ParsedCookie::ParseValueString(value); + if (parsed_value != value) + return NULL; + + std::string parsed_domain = ParsedCookie::ParseValueString(domain); + if (parsed_domain != domain) + return NULL; + std::string cookie_domain; + if (!cookie_util::GetCookieDomainWithString(url, parsed_domain, + &cookie_domain)) { + return NULL; + } + + std::string parsed_path = ParsedCookie::ParseValueString(path); + if (parsed_path != path) + return NULL; + + std::string cookie_path = CanonPathWithString(url, parsed_path); + // Expect that the path was either not specified (empty), or is valid. + if (!parsed_path.empty() && cookie_path != parsed_path) + return NULL; + // Canonicalize path again to make sure it escapes characters as needed. + url_parse::Component path_component(0, cookie_path.length()); + url_canon::RawCanonOutputT<char> canon_path; + url_parse::Component canon_path_component; + url_canon::CanonicalizePath(cookie_path.data(), path_component, + &canon_path, &canon_path_component); + cookie_path = std::string(canon_path.data() + canon_path_component.begin, + canon_path_component.len); + + return new CanonicalCookie(url, parsed_name, parsed_value, cookie_domain, + cookie_path, mac_key, mac_algorithm, creation, + expiration, creation, secure, http_only, + !expiration.is_null(), is_persistent); +} + +bool CookieMonster::CanonicalCookie::IsOnPath( + const std::string& url_path) const { + + // A zero length would be unsafe for our trailing '/' checks, and + // would also make no sense for our prefix match. The code that + // creates a CanonicalCookie should make sure the path is never zero length, + // but we double check anyway. + if (path_.empty()) + return false; + + // The Mozilla code broke it into 3 cases, if it's strings lengths + // are less than, equal, or greater. I think this is simpler: + + // Make sure the cookie path is a prefix of the url path. If the + // url path is shorter than the cookie path, then the cookie path + // can't be a prefix. + if (url_path.find(path_) != 0) + return false; + + // Now we know that url_path is >= cookie_path, and that cookie_path + // is a prefix of url_path. If they are the are the same length then + // they are identical, otherwise we need an additional check: + + // In order to avoid in correctly matching a cookie path of /blah + // with a request path of '/blahblah/', we need to make sure that either + // the cookie path ends in a trailing '/', or that we prefix up to a '/' + // in the url path. Since we know that the url path length is greater + // than the cookie path length, it's safe to index one byte past. + if (path_.length() != url_path.length() && + path_[path_.length() - 1] != '/' && + url_path[path_.length()] != '/') + return false; + + return true; +} + +bool CookieMonster::CanonicalCookie::IsDomainMatch( + const std::string& scheme, + const std::string& host) const { + // Can domain match in two ways; as a domain cookie (where the cookie + // domain begins with ".") or as a host cookie (where it doesn't). + + // Some consumers of the CookieMonster expect to set cookies on + // URLs like http://.strange.url. To retrieve cookies in this instance, + // we allow matching as a host cookie even when the domain_ starts with + // a period. + if (host == domain_) + return true; + + // Domain cookie must have an initial ".". To match, it must be + // equal to url's host with initial period removed, or a suffix of + // it. + + // Arguably this should only apply to "http" or "https" cookies, but + // extension cookie tests currently use the funtionality, and if we + // ever decide to implement that it should be done by preventing + // such cookies from being set. + if (domain_.empty() || domain_[0] != '.') + return false; + + // The host with a "." prefixed. + if (domain_.compare(1, std::string::npos, host) == 0) + return true; + + // A pure suffix of the host (ok since we know the domain already + // starts with a ".") + return (host.length() > domain_.length() && + host.compare(host.length() - domain_.length(), + domain_.length(), domain_) == 0); +} + +std::string CookieMonster::CanonicalCookie::DebugString() const { + return base::StringPrintf( + "name: %s value: %s domain: %s path: %s creation: %" + PRId64, + name_.c_str(), value_.c_str(), + domain_.c_str(), path_.c_str(), + static_cast<int64>(creation_date_.ToTimeT())); +} + +} // namespace diff --git a/net/cookies/cookie_monster.h b/net/cookies/cookie_monster.h new file mode 100644 index 0000000..b23dcfb --- /dev/null +++ b/net/cookies/cookie_monster.h @@ -0,0 +1,971 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Brought to you by the letter D and the number 2. + +#ifndef NET_COOKIES_COOKIE_MONSTER_H_ +#define NET_COOKIES_COOKIE_MONSTER_H_ +#pragma once + +#include <deque> +#include <map> +#include <queue> +#include <set> +#include <string> +#include <utility> +#include <vector> + +#include "base/basictypes.h" +#include "base/callback_forward.h" +#include "base/gtest_prod_util.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/synchronization/lock.h" +#include "base/time.h" +#include "net/cookies/cookie_store.h" +#include "net/base/net_export.h" + +class GURL; + +namespace base { +class Histogram; +class TimeTicks; +} // namespace base + +namespace net { + +class CookieList; + +// The cookie monster is the system for storing and retrieving cookies. It has +// an in-memory list of all cookies, and synchronizes non-session cookies to an +// optional permanent storage that implements the PersistentCookieStore +// interface. +// +// This class IS thread-safe. Normally, it is only used on the I/O thread, but +// is also accessed directly through Automation for UI testing. +// +// All cookie tasks are handled asynchronously. Tasks may be deferred if +// all affected cookies are not yet loaded from the backing store. Otherwise, +// the callback may be invoked immediately (prior to return of the asynchronous +// function). +// +// A cookie task is either pending loading of the entire cookie store, or +// loading of cookies for a specfic domain key(eTLD+1). In the former case, the +// cookie task will be queued in queue_ while PersistentCookieStore chain loads +// the cookie store on DB thread. In the latter case, the cookie task will be +// queued in tasks_queued_ while PermanentCookieStore loads cookies for the +// specified domain key(eTLD+1) on DB thread. +// +// Callbacks are guaranteed to be invoked on the calling thread. +// +// TODO(deanm) Implement CookieMonster, the cookie database. +// - Verify that our domain enforcement and non-dotted handling is correct +class NET_EXPORT CookieMonster : public CookieStore { + public: + class CanonicalCookie; + class Delegate; + class ParsedCookie; + class PersistentCookieStore; + + // Terminology: + // * The 'top level domain' (TLD) of an internet domain name is + // the terminal "." free substring (e.g. "com" for google.com + // or world.std.com). + // * The 'effective top level domain' (eTLD) is the longest + // "." initiated terminal substring of an internet domain name + // that is controlled by a general domain registrar. + // (e.g. "co.uk" for news.bbc.co.uk). + // * The 'effective top level domain plus one' (eTLD+1) is the + // shortest "." delimited terminal substring of an internet + // domain name that is not controlled by a general domain + // registrar (e.g. "bbc.co.uk" for news.bbc.co.uk, or + // "google.com" for news.google.com). The general assumption + // is that all hosts and domains under an eTLD+1 share some + // administrative control. + + // CookieMap is the central data structure of the CookieMonster. It + // is a map whose values are pointers to CanonicalCookie data + // structures (the data structures are owned by the CookieMonster + // and must be destroyed when removed from the map). The key is based on the + // effective domain of the cookies. If the domain of the cookie has an + // eTLD+1, that is the key for the map. If the domain of the cookie does not + // have an eTLD+1, the key of the map is the host the cookie applies to (it is + // not legal to have domain cookies without an eTLD+1). This rule + // excludes cookies for, e.g, ".com", ".co.uk", or ".internalnetwork". + // This behavior is the same as the behavior in Firefox v 3.6.10. + + // NOTE(deanm): + // I benchmarked hash_multimap vs multimap. We're going to be query-heavy + // so it would seem like hashing would help. However they were very + // close, with multimap being a tiny bit faster. I think this is because + // our map is at max around 1000 entries, and the additional complexity + // for the hashing might not overcome the O(log(1000)) for querying + // a multimap. Also, multimap is standard, another reason to use it. + // TODO(rdsmith): This benchmark should be re-done now that we're allowing + // subtantially more entries in the map. + typedef std::multimap<std::string, CanonicalCookie*> CookieMap; + typedef std::pair<CookieMap::iterator, CookieMap::iterator> CookieMapItPair; + + // The store passed in should not have had Init() called on it yet. This + // class will take care of initializing it. The backing store is NOT owned by + // this class, but it must remain valid for the duration of the cookie + // monster's existence. If |store| is NULL, then no backing store will be + // updated. If |delegate| is non-NULL, it will be notified on + // creation/deletion of cookies. + CookieMonster(PersistentCookieStore* store, Delegate* delegate); + + // Only used during unit testing. + CookieMonster(PersistentCookieStore* store, + Delegate* delegate, + int last_access_threshold_milliseconds); + + // Parses the string with the cookie time (very forgivingly). + static base::Time ParseCookieTime(const std::string& time_string); + + // Helper function that adds all cookies from |list| into this instance. + bool InitializeFrom(const CookieList& list); + + typedef base::Callback<void(const CookieList& cookies)> GetCookieListCallback; + typedef base::Callback<void(bool success)> DeleteCookieCallback; + + // Sets a cookie given explicit user-provided cookie attributes. The cookie + // name, value, domain, etc. are each provided as separate strings. This + // function expects each attribute to be well-formed. It will check for + // disallowed characters (e.g. the ';' character is disallowed within the + // cookie value attribute) and will return false without setting the cookie + // if such characters are found. + void SetCookieWithDetailsAsync(const GURL& url, + const std::string& name, + const std::string& value, + const std::string& domain, + const std::string& path, + const base::Time& expiration_time, + bool secure, bool http_only, + const SetCookiesCallback& callback); + + + // Returns all the cookies, for use in management UI, etc. This does not mark + // the cookies as having been accessed. + // The returned cookies are ordered by longest path, then by earliest + // creation date. + void GetAllCookiesAsync(const GetCookieListCallback& callback); + + // Returns all the cookies, for use in management UI, etc. Filters results + // using given url scheme, host / domain and path and options. This does not + // mark the cookies as having been accessed. + // The returned cookies are ordered by longest path, then earliest + // creation date. + void GetAllCookiesForURLWithOptionsAsync( + const GURL& url, + const CookieOptions& options, + const GetCookieListCallback& callback); + + // Invokes GetAllCookiesForURLWithOptions with options set to include HTTP + // only cookies. + void GetAllCookiesForURLAsync(const GURL& url, + const GetCookieListCallback& callback); + + // Deletes all of the cookies. + void DeleteAllAsync(const DeleteCallback& callback); + + // Deletes all cookies that match the host of the given URL + // regardless of path. This includes all http_only and secure cookies, + // but does not include any domain cookies that may apply to this host. + // Returns the number of cookies deleted. + void DeleteAllForHostAsync(const GURL& url, + const DeleteCallback& callback); + + // Deletes one specific cookie. + void DeleteCanonicalCookieAsync(const CanonicalCookie& cookie, + const DeleteCookieCallback& callback); + + // Override the default list of schemes that are allowed to be set in + // this cookie store. Calling his overrides the value of + // "enable_file_scheme_". + // If this this method is called, it must be called before first use of + // the instance (i.e. as part of the instance initialization process). + void SetCookieableSchemes(const char* schemes[], size_t num_schemes); + + // Instructs the cookie monster to not delete expired cookies. This is used + // in cases where the cookie monster is used as a data structure to keep + // arbitrary cookies. + void SetKeepExpiredCookies(); + + // Delegates the call to set the |clear_local_store_on_exit_| flag of the + // PersistentStore if it exists. + void SetClearPersistentStoreOnExit(bool clear_local_store); + + // There are some unknowns about how to correctly handle file:// cookies, + // and our implementation for this is not robust enough. This allows you + // to enable support, but it should only be used for testing. Bug 1157243. + // Must be called before creating a CookieMonster instance. + static void EnableFileScheme(); + + // Flush the backing store (if any) to disk and post the given callback when + // done. + // WARNING: THE CALLBACK WILL RUN ON A RANDOM THREAD. IT MUST BE THREAD SAFE. + // It may be posted to the current thread, or it may run on the thread that + // actually does the flushing. Your Task should generally post a notification + // to the thread you actually want to be notified on. + void FlushStore(const base::Closure& callback); + + // CookieStore implementation. + + // Sets the cookies specified by |cookie_list| returned from |url| + // with options |options| in effect. + virtual void SetCookieWithOptionsAsync( + const GURL& url, + const std::string& cookie_line, + const CookieOptions& options, + const SetCookiesCallback& callback) OVERRIDE; + + // Gets all cookies that apply to |url| given |options|. + // The returned cookies are ordered by longest path, then earliest + // creation date. + virtual void GetCookiesWithOptionsAsync( + const GURL& url, + const CookieOptions& options, + const GetCookiesCallback& callback) OVERRIDE; + + virtual void GetCookiesWithInfoAsync( + const GURL& url, + const CookieOptions& options, + const GetCookieInfoCallback& callback) OVERRIDE; + + // Deletes all cookies with that might apply to |url| that has |cookie_name|. + virtual void DeleteCookieAsync( + const GURL& url, const std::string& cookie_name, + const base::Closure& callback) OVERRIDE; + + // Deletes all of the cookies that have a creation_date greater than or equal + // to |delete_begin| and less than |delete_end| + // Returns the number of cookies that have been deleted. + virtual void DeleteAllCreatedBetweenAsync( + const base::Time& delete_begin, + const base::Time& delete_end, + const DeleteCallback& callback) OVERRIDE; + + virtual CookieMonster* GetCookieMonster() OVERRIDE; + + // Enables writing session cookies into the cookie database. If this this + // method is called, it must be called before first use of the instance + // (i.e. as part of the instance initialization process). + void SetPersistSessionCookies(bool persist_session_cookies); + + // Protects session cookies from deletion on shutdown. + void SaveSessionCookies(); + + // Debugging method to perform various validation checks on the map. + // Currently just checking that there are no null CanonicalCookie pointers + // in the map. + // Argument |arg| is to allow retaining of arbitrary data if the CHECKs + // in the function trip. TODO(rdsmith):Remove hack. + void ValidateMap(int arg); + + // The default list of schemes the cookie monster can handle. + static const char* kDefaultCookieableSchemes[]; + static const int kDefaultCookieableSchemesCount; + + private: + // For queueing the cookie monster calls. + class CookieMonsterTask; + class DeleteAllCreatedBetweenTask; + class DeleteAllForHostTask; + class DeleteAllTask; + class DeleteCookieTask; + class DeleteCanonicalCookieTask; + class GetAllCookiesForURLWithOptionsTask; + class GetAllCookiesTask; + class GetCookiesWithOptionsTask; + class GetCookiesWithInfoTask; + class SetCookieWithDetailsTask; + class SetCookieWithOptionsTask; + + // Testing support. + // For SetCookieWithCreationTime. + FRIEND_TEST_ALL_PREFIXES(CookieMonsterTest, + TestCookieDeleteAllCreatedBetweenTimestamps); + + // For gargage collection constants. + FRIEND_TEST_ALL_PREFIXES(CookieMonsterTest, TestHostGarbageCollection); + FRIEND_TEST_ALL_PREFIXES(CookieMonsterTest, TestTotalGarbageCollection); + FRIEND_TEST_ALL_PREFIXES(CookieMonsterTest, GarbageCollectionTriggers); + FRIEND_TEST_ALL_PREFIXES(CookieMonsterTest, TestGCTimes); + + // For validation of key values. + FRIEND_TEST_ALL_PREFIXES(CookieMonsterTest, TestDomainTree); + FRIEND_TEST_ALL_PREFIXES(CookieMonsterTest, TestImport); + FRIEND_TEST_ALL_PREFIXES(CookieMonsterTest, GetKey); + FRIEND_TEST_ALL_PREFIXES(CookieMonsterTest, TestGetKey); + + // For FindCookiesForKey. + FRIEND_TEST_ALL_PREFIXES(CookieMonsterTest, ShortLivedSessionCookies); + + // Internal reasons for deletion, used to populate informative histograms + // and to provide a public cause for onCookieChange notifications. + // + // If you add or remove causes from this list, please be sure to also update + // the Delegate::ChangeCause mapping inside ChangeCauseMapping. Moreover, + // these are used as array indexes, so avoid reordering to keep the + // histogram buckets consistent. New items (if necessary) should be added + // at the end of the list, just before DELETE_COOKIE_LAST_ENTRY. + enum DeletionCause { + DELETE_COOKIE_EXPLICIT = 0, + DELETE_COOKIE_OVERWRITE, + DELETE_COOKIE_EXPIRED, + DELETE_COOKIE_EVICTED, + DELETE_COOKIE_DUPLICATE_IN_BACKING_STORE, + DELETE_COOKIE_DONT_RECORD, // e.g. For final cleanup after flush to store. + DELETE_COOKIE_EVICTED_DOMAIN, + DELETE_COOKIE_EVICTED_GLOBAL, + + // Cookies evicted during domain level garbage collection that + // were accessed longer ago than kSafeFromGlobalPurgeDays + DELETE_COOKIE_EVICTED_DOMAIN_PRE_SAFE, + + // Cookies evicted during domain level garbage collection that + // were accessed more recently than kSafeFromGlobalPurgeDays + // (and thus would have been preserved by global garbage collection). + DELETE_COOKIE_EVICTED_DOMAIN_POST_SAFE, + + // A common idiom is to remove a cookie by overwriting it with an + // already-expired expiration date. This captures that case. + DELETE_COOKIE_EXPIRED_OVERWRITE, + + DELETE_COOKIE_LAST_ENTRY + }; + + // Cookie garbage collection thresholds. Based off of the Mozilla defaults. + // When the number of cookies gets to k{Domain,}MaxCookies + // purge down to k{Domain,}MaxCookies - k{Domain,}PurgeCookies. + // It might seem scary to have a high purge value, but really it's not. + // You just make sure that you increase the max to cover the increase + // in purge, and we would have been purging the same amount of cookies. + // We're just going through the garbage collection process less often. + // Note that the DOMAIN values are per eTLD+1; see comment for the + // CookieMap typedef. So, e.g., the maximum number of cookies allowed for + // google.com and all of its subdomains will be 150-180. + // + // Any cookies accessed more recently than kSafeFromGlobalPurgeDays will not + // be evicted by global garbage collection, even if we have more than + // kMaxCookies. This does not affect domain garbage collection. + // + // Present in .h file to make accessible to tests through FRIEND_TEST. + // Actual definitions are in cookie_monster.cc. + static const size_t kDomainMaxCookies; + static const size_t kDomainPurgeCookies; + static const size_t kMaxCookies; + static const size_t kPurgeCookies; + + // The number of days since last access that cookies will not be subject + // to global garbage collection. + static const int kSafeFromGlobalPurgeDays; + + // Record statistics every kRecordStatisticsIntervalSeconds of uptime. + static const int kRecordStatisticsIntervalSeconds = 10 * 60; + + virtual ~CookieMonster(); + + // The following are synchronous calls to which the asynchronous methods + // delegate either immediately (if the store is loaded) or through a deferred + // task (if the store is not yet loaded). + bool SetCookieWithDetails(const GURL& url, + const std::string& name, + const std::string& value, + const std::string& domain, + const std::string& path, + const base::Time& expiration_time, + bool secure, bool http_only); + + CookieList GetAllCookies(); + + CookieList GetAllCookiesForURLWithOptions(const GURL& url, + const CookieOptions& options); + + CookieList GetAllCookiesForURL(const GURL& url); + + int DeleteAll(bool sync_to_store); + + int DeleteAllCreatedBetween(const base::Time& delete_begin, + const base::Time& delete_end); + + int DeleteAllForHost(const GURL& url); + + bool DeleteCanonicalCookie(const CanonicalCookie& cookie); + + bool SetCookieWithOptions(const GURL& url, + const std::string& cookie_line, + const CookieOptions& options); + + std::string GetCookiesWithOptions(const GURL& url, + const CookieOptions& options); + + void GetCookiesWithInfo(const GURL& url, + const CookieOptions& options, + std::string* cookie_line, + std::vector<CookieInfo>* cookie_infos); + + void DeleteCookie(const GURL& url, const std::string& cookie_name); + + bool SetCookieWithCreationTime(const GURL& url, + const std::string& cookie_line, + const base::Time& creation_time); + + // Called by all non-static functions to ensure that the cookies store has + // been initialized. This is not done during creating so it doesn't block + // the window showing. + // Note: this method should always be called with lock_ held. + void InitIfNecessary() { + if (!initialized_) { + if (store_) { + InitStore(); + } else { + loaded_ = true; + } + initialized_ = true; + } + } + + // Initializes the backing store and reads existing cookies from it. + // Should only be called by InitIfNecessary(). + void InitStore(); + + // Stores cookies loaded from the backing store and invokes any deferred + // calls. |beginning_time| should be the moment PersistentCookieStore::Load + // was invoked and is used for reporting histogram_time_blocked_on_load_. + // See PersistentCookieStore::Load for details on the contents of cookies. + void OnLoaded(base::TimeTicks beginning_time, + const std::vector<CanonicalCookie*>& cookies); + + // Stores cookies loaded from the backing store and invokes the deferred + // task(s) pending loading of cookies associated with the domain key + // (eTLD+1). Called when all cookies for the domain key(eTLD+1) have been + // loaded from DB. See PersistentCookieStore::Load for details on the contents + // of cookies. + void OnKeyLoaded( + const std::string& key, + const std::vector<CanonicalCookie*>& cookies); + + // Stores the loaded cookies. + void StoreLoadedCookies(const std::vector<CanonicalCookie*>& cookies); + + // Invokes deferred calls. + void InvokeQueue(); + + // Checks that |cookies_| matches our invariants, and tries to repair any + // inconsistencies. (In other words, it does not have duplicate cookies). + void EnsureCookiesMapIsValid(); + + // Checks for any duplicate cookies for CookieMap key |key| which lie between + // |begin| and |end|. If any are found, all but the most recent are deleted. + // Returns the number of duplicate cookies that were deleted. + int TrimDuplicateCookiesForKey(const std::string& key, + CookieMap::iterator begin, + CookieMap::iterator end); + + void SetDefaultCookieableSchemes(); + + void FindCookiesForHostAndDomain(const GURL& url, + const CookieOptions& options, + bool update_access_time, + std::vector<CanonicalCookie*>* cookies); + + void FindCookiesForKey(const std::string& key, + const GURL& url, + const CookieOptions& options, + const base::Time& current, + bool update_access_time, + std::vector<CanonicalCookie*>* cookies); + + // Delete any cookies that are equivalent to |ecc| (same path, domain, etc). + // If |skip_httponly| is true, httponly cookies will not be deleted. The + // return value with be true if |skip_httponly| skipped an httponly cookie. + // |key| is the key to find the cookie in cookies_; see the comment before + // the CookieMap typedef for details. + // NOTE: There should never be more than a single matching equivalent cookie. + bool DeleteAnyEquivalentCookie(const std::string& key, + const CanonicalCookie& ecc, + bool skip_httponly, + bool already_expired); + + // Takes ownership of *cc. + void InternalInsertCookie(const std::string& key, + CanonicalCookie* cc, + bool sync_to_store); + + // Helper function that sets cookies with more control. + // Not exposed as we don't want callers to have the ability + // to specify (potentially duplicate) creation times. + bool SetCookieWithCreationTimeAndOptions(const GURL& url, + const std::string& cookie_line, + const base::Time& creation_time, + const CookieOptions& options); + + // Helper function that sets a canonical cookie, deleting equivalents and + // performing garbage collection. + bool SetCanonicalCookie(scoped_ptr<CanonicalCookie>* cc, + const base::Time& creation_time, + const CookieOptions& options); + + void InternalUpdateCookieAccessTime(CanonicalCookie* cc, + const base::Time& current_time); + + // |deletion_cause| argument is used for collecting statistics and choosing + // the correct Delegate::ChangeCause for OnCookieChanged notifications. + void InternalDeleteCookie(CookieMap::iterator it, bool sync_to_store, + DeletionCause deletion_cause); + + // If the number of cookies for CookieMap key |key|, or globally, are + // over the preset maximums above, garbage collect, first for the host and + // then globally. See comments above garbage collection threshold + // constants for details. + // + // Returns the number of cookies deleted (useful for debugging). + int GarbageCollect(const base::Time& current, const std::string& key); + + // Helper for GarbageCollect(); can be called directly as well. Deletes + // all expired cookies in |itpair|. If |cookie_its| is non-NULL, it is + // populated with all the non-expired cookies from |itpair|. + // + // Returns the number of cookies deleted. + int GarbageCollectExpired(const base::Time& current, + const CookieMapItPair& itpair, + std::vector<CookieMap::iterator>* cookie_its); + + // Helper for GarbageCollect(). Deletes all cookies in the list + // that were accessed before |keep_accessed_after|, using DeletionCause + // |cause|. If |keep_accessed_after| is null, deletes all cookies in the + // list. Returns the number of cookies deleted. + int GarbageCollectDeleteList(const base::Time& current, + const base::Time& keep_accessed_after, + DeletionCause cause, + std::vector<CookieMap::iterator>& cookie_its); + + // Find the key (for lookup in cookies_) based on the given domain. + // See comment on keys before the CookieMap typedef. + std::string GetKey(const std::string& domain) const; + + bool HasCookieableScheme(const GURL& url); + + // Statistics support + + // This function should be called repeatedly, and will record + // statistics if a sufficient time period has passed. + void RecordPeriodicStats(const base::Time& current_time); + + // Initialize the above variables; should only be called from + // the constructor. + void InitializeHistograms(); + + // The resolution of our time isn't enough, so we do something + // ugly and increment when we've seen the same time twice. + base::Time CurrentTime(); + + // Runs the task if, or defers the task until, the full cookie database is + // loaded. + void DoCookieTask(const scoped_refptr<CookieMonsterTask>& task_item); + + // Runs the task if, or defers the task until, the cookies for the given URL + // are loaded. + void DoCookieTaskForURL(const scoped_refptr<CookieMonsterTask>& task_item, + const GURL& url); + + // Histogram variables; see CookieMonster::InitializeHistograms() in + // cookie_monster.cc for details. + base::Histogram* histogram_expiration_duration_minutes_; + base::Histogram* histogram_between_access_interval_minutes_; + base::Histogram* histogram_evicted_last_access_minutes_; + base::Histogram* histogram_count_; + base::Histogram* histogram_domain_count_; + base::Histogram* histogram_etldp1_count_; + base::Histogram* histogram_domain_per_etldp1_count_; + base::Histogram* histogram_number_duplicate_db_cookies_; + base::Histogram* histogram_cookie_deletion_cause_; + base::Histogram* histogram_time_get_; + base::Histogram* histogram_time_mac_; + base::Histogram* histogram_time_blocked_on_load_; + + CookieMap cookies_; + + // Indicates whether the cookie store has been initialized. This happens + // lazily in InitStoreIfNecessary(). + bool initialized_; + + // Indicates whether loading from the backend store is completed and + // calls may be immediately processed. + bool loaded_; + + // List of domain keys that have been loaded from the DB. + std::set<std::string> keys_loaded_; + + // Map of domain keys to their associated task queues. These tasks are blocked + // until all cookies for the associated domain key eTLD+1 are loaded from the + // backend store. + std::map<std::string, std::deque<scoped_refptr<CookieMonsterTask> > > + tasks_queued_; + + // Queues tasks that are blocked until all cookies are loaded from the backend + // store. + std::queue<scoped_refptr<CookieMonsterTask> > queue_; + + scoped_refptr<PersistentCookieStore> store_; + + base::Time last_time_seen_; + + // Minimum delay after updating a cookie's LastAccessDate before we will + // update it again. + const base::TimeDelta last_access_threshold_; + + // Approximate date of access time of least recently accessed cookie + // in |cookies_|. Note that this is not guaranteed to be accurate, only a) + // to be before or equal to the actual time, and b) to be accurate + // immediately after a garbage collection that scans through all the cookies. + // This value is used to determine whether global garbage collection might + // find cookies to purge. + // Note: The default Time() constructor will create a value that compares + // earlier than any other time value, which is wanted. Thus this + // value is not initialized. + base::Time earliest_access_time_; + + // During loading, holds the set of all loaded cookie creation times. Used to + // avoid ever letting cookies with duplicate creation times into the store; + // that way we don't have to worry about what sections of code are safe + // to call while it's in that state. + std::set<int64> creation_times_; + + std::vector<std::string> cookieable_schemes_; + + scoped_refptr<Delegate> delegate_; + + // Lock for thread-safety + base::Lock lock_; + + base::Time last_statistic_record_time_; + + bool keep_expired_cookies_; + bool persist_session_cookies_; + + static bool enable_file_scheme_; + + DISALLOW_COPY_AND_ASSIGN(CookieMonster); +}; + +class NET_EXPORT CookieMonster::CanonicalCookie { + public: + + // These constructors do no validation or canonicalization of their inputs; + // the resulting CanonicalCookies should not be relied on to be canonical + // unless the caller has done appropriate validation and canonicalization + // themselves. + CanonicalCookie(); + CanonicalCookie(const GURL& url, + const std::string& name, + const std::string& value, + const std::string& domain, + const std::string& path, + const std::string& mac_key, + const std::string& mac_algorithm, + const base::Time& creation, + const base::Time& expiration, + const base::Time& last_access, + bool secure, + bool httponly, + bool has_expires, + bool is_persistent); + + // This constructor does canonicalization but not validation. + // The result of this constructor should not be relied on in contexts + // in which pre-validation of the ParsedCookie has not been done. + CanonicalCookie(const GURL& url, const ParsedCookie& pc); + + ~CanonicalCookie(); + + // Supports the default copy constructor. + + // Creates a canonical cookie from parsed cookie. + // Canonicalizes and validates inputs. May return NULL if an attribute + // value is invalid. + static CanonicalCookie* Create(const GURL& url, + const ParsedCookie& pc); + + // Creates a canonical cookie from unparsed attribute values. + // Canonicalizes and validates inputs. May return NULL if an attribute + // value is invalid. + static CanonicalCookie* Create(const GURL& url, + const std::string& name, + const std::string& value, + const std::string& domain, + const std::string& path, + const std::string& mac_key, + const std::string& mac_algorithm, + const base::Time& creation, + const base::Time& expiration, + bool secure, + bool http_only, + bool is_persistent); + + const std::string& Source() const { return source_; } + const std::string& Name() const { return name_; } + const std::string& Value() const { return value_; } + const std::string& Domain() const { return domain_; } + const std::string& Path() const { return path_; } + const std::string& MACKey() const { return mac_key_; } + const std::string& MACAlgorithm() const { return mac_algorithm_; } + const base::Time& CreationDate() const { return creation_date_; } + const base::Time& LastAccessDate() const { return last_access_date_; } + bool DoesExpire() const { return has_expires_; } + bool IsPersistent() const { return is_persistent_; } + const base::Time& ExpiryDate() const { return expiry_date_; } + bool IsSecure() const { return secure_; } + bool IsHttpOnly() const { return httponly_; } + bool IsDomainCookie() const { + return !domain_.empty() && domain_[0] == '.'; } + bool IsHostCookie() const { return !IsDomainCookie(); } + + bool IsExpired(const base::Time& current) { + return has_expires_ && current >= expiry_date_; + } + + // Are the cookies considered equivalent in the eyes of RFC 2965. + // The RFC says that name must match (case-sensitive), domain must + // match (case insensitive), and path must match (case sensitive). + // For the case insensitive domain compare, we rely on the domain + // having been canonicalized (in + // GetCookieDomainWithString->CanonicalizeHost). + bool IsEquivalent(const CanonicalCookie& ecc) const { + // It seems like it would make sense to take secure and httponly into + // account, but the RFC doesn't specify this. + // NOTE: Keep this logic in-sync with TrimDuplicateCookiesForHost(). + return (name_ == ecc.Name() && domain_ == ecc.Domain() + && path_ == ecc.Path()); + } + + void SetLastAccessDate(const base::Time& date) { + last_access_date_ = date; + } + + bool IsOnPath(const std::string& url_path) const; + bool IsDomainMatch(const std::string& scheme, const std::string& host) const; + + std::string DebugString() const; + + // Returns the cookie source when cookies are set for |url|. This function + // is public for unit test purposes only. + static std::string GetCookieSourceFromURL(const GURL& url); + + private: + // Gives the session cookie an expiration time if needed + void SetSessionCookieExpiryTime(); + + // The source member of a canonical cookie is the origin of the URL that tried + // to set this cookie, minus the port number if any. This field is not + // persistent though; its only used in the in-tab cookies dialog to show the + // user the source URL. This is used for both allowed and blocked cookies. + // When a CanonicalCookie is constructed from the backing store (common case) + // this field will be null. CanonicalCookie consumers should not rely on + // this field unless they guarantee that the creator of those + // CanonicalCookies properly initialized the field. + // TODO(abarth): We might need to make this field persistent for MAC cookies. + std::string source_; + std::string name_; + std::string value_; + std::string domain_; + std::string path_; + std::string mac_key_; // TODO(abarth): Persist to disk. + std::string mac_algorithm_; // TODO(abarth): Persist to disk. + base::Time creation_date_; + base::Time expiry_date_; + base::Time last_access_date_; + bool secure_; + bool httponly_; + bool has_expires_; + bool is_persistent_; +}; + +class CookieMonster::Delegate + : public base::RefCountedThreadSafe<CookieMonster::Delegate> { + public: + // The publicly relevant reasons a cookie might be changed. + enum ChangeCause { + // The cookie was changed directly by a consumer's action. + CHANGE_COOKIE_EXPLICIT, + // The cookie was automatically removed due to an insert operation that + // overwrote it. + CHANGE_COOKIE_OVERWRITE, + // The cookie was automatically removed as it expired. + CHANGE_COOKIE_EXPIRED, + // The cookie was automatically evicted during garbage collection. + CHANGE_COOKIE_EVICTED, + // The cookie was overwritten with an already-expired expiration date. + CHANGE_COOKIE_EXPIRED_OVERWRITE + }; + + // Will be called when a cookie is added or removed. The function is passed + // the respective |cookie| which was added to or removed from the cookies. + // If |removed| is true, the cookie was deleted, and |cause| will be set + // to the reason for it's removal. If |removed| is false, the cookie was + // added, and |cause| will be set to CHANGE_COOKIE_EXPLICIT. + // + // As a special case, note that updating a cookie's properties is implemented + // as a two step process: the cookie to be updated is first removed entirely, + // generating a notification with cause CHANGE_COOKIE_OVERWRITE. Afterwards, + // a new cookie is written with the updated values, generating a notification + // with cause CHANGE_COOKIE_EXPLICIT. + virtual void OnCookieChanged(const CookieMonster::CanonicalCookie& cookie, + bool removed, + ChangeCause cause) = 0; + protected: + friend class base::RefCountedThreadSafe<CookieMonster::Delegate>; + virtual ~Delegate() {} +}; + +class NET_EXPORT CookieMonster::ParsedCookie { + public: + typedef std::pair<std::string, std::string> TokenValuePair; + typedef std::vector<TokenValuePair> PairList; + + // The maximum length of a cookie string we will try to parse + static const size_t kMaxCookieSize = 4096; + // The maximum number of Token/Value pairs. Shouldn't have more than 8. + static const int kMaxPairs = 16; + + // Construct from a cookie string like "BLAH=1; path=/; domain=.google.com" + ParsedCookie(const std::string& cookie_line); + ~ParsedCookie(); + + // You should not call any other methods on the class if !IsValid + bool IsValid() const { return is_valid_; } + + const std::string& Name() const { return pairs_[0].first; } + const std::string& Token() const { return Name(); } + const std::string& Value() const { return pairs_[0].second; } + + bool HasPath() const { return path_index_ != 0; } + const std::string& Path() const { return pairs_[path_index_].second; } + bool HasDomain() const { return domain_index_ != 0; } + const std::string& Domain() const { return pairs_[domain_index_].second; } + bool HasMACKey() const { return mac_key_index_ != 0; } + const std::string& MACKey() const { return pairs_[mac_key_index_].second; } + bool HasMACAlgorithm() const { return mac_algorithm_index_ != 0; } + const std::string& MACAlgorithm() const { + return pairs_[mac_algorithm_index_].second; + } + bool HasExpires() const { return expires_index_ != 0; } + const std::string& Expires() const { return pairs_[expires_index_].second; } + bool HasMaxAge() const { return maxage_index_ != 0; } + const std::string& MaxAge() const { return pairs_[maxage_index_].second; } + bool IsSecure() const { return secure_index_ != 0; } + bool IsHttpOnly() const { return httponly_index_ != 0; } + + // Returns the number of attributes, for example, returning 2 for: + // "BLAH=hah; path=/; domain=.google.com" + size_t NumberOfAttributes() const { return pairs_.size() - 1; } + + // For debugging only! + std::string DebugString() const; + + // Returns an iterator pointing to the first terminator character found in + // the given string. + static std::string::const_iterator FindFirstTerminator(const std::string& s); + + // Given iterators pointing to the beginning and end of a string segment, + // returns as output arguments token_start and token_end to the start and end + // positions of a cookie attribute token name parsed from the segment, and + // updates the segment iterator to point to the next segment to be parsed. + // If no token is found, the function returns false. + static bool ParseToken(std::string::const_iterator* it, + const std::string::const_iterator& end, + std::string::const_iterator* token_start, + std::string::const_iterator* token_end); + + // Given iterators pointing to the beginning and end of a string segment, + // returns as output arguments value_start and value_end to the start and end + // positions of a cookie attribute value parsed from the segment, and updates + // the segment iterator to point to the next segment to be parsed. + static void ParseValue(std::string::const_iterator* it, + const std::string::const_iterator& end, + std::string::const_iterator* value_start, + std::string::const_iterator* value_end); + + // Same as the above functions, except the input is assumed to contain the + // desired token/value and nothing else. + static std::string ParseTokenString(const std::string& token); + static std::string ParseValueString(const std::string& value); + + private: + static const char kTerminator[]; + static const int kTerminatorLen; + static const char kWhitespace[]; + static const char kValueSeparator[]; + static const char kTokenSeparator[]; + + void ParseTokenValuePairs(const std::string& cookie_line); + void SetupAttributes(); + + PairList pairs_; + bool is_valid_; + // These will default to 0, but that should never be valid since the + // 0th index is the user supplied token/value, not an attribute. + // We're really never going to have more than like 8 attributes, so we + // could fit these into 3 bits each if we're worried about size... + size_t path_index_; + size_t domain_index_; + size_t mac_key_index_; + size_t mac_algorithm_index_; + size_t expires_index_; + size_t maxage_index_; + size_t secure_index_; + size_t httponly_index_; + + DISALLOW_COPY_AND_ASSIGN(ParsedCookie); +}; + +typedef base::RefCountedThreadSafe<CookieMonster::PersistentCookieStore> + RefcountedPersistentCookieStore; + +class CookieMonster::PersistentCookieStore + : public RefcountedPersistentCookieStore { + public: + virtual ~PersistentCookieStore() {} + + typedef base::Callback<void(const std::vector< + CookieMonster::CanonicalCookie*>&)> LoadedCallback; + + // Initializes the store and retrieves the existing cookies. This will be + // called only once at startup. The callback will return all the cookies + // that are not yet returned to CookieMonster by previous priority loads. + virtual void Load(const LoadedCallback& loaded_callback) = 0; + + // Does a priority load of all cookies for the domain key (eTLD+1). The + // callback will return all the cookies that are not yet returned by previous + // loads, which includes cookies for the requested domain key if they are not + // already returned, plus all cookies that are chain-loaded and not yet + // returned to CookieMonster. + virtual void LoadCookiesForKey(const std::string& key, + const LoadedCallback& loaded_callback) = 0; + + virtual void AddCookie(const CanonicalCookie& cc) = 0; + virtual void UpdateCookieAccessTime(const CanonicalCookie& cc) = 0; + virtual void DeleteCookie(const CanonicalCookie& cc) = 0; + + // Sets the value of the user preference whether the persistent storage + // must be deleted upon destruction. + virtual void SetClearLocalStateOnExit(bool clear_local_state) = 0; + + // Flushes the store and posts |callback| when complete. + virtual void Flush(const base::Closure& callback) = 0; + + protected: + PersistentCookieStore() {} + + private: + DISALLOW_COPY_AND_ASSIGN(PersistentCookieStore); +}; + +class CookieList : public std::vector<CookieMonster::CanonicalCookie> { +}; + +} // namespace net + +#endif // NET_COOKIES_COOKIE_MONSTER_H_ diff --git a/net/cookies/cookie_monster_perftest.cc b/net/cookies/cookie_monster_perftest.cc new file mode 100644 index 0000000..eeab92f --- /dev/null +++ b/net/cookies/cookie_monster_perftest.cc @@ -0,0 +1,430 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <algorithm> + +#include "base/bind.h" +#include "base/message_loop.h" +#include "base/perftimer.h" +#include "base/string_util.h" +#include "base/stringprintf.h" +#include "googleurl/src/gurl.h" +#include "net/cookies/cookie_monster.h" +#include "net/cookies/cookie_monster_store_test.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { +class CookieMonsterTest : public testing::Test { + public: + CookieMonsterTest() : message_loop_(new MessageLoopForIO()) {} + + private: + scoped_ptr<MessageLoop> message_loop_; +}; +} + +static const int kNumCookies = 20000; +static const char kCookieLine[] = "A = \"b=;\\\"\" ;secure;;;"; + +namespace net { + +TEST(ParsedCookieTest, TestParseCookies) { + std::string cookie(kCookieLine); + PerfTimeLogger timer("Parsed_cookie_parse_cookies"); + for (int i = 0; i < kNumCookies; ++i) { + CookieMonster::ParsedCookie pc(cookie); + EXPECT_TRUE(pc.IsValid()); + } + timer.Done(); +} + +TEST(ParsedCookieTest, TestParseBigCookies) { + std::string cookie(3800, 'z'); + cookie += kCookieLine; + PerfTimeLogger timer("Parsed_cookie_parse_big_cookies"); + for (int i = 0; i < kNumCookies; ++i) { + CookieMonster::ParsedCookie pc(cookie); + EXPECT_TRUE(pc.IsValid()); + } + timer.Done(); +} + +static const GURL kUrlGoogle("http://www.google.izzle"); + +class BaseCallback { + public: + BaseCallback() : has_run_(false) {} + + protected: + void WaitForCallback() { + // Note that the performance tests currently all operate on a loaded cookie + // store (or, more precisely, one that has no backing persistent store). + // Therefore, callbacks will actually always complete synchronously. If the + // tests get more advanced we need to add other means of signaling + // completion. + MessageLoop::current()->RunAllPending(); + EXPECT_TRUE(has_run_); + has_run_ = false; + } + + void Run() { + has_run_ = true; + } + + bool has_run_; +}; + + +class SetCookieCallback : public BaseCallback { + public: + void SetCookie( + CookieMonster* cm, const GURL& gurl, const std::string& cookie) { + cm->SetCookieWithOptionsAsync(gurl, cookie, options_, base::Bind( + &SetCookieCallback::Run, base::Unretained(this))); + WaitForCallback(); + } + private: + void Run(bool success) { + EXPECT_TRUE(success); + BaseCallback::Run(); + } + net::CookieOptions options_; +}; + +class GetCookiesCallback : public BaseCallback { + public: + const std::string& GetCookies(CookieMonster* cm, const GURL& gurl) { + cm->GetCookiesWithOptionsAsync(gurl, options_, base::Bind( + &GetCookiesCallback::Run, base::Unretained(this))); + WaitForCallback(); + return cookies_; + } + + private: + void Run(const std::string& cookies) { + cookies_ = cookies; + BaseCallback::Run(); + } + std::string cookies_; + net::CookieOptions options_; +}; + +class GetCookiesWithInfoCallback : public BaseCallback { + public: + const std::string& GetCookiesWithInfo(CookieMonster* cm, const GURL& gurl) { + cm->GetCookiesWithInfoAsync(gurl, options_, base::Bind( + &GetCookiesWithInfoCallback::Run, + base::Unretained(this))); + WaitForCallback(); + return cookies_; + } + + private: + void Run( + const std::string& cookie_line, + const std::vector<CookieStore::CookieInfo>& cookie_infos) { + cookies_ = cookie_line; + BaseCallback::Run(); + } + + std::string cookies_; + net::CookieOptions options_; +}; + + +TEST_F(CookieMonsterTest, TestAddCookiesOnSingleHost) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + std::vector<std::string> cookies; + for (int i = 0; i < kNumCookies; i++) { + cookies.push_back(base::StringPrintf("a%03d=b", i)); + } + + SetCookieCallback setCookieCallback; + + // Add a bunch of cookies on a single host + PerfTimeLogger timer("Cookie_monster_add_single_host"); + + for (std::vector<std::string>::const_iterator it = cookies.begin(); + it != cookies.end(); ++it) { + setCookieCallback.SetCookie(cm, kUrlGoogle, *it); + } + timer.Done(); + + GetCookiesCallback getCookiesCallback; + + PerfTimeLogger timer2("Cookie_monster_query_single_host"); + for (std::vector<std::string>::const_iterator it = cookies.begin(); + it != cookies.end(); ++it) { + getCookiesCallback.GetCookies(cm, kUrlGoogle); + } + timer2.Done(); + + PerfTimeLogger timer3("Cookie_monster_deleteall_single_host"); + cm->DeleteAllAsync(CookieMonster::DeleteCallback()); + MessageLoop::current()->RunAllPending(); + timer3.Done(); +} + +TEST_F(CookieMonsterTest, TestAddCookieOnManyHosts) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + std::string cookie(kCookieLine); + std::vector<GURL> gurls; // just wanna have ffffuunnn + for (int i = 0; i < kNumCookies; ++i) { + gurls.push_back(GURL(base::StringPrintf("https://a%04d.izzle", i))); + } + + SetCookieCallback setCookieCallback; + + // Add a cookie on a bunch of host + PerfTimeLogger timer("Cookie_monster_add_many_hosts"); + for (std::vector<GURL>::const_iterator it = gurls.begin(); + it != gurls.end(); ++it) { + setCookieCallback.SetCookie(cm, *it, cookie); + } + timer.Done(); + + GetCookiesCallback getCookiesCallback; + + PerfTimeLogger timer2("Cookie_monster_query_many_hosts"); + for (std::vector<GURL>::const_iterator it = gurls.begin(); + it != gurls.end(); ++it) { + getCookiesCallback.GetCookies(cm, *it); + } + timer2.Done(); + + PerfTimeLogger timer3("Cookie_monster_deleteall_many_hosts"); + cm->DeleteAllAsync(CookieMonster::DeleteCallback()); + MessageLoop::current()->RunAllPending(); + timer3.Done(); +} + +TEST_F(CookieMonsterTest, TestGetCookiesWithInfo) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + + std::vector<GURL> gurls; + for (int i = 0; i < kNumCookies; ++i) + gurls.push_back(GURL(base::StringPrintf("https://a%04d.izzle", i))); + + SetCookieCallback setCookieCallback; + + for (std::vector<GURL>::const_iterator it = gurls.begin(); + it != gurls.end(); ++it) { + setCookieCallback.SetCookie(cm, *it, kCookieLine); + } + + GetCookiesWithInfoCallback getCookiesCallback; + + PerfTimeLogger timer("Cookie_monster_get_cookie_info"); + for (std::vector<GURL>::const_iterator it = gurls.begin(); + it != gurls.end(); ++it) { + getCookiesCallback.GetCookiesWithInfo(cm, *it); + } + timer.Done(); +} + +static int CountInString(const std::string& str, char c) { + return std::count(str.begin(), str.end(), c); +} + +TEST_F(CookieMonsterTest, TestDomainTree) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + GetCookiesCallback getCookiesCallback; + SetCookieCallback setCookieCallback; + const char* domain_cookie_format_tree = "a=b; domain=%s"; + const std::string domain_base("top.com"); + + std::vector<std::string> domain_list; + + // Create a balanced binary tree of domains on which the cookie is set. + domain_list.push_back(domain_base); + for (int i1 = 0; i1 < 2; i1++) { + std::string domain_base_1((i1 ? "a." : "b.") + domain_base); + EXPECT_EQ("top.com", cm->GetKey(domain_base_1)); + domain_list.push_back(domain_base_1); + for (int i2 = 0; i2 < 2; i2++) { + std::string domain_base_2((i2 ? "a." : "b.") + domain_base_1); + EXPECT_EQ("top.com", cm->GetKey(domain_base_2)); + domain_list.push_back(domain_base_2); + for (int i3 = 0; i3 < 2; i3++) { + std::string domain_base_3((i3 ? "a." : "b.") + domain_base_2); + EXPECT_EQ("top.com", cm->GetKey(domain_base_3)); + domain_list.push_back(domain_base_3); + for (int i4 = 0; i4 < 2; i4++) { + std::string domain_base_4((i4 ? "a." : "b.") + domain_base_3); + EXPECT_EQ("top.com", cm->GetKey(domain_base_4)); + domain_list.push_back(domain_base_4); + } + } + } + } + + + EXPECT_EQ(31u, domain_list.size()); + for (std::vector<std::string>::const_iterator it = domain_list.begin(); + it != domain_list.end(); it++) { + GURL gurl("https://" + *it + "/"); + const std::string cookie = base::StringPrintf(domain_cookie_format_tree, + it->c_str()); + setCookieCallback.SetCookie(cm, gurl, cookie); + } + EXPECT_EQ(31u, cm->GetAllCookies().size()); + + GURL probe_gurl("https://b.a.b.a.top.com/"); + std::string cookie_line = getCookiesCallback.GetCookies(cm, probe_gurl); + EXPECT_EQ(5, CountInString(cookie_line, '=')) << "Cookie line: " << + cookie_line; + PerfTimeLogger timer("Cookie_monster_query_domain_tree"); + for (int i = 0; i < kNumCookies; i++) { + getCookiesCallback.GetCookies(cm, probe_gurl); + } + timer.Done(); +} + +TEST_F(CookieMonsterTest, TestDomainLine) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + SetCookieCallback setCookieCallback; + GetCookiesCallback getCookiesCallback; + std::vector<std::string> domain_list; + GURL probe_gurl("https://b.a.b.a.top.com/"); + std::string cookie_line; + + // Create a line of 32 domain cookies such that all cookies stored + // by effective TLD+1 will apply to probe GURL. + // (TLD + 1 is the level above .com/org/net/etc, e.g. "top.com" + // or "google.com". "Effective" is added to include sites like + // bbc.co.uk, where the effetive TLD+1 is more than one level + // below the top level.) + domain_list.push_back("a.top.com"); + domain_list.push_back("b.a.top.com"); + domain_list.push_back("a.b.a.top.com"); + domain_list.push_back("b.a.b.a.top.com"); + EXPECT_EQ(4u, domain_list.size()); + + const char* domain_cookie_format_line = "a%03d=b; domain=%s"; + for (int i = 0; i < 8; i++) { + for (std::vector<std::string>::const_iterator it = domain_list.begin(); + it != domain_list.end(); it++) { + GURL gurl("https://" + *it + "/"); + const std::string cookie = base::StringPrintf(domain_cookie_format_line, + i, it->c_str()); + setCookieCallback.SetCookie(cm, gurl, cookie); + } + } + + cookie_line = getCookiesCallback.GetCookies(cm, probe_gurl); + EXPECT_EQ(32, CountInString(cookie_line, '=')); + PerfTimeLogger timer2("Cookie_monster_query_domain_line"); + for (int i = 0; i < kNumCookies; i++) { + getCookiesCallback.GetCookies(cm, probe_gurl); + } + timer2.Done(); +} + +TEST_F(CookieMonsterTest, TestImport) { + scoped_refptr<MockPersistentCookieStore> store(new MockPersistentCookieStore); + std::vector<CookieMonster::CanonicalCookie*> initial_cookies; + GetCookiesCallback getCookiesCallback; + + // We want to setup a fairly large backing store, with 300 domains of 50 + // cookies each. Creation times must be unique. + int64 time_tick(base::Time::Now().ToInternalValue()); + + for (int domain_num = 0; domain_num < 300; domain_num++) { + std::string domain_name(base::StringPrintf(".Domain_%d.com", domain_num)); + std::string gurl("www" + domain_name); + for (int cookie_num = 0; cookie_num < 50; cookie_num++) { + std::string cookie_line(base::StringPrintf("Cookie_%d=1; Path=/", + cookie_num)); + AddCookieToList(gurl, cookie_line, + base::Time::FromInternalValue(time_tick++), + &initial_cookies); + } + } + + store->SetLoadExpectation(true, initial_cookies); + + scoped_refptr<CookieMonster> cm(new CookieMonster(store, NULL)); + + // Import will happen on first access. + GURL gurl("www.google.com"); + CookieOptions options; + PerfTimeLogger timer("Cookie_monster_import_from_store"); + getCookiesCallback.GetCookies(cm, gurl); + timer.Done(); + + // Just confirm keys were set as expected. + EXPECT_EQ("domain_1.com", cm->GetKey("www.Domain_1.com")); +} + +TEST_F(CookieMonsterTest, TestGetKey) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + PerfTimeLogger timer("Cookie_monster_get_key"); + for (int i = 0; i < kNumCookies; i++) + cm->GetKey("www.google.com"); + timer.Done(); +} + +// This test is probing for whether garbage collection happens when it +// shouldn't. This will not in general be visible functionally, since +// if GC runs twice in a row without any change to the store, the second +// GC run will not do anything the first one didn't. That's why this is +// a performance test. The test should be considered to pass if all the +// times reported are approximately the same--this indicates that no GC +// happened repeatedly for any case. +TEST_F(CookieMonsterTest, TestGCTimes) { + SetCookieCallback setCookieCallback; + + const struct TestCase { + const char* name; + int num_cookies; + int num_old_cookies; + } test_cases[] = { + { + // A whole lot of recent cookies; gc shouldn't happen. + "all_recent", + CookieMonster::kMaxCookies * 2, + 0, + }, { + // Some old cookies, but still overflowing max. + "mostly_recent", + CookieMonster::kMaxCookies * 2, + CookieMonster::kMaxCookies / 2, + }, { + // Old cookies enough to bring us right down to our purge line. + "balanced", + CookieMonster::kMaxCookies * 2, + CookieMonster::kMaxCookies + CookieMonster::kPurgeCookies + 1, + }, { + "mostly_old", + // Old cookies enough to bring below our purge line (which we + // shouldn't do). + CookieMonster::kMaxCookies * 2, + CookieMonster::kMaxCookies * 3 / 4, + }, { + "less_than_gc_thresh", + // Few enough cookies that gc shouldn't happen at all. + CookieMonster::kMaxCookies - 5, + 0, + }, + }; + for (int ci = 0; ci < static_cast<int>(ARRAYSIZE_UNSAFE(test_cases)); ++ci) { + const TestCase& test_case(test_cases[ci]); + scoped_refptr<CookieMonster> cm( + CreateMonsterFromStoreForGC( + test_case.num_cookies, test_case.num_old_cookies, + CookieMonster::kSafeFromGlobalPurgeDays * 2)); + + GURL gurl("http://google.com"); + std::string cookie_line("z=3"); + // Trigger the Garbage collection we're allowed. + setCookieCallback.SetCookie(cm, gurl, cookie_line); + + PerfTimeLogger timer((std::string("GC_") + test_case.name).c_str()); + for (int i = 0; i < kNumCookies; i++) + setCookieCallback.SetCookie(cm, gurl, cookie_line); + timer.Done(); + } +} + +} // namespace diff --git a/net/cookies/cookie_monster_store_test.cc b/net/cookies/cookie_monster_store_test.cc new file mode 100644 index 0000000..822aacc --- /dev/null +++ b/net/cookies/cookie_monster_store_test.cc @@ -0,0 +1,231 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/cookies/cookie_monster_store_test.h" + +#include "base/bind.h" +#include "base/message_loop.h" +#include "base/stringprintf.h" +#include "base/time.h" +#include "googleurl/src/gurl.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { +LoadedCallbackTask::LoadedCallbackTask(LoadedCallback loaded_callback, + std::vector<CookieMonster::CanonicalCookie*> cookies) + : loaded_callback_(loaded_callback), + cookies_(cookies) { +} + +LoadedCallbackTask::~LoadedCallbackTask() {} + +MockPersistentCookieStore::MockPersistentCookieStore() + : load_return_value_(true), + loaded_(false) { +} + +MockPersistentCookieStore::~MockPersistentCookieStore() {} + +void MockPersistentCookieStore::SetLoadExpectation( + bool return_value, + const std::vector<CookieMonster::CanonicalCookie*>& result) { + load_return_value_ = return_value; + load_result_ = result; +} + +void MockPersistentCookieStore::Load(const LoadedCallback& loaded_callback) { + std::vector<CookieMonster::CanonicalCookie*> out_cookies; + if (load_return_value_) { + out_cookies = load_result_; + loaded_ = true; + } + MessageLoop::current()->PostTask(FROM_HERE, + base::Bind(&LoadedCallbackTask::Run, + new LoadedCallbackTask(loaded_callback, out_cookies))); +} + +void MockPersistentCookieStore::LoadCookiesForKey(const std::string& key, + const LoadedCallback& loaded_callback) { + if (!loaded_) { + Load(loaded_callback); + } else { + MessageLoop::current()->PostTask(FROM_HERE, + base::Bind(&LoadedCallbackTask::Run, + new LoadedCallbackTask(loaded_callback, + std::vector<CookieMonster::CanonicalCookie*>()))); + } +} + +void MockPersistentCookieStore::AddCookie( + const CookieMonster::CanonicalCookie& cookie) { + commands_.push_back( + CookieStoreCommand(CookieStoreCommand::ADD, cookie)); +} + +void MockPersistentCookieStore::UpdateCookieAccessTime( + const CookieMonster::CanonicalCookie& cookie) { + commands_.push_back(CookieStoreCommand( + CookieStoreCommand::UPDATE_ACCESS_TIME, cookie)); +} + +void MockPersistentCookieStore::DeleteCookie( + const CookieMonster::CanonicalCookie& cookie) { + commands_.push_back( + CookieStoreCommand(CookieStoreCommand::REMOVE, cookie)); +} + +void MockPersistentCookieStore::Flush(const base::Closure& callback) { + if (!callback.is_null()) + MessageLoop::current()->PostTask(FROM_HERE, callback); +} + +// No files are created so nothing to clear either +void +MockPersistentCookieStore::SetClearLocalStateOnExit(bool clear_local_state) { +} + +MockCookieMonsterDelegate::MockCookieMonsterDelegate() {} + +void MockCookieMonsterDelegate::OnCookieChanged( + const CookieMonster::CanonicalCookie& cookie, + bool removed, + CookieMonster::Delegate::ChangeCause cause) { + CookieNotification notification(cookie, removed); + changes_.push_back(notification); +} + +MockCookieMonsterDelegate::~MockCookieMonsterDelegate() {} + +void AddCookieToList( + const std::string& key, + const std::string& cookie_line, + const base::Time& creation_time, + std::vector<CookieMonster::CanonicalCookie*>* out_list) { + scoped_ptr<CookieMonster::CanonicalCookie> cookie( + new CookieMonster::CanonicalCookie( + BuildCanonicalCookie(key, cookie_line, creation_time))); + + out_list->push_back(cookie.release()); +} + +CookieMonster::CanonicalCookie BuildCanonicalCookie( + const std::string& key, + const std::string& cookie_line, + const base::Time& creation_time) { + + // Parse the cookie line. + CookieMonster::ParsedCookie pc(cookie_line); + EXPECT_TRUE(pc.IsValid()); + + // This helper is simplistic in interpreting a parsed cookie, in order to + // avoid duplicated CookieMonster's CanonPath() and CanonExpiration() + // functions. Would be nice to export them, and re-use here. + EXPECT_FALSE(pc.HasMaxAge()); + EXPECT_TRUE(pc.HasPath()); + base::Time cookie_expires = pc.HasExpires() ? + CookieMonster::ParseCookieTime(pc.Expires()) : base::Time(); + std::string cookie_path = pc.Path(); + + return CookieMonster::CanonicalCookie( + GURL(), pc.Name(), pc.Value(), key, cookie_path, + pc.MACKey(), pc.MACAlgorithm(), + creation_time, creation_time, cookie_expires, + pc.IsSecure(), pc.IsHttpOnly(), + !cookie_expires.is_null(), !cookie_expires.is_null()); +} + +MockSimplePersistentCookieStore::MockSimplePersistentCookieStore() + : loaded_(false) {} + +MockSimplePersistentCookieStore::~MockSimplePersistentCookieStore() {} + +void MockSimplePersistentCookieStore::Load( + const LoadedCallback& loaded_callback) { + std::vector<CookieMonster::CanonicalCookie*> out_cookies; + + for (CanonicalCookieMap::const_iterator it = cookies_.begin(); + it != cookies_.end(); it++) + out_cookies.push_back( + new CookieMonster::CanonicalCookie(it->second)); + + MessageLoop::current()->PostTask(FROM_HERE, + base::Bind(&LoadedCallbackTask::Run, + new LoadedCallbackTask(loaded_callback, out_cookies))); + loaded_ = true; +} + +void MockSimplePersistentCookieStore::LoadCookiesForKey(const std::string& key, + const LoadedCallback& loaded_callback) { + if (!loaded_) { + Load(loaded_callback); + } else { + MessageLoop::current()->PostTask(FROM_HERE, + base::Bind(&LoadedCallbackTask::Run, + new LoadedCallbackTask(loaded_callback, + std::vector<CookieMonster::CanonicalCookie*>()))); + } +} + +void MockSimplePersistentCookieStore::AddCookie( + const CookieMonster::CanonicalCookie& cookie) { + int64 creation_time = cookie.CreationDate().ToInternalValue(); + EXPECT_TRUE(cookies_.find(creation_time) == cookies_.end()); + cookies_[creation_time] = cookie; +} + +void MockSimplePersistentCookieStore::UpdateCookieAccessTime( + const CookieMonster::CanonicalCookie& cookie) { + int64 creation_time = cookie.CreationDate().ToInternalValue(); + ASSERT_TRUE(cookies_.find(creation_time) != cookies_.end()); + cookies_[creation_time].SetLastAccessDate(base::Time::Now()); +} + +void MockSimplePersistentCookieStore::DeleteCookie( + const CookieMonster::CanonicalCookie& cookie) { + int64 creation_time = cookie.CreationDate().ToInternalValue(); + CanonicalCookieMap::iterator it = cookies_.find(creation_time); + ASSERT_TRUE(it != cookies_.end()); + cookies_.erase(it); +} + +void MockSimplePersistentCookieStore::Flush(const base::Closure& callback) { + if (!callback.is_null()) + MessageLoop::current()->PostTask(FROM_HERE, callback); +} + +void MockSimplePersistentCookieStore::SetClearLocalStateOnExit( + bool clear_local_state) { +} + +CookieMonster* CreateMonsterFromStoreForGC( + int num_cookies, + int num_old_cookies, + int days_old) { + base::Time current(base::Time::Now()); + base::Time past_creation(base::Time::Now() - base::TimeDelta::FromDays(1000)); + scoped_refptr<MockSimplePersistentCookieStore> store( + new MockSimplePersistentCookieStore); + // Must expire to be persistent + for (int i = 0; i < num_cookies; i++) { + base::Time creation_time = + past_creation + base::TimeDelta::FromMicroseconds(i); + base::Time expiration_time = current + base::TimeDelta::FromDays(30); + base::Time last_access_time = + (i < num_old_cookies) ? current - base::TimeDelta::FromDays(days_old) : + current; + + std::string mac_key; + std::string mac_algorithm; + + CookieMonster::CanonicalCookie cc( + GURL(), "a", "1", base::StringPrintf("h%05d.izzle", i), "/path", + mac_key, mac_algorithm, creation_time, expiration_time, + last_access_time, false, false, true, true); + store->AddCookie(cc); + } + + return new CookieMonster(store, NULL); +} + +} // namespace net diff --git a/net/cookies/cookie_monster_store_test.h b/net/cookies/cookie_monster_store_test.h new file mode 100644 index 0000000..a05ddd3 --- /dev/null +++ b/net/cookies/cookie_monster_store_test.h @@ -0,0 +1,206 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This file contains test infrastructure for multiple files +// (current cookie_monster_unittest.cc and cookie_monster_perftest.cc) +// that need to test out CookieMonster interactions with the backing store. +// It should only be included by test code. + +#ifndef NET_COOKIES_COOKIE_MONSTER_STORE_TEST_H_ +#define NET_COOKIES_COOKIE_MONSTER_STORE_TEST_H_ +#pragma once + +#include <map> +#include <string> +#include <utility> +#include <vector> +#include "net/cookies/cookie_monster.h" + +namespace base { +class Time; +} + +namespace net { + +// Wrapper class for posting a loaded callback. Since the Callback class is not +// reference counted, we cannot post a callback to the message loop directly, +// instead we post a LoadedCallbackTask. +class LoadedCallbackTask + : public base::RefCountedThreadSafe<LoadedCallbackTask> { + public: + typedef CookieMonster::PersistentCookieStore::LoadedCallback LoadedCallback; + + LoadedCallbackTask(LoadedCallback loaded_callback, + std::vector<CookieMonster::CanonicalCookie*> cookies); + ~LoadedCallbackTask(); + + void Run() { + loaded_callback_.Run(cookies_); + } + + private: + LoadedCallback loaded_callback_; + std::vector<CookieMonster::CanonicalCookie*> cookies_; + + DISALLOW_COPY_AND_ASSIGN(LoadedCallbackTask); +}; // Wrapper class LoadedCallbackTask + +// Describes a call to one of the 3 functions of PersistentCookieStore. +struct CookieStoreCommand { + enum Type { + ADD, + UPDATE_ACCESS_TIME, + REMOVE, + }; + + CookieStoreCommand(Type type, + const CookieMonster::CanonicalCookie& cookie) + : type(type), + cookie(cookie) {} + + Type type; + CookieMonster::CanonicalCookie cookie; +}; + +// Implementation of PersistentCookieStore that captures the +// received commands and saves them to a list. +// The result of calls to Load() can be configured using SetLoadExpectation(). +class MockPersistentCookieStore + : public CookieMonster::PersistentCookieStore { + public: + typedef std::vector<CookieStoreCommand> CommandList; + + MockPersistentCookieStore(); + virtual ~MockPersistentCookieStore(); + + void SetLoadExpectation( + bool return_value, + const std::vector<CookieMonster::CanonicalCookie*>& result); + + const CommandList& commands() const { + return commands_; + } + + virtual void Load(const LoadedCallback& loaded_callback) OVERRIDE; + + virtual void LoadCookiesForKey(const std::string& key, + const LoadedCallback& loaded_callback) OVERRIDE; + + virtual void AddCookie(const CookieMonster::CanonicalCookie& cookie) OVERRIDE; + + virtual void UpdateCookieAccessTime( + const CookieMonster::CanonicalCookie& cookie) OVERRIDE; + + virtual void DeleteCookie( + const CookieMonster::CanonicalCookie& cookie) OVERRIDE; + + virtual void Flush(const base::Closure& callback) OVERRIDE; + + // No files are created so nothing to clear either + virtual void SetClearLocalStateOnExit(bool clear_local_state) OVERRIDE; + + private: + CommandList commands_; + + // Deferred result to use when Load() is called. + bool load_return_value_; + std::vector<CookieMonster::CanonicalCookie*> load_result_; + // Indicates if the store has been fully loaded to avoid returning duplicate + // cookies. + bool loaded_; + + DISALLOW_COPY_AND_ASSIGN(MockPersistentCookieStore); +}; + +// Mock for CookieMonster::Delegate +class MockCookieMonsterDelegate : public CookieMonster::Delegate { + public: + typedef std::pair<CookieMonster::CanonicalCookie, bool> + CookieNotification; + + MockCookieMonsterDelegate(); + + const std::vector<CookieNotification>& changes() const { return changes_; } + + void reset() { changes_.clear(); } + + virtual void OnCookieChanged( + const CookieMonster::CanonicalCookie& cookie, + bool removed, + CookieMonster::Delegate::ChangeCause cause) OVERRIDE; + + private: + virtual ~MockCookieMonsterDelegate(); + + std::vector<CookieNotification> changes_; + + DISALLOW_COPY_AND_ASSIGN(MockCookieMonsterDelegate); +}; + +// Helper to build a single CanonicalCookie. +CookieMonster::CanonicalCookie BuildCanonicalCookie( + const std::string& key, + const std::string& cookie_line, + const base::Time& creation_time); + +// Helper to build a list of CanonicalCookie*s. +void AddCookieToList( + const std::string& key, + const std::string& cookie_line, + const base::Time& creation_time, + std::vector<CookieMonster::CanonicalCookie*>* out_list); + +// Just act like a backing database. Keep cookie information from +// Add/Update/Delete and regurgitate it when Load is called. +class MockSimplePersistentCookieStore + : public CookieMonster::PersistentCookieStore { + public: + MockSimplePersistentCookieStore(); + virtual ~MockSimplePersistentCookieStore(); + + virtual void Load(const LoadedCallback& loaded_callback) OVERRIDE; + + virtual void LoadCookiesForKey(const std::string& key, + const LoadedCallback& loaded_callback) OVERRIDE; + + virtual void AddCookie( + const CookieMonster::CanonicalCookie& cookie) OVERRIDE; + + virtual void UpdateCookieAccessTime( + const CookieMonster::CanonicalCookie& cookie) OVERRIDE; + + virtual void DeleteCookie( + const CookieMonster::CanonicalCookie& cookie) OVERRIDE; + + virtual void Flush(const base::Closure& callback) OVERRIDE; + + virtual void SetClearLocalStateOnExit(bool clear_local_state) OVERRIDE; + + private: + typedef std::map<int64, CookieMonster::CanonicalCookie> + CanonicalCookieMap; + + CanonicalCookieMap cookies_; + + // Indicates if the store has been fully loaded to avoid return duplicate + // cookies in subsequent load requests + bool loaded_; +}; + +// Helper function for creating a CookieMonster backed by a +// MockSimplePersistentCookieStore for garbage collection testing. +// +// Fill the store through import with |num_cookies| cookies, |num_old_cookies| +// with access time Now()-days_old, the rest with access time Now(). +// Do two SetCookies(). Return whether each of the two SetCookies() took +// longer than |gc_perf_micros| to complete, and how many cookie were +// left in the store afterwards. +CookieMonster* CreateMonsterFromStoreForGC( + int num_cookies, + int num_old_cookies, + int days_old); + +} // namespace net + +#endif // NET_COOKIES_COOKIE_MONSTER_STORE_TEST_H_ diff --git a/net/cookies/cookie_monster_unittest.cc b/net/cookies/cookie_monster_unittest.cc new file mode 100644 index 0000000..24390bc --- /dev/null +++ b/net/cookies/cookie_monster_unittest.cc @@ -0,0 +1,2687 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/cookies/cookie_store_unittest.h" + +#include <time.h> +#include <string> + +#include "base/basictypes.h" +#include "base/bind.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/message_loop.h" +#include "base/metrics/histogram.h" +#include "base/stringprintf.h" +#include "base/string_tokenizer.h" +#include "base/threading/thread.h" +#include "base/time.h" +#include "googleurl/src/gurl.h" +#include "net/cookies/cookie_monster.h" +#include "net/cookies/cookie_monster_store_test.h" // For CookieStore mock +#include "net/cookies/cookie_util.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { + +using base::Time; +using base::TimeDelta; + +namespace { + +// TODO(erikwright): Replace the pre-existing MockPersistentCookieStore (and +// brethren) with this one, and remove the 'New' prefix. +class NewMockPersistentCookieStore + : public CookieMonster::PersistentCookieStore { + public: + MOCK_METHOD1(Load, void(const LoadedCallback& loaded_callback)); + MOCK_METHOD2(LoadCookiesForKey, void(const std::string& key, + const LoadedCallback& loaded_callback)); + MOCK_METHOD1(AddCookie, void(const CookieMonster::CanonicalCookie& cc)); + MOCK_METHOD1(UpdateCookieAccessTime, + void(const CookieMonster::CanonicalCookie& cc)); + MOCK_METHOD1(DeleteCookie, void(const CookieMonster::CanonicalCookie& cc)); + MOCK_METHOD1(SetClearLocalStateOnExit, void(bool clear_local_state)); + MOCK_METHOD1(Flush, void(const base::Closure& callback)); +}; + +const char* kTopLevelDomainPlus1 = "http://www.harvard.edu"; +const char* kTopLevelDomainPlus2 = "http://www.math.harvard.edu"; +const char* kTopLevelDomainPlus2Secure = "https://www.math.harvard.edu"; +const char* kTopLevelDomainPlus3 = + "http://www.bourbaki.math.harvard.edu"; +const char* kOtherDomain = "http://www.mit.edu"; +const char kUrlGoogleSpecific[] = "http://www.gmail.google.izzle"; + +class GetCookieListCallback : public CookieCallback { + public: + GetCookieListCallback() {} + explicit GetCookieListCallback(Thread* run_in_thread) + : CookieCallback(run_in_thread) {} + + void Run(const CookieList& cookies) { + cookies_ = cookies; + CallbackEpilogue(); + } + + const CookieList& cookies() { return cookies_; } + + private: + CookieList cookies_; +}; + +class ParsedCookieTest : public testing::Test { }; + +} // namespace + +TEST(ParsedCookieTest, TestBasic) { + CookieMonster::ParsedCookie pc("a=b"); + EXPECT_TRUE(pc.IsValid()); + EXPECT_FALSE(pc.IsSecure()); + EXPECT_EQ("a", pc.Name()); + EXPECT_EQ("b", pc.Value()); +} + +TEST(ParsedCookieTest, TestQuoted) { + // These are some quoting cases which the major browsers all + // handle differently. I've tested Internet Explorer 6, Opera 9.6, + // Firefox 3, and Safari Windows 3.2.1. We originally tried to match + // Firefox closely, however we now match Internet Explorer and Safari. + const char* values[] = { + // Trailing whitespace after a quoted value. The whitespace after + // the quote is stripped in all browsers. + "\"zzz \" ", "\"zzz \"", + // Handling a quoted value with a ';', like FOO="zz;pp" ; + // IE and Safari: "zz; + // Firefox and Opera: "zz;pp" + "\"zz;pp\" ;", "\"zz", + // Handling a value with multiple quoted parts, like FOO="zzz " "ppp" ; + // IE and Safari: "zzz " "ppp"; + // Firefox: "zzz "; + // Opera: <rejects cookie> + "\"zzz \" \"ppp\" ", "\"zzz \" \"ppp\"", + // A quote in a value that didn't start quoted. like FOO=A"B ; + // IE, Safari, and Firefox: A"B; + // Opera: <rejects cookie> + "A\"B", "A\"B", + }; + + for (size_t i = 0; i < arraysize(values); i += 2) { + std::string input(values[i]); + std::string expected(values[i + 1]); + + CookieMonster::ParsedCookie pc( + "aBc=" + input + " ; path=\"/\" ; httponly "); + EXPECT_TRUE(pc.IsValid()); + EXPECT_FALSE(pc.IsSecure()); + EXPECT_TRUE(pc.IsHttpOnly()); + EXPECT_TRUE(pc.HasPath()); + EXPECT_EQ("aBc", pc.Name()); + EXPECT_EQ(expected, pc.Value()); + + // If a path was quoted, the path attribute keeps the quotes. This will + // make the cookie effectively useless, but path parameters aren't supposed + // to be quoted. Bug 1261605. + EXPECT_EQ("\"/\"", pc.Path()); + } +} + +TEST(ParsedCookieTest, TestNameless) { + CookieMonster::ParsedCookie pc("BLAHHH; path=/; secure;"); + EXPECT_TRUE(pc.IsValid()); + EXPECT_TRUE(pc.IsSecure()); + EXPECT_TRUE(pc.HasPath()); + EXPECT_EQ("/", pc.Path()); + EXPECT_EQ("", pc.Name()); + EXPECT_EQ("BLAHHH", pc.Value()); +} + +TEST(ParsedCookieTest, TestAttributeCase) { + CookieMonster::ParsedCookie pc("BLAHHH; Path=/; sECuRe; httpONLY"); + EXPECT_TRUE(pc.IsValid()); + EXPECT_TRUE(pc.IsSecure()); + EXPECT_TRUE(pc.IsHttpOnly()); + EXPECT_TRUE(pc.HasPath()); + EXPECT_EQ("/", pc.Path()); + EXPECT_EQ("", pc.Name()); + EXPECT_EQ("BLAHHH", pc.Value()); + EXPECT_EQ(3U, pc.NumberOfAttributes()); +} + +TEST(ParsedCookieTest, TestDoubleQuotedNameless) { + CookieMonster::ParsedCookie pc("\"BLA\\\"HHH\"; path=/; secure;"); + EXPECT_TRUE(pc.IsValid()); + EXPECT_TRUE(pc.IsSecure()); + EXPECT_TRUE(pc.HasPath()); + EXPECT_EQ("/", pc.Path()); + EXPECT_EQ("", pc.Name()); + EXPECT_EQ("\"BLA\\\"HHH\"", pc.Value()); + EXPECT_EQ(2U, pc.NumberOfAttributes()); +} + +TEST(ParsedCookieTest, QuoteOffTheEnd) { + CookieMonster::ParsedCookie pc("a=\"B"); + EXPECT_TRUE(pc.IsValid()); + EXPECT_EQ("a", pc.Name()); + EXPECT_EQ("\"B", pc.Value()); + EXPECT_EQ(0U, pc.NumberOfAttributes()); +} + +TEST(ParsedCookieTest, MissingName) { + CookieMonster::ParsedCookie pc("=ABC"); + EXPECT_TRUE(pc.IsValid()); + EXPECT_EQ("", pc.Name()); + EXPECT_EQ("ABC", pc.Value()); + EXPECT_EQ(0U, pc.NumberOfAttributes()); +} + +TEST(ParsedCookieTest, MissingValue) { + CookieMonster::ParsedCookie pc("ABC=; path = /wee"); + EXPECT_TRUE(pc.IsValid()); + EXPECT_EQ("ABC", pc.Name()); + EXPECT_EQ("", pc.Value()); + EXPECT_TRUE(pc.HasPath()); + EXPECT_EQ("/wee", pc.Path()); + EXPECT_EQ(1U, pc.NumberOfAttributes()); +} + +TEST(ParsedCookieTest, Whitespace) { + CookieMonster::ParsedCookie pc(" A = BC ;secure;;; httponly"); + EXPECT_TRUE(pc.IsValid()); + EXPECT_EQ("A", pc.Name()); + EXPECT_EQ("BC", pc.Value()); + EXPECT_FALSE(pc.HasPath()); + EXPECT_FALSE(pc.HasDomain()); + EXPECT_TRUE(pc.IsSecure()); + EXPECT_TRUE(pc.IsHttpOnly()); + // We parse anything between ; as attributes, so we end up with two + // attributes with an empty string name and value. + EXPECT_EQ(4U, pc.NumberOfAttributes()); +} +TEST(ParsedCookieTest, MultipleEquals) { + CookieMonster::ParsedCookie pc(" A=== BC ;secure;;; httponly"); + EXPECT_TRUE(pc.IsValid()); + EXPECT_EQ("A", pc.Name()); + EXPECT_EQ("== BC", pc.Value()); + EXPECT_FALSE(pc.HasPath()); + EXPECT_FALSE(pc.HasDomain()); + EXPECT_TRUE(pc.IsSecure()); + EXPECT_TRUE(pc.IsHttpOnly()); + EXPECT_EQ(4U, pc.NumberOfAttributes()); +} + +TEST(ParsedCookieTest, MACKey) { + CookieMonster::ParsedCookie pc("foo=bar; MAC-Key=3900ac9anw9incvw9f"); + EXPECT_TRUE(pc.IsValid()); + EXPECT_EQ("foo", pc.Name()); + EXPECT_EQ("bar", pc.Value()); + EXPECT_EQ("3900ac9anw9incvw9f", pc.MACKey()); + EXPECT_EQ(1U, pc.NumberOfAttributes()); +} + +TEST(ParsedCookieTest, MACAlgorithm) { + CookieMonster::ParsedCookie pc("foo=bar; MAC-Algorithm=hmac-sha-1"); + EXPECT_TRUE(pc.IsValid()); + EXPECT_EQ("foo", pc.Name()); + EXPECT_EQ("bar", pc.Value()); + EXPECT_EQ("hmac-sha-1", pc.MACAlgorithm()); + EXPECT_EQ(1U, pc.NumberOfAttributes()); +} + +TEST(ParsedCookieTest, MACKeyAndMACAlgorithm) { + CookieMonster::ParsedCookie pc( + "foo=bar; MAC-Key=voiae-09fj0302nfqf; MAC-Algorithm=hmac-sha-256"); + EXPECT_TRUE(pc.IsValid()); + EXPECT_EQ("foo", pc.Name()); + EXPECT_EQ("bar", pc.Value()); + EXPECT_EQ("voiae-09fj0302nfqf", pc.MACKey()); + EXPECT_EQ("hmac-sha-256", pc.MACAlgorithm()); + EXPECT_EQ(2U, pc.NumberOfAttributes()); +} + +TEST(ParsedCookieTest, QuotedTrailingWhitespace) { + CookieMonster::ParsedCookie pc("ANCUUID=\"zohNumRKgI0oxyhSsV3Z7D\" ; " + "expires=Sun, 18-Apr-2027 21:06:29 GMT ; " + "path=/ ; "); + EXPECT_TRUE(pc.IsValid()); + EXPECT_EQ("ANCUUID", pc.Name()); + // Stripping whitespace after the quotes matches all other major browsers. + EXPECT_EQ("\"zohNumRKgI0oxyhSsV3Z7D\"", pc.Value()); + EXPECT_TRUE(pc.HasExpires()); + EXPECT_TRUE(pc.HasPath()); + EXPECT_EQ("/", pc.Path()); + EXPECT_EQ(2U, pc.NumberOfAttributes()); +} + +TEST(ParsedCookieTest, TrailingWhitespace) { + CookieMonster::ParsedCookie pc("ANCUUID=zohNumRKgI0oxyhSsV3Z7D ; " + "expires=Sun, 18-Apr-2027 21:06:29 GMT ; " + "path=/ ; "); + EXPECT_TRUE(pc.IsValid()); + EXPECT_EQ("ANCUUID", pc.Name()); + EXPECT_EQ("zohNumRKgI0oxyhSsV3Z7D", pc.Value()); + EXPECT_TRUE(pc.HasExpires()); + EXPECT_TRUE(pc.HasPath()); + EXPECT_EQ("/", pc.Path()); + EXPECT_EQ(2U, pc.NumberOfAttributes()); +} + +TEST(ParsedCookieTest, TooManyPairs) { + std::string blankpairs; + blankpairs.resize(CookieMonster::ParsedCookie::kMaxPairs - 1, ';'); + + CookieMonster::ParsedCookie pc1(blankpairs + "secure"); + EXPECT_TRUE(pc1.IsValid()); + EXPECT_TRUE(pc1.IsSecure()); + + CookieMonster::ParsedCookie pc2(blankpairs + ";secure"); + EXPECT_TRUE(pc2.IsValid()); + EXPECT_FALSE(pc2.IsSecure()); +} + +// TODO(erikwright): some better test cases for invalid cookies. +TEST(ParsedCookieTest, InvalidWhitespace) { + CookieMonster::ParsedCookie pc(" "); + EXPECT_FALSE(pc.IsValid()); +} + +TEST(ParsedCookieTest, InvalidTooLong) { + std::string maxstr; + maxstr.resize(CookieMonster::ParsedCookie::kMaxCookieSize, 'a'); + + CookieMonster::ParsedCookie pc1(maxstr); + EXPECT_TRUE(pc1.IsValid()); + + CookieMonster::ParsedCookie pc2(maxstr + "A"); + EXPECT_FALSE(pc2.IsValid()); +} + +TEST(ParsedCookieTest, InvalidEmpty) { + CookieMonster::ParsedCookie pc(""); + EXPECT_FALSE(pc.IsValid()); +} + +TEST(ParsedCookieTest, EmbeddedTerminator) { + CookieMonster::ParsedCookie pc1("AAA=BB\0ZYX"); + CookieMonster::ParsedCookie pc2("AAA=BB\rZYX"); + CookieMonster::ParsedCookie pc3("AAA=BB\nZYX"); + EXPECT_TRUE(pc1.IsValid()); + EXPECT_EQ("AAA", pc1.Name()); + EXPECT_EQ("BB", pc1.Value()); + EXPECT_TRUE(pc2.IsValid()); + EXPECT_EQ("AAA", pc2.Name()); + EXPECT_EQ("BB", pc2.Value()); + EXPECT_TRUE(pc3.IsValid()); + EXPECT_EQ("AAA", pc3.Name()); + EXPECT_EQ("BB", pc3.Value()); +} + +TEST(ParsedCookieTest, ParseTokensAndValues) { + EXPECT_EQ("hello", + CookieMonster::ParsedCookie::ParseTokenString( + "hello\nworld")); + EXPECT_EQ("fs!!@", + CookieMonster::ParsedCookie::ParseTokenString( + "fs!!@;helloworld")); + EXPECT_EQ("hello world\tgood", + CookieMonster::ParsedCookie::ParseTokenString( + "hello world\tgood\rbye")); + EXPECT_EQ("A", + CookieMonster::ParsedCookie::ParseTokenString( + "A=B=C;D=E")); + EXPECT_EQ("hello", + CookieMonster::ParsedCookie::ParseValueString( + "hello\nworld")); + EXPECT_EQ("fs!!@", + CookieMonster::ParsedCookie::ParseValueString( + "fs!!@;helloworld")); + EXPECT_EQ("hello world\tgood", + CookieMonster::ParsedCookie::ParseValueString( + "hello world\tgood\rbye")); + EXPECT_EQ("A=B=C", + CookieMonster::ParsedCookie::ParseValueString( + "A=B=C;D=E")); +} + +namespace { + +struct CookieMonsterTestTraits { + static scoped_refptr<CookieStore> Create() { + return new CookieMonster(NULL, NULL); + } + + static const bool is_cookie_monster = true; + static const bool supports_http_only = true; + static const bool supports_cookies_with_info = true; + static const bool supports_non_dotted_domains = true; + static const bool supports_trailing_dots = true; + static const bool filters_schemes = true; + static const bool has_path_prefix_bug = false; + static const int creation_time_granularity_in_ms = 0; +}; + +INSTANTIATE_TYPED_TEST_CASE_P(CookieMonster, + CookieStoreTest, + CookieMonsterTestTraits); + +INSTANTIATE_TYPED_TEST_CASE_P(CookieMonster, + MultiThreadedCookieStoreTest, + CookieMonsterTestTraits); + +class CookieMonsterTest : public CookieStoreTest<CookieMonsterTestTraits> { + protected: + + CookieList GetAllCookies(CookieMonster* cm) { + DCHECK(cm); + GetCookieListCallback callback; + cm->GetAllCookiesAsync( + base::Bind(&GetCookieListCallback::Run, + base::Unretained(&callback))); + RunFor(kTimeout); + EXPECT_TRUE(callback.did_run()); + return callback.cookies(); + } + + CookieList GetAllCookiesForURL(CookieMonster* cm, + const GURL& url) { + DCHECK(cm); + GetCookieListCallback callback; + cm->GetAllCookiesForURLAsync( + url, base::Bind(&GetCookieListCallback::Run, + base::Unretained(&callback))); + RunFor(kTimeout); + EXPECT_TRUE(callback.did_run()); + return callback.cookies(); + } + + CookieList GetAllCookiesForURLWithOptions(CookieMonster* cm, + const GURL& url, + const CookieOptions& options) { + DCHECK(cm); + GetCookieListCallback callback; + cm->GetAllCookiesForURLWithOptionsAsync( + url, options, base::Bind(&GetCookieListCallback::Run, + base::Unretained(&callback))); + RunFor(kTimeout); + EXPECT_TRUE(callback.did_run()); + return callback.cookies(); + } + + bool SetCookieWithDetails(CookieMonster* cm, + const GURL& url, + const std::string& name, + const std::string& value, + const std::string& domain, + const std::string& path, + const base::Time& expiration_time, + bool secure, bool http_only) { + DCHECK(cm); + SetCookieCallback callback; + cm->SetCookieWithDetailsAsync( + url, name, value, domain, path, expiration_time, secure, http_only, + base::Bind(&SetCookieCallback::Run, base::Unretained(&callback))); + RunFor(kTimeout); + EXPECT_TRUE(callback.did_run()); + return callback.result(); + } + + int DeleteAll(CookieMonster*cm) { + DCHECK(cm); + DeleteCallback callback; + cm->DeleteAllAsync( + base::Bind(&DeleteCallback::Run, base::Unretained(&callback))); + RunFor(kTimeout); + EXPECT_TRUE(callback.did_run()); + return callback.num_deleted(); + } + + int DeleteAllCreatedBetween(CookieMonster*cm, + const base::Time& delete_begin, + const base::Time& delete_end) { + DCHECK(cm); + DeleteCallback callback; + cm->DeleteAllCreatedBetweenAsync( + delete_begin, delete_end, + base::Bind(&DeleteCallback::Run, base::Unretained(&callback))); + RunFor(kTimeout); + EXPECT_TRUE(callback.did_run()); + return callback.num_deleted(); + } + + int DeleteAllForHost(CookieMonster*cm, + const GURL& url) { + DCHECK(cm); + DeleteCallback callback; + cm->DeleteAllForHostAsync( + url, base::Bind(&DeleteCallback::Run, base::Unretained(&callback))); + RunFor(kTimeout); + EXPECT_TRUE(callback.did_run()); + return callback.num_deleted(); + } + + bool DeleteCanonicalCookie(CookieMonster*cm, + const CookieMonster::CanonicalCookie& cookie) { + DCHECK(cm); + SetCookieCallback callback; + cm->DeleteCanonicalCookieAsync( + cookie, + base::Bind(&SetCookieCallback::Run, base::Unretained(&callback))); + RunFor(kTimeout); + EXPECT_TRUE(callback.did_run()); + return callback.result(); + } + + // Helper for DeleteAllForHost test; repopulates CM with same layout + // each time. + void PopulateCmForDeleteAllForHost(scoped_refptr<CookieMonster> cm) { + GURL url_top_level_domain_plus_1(kTopLevelDomainPlus1); + GURL url_top_level_domain_plus_2(kTopLevelDomainPlus2); + GURL url_top_level_domain_plus_2_secure(kTopLevelDomainPlus2Secure); + GURL url_top_level_domain_plus_3(kTopLevelDomainPlus3); + GURL url_other(kOtherDomain); + + DeleteAll(cm); + + // Static population for probe: + // * Three levels of domain cookie (.b.a, .c.b.a, .d.c.b.a) + // * Three levels of host cookie (w.b.a, w.c.b.a, w.d.c.b.a) + // * http_only cookie (w.c.b.a) + // * Two secure cookies (.c.b.a, w.c.b.a) + // * Two domain path cookies (.c.b.a/dir1, .c.b.a/dir1/dir2) + // * Two host path cookies (w.c.b.a/dir1, w.c.b.a/dir1/dir2) + + // Domain cookies + EXPECT_TRUE(this->SetCookieWithDetails(cm, url_top_level_domain_plus_1, + "dom_1", "X", ".harvard.edu", "/", + base::Time(), false, false)); + EXPECT_TRUE(this->SetCookieWithDetails(cm, url_top_level_domain_plus_2, + "dom_2", "X", ".math.harvard.edu", + "/", base::Time(), false, false)); + EXPECT_TRUE(this->SetCookieWithDetails(cm, url_top_level_domain_plus_3, + "dom_3", "X", + ".bourbaki.math.harvard.edu", "/", + base::Time(), false, false)); + + // Host cookies + EXPECT_TRUE(this->SetCookieWithDetails(cm, url_top_level_domain_plus_1, + "host_1", "X", "", "/", + base::Time(), false, false)); + EXPECT_TRUE(this->SetCookieWithDetails(cm, url_top_level_domain_plus_2, + "host_2", "X", "", "/", + base::Time(), false, false)); + EXPECT_TRUE(this->SetCookieWithDetails(cm, url_top_level_domain_plus_3, + "host_3", "X", "", "/", + base::Time(), false, false)); + + // Http_only cookie + EXPECT_TRUE(this->SetCookieWithDetails(cm, url_top_level_domain_plus_2, + "httpo_check", "X", "", "/", + base::Time(), false, true)); + + // Secure cookies + EXPECT_TRUE(this->SetCookieWithDetails(cm, + url_top_level_domain_plus_2_secure, + "sec_dom", "X", ".math.harvard.edu", + "/", base::Time(), true, false)); + EXPECT_TRUE(this->SetCookieWithDetails(cm, + url_top_level_domain_plus_2_secure, + "sec_host", "X", "", "/", + base::Time(), true, false)); + + // Domain path cookies + EXPECT_TRUE(this->SetCookieWithDetails(cm, url_top_level_domain_plus_2, + "dom_path_1", "X", + ".math.harvard.edu", "/dir1", + base::Time(), false, false)); + EXPECT_TRUE(this->SetCookieWithDetails(cm, url_top_level_domain_plus_2, + "dom_path_2", "X", + ".math.harvard.edu", "/dir1/dir2", + base::Time(), false, false)); + + // Host path cookies + EXPECT_TRUE(this->SetCookieWithDetails(cm, url_top_level_domain_plus_2, + "host_path_1", "X", + "", "/dir1", + base::Time(), false, false)); + EXPECT_TRUE(this->SetCookieWithDetails(cm, url_top_level_domain_plus_2, + "host_path_2", "X", + "", "/dir1/dir2", + base::Time(), false, false)); + + EXPECT_EQ(13U, this->GetAllCookies(cm).size()); + } + + Time GetFirstCookieAccessDate(CookieMonster* cm) { + const CookieList all_cookies(this->GetAllCookies(cm)); + return all_cookies.front().LastAccessDate(); + } + + bool FindAndDeleteCookie(CookieMonster* cm, + const std::string& domain, + const std::string& name) { + CookieList cookies = this->GetAllCookies(cm); + for (CookieList::iterator it = cookies.begin(); + it != cookies.end(); ++it) + if (it->Domain() == domain && it->Name() == name) + return this->DeleteCanonicalCookie(cm, *it); + return false; + } + + int CountInString(const std::string& str, char c) { + return std::count(str.begin(), str.end(), c); + } + + void TestHostGarbageCollectHelper( + int domain_max_cookies, + int domain_purge_cookies) { + const int more_than_enough_cookies = + (domain_max_cookies + domain_purge_cookies) * 2; + // Add a bunch of cookies on a single host, should purge them. + { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + for (int i = 0; i < more_than_enough_cookies; ++i) { + std::string cookie = base::StringPrintf("a%03d=b", i); + EXPECT_TRUE(SetCookie(cm, url_google_, cookie)); + std::string cookies = this->GetCookies(cm, url_google_); + // Make sure we find it in the cookies. + EXPECT_NE(cookies.find(cookie), std::string::npos); + // Count the number of cookies. + EXPECT_LE(CountInString(cookies, '='), domain_max_cookies); + } + } + + // Add a bunch of cookies on multiple hosts within a single eTLD. + // Should keep at least kDomainMaxCookies - kDomainPurgeCookies + // between them. We shouldn't go above kDomainMaxCookies for both together. + GURL url_google_specific(kUrlGoogleSpecific); + { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + for (int i = 0; i < more_than_enough_cookies; ++i) { + std::string cookie_general = base::StringPrintf("a%03d=b", i); + EXPECT_TRUE(SetCookie(cm, url_google_, cookie_general)); + std::string cookie_specific = base::StringPrintf("c%03d=b", i); + EXPECT_TRUE(SetCookie(cm, url_google_specific, cookie_specific)); + std::string cookies_general = this->GetCookies(cm, url_google_); + EXPECT_NE(cookies_general.find(cookie_general), std::string::npos); + std::string cookies_specific = + this->GetCookies(cm, url_google_specific); + EXPECT_NE(cookies_specific.find(cookie_specific), std::string::npos); + EXPECT_LE((CountInString(cookies_general, '=') + + CountInString(cookies_specific, '=')), + domain_max_cookies); + } + // After all this, there should be at least + // kDomainMaxCookies - kDomainPurgeCookies for both URLs. + std::string cookies_general = this->GetCookies(cm, url_google_); + std::string cookies_specific = this->GetCookies(cm, url_google_specific); + int total_cookies = (CountInString(cookies_general, '=') + + CountInString(cookies_specific, '=')); + EXPECT_GE(total_cookies, + domain_max_cookies - domain_purge_cookies); + EXPECT_LE(total_cookies, domain_max_cookies); + } + } + + // Function for creating a CM with a number of cookies in it, + // no store (and hence no ability to affect access time). + CookieMonster* CreateMonsterForGC(int num_cookies) { + CookieMonster* cm(new CookieMonster(NULL, NULL)); + for (int i = 0; i < num_cookies; i++) { + SetCookie(cm, GURL(StringPrintf("http://h%05d.izzle", i)), "a=1"); + } + return cm; + } +}; + +// TODO(erikwright): Replace the other callbacks and synchronous helper methods +// in this test suite with these Mocks. +template<typename T, typename C> class MockCookieCallback { + public: + C AsCallback() { + return base::Bind(&T::Invoke, base::Unretained(static_cast<T*>(this))); + } +}; + +class MockGetCookiesCallback + : public MockCookieCallback<MockGetCookiesCallback, + CookieStore::GetCookiesCallback> { + public: + MOCK_METHOD1(Invoke, void(const std::string& cookies)); +}; + +class MockGetCookieInfoCallback + : public MockCookieCallback<MockGetCookieInfoCallback, + CookieStore::GetCookieInfoCallback> { + public: + MOCK_METHOD2(Invoke, + void(const std::string& cookies, + const std::vector<CookieStore::CookieInfo>& cookie_infos)); +}; + +class MockSetCookiesCallback + : public MockCookieCallback<MockSetCookiesCallback, + CookieStore::SetCookiesCallback> { + public: + MOCK_METHOD1(Invoke, void(bool success)); +}; + +class MockClosure + : public MockCookieCallback<MockClosure, base::Closure> { + public: + MOCK_METHOD0(Invoke, void(void)); +}; + +class MockGetCookieListCallback + : public MockCookieCallback<MockGetCookieListCallback, + CookieMonster::GetCookieListCallback> { + public: + MOCK_METHOD1(Invoke, void(const CookieList& cookies)); +}; + +class MockDeleteCallback + : public MockCookieCallback<MockDeleteCallback, + CookieMonster::DeleteCallback> { + public: + MOCK_METHOD1(Invoke, void(int num_deleted)); +}; + +class MockDeleteCookieCallback + : public MockCookieCallback<MockDeleteCookieCallback, + CookieMonster::DeleteCookieCallback> { + public: + MOCK_METHOD1(Invoke, void(bool success)); +}; + +ACTION(QuitCurrentMessageLoop) { + MessageLoop::current()->PostTask(FROM_HERE, MessageLoop::QuitClosure()); +} + +// TODO(erikwright): When the synchronous helpers 'GetCookies' etc. are removed, +// rename these, removing the 'Action' suffix. +ACTION_P4(DeleteCookieAction, cookie_monster, url, name, callback) { + cookie_monster->DeleteCookieAsync(url, name, callback->AsCallback()); +} +ACTION_P3(GetCookiesAction, cookie_monster, url, callback) { + cookie_monster->GetCookiesWithOptionsAsync( + url, CookieOptions(), callback->AsCallback()); +} +ACTION_P3(GetCookiesWithInfoAction, cookie_monster, url, callback) { + cookie_monster->GetCookiesWithInfoAsync( + url, CookieOptions(), callback->AsCallback()); +} +ACTION_P4(SetCookieAction, cookie_monster, url, cookie_line, callback) { + cookie_monster->SetCookieWithOptionsAsync( + url, cookie_line, CookieOptions(), callback->AsCallback()); +} +ACTION_P4(DeleteAllCreatedBetweenAction, + cookie_monster, delete_begin, delete_end, callback) { + cookie_monster->DeleteAllCreatedBetweenAsync( + delete_begin, delete_end, callback->AsCallback()); +} +ACTION_P10(SetCookieWithDetailsAction, + cookie_monster, url, name, value, domain, path, expiration_time, + secure, http_only, callback) { + cookie_monster->SetCookieWithDetailsAsync( + url, name, value, domain, path, expiration_time, secure, http_only, + callback->AsCallback()); +} + +ACTION_P2(GetAllCookiesAction, cookie_monster, callback) { + cookie_monster->GetAllCookiesAsync(callback->AsCallback()); +} + +ACTION_P3(DeleteAllForHostAction, cookie_monster, url, callback) { + cookie_monster->DeleteAllForHostAsync(url, callback->AsCallback()); +} + +ACTION_P3(DeleteCanonicalCookieAction, cookie_monster, cookie, callback) { + cookie_monster->DeleteCanonicalCookieAsync(cookie, callback->AsCallback()); +} + +ACTION_P2(DeleteAllAction, cookie_monster, callback) { + cookie_monster->DeleteAllAsync(callback->AsCallback()); +} + +ACTION_P3(GetAllCookiesForUrlWithOptionsAction, cookie_monster, url, callback) { + cookie_monster->GetAllCookiesForURLWithOptionsAsync( + url, CookieOptions(), callback->AsCallback()); +} + +ACTION_P3(GetAllCookiesForUrlAction, cookie_monster, url, callback) { + cookie_monster->GetAllCookiesForURLAsync(url, callback->AsCallback()); +} + +ACTION_P(PushCallbackAction, callback_vector) { + callback_vector->push(arg1); +} + +} // namespace + +// This test suite verifies the task deferral behaviour of the CookieMonster. +// Specifically, for each asynchronous method, verify that: +// 1. invoking it on an uninitialized cookie store causes the store to begin +// chain-loading its backing data or loading data for a specific domain key +// (eTLD+1). +// 2. The initial invocation does not complete until the loading completes. +// 3. Invocations after the loading has completed complete immediately. +class DeferredCookieTaskTest : public CookieMonsterTest { + protected: + DeferredCookieTaskTest() { + persistent_store_ = new NewMockPersistentCookieStore(); + cookie_monster_ = new CookieMonster(persistent_store_.get(), NULL); + } + + // Defines a cookie to be returned from PersistentCookieStore::Load + void DeclareLoadedCookie(const std::string& key, + const std::string& cookie_line, + const base::Time& creation_time) { + AddCookieToList(key, cookie_line, creation_time, &loaded_cookies_); + } + + // Runs the message loop, waiting until PersistentCookieStore::Load is called. + // Call CompleteLoadingAndWait to cause the load to complete. + void WaitForLoadCall() { + RunFor(kTimeout); + + // Verify that PeristentStore::Load was called. + testing::Mock::VerifyAndClear(persistent_store_.get()); + } + + // Invokes the PersistentCookieStore::LoadCookiesForKey completion callbacks + // and PersistentCookieStore::Load completion callback and waits + // until the message loop is quit. + void CompleteLoadingAndWait() { + while (!loaded_for_key_callbacks_.empty()) { + loaded_for_key_callbacks_.front().Run(loaded_cookies_); + loaded_cookies_.clear(); + loaded_for_key_callbacks_.pop(); + } + + loaded_callback_.Run(loaded_cookies_); + RunFor(kTimeout); + } + + // Performs the provided action, expecting it to cause a call to + // PersistentCookieStore::Load. Call WaitForLoadCall to verify the load call + // is received. + void BeginWith(testing::Action<void(void)> action) { + EXPECT_CALL(*this, Begin()).WillOnce(action); + ExpectLoadCall(); + Begin(); + } + + void BeginWithForDomainKey(std::string key, + testing::Action<void(void)> action) { + EXPECT_CALL(*this, Begin()).WillOnce(action); + ExpectLoadCall(); + ExpectLoadForKeyCall(key, false); + Begin(); + } + + // Declares an expectation that PersistentCookieStore::Load will be called, + // saving the provided callback and sending a quit to the message loop. + void ExpectLoadCall() { + EXPECT_CALL(*persistent_store_, Load(testing::_)).WillOnce(testing::DoAll( + testing::SaveArg<0>(&loaded_callback_), + QuitCurrentMessageLoop())); + } + + // Declares an expectation that PersistentCookieStore::LoadCookiesForKey + // will be called, saving the provided callback and sending a quit to the + // message loop. + void ExpectLoadForKeyCall(std::string key, bool quit_queue) { + if (quit_queue) + EXPECT_CALL(*persistent_store_, LoadCookiesForKey(key, testing::_)). + WillOnce(testing::DoAll( + PushCallbackAction(&loaded_for_key_callbacks_), + QuitCurrentMessageLoop())); + else + EXPECT_CALL(*persistent_store_, LoadCookiesForKey(key, testing::_)). + WillOnce(PushCallbackAction(&loaded_for_key_callbacks_)); + } + + // Invokes the initial action. + MOCK_METHOD0(Begin, void(void)); + + // Returns the CookieMonster instance under test. + CookieMonster& cookie_monster() { return *cookie_monster_; } + + private: + // Declares that mock expectations in this test suite are strictly ordered. + testing::InSequence in_sequence_; + // Holds cookies to be returned from PersistentCookieStore::Load or + // PersistentCookieStore::LoadCookiesForKey. + std::vector<CookieMonster::CanonicalCookie*> loaded_cookies_; + // Stores the callback passed from the CookieMonster to the + // PersistentCookieStore::Load + CookieMonster::PersistentCookieStore::LoadedCallback loaded_callback_; + // Stores the callback passed from the CookieMonster to the + // PersistentCookieStore::LoadCookiesForKey + std::queue<CookieMonster::PersistentCookieStore::LoadedCallback> + loaded_for_key_callbacks_; + + // Stores the CookieMonster under test. + scoped_refptr<CookieMonster> cookie_monster_; + // Stores the mock PersistentCookieStore. + scoped_refptr<NewMockPersistentCookieStore> persistent_store_; +}; + +TEST_F(DeferredCookieTaskTest, DeferredGetCookies) { + DeclareLoadedCookie("www.google.izzle", + "X=1; path=/; expires=Mon, 18-Apr-22 22:50:14 GMT", + Time::Now() + TimeDelta::FromDays(3)); + + MockGetCookiesCallback get_cookies_callback; + + BeginWithForDomainKey("google.izzle", GetCookiesAction( + &cookie_monster(), url_google_, &get_cookies_callback)); + + WaitForLoadCall(); + + EXPECT_CALL(get_cookies_callback, Invoke("X=1")).WillOnce( + GetCookiesAction(&cookie_monster(), url_google_, &get_cookies_callback)); + EXPECT_CALL(get_cookies_callback, Invoke("X=1")).WillOnce( + QuitCurrentMessageLoop()); + + CompleteLoadingAndWait(); +} + +TEST_F(DeferredCookieTaskTest, DeferredGetCookiesWithInfo) { + DeclareLoadedCookie("www.google.izzle", + "X=1; path=/; expires=Mon, 18-Apr-22 22:50:14 GMT", + Time::Now() + TimeDelta::FromDays(3)); + + MockGetCookieInfoCallback get_cookie_info_callback; + + BeginWithForDomainKey("google.izzle", GetCookiesWithInfoAction( + &cookie_monster(), url_google_, &get_cookie_info_callback)); + + WaitForLoadCall(); + + EXPECT_CALL(get_cookie_info_callback, Invoke("X=1", testing::_)).WillOnce( + GetCookiesWithInfoAction( + &cookie_monster(), url_google_, &get_cookie_info_callback)); + EXPECT_CALL(get_cookie_info_callback, Invoke("X=1", testing::_)).WillOnce( + QuitCurrentMessageLoop()); + + CompleteLoadingAndWait(); +} + +TEST_F(DeferredCookieTaskTest, DeferredSetCookie) { + MockSetCookiesCallback set_cookies_callback; + + BeginWithForDomainKey("google.izzle", SetCookieAction( + &cookie_monster(), url_google_, "A=B", &set_cookies_callback)); + + WaitForLoadCall(); + + EXPECT_CALL(set_cookies_callback, Invoke(true)).WillOnce( + SetCookieAction( + &cookie_monster(), url_google_, "X=Y", &set_cookies_callback)); + EXPECT_CALL(set_cookies_callback, Invoke(true)).WillOnce( + QuitCurrentMessageLoop()); + + CompleteLoadingAndWait(); +} + +TEST_F(DeferredCookieTaskTest, DeferredDeleteCookie) { + MockClosure delete_cookie_callback; + + BeginWithForDomainKey("google.izzle", DeleteCookieAction( + &cookie_monster(), url_google_, "A", &delete_cookie_callback)); + + WaitForLoadCall(); + + EXPECT_CALL(delete_cookie_callback, Invoke()).WillOnce( + DeleteCookieAction( + &cookie_monster(), url_google_, "X", &delete_cookie_callback)); + EXPECT_CALL(delete_cookie_callback, Invoke()).WillOnce( + QuitCurrentMessageLoop()); + + CompleteLoadingAndWait(); +} + +TEST_F(DeferredCookieTaskTest, DeferredSetCookieWithDetails) { + MockSetCookiesCallback set_cookies_callback; + + BeginWithForDomainKey("google.izzle", SetCookieWithDetailsAction( + &cookie_monster(), url_google_foo_, "A", "B", std::string(), "/foo", + base::Time(), false, false, &set_cookies_callback)); + + WaitForLoadCall(); + + EXPECT_CALL(set_cookies_callback, Invoke(true)).WillOnce( + SetCookieWithDetailsAction( + &cookie_monster(), url_google_foo_, "A", "B", std::string(), "/foo", + base::Time(), false, false, &set_cookies_callback)); + EXPECT_CALL(set_cookies_callback, Invoke(true)).WillOnce( + QuitCurrentMessageLoop()); + + CompleteLoadingAndWait(); +} + +TEST_F(DeferredCookieTaskTest, DeferredGetAllCookies) { + DeclareLoadedCookie("www.google.izzle", + "X=1; path=/; expires=Mon, 18-Apr-22 22:50:14 GMT", + Time::Now() + TimeDelta::FromDays(3)); + + MockGetCookieListCallback get_cookie_list_callback; + + BeginWith(GetAllCookiesAction( + &cookie_monster(), &get_cookie_list_callback)); + + WaitForLoadCall(); + + EXPECT_CALL(get_cookie_list_callback, Invoke(testing::_)).WillOnce( + GetAllCookiesAction(&cookie_monster(), &get_cookie_list_callback)); + EXPECT_CALL(get_cookie_list_callback, Invoke(testing::_)).WillOnce( + QuitCurrentMessageLoop()); + + CompleteLoadingAndWait(); +} + +TEST_F(DeferredCookieTaskTest, DeferredGetAllForUrlCookies) { + DeclareLoadedCookie("www.google.izzle", + "X=1; path=/; expires=Mon, 18-Apr-22 22:50:14 GMT", + Time::Now() + TimeDelta::FromDays(3)); + + MockGetCookieListCallback get_cookie_list_callback; + + BeginWithForDomainKey("google.izzle", GetAllCookiesForUrlAction( + &cookie_monster(), url_google_, &get_cookie_list_callback)); + + WaitForLoadCall(); + + EXPECT_CALL(get_cookie_list_callback, Invoke(testing::_)).WillOnce( + GetAllCookiesForUrlAction( + &cookie_monster(), url_google_, &get_cookie_list_callback)); + EXPECT_CALL(get_cookie_list_callback, Invoke(testing::_)).WillOnce( + QuitCurrentMessageLoop()); + + CompleteLoadingAndWait(); +} + +TEST_F(DeferredCookieTaskTest, DeferredGetAllForUrlWithOptionsCookies) { + DeclareLoadedCookie("www.google.izzle", + "X=1; path=/; expires=Mon, 18-Apr-22 22:50:14 GMT", + Time::Now() + TimeDelta::FromDays(3)); + + MockGetCookieListCallback get_cookie_list_callback; + + BeginWithForDomainKey("google.izzle", GetAllCookiesForUrlWithOptionsAction( + &cookie_monster(), url_google_, &get_cookie_list_callback)); + + WaitForLoadCall(); + + EXPECT_CALL(get_cookie_list_callback, Invoke(testing::_)).WillOnce( + GetAllCookiesForUrlWithOptionsAction( + &cookie_monster(), url_google_, &get_cookie_list_callback)); + EXPECT_CALL(get_cookie_list_callback, Invoke(testing::_)).WillOnce( + QuitCurrentMessageLoop()); + + CompleteLoadingAndWait(); +} + +TEST_F(DeferredCookieTaskTest, DeferredDeleteAllCookies) { + MockDeleteCallback delete_callback; + + BeginWith(DeleteAllAction( + &cookie_monster(), &delete_callback)); + + WaitForLoadCall(); + + EXPECT_CALL(delete_callback, Invoke(false)).WillOnce( + DeleteAllAction(&cookie_monster(), &delete_callback)); + EXPECT_CALL(delete_callback, Invoke(false)).WillOnce( + QuitCurrentMessageLoop()); + + CompleteLoadingAndWait(); +} + +TEST_F(DeferredCookieTaskTest, DeferredDeleteAllCreatedBetweenCookies) { + MockDeleteCallback delete_callback; + + BeginWith(DeleteAllCreatedBetweenAction( + &cookie_monster(), base::Time(), base::Time::Now(), &delete_callback)); + + WaitForLoadCall(); + + EXPECT_CALL(delete_callback, Invoke(false)).WillOnce( + DeleteAllCreatedBetweenAction( + &cookie_monster(), base::Time(), base::Time::Now(), + &delete_callback)); + EXPECT_CALL(delete_callback, Invoke(false)).WillOnce( + QuitCurrentMessageLoop()); + + CompleteLoadingAndWait(); +} + +TEST_F(DeferredCookieTaskTest, DeferredDeleteAllForHostCookies) { + MockDeleteCallback delete_callback; + + BeginWithForDomainKey("google.izzle", DeleteAllForHostAction( + &cookie_monster(), url_google_, &delete_callback)); + + WaitForLoadCall(); + + EXPECT_CALL(delete_callback, Invoke(false)).WillOnce( + DeleteAllForHostAction( + &cookie_monster(), url_google_, &delete_callback)); + EXPECT_CALL(delete_callback, Invoke(false)).WillOnce( + QuitCurrentMessageLoop()); + + CompleteLoadingAndWait(); +} + +TEST_F(DeferredCookieTaskTest, DeferredDeleteCanonicalCookie) { + std::vector<CookieMonster::CanonicalCookie*> cookies; + CookieMonster::CanonicalCookie cookie = BuildCanonicalCookie( + "www.google.com", "X=1; path=/", base::Time::Now()); + + MockDeleteCookieCallback delete_cookie_callback; + + BeginWith(DeleteCanonicalCookieAction( + &cookie_monster(), cookie, &delete_cookie_callback)); + + WaitForLoadCall(); + + EXPECT_CALL(delete_cookie_callback, Invoke(false)).WillOnce( + DeleteCanonicalCookieAction( + &cookie_monster(), cookie, &delete_cookie_callback)); + EXPECT_CALL(delete_cookie_callback, Invoke(false)).WillOnce( + QuitCurrentMessageLoop()); + + CompleteLoadingAndWait(); +} + +// Verify that a series of queued tasks are executed in order upon loading of +// the backing store and that new tasks received while the queued tasks are +// being dispatched go to the end of the queue. +TEST_F(DeferredCookieTaskTest, DeferredTaskOrder) { + DeclareLoadedCookie("www.google.izzle", + "X=1; path=/; expires=Mon, 18-Apr-22 22:50:14 GMT", + Time::Now() + TimeDelta::FromDays(3)); + + MockGetCookiesCallback get_cookies_callback; + MockSetCookiesCallback set_cookies_callback; + MockClosure delete_cookie_callback; + MockGetCookieInfoCallback get_cookie_info_callback; + + EXPECT_CALL(*this, Begin()).WillOnce(testing::DoAll( + GetCookiesAction( + &cookie_monster(), url_google_, &get_cookies_callback), + SetCookieAction( + &cookie_monster(), url_google_, "A=B", &set_cookies_callback), + DeleteCookieAction( + &cookie_monster(), url_google_, "A", &delete_cookie_callback))); + ExpectLoadCall(); + ExpectLoadForKeyCall("google.izzle", false); + Begin(); + + WaitForLoadCall(); + EXPECT_CALL(get_cookies_callback, Invoke("X=1")).WillOnce( + GetCookiesWithInfoAction( + &cookie_monster(), url_google_, &get_cookie_info_callback)); + EXPECT_CALL(get_cookie_info_callback, Invoke("X=1", testing::_)).WillOnce( + QuitCurrentMessageLoop()); + EXPECT_CALL(set_cookies_callback, Invoke(true)); + EXPECT_CALL(delete_cookie_callback, Invoke()); + + CompleteLoadingAndWait(); +} + +TEST_F(CookieMonsterTest, TestCookieDateParsing) { + const struct { + const char* str; + const bool valid; + const time_t epoch; + } tests[] = { + { "Sat, 15-Apr-17 21:01:22 GMT", true, 1492290082 }, + { "Thu, 19-Apr-2007 16:00:00 GMT", true, 1176998400 }, + { "Wed, 25 Apr 2007 21:02:13 GMT", true, 1177534933 }, + { "Thu, 19/Apr\\2007 16:00:00 GMT", true, 1176998400 }, + { "Fri, 1 Jan 2010 01:01:50 GMT", true, 1262307710 }, + { "Wednesday, 1-Jan-2003 00:00:00 GMT", true, 1041379200 }, + { ", 1-Jan-2003 00:00:00 GMT", true, 1041379200 }, + { " 1-Jan-2003 00:00:00 GMT", true, 1041379200 }, + { "1-Jan-2003 00:00:00 GMT", true, 1041379200 }, + { "Wed,18-Apr-07 22:50:12 GMT", true, 1176936612 }, + { "WillyWonka , 18-Apr-07 22:50:12 GMT", true, 1176936612 }, + { "WillyWonka , 18-Apr-07 22:50:12", true, 1176936612 }, + { "WillyWonka , 18-apr-07 22:50:12", true, 1176936612 }, + { "Mon, 18-Apr-1977 22:50:13 GMT", true, 230251813 }, + { "Mon, 18-Apr-77 22:50:13 GMT", true, 230251813 }, + // If the cookie came in with the expiration quoted (which in terms of + // the RFC you shouldn't do), we will get string quoted. Bug 1261605. + { "\"Sat, 15-Apr-17\\\"21:01:22\\\"GMT\"", true, 1492290082 }, + // Test with full month names and partial names. + { "Partyday, 18- April-07 22:50:12", true, 1176936612 }, + { "Partyday, 18 - Apri-07 22:50:12", true, 1176936612 }, + { "Wednes, 1-Januar-2003 00:00:00 GMT", true, 1041379200 }, + // Test that we always take GMT even with other time zones or bogus + // values. The RFC says everything should be GMT, and in the worst case + // we are 24 hours off because of zone issues. + { "Sat, 15-Apr-17 21:01:22", true, 1492290082 }, + { "Sat, 15-Apr-17 21:01:22 GMT-2", true, 1492290082 }, + { "Sat, 15-Apr-17 21:01:22 GMT BLAH", true, 1492290082 }, + { "Sat, 15-Apr-17 21:01:22 GMT-0400", true, 1492290082 }, + { "Sat, 15-Apr-17 21:01:22 GMT-0400 (EDT)",true, 1492290082 }, + { "Sat, 15-Apr-17 21:01:22 DST", true, 1492290082 }, + { "Sat, 15-Apr-17 21:01:22 -0400", true, 1492290082 }, + { "Sat, 15-Apr-17 21:01:22 (hello there)", true, 1492290082 }, + // Test that if we encounter multiple : fields, that we take the first + // that correctly parses. + { "Sat, 15-Apr-17 21:01:22 11:22:33", true, 1492290082 }, + { "Sat, 15-Apr-17 ::00 21:01:22", true, 1492290082 }, + { "Sat, 15-Apr-17 boink:z 21:01:22", true, 1492290082 }, + // We take the first, which in this case is invalid. + { "Sat, 15-Apr-17 91:22:33 21:01:22", false, 0 }, + // amazon.com formats their cookie expiration like this. + { "Thu Apr 18 22:50:12 2007 GMT", true, 1176936612 }, + // Test that hh:mm:ss can occur anywhere. + { "22:50:12 Thu Apr 18 2007 GMT", true, 1176936612 }, + { "Thu 22:50:12 Apr 18 2007 GMT", true, 1176936612 }, + { "Thu Apr 22:50:12 18 2007 GMT", true, 1176936612 }, + { "Thu Apr 18 22:50:12 2007 GMT", true, 1176936612 }, + { "Thu Apr 18 2007 22:50:12 GMT", true, 1176936612 }, + { "Thu Apr 18 2007 GMT 22:50:12", true, 1176936612 }, + // Test that the day and year can be anywhere if they are unambigious. + { "Sat, 15-Apr-17 21:01:22 GMT", true, 1492290082 }, + { "15-Sat, Apr-17 21:01:22 GMT", true, 1492290082 }, + { "15-Sat, Apr 21:01:22 GMT 17", true, 1492290082 }, + { "15-Sat, Apr 21:01:22 GMT 2017", true, 1492290082 }, + { "15 Apr 21:01:22 2017", true, 1492290082 }, + { "15 17 Apr 21:01:22", true, 1492290082 }, + { "Apr 15 17 21:01:22", true, 1492290082 }, + { "Apr 15 21:01:22 17", true, 1492290082 }, + { "2017 April 15 21:01:22", true, 1492290082 }, + { "15 April 2017 21:01:22", true, 1492290082 }, + // Some invalid dates + { "98 April 17 21:01:22", false, 0 }, + { "Thu, 012-Aug-2008 20:49:07 GMT", false, 0 }, + { "Thu, 12-Aug-31841 20:49:07 GMT", false, 0 }, + { "Thu, 12-Aug-9999999999 20:49:07 GMT", false, 0 }, + { "Thu, 999999999999-Aug-2007 20:49:07 GMT", false, 0 }, + { "Thu, 12-Aug-2007 20:61:99999999999 GMT", false, 0 }, + { "IAintNoDateFool", false, 0 }, + }; + + Time parsed_time; + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { + parsed_time = CookieMonster::ParseCookieTime(tests[i].str); + if (!tests[i].valid) { + EXPECT_FALSE(!parsed_time.is_null()) << tests[i].str; + continue; + } + EXPECT_TRUE(!parsed_time.is_null()) << tests[i].str; + EXPECT_EQ(tests[i].epoch, parsed_time.ToTimeT()) << tests[i].str; + } +} + +TEST_F(CookieMonsterTest, TestCookieDeleteAll) { + scoped_refptr<MockPersistentCookieStore> store( + new MockPersistentCookieStore); + scoped_refptr<CookieMonster> cm(new CookieMonster(store, NULL)); + CookieOptions options; + options.set_include_httponly(); + + EXPECT_TRUE(SetCookie(cm, url_google_, kValidCookieLine)); + EXPECT_EQ("A=B", GetCookies(cm, url_google_)); + + EXPECT_TRUE(SetCookieWithOptions(cm, url_google_, "C=D; httponly", options)); + EXPECT_EQ("A=B; C=D", GetCookiesWithOptions(cm, url_google_, options)); + + EXPECT_EQ(2, DeleteAll(cm)); + EXPECT_EQ("", GetCookiesWithOptions(cm, url_google_, options)); + + EXPECT_EQ(0u, store->commands().size()); + + // Create a persistent cookie. + EXPECT_TRUE(SetCookie(cm, url_google_, + std::string(kValidCookieLine) + + "; expires=Mon, 18-Apr-22 22:50:13 GMT")); + ASSERT_EQ(1u, store->commands().size()); + EXPECT_EQ(CookieStoreCommand::ADD, store->commands()[0].type); + + EXPECT_EQ(1, DeleteAll(cm)); // sync_to_store = true. + ASSERT_EQ(2u, store->commands().size()); + EXPECT_EQ(CookieStoreCommand::REMOVE, store->commands()[1].type); + + EXPECT_EQ("", GetCookiesWithOptions(cm, url_google_, options)); +} + +TEST_F(CookieMonsterTest, TestCookieDeleteAllCreatedBetweenTimestamps) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + Time now = Time::Now(); + + // Nothing has been added so nothing should be deleted. + EXPECT_EQ(0, DeleteAllCreatedBetween(cm, now - TimeDelta::FromDays(99), + Time())); + + // Create 3 cookies with creation date of today, yesterday and the day before. + EXPECT_TRUE(cm->SetCookieWithCreationTime(url_google_, "T-0=Now", now)); + EXPECT_TRUE(cm->SetCookieWithCreationTime(url_google_, "T-1=Yesterday", + now - TimeDelta::FromDays(1))); + EXPECT_TRUE(cm->SetCookieWithCreationTime(url_google_, "T-2=DayBefore", + now - TimeDelta::FromDays(2))); + EXPECT_TRUE(cm->SetCookieWithCreationTime(url_google_, "T-3=ThreeDays", + now - TimeDelta::FromDays(3))); + EXPECT_TRUE(cm->SetCookieWithCreationTime(url_google_, "T-7=LastWeek", + now - TimeDelta::FromDays(7))); + + // Try to delete threedays and the daybefore. + EXPECT_EQ(2, DeleteAllCreatedBetween(cm, now - TimeDelta::FromDays(3), + now - TimeDelta::FromDays(1))); + + // Try to delete yesterday, also make sure that delete_end is not + // inclusive. + EXPECT_EQ(1, DeleteAllCreatedBetween(cm, now - TimeDelta::FromDays(2), + now)); + + // Make sure the delete_begin is inclusive. + EXPECT_EQ(1, DeleteAllCreatedBetween(cm, now - TimeDelta::FromDays(7), + now)); + + // Delete the last (now) item. + EXPECT_EQ(1, DeleteAllCreatedBetween(cm, Time(), Time())); + + // Really make sure everything is gone. + EXPECT_EQ(0, DeleteAll(cm)); +} + +static const int kAccessDelayMs = kLastAccessThresholdMilliseconds + 20; + +TEST_F(CookieMonsterTest, TestLastAccess) { + scoped_refptr<CookieMonster> cm( + new CookieMonster(NULL, NULL, kLastAccessThresholdMilliseconds)); + + EXPECT_TRUE(SetCookie(cm, url_google_, "A=B")); + const Time last_access_date(GetFirstCookieAccessDate(cm)); + + // Reading the cookie again immediately shouldn't update the access date, + // since we're inside the threshold. + EXPECT_EQ("A=B", GetCookies(cm, url_google_)); + EXPECT_TRUE(last_access_date == GetFirstCookieAccessDate(cm)); + + // Reading after a short wait should update the access date. + base::PlatformThread::Sleep( + base::TimeDelta::FromMilliseconds(kAccessDelayMs)); + EXPECT_EQ("A=B", GetCookies(cm, url_google_)); + EXPECT_FALSE(last_access_date == GetFirstCookieAccessDate(cm)); +} + +TEST_F(CookieMonsterTest, TestHostGarbageCollection) { + TestHostGarbageCollectHelper( + CookieMonster::kDomainMaxCookies, CookieMonster::kDomainPurgeCookies); +} + +TEST_F(CookieMonsterTest, TestDeleteSingleCookie) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + + EXPECT_TRUE(SetCookie(cm, url_google_, "A=B")); + EXPECT_TRUE(SetCookie(cm, url_google_, "C=D")); + EXPECT_TRUE(SetCookie(cm, url_google_, "E=F")); + EXPECT_EQ("A=B; C=D; E=F", GetCookies(cm, url_google_)); + + EXPECT_TRUE(FindAndDeleteCookie(cm, url_google_.host(), "C")); + EXPECT_EQ("A=B; E=F", GetCookies(cm, url_google_)); + + EXPECT_FALSE(FindAndDeleteCookie(cm, "random.host", "E")); + EXPECT_EQ("A=B; E=F", GetCookies(cm, url_google_)); +} + +TEST_F(CookieMonsterTest, SetCookieableSchemes) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + scoped_refptr<CookieMonster> cm_foo(new CookieMonster(NULL, NULL)); + + // Only cm_foo should allow foo:// cookies. + const char* kSchemes[] = {"foo"}; + cm_foo->SetCookieableSchemes(kSchemes, 1); + + GURL foo_url("foo://host/path"); + GURL http_url("http://host/path"); + + EXPECT_TRUE(SetCookie(cm, http_url, "x=1")); + EXPECT_FALSE(SetCookie(cm, foo_url, "x=1")); + EXPECT_TRUE(SetCookie(cm_foo, foo_url, "x=1")); + EXPECT_FALSE(SetCookie(cm_foo, http_url, "x=1")); +} + +TEST_F(CookieMonsterTest, GetAllCookiesForURL) { + scoped_refptr<CookieMonster> cm( + new CookieMonster(NULL, NULL, kLastAccessThresholdMilliseconds)); + + // Create an httponly cookie. + CookieOptions options; + options.set_include_httponly(); + + EXPECT_TRUE(SetCookieWithOptions(cm, url_google_, "A=B; httponly", options)); + EXPECT_TRUE(SetCookieWithOptions(cm, url_google_, + "C=D; domain=.google.izzle", + options)); + EXPECT_TRUE(SetCookieWithOptions(cm, url_google_secure_, + "E=F; domain=.google.izzle; secure", + options)); + + const Time last_access_date(GetFirstCookieAccessDate(cm)); + + base::PlatformThread::Sleep( + base::TimeDelta::FromMilliseconds(kAccessDelayMs)); + + // Check cookies for url. + CookieList cookies = GetAllCookiesForURL(cm, url_google_); + CookieList::iterator it = cookies.begin(); + + ASSERT_TRUE(it != cookies.end()); + EXPECT_EQ("www.google.izzle", it->Domain()); + EXPECT_EQ("A", it->Name()); + + ASSERT_TRUE(++it != cookies.end()); + EXPECT_EQ(".google.izzle", it->Domain()); + EXPECT_EQ("C", it->Name()); + + ASSERT_TRUE(++it == cookies.end()); + + // Check cookies for url excluding http-only cookies. + cookies = + GetAllCookiesForURLWithOptions(cm, url_google_, CookieOptions()); + it = cookies.begin(); + + ASSERT_TRUE(it != cookies.end()); + EXPECT_EQ(".google.izzle", it->Domain()); + EXPECT_EQ("C", it->Name()); + + ASSERT_TRUE(++it == cookies.end()); + + // Test secure cookies. + cookies = GetAllCookiesForURL(cm, url_google_secure_); + it = cookies.begin(); + + ASSERT_TRUE(it != cookies.end()); + EXPECT_EQ("www.google.izzle", it->Domain()); + EXPECT_EQ("A", it->Name()); + + ASSERT_TRUE(++it != cookies.end()); + EXPECT_EQ(".google.izzle", it->Domain()); + EXPECT_EQ("C", it->Name()); + + ASSERT_TRUE(++it != cookies.end()); + EXPECT_EQ(".google.izzle", it->Domain()); + EXPECT_EQ("E", it->Name()); + + ASSERT_TRUE(++it == cookies.end()); + + // Reading after a short wait should not update the access date. + EXPECT_TRUE(last_access_date == GetFirstCookieAccessDate(cm)); +} + +TEST_F(CookieMonsterTest, GetAllCookiesForURLPathMatching) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + CookieOptions options; + + EXPECT_TRUE(SetCookieWithOptions(cm, url_google_foo_, + "A=B; path=/foo;", options)); + EXPECT_TRUE(SetCookieWithOptions(cm, url_google_bar_, + "C=D; path=/bar;", options)); + EXPECT_TRUE(SetCookieWithOptions(cm, url_google_, + "E=F;", options)); + + CookieList cookies = GetAllCookiesForURL(cm, url_google_foo_); + CookieList::iterator it = cookies.begin(); + + ASSERT_TRUE(it != cookies.end()); + EXPECT_EQ("A", it->Name()); + EXPECT_EQ("/foo", it->Path()); + + ASSERT_TRUE(++it != cookies.end()); + EXPECT_EQ("E", it->Name()); + EXPECT_EQ("/", it->Path()); + + ASSERT_TRUE(++it == cookies.end()); + + cookies = GetAllCookiesForURL(cm, url_google_bar_); + it = cookies.begin(); + + ASSERT_TRUE(it != cookies.end()); + EXPECT_EQ("C", it->Name()); + EXPECT_EQ("/bar", it->Path()); + + ASSERT_TRUE(++it != cookies.end()); + EXPECT_EQ("E", it->Name()); + EXPECT_EQ("/", it->Path()); + + ASSERT_TRUE(++it == cookies.end()); +} + +TEST_F(CookieMonsterTest, DeleteCookieByName) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + + EXPECT_TRUE(SetCookie(cm, url_google_, "A=A1; path=/")); + EXPECT_TRUE(SetCookie(cm, url_google_, "A=A2; path=/foo")); + EXPECT_TRUE(SetCookie(cm, url_google_, "A=A3; path=/bar")); + EXPECT_TRUE(SetCookie(cm, url_google_, "B=B1; path=/")); + EXPECT_TRUE(SetCookie(cm, url_google_, "B=B2; path=/foo")); + EXPECT_TRUE(SetCookie(cm, url_google_, "B=B3; path=/bar")); + + DeleteCookie(cm, GURL(std::string(kUrlGoogle) + "/foo/bar"), "A"); + + CookieList cookies = GetAllCookies(cm); + size_t expected_size = 4; + EXPECT_EQ(expected_size, cookies.size()); + for (CookieList::iterator it = cookies.begin(); + it != cookies.end(); ++it) { + EXPECT_NE("A1", it->Value()); + EXPECT_NE("A2", it->Value()); + } +} + +TEST_F(CookieMonsterTest, InitializeFromCookieMonster) { + scoped_refptr<CookieMonster> cm_1(new CookieMonster(NULL, NULL)); + CookieOptions options; + + EXPECT_TRUE(SetCookieWithOptions(cm_1.get(), url_google_foo_, + "A1=B; path=/foo;", + options)); + EXPECT_TRUE(SetCookieWithOptions(cm_1.get(), url_google_bar_, + "A2=D; path=/bar;", + options)); + EXPECT_TRUE(SetCookieWithOptions(cm_1.get(), url_google_, + "A3=F;", + options)); + + CookieList cookies_1 = GetAllCookies(cm_1); + scoped_refptr<CookieMonster> cm_2(new CookieMonster(NULL, NULL)); + ASSERT_TRUE(cm_2->InitializeFrom(cookies_1)); + CookieList cookies_2 = GetAllCookies(cm_2); + + size_t expected_size = 3; + EXPECT_EQ(expected_size, cookies_2.size()); + + CookieList::iterator it = cookies_2.begin(); + + ASSERT_TRUE(it != cookies_2.end()); + EXPECT_EQ("A1", it->Name()); + EXPECT_EQ("/foo", it->Path()); + + ASSERT_TRUE(++it != cookies_2.end()); + EXPECT_EQ("A2", it->Name()); + EXPECT_EQ("/bar", it->Path()); + + ASSERT_TRUE(++it != cookies_2.end()); + EXPECT_EQ("A3", it->Name()); + EXPECT_EQ("/", it->Path()); +} + +// Tests importing from a persistent cookie store that contains duplicate +// equivalent cookies. This situation should be handled by removing the +// duplicate cookie (both from the in-memory cache, and from the backing store). +// +// This is a regression test for: http://crbug.com/17855. +TEST_F(CookieMonsterTest, DontImportDuplicateCookies) { + scoped_refptr<MockPersistentCookieStore> store( + new MockPersistentCookieStore); + + // We will fill some initial cookies into the PersistentCookieStore, + // to simulate a database with 4 duplicates. Note that we need to + // be careful not to have any duplicate creation times at all (as it's a + // violation of a CookieMonster invariant) even if Time::Now() doesn't + // move between calls. + std::vector<CookieMonster::CanonicalCookie*> initial_cookies; + + // Insert 4 cookies with name "X" on path "/", with varying creation + // dates. We expect only the most recent one to be preserved following + // the import. + + AddCookieToList("www.google.com", + "X=1; path=/; expires=Mon, 18-Apr-22 22:50:14 GMT", + Time::Now() + TimeDelta::FromDays(3), + &initial_cookies); + + AddCookieToList("www.google.com", + "X=2; path=/; expires=Mon, 18-Apr-22 22:50:14 GMT", + Time::Now() + TimeDelta::FromDays(1), + &initial_cookies); + + // ===> This one is the WINNER (biggest creation time). <==== + AddCookieToList("www.google.com", + "X=3; path=/; expires=Mon, 18-Apr-22 22:50:14 GMT", + Time::Now() + TimeDelta::FromDays(4), + &initial_cookies); + + AddCookieToList("www.google.com", + "X=4; path=/; expires=Mon, 18-Apr-22 22:50:14 GMT", + Time::Now(), + &initial_cookies); + + // Insert 2 cookies with name "X" on path "/2", with varying creation + // dates. We expect only the most recent one to be preserved the import. + + // ===> This one is the WINNER (biggest creation time). <==== + AddCookieToList("www.google.com", + "X=a1; path=/2; expires=Mon, 18-Apr-22 22:50:14 GMT", + Time::Now() + TimeDelta::FromDays(9), + &initial_cookies); + + AddCookieToList("www.google.com", + "X=a2; path=/2; expires=Mon, 18-Apr-22 22:50:14 GMT", + Time::Now() + TimeDelta::FromDays(2), + &initial_cookies); + + // Insert 1 cookie with name "Y" on path "/". + AddCookieToList("www.google.com", + "Y=a; path=/; expires=Mon, 18-Apr-22 22:50:14 GMT", + Time::Now() + TimeDelta::FromDays(10), + &initial_cookies); + + // Inject our initial cookies into the mock PersistentCookieStore. + store->SetLoadExpectation(true, initial_cookies); + + scoped_refptr<CookieMonster> cm(new CookieMonster(store, NULL)); + + // Verify that duplicates were not imported for path "/". + // (If this had failed, GetCookies() would have also returned X=1, X=2, X=4). + EXPECT_EQ("X=3; Y=a", GetCookies(cm, GURL("http://www.google.com/"))); + + // Verify that same-named cookie on a different path ("/x2") didn't get + // messed up. + EXPECT_EQ("X=a1; X=3; Y=a", + GetCookies(cm, GURL("http://www.google.com/2/x"))); + + // Verify that the PersistentCookieStore was told to kill its 4 duplicates. + ASSERT_EQ(4u, store->commands().size()); + EXPECT_EQ(CookieStoreCommand::REMOVE, store->commands()[0].type); + EXPECT_EQ(CookieStoreCommand::REMOVE, store->commands()[1].type); + EXPECT_EQ(CookieStoreCommand::REMOVE, store->commands()[2].type); + EXPECT_EQ(CookieStoreCommand::REMOVE, store->commands()[3].type); +} + +// Tests importing from a persistent cookie store that contains cookies +// with duplicate creation times. This situation should be handled by +// dropping the cookies before insertion/visibility to user. +// +// This is a regression test for: http://crbug.com/43188. +TEST_F(CookieMonsterTest, DontImportDuplicateCreationTimes) { + scoped_refptr<MockPersistentCookieStore> store( + new MockPersistentCookieStore); + + Time now(Time::Now()); + Time earlier(now - TimeDelta::FromDays(1)); + + // Insert 8 cookies, four with the current time as creation times, and + // four with the earlier time as creation times. We should only get + // two cookies remaining, but which two (other than that there should + // be one from each set) will be random. + std::vector<CookieMonster::CanonicalCookie*> initial_cookies; + AddCookieToList("www.google.com", "X=1; path=/", now, &initial_cookies); + AddCookieToList("www.google.com", "X=2; path=/", now, &initial_cookies); + AddCookieToList("www.google.com", "X=3; path=/", now, &initial_cookies); + AddCookieToList("www.google.com", "X=4; path=/", now, &initial_cookies); + + AddCookieToList("www.google.com", "Y=1; path=/", earlier, &initial_cookies); + AddCookieToList("www.google.com", "Y=2; path=/", earlier, &initial_cookies); + AddCookieToList("www.google.com", "Y=3; path=/", earlier, &initial_cookies); + AddCookieToList("www.google.com", "Y=4; path=/", earlier, &initial_cookies); + + // Inject our initial cookies into the mock PersistentCookieStore. + store->SetLoadExpectation(true, initial_cookies); + + scoped_refptr<CookieMonster> cm(new CookieMonster(store, NULL)); + + CookieList list(GetAllCookies(cm)); + EXPECT_EQ(2U, list.size()); + // Confirm that we have one of each. + std::string name1(list[0].Name()); + std::string name2(list[1].Name()); + EXPECT_TRUE(name1 == "X" || name2 == "X"); + EXPECT_TRUE(name1 == "Y" || name2 == "Y"); + EXPECT_NE(name1, name2); +} + +TEST_F(CookieMonsterTest, Delegate) { + scoped_refptr<MockPersistentCookieStore> store( + new MockPersistentCookieStore); + scoped_refptr<MockCookieMonsterDelegate> delegate( + new MockCookieMonsterDelegate); + scoped_refptr<CookieMonster> cm(new CookieMonster(store, delegate)); + + EXPECT_TRUE(SetCookie(cm, url_google_, "A=B")); + EXPECT_TRUE(SetCookie(cm, url_google_, "C=D")); + EXPECT_TRUE(SetCookie(cm, url_google_, "E=F")); + EXPECT_EQ("A=B; C=D; E=F", GetCookies(cm, url_google_)); + ASSERT_EQ(3u, delegate->changes().size()); + EXPECT_FALSE(delegate->changes()[0].second); + EXPECT_EQ(url_google_.host(), delegate->changes()[0].first.Domain()); + EXPECT_EQ("A", delegate->changes()[0].first.Name()); + EXPECT_EQ("B", delegate->changes()[0].first.Value()); + EXPECT_EQ(url_google_.host(), delegate->changes()[1].first.Domain()); + EXPECT_FALSE(delegate->changes()[1].second); + EXPECT_EQ("C", delegate->changes()[1].first.Name()); + EXPECT_EQ("D", delegate->changes()[1].first.Value()); + EXPECT_EQ(url_google_.host(), delegate->changes()[2].first.Domain()); + EXPECT_FALSE(delegate->changes()[2].second); + EXPECT_EQ("E", delegate->changes()[2].first.Name()); + EXPECT_EQ("F", delegate->changes()[2].first.Value()); + delegate->reset(); + + EXPECT_TRUE(FindAndDeleteCookie(cm, url_google_.host(), "C")); + EXPECT_EQ("A=B; E=F", GetCookies(cm, url_google_)); + ASSERT_EQ(1u, delegate->changes().size()); + EXPECT_EQ(url_google_.host(), delegate->changes()[0].first.Domain()); + EXPECT_TRUE(delegate->changes()[0].second); + EXPECT_EQ("C", delegate->changes()[0].first.Name()); + EXPECT_EQ("D", delegate->changes()[0].first.Value()); + delegate->reset(); + + EXPECT_FALSE(FindAndDeleteCookie(cm, "random.host", "E")); + EXPECT_EQ("A=B; E=F", GetCookies(cm, url_google_)); + EXPECT_EQ(0u, delegate->changes().size()); + + // Insert a cookie "a" for path "/path1" + EXPECT_TRUE( + SetCookie(cm, url_google_, "a=val1; path=/path1; " + "expires=Mon, 18-Apr-22 22:50:13 GMT")); + ASSERT_EQ(1u, store->commands().size()); + EXPECT_EQ(CookieStoreCommand::ADD, store->commands()[0].type); + ASSERT_EQ(1u, delegate->changes().size()); + EXPECT_FALSE(delegate->changes()[0].second); + EXPECT_EQ(url_google_.host(), delegate->changes()[0].first.Domain()); + EXPECT_EQ("a", delegate->changes()[0].first.Name()); + EXPECT_EQ("val1", delegate->changes()[0].first.Value()); + delegate->reset(); + + // Insert a cookie "a" for path "/path1", that is httponly. This should + // overwrite the non-http-only version. + CookieOptions allow_httponly; + allow_httponly.set_include_httponly(); + EXPECT_TRUE( + SetCookieWithOptions(cm, url_google_, + "a=val2; path=/path1; httponly; " + "expires=Mon, 18-Apr-22 22:50:14 GMT", + allow_httponly)); + ASSERT_EQ(3u, store->commands().size()); + EXPECT_EQ(CookieStoreCommand::REMOVE, store->commands()[1].type); + EXPECT_EQ(CookieStoreCommand::ADD, store->commands()[2].type); + ASSERT_EQ(2u, delegate->changes().size()); + EXPECT_EQ(url_google_.host(), delegate->changes()[0].first.Domain()); + EXPECT_TRUE(delegate->changes()[0].second); + EXPECT_EQ("a", delegate->changes()[0].first.Name()); + EXPECT_EQ("val1", delegate->changes()[0].first.Value()); + EXPECT_EQ(url_google_.host(), delegate->changes()[1].first.Domain()); + EXPECT_FALSE(delegate->changes()[1].second); + EXPECT_EQ("a", delegate->changes()[1].first.Name()); + EXPECT_EQ("val2", delegate->changes()[1].first.Value()); + delegate->reset(); +} + +TEST_F(CookieMonsterTest, SetCookieWithDetails) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + + EXPECT_TRUE(SetCookieWithDetails( + cm, url_google_foo_, "A", "B", std::string(), "/foo", base::Time(), + false, false)); + EXPECT_TRUE(SetCookieWithDetails( + cm, url_google_bar_, "C", "D", "google.izzle", "/bar", base::Time(), + false, true)); + EXPECT_TRUE(SetCookieWithDetails( + cm, url_google_, "E", "F", std::string(), std::string(), base::Time(), + true, false)); + + // Test that malformed attributes fail to set the cookie. + EXPECT_FALSE(SetCookieWithDetails( + cm, url_google_foo_, " A", "B", std::string(), "/foo", base::Time(), + false, false)); + EXPECT_FALSE(SetCookieWithDetails( + cm, url_google_foo_, "A;", "B", std::string(), "/foo", base::Time(), + false, false)); + EXPECT_FALSE(SetCookieWithDetails( + cm, url_google_foo_, "A=", "B", std::string(), "/foo", base::Time(), + false, false)); + EXPECT_FALSE(SetCookieWithDetails( + cm, url_google_foo_, "A", "B", "google.ozzzzzzle", "foo", base::Time(), + false, false)); + EXPECT_FALSE(SetCookieWithDetails( + cm, url_google_foo_, "A=", "B", std::string(), "foo", base::Time(), + false, false)); + + CookieList cookies = GetAllCookiesForURL(cm, url_google_foo_); + CookieList::iterator it = cookies.begin(); + + ASSERT_TRUE(it != cookies.end()); + EXPECT_EQ("A", it->Name()); + EXPECT_EQ("B", it->Value()); + EXPECT_EQ("www.google.izzle", it->Domain()); + EXPECT_EQ("/foo", it->Path()); + EXPECT_FALSE(it->DoesExpire()); + EXPECT_FALSE(it->IsSecure()); + EXPECT_FALSE(it->IsHttpOnly()); + + ASSERT_TRUE(++it == cookies.end()); + + cookies = GetAllCookiesForURL(cm, url_google_bar_); + it = cookies.begin(); + + ASSERT_TRUE(it != cookies.end()); + EXPECT_EQ("C", it->Name()); + EXPECT_EQ("D", it->Value()); + EXPECT_EQ(".google.izzle", it->Domain()); + EXPECT_EQ("/bar", it->Path()); + EXPECT_FALSE(it->IsSecure()); + EXPECT_TRUE(it->IsHttpOnly()); + + ASSERT_TRUE(++it == cookies.end()); + + cookies = GetAllCookiesForURL(cm, url_google_secure_); + it = cookies.begin(); + + ASSERT_TRUE(it != cookies.end()); + EXPECT_EQ("E", it->Name()); + EXPECT_EQ("F", it->Value()); + EXPECT_EQ("/", it->Path()); + EXPECT_EQ("www.google.izzle", it->Domain()); + EXPECT_TRUE(it->IsSecure()); + EXPECT_FALSE(it->IsHttpOnly()); + + ASSERT_TRUE(++it == cookies.end()); +} + +TEST_F(CookieMonsterTest, DeleteAllForHost) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + + // Test probes: + // * Non-secure URL, mid-level (http://w.c.b.a) + // * Secure URL, mid-level (https://w.c.b.a) + // * URL with path, mid-level (https:/w.c.b.a/dir1/xx) + // All three tests should nuke only the midlevel host cookie, + // the http_only cookie, the host secure cookie, and the two host + // path cookies. http_only, secure, and paths are ignored by + // this call, and domain cookies arent touched. + PopulateCmForDeleteAllForHost(cm); + EXPECT_EQ("dom_1=X; dom_2=X; dom_3=X; host_3=X", + GetCookies(cm, GURL(kTopLevelDomainPlus3))); + EXPECT_EQ("dom_1=X; dom_2=X; host_2=X; sec_dom=X; sec_host=X", + GetCookies(cm, GURL(kTopLevelDomainPlus2Secure))); + EXPECT_EQ("dom_1=X; host_1=X", GetCookies(cm, GURL(kTopLevelDomainPlus1))); + EXPECT_EQ("dom_path_2=X; host_path_2=X; dom_path_1=X; host_path_1=X; " + "dom_1=X; dom_2=X; host_2=X; sec_dom=X; sec_host=X", + GetCookies(cm, GURL(kTopLevelDomainPlus2Secure + + std::string("/dir1/dir2/xxx")))); + + EXPECT_EQ(5, DeleteAllForHost(cm, GURL(kTopLevelDomainPlus2))); + EXPECT_EQ(8U, GetAllCookies(cm).size()); + + EXPECT_EQ("dom_1=X; dom_2=X; dom_3=X; host_3=X", + GetCookies(cm, GURL(kTopLevelDomainPlus3))); + EXPECT_EQ("dom_1=X; dom_2=X; sec_dom=X", + GetCookies(cm, GURL(kTopLevelDomainPlus2Secure))); + EXPECT_EQ("dom_1=X; host_1=X", GetCookies(cm, GURL(kTopLevelDomainPlus1))); + EXPECT_EQ("dom_path_2=X; dom_path_1=X; dom_1=X; dom_2=X; sec_dom=X", + GetCookies(cm, GURL(kTopLevelDomainPlus2Secure + + std::string("/dir1/dir2/xxx")))); + + PopulateCmForDeleteAllForHost(cm); + EXPECT_EQ(5, DeleteAllForHost(cm, GURL(kTopLevelDomainPlus2Secure))); + EXPECT_EQ(8U, GetAllCookies(cm).size()); + + EXPECT_EQ("dom_1=X; dom_2=X; dom_3=X; host_3=X", + GetCookies(cm, GURL(kTopLevelDomainPlus3))); + EXPECT_EQ("dom_1=X; dom_2=X; sec_dom=X", + GetCookies(cm, GURL(kTopLevelDomainPlus2Secure))); + EXPECT_EQ("dom_1=X; host_1=X", GetCookies(cm, GURL(kTopLevelDomainPlus1))); + EXPECT_EQ("dom_path_2=X; dom_path_1=X; dom_1=X; dom_2=X; sec_dom=X", + GetCookies(cm, GURL(kTopLevelDomainPlus2Secure + + std::string("/dir1/dir2/xxx")))); + + PopulateCmForDeleteAllForHost(cm); + EXPECT_EQ(5, DeleteAllForHost(cm, GURL(kTopLevelDomainPlus2Secure + + std::string("/dir1/xxx")))); + EXPECT_EQ(8U, GetAllCookies(cm).size()); + + EXPECT_EQ("dom_1=X; dom_2=X; dom_3=X; host_3=X", + GetCookies(cm, GURL(kTopLevelDomainPlus3))); + EXPECT_EQ("dom_1=X; dom_2=X; sec_dom=X", + GetCookies(cm, GURL(kTopLevelDomainPlus2Secure))); + EXPECT_EQ("dom_1=X; host_1=X", GetCookies(cm, GURL(kTopLevelDomainPlus1))); + EXPECT_EQ("dom_path_2=X; dom_path_1=X; dom_1=X; dom_2=X; sec_dom=X", + GetCookies(cm, GURL(kTopLevelDomainPlus2Secure + + std::string("/dir1/dir2/xxx")))); +} + +TEST_F(CookieMonsterTest, UniqueCreationTime) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + CookieOptions options; + + // Add in three cookies through every public interface to the + // CookieMonster and confirm that none of them have duplicate + // creation times. + + // SetCookieWithCreationTime and SetCookieWithCreationTimeAndOptions + // are not included as they aren't going to be public for very much + // longer. + + // SetCookie, SetCookieWithOptions, SetCookieWithDetails + + SetCookie(cm, url_google_, "SetCookie1=A"); + SetCookie(cm, url_google_, "SetCookie2=A"); + SetCookie(cm, url_google_, "SetCookie3=A"); + + SetCookieWithOptions(cm, url_google_, "setCookieWithOptions1=A", options); + SetCookieWithOptions(cm, url_google_, "setCookieWithOptions2=A", options); + SetCookieWithOptions(cm, url_google_, "setCookieWithOptions3=A", options); + + SetCookieWithDetails(cm, url_google_, "setCookieWithDetails1", "A", + ".google.com", "/", Time(), false, false); + SetCookieWithDetails(cm, url_google_, "setCookieWithDetails2", "A", + ".google.com", "/", Time(), false, false); + SetCookieWithDetails(cm, url_google_, "setCookieWithDetails3", "A", + ".google.com", "/", Time(), false, false); + + // Now we check + CookieList cookie_list(GetAllCookies(cm)); + typedef std::map<int64, CookieMonster::CanonicalCookie> TimeCookieMap; + TimeCookieMap check_map; + for (CookieList::const_iterator it = cookie_list.begin(); + it != cookie_list.end(); it++) { + const int64 creation_date = it->CreationDate().ToInternalValue(); + TimeCookieMap::const_iterator + existing_cookie_it(check_map.find(creation_date)); + EXPECT_TRUE(existing_cookie_it == check_map.end()) + << "Cookie " << it->Name() << " has same creation date (" + << it->CreationDate().ToInternalValue() + << ") as previously entered cookie " + << existing_cookie_it->second.Name(); + + if (existing_cookie_it == check_map.end()) { + check_map.insert(TimeCookieMap::value_type( + it->CreationDate().ToInternalValue(), *it)); + } + } +} + +// Mainly a test of GetEffectiveDomain, or more specifically, of the +// expected behavior of GetEffectiveDomain within the CookieMonster. +TEST_F(CookieMonsterTest, GetKey) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + + // This test is really only interesting if GetKey() actually does something. + EXPECT_EQ("google.com", cm->GetKey("www.google.com")); + EXPECT_EQ("google.izzie", cm->GetKey("www.google.izzie")); + EXPECT_EQ("google.izzie", cm->GetKey(".google.izzie")); + EXPECT_EQ("bbc.co.uk", cm->GetKey("bbc.co.uk")); + EXPECT_EQ("bbc.co.uk", cm->GetKey("a.b.c.d.bbc.co.uk")); + EXPECT_EQ("apple.com", cm->GetKey("a.b.c.d.apple.com")); + EXPECT_EQ("apple.izzie", cm->GetKey("a.b.c.d.apple.izzie")); + + // Cases where the effective domain is null, so we use the host + // as the key. + EXPECT_EQ("co.uk", cm->GetKey("co.uk")); + const std::string extension_name("iehocdgbbocmkdidlbnnfbmbinnahbae"); + EXPECT_EQ(extension_name, cm->GetKey(extension_name)); + EXPECT_EQ("com", cm->GetKey("com")); + EXPECT_EQ("hostalias", cm->GetKey("hostalias")); + EXPECT_EQ("localhost", cm->GetKey("localhost")); +} + +// Test that cookies transfer from/to the backing store correctly. +TEST_F(CookieMonsterTest, BackingStoreCommunication) { + // Store details for cookies transforming through the backing store interface. + + base::Time current(base::Time::Now()); + scoped_refptr<MockSimplePersistentCookieStore> store( + new MockSimplePersistentCookieStore); + base::Time new_access_time; + base::Time expires(base::Time::Now() + base::TimeDelta::FromSeconds(100)); + + struct CookiesInputInfo { + std::string gurl; + std::string name; + std::string value; + std::string domain; + std::string path; + base::Time expires; + bool secure; + bool http_only; + }; + const CookiesInputInfo input_info[] = { + {"http://a.b.google.com", "a", "1", "", "/path/to/cookie", expires, + false, false}, + {"https://www.google.com", "b", "2", ".google.com", "/path/from/cookie", + expires + TimeDelta::FromSeconds(10), true, true}, + {"https://google.com", "c", "3", "", "/another/path/to/cookie", + base::Time::Now() + base::TimeDelta::FromSeconds(100), + true, false} + }; + const int INPUT_DELETE = 1; + + // Create new cookies and flush them to the store. + { + scoped_refptr<CookieMonster> cmout(new CookieMonster(store, NULL)); + for (const CookiesInputInfo* p = input_info; + p < &input_info[ARRAYSIZE_UNSAFE(input_info)]; p++) { + EXPECT_TRUE(SetCookieWithDetails(cmout, GURL(p->gurl), p->name, p->value, + p->domain, p->path, p->expires, + p->secure, p->http_only)); + } + DeleteCookie(cmout, GURL(std::string(input_info[INPUT_DELETE].gurl) + + input_info[INPUT_DELETE].path), + input_info[INPUT_DELETE].name); + } + + // Create a new cookie monster and make sure that everything is correct + { + scoped_refptr<CookieMonster> cmin(new CookieMonster(store, NULL)); + CookieList cookies(GetAllCookies(cmin)); + ASSERT_EQ(2u, cookies.size()); + // Ordering is path length, then creation time. So second cookie + // will come first, and we need to swap them. + std::swap(cookies[0], cookies[1]); + for (int output_index = 0; output_index < 2; output_index++) { + int input_index = output_index * 2; + const CookiesInputInfo* input = &input_info[input_index]; + const CookieMonster::CanonicalCookie* output = &cookies[output_index]; + + EXPECT_EQ(input->name, output->Name()); + EXPECT_EQ(input->value, output->Value()); + EXPECT_EQ(GURL(input->gurl).host(), output->Domain()); + EXPECT_EQ(input->path, output->Path()); + EXPECT_LE(current.ToInternalValue(), + output->CreationDate().ToInternalValue()); + EXPECT_EQ(input->secure, output->IsSecure()); + EXPECT_EQ(input->http_only, output->IsHttpOnly()); + EXPECT_TRUE(output->IsPersistent()); + EXPECT_EQ(input->expires.ToInternalValue(), + output->ExpiryDate().ToInternalValue()); + } + } +} + +TEST_F(CookieMonsterTest, CookieListOrdering) { + // Put a random set of cookies into a monster and make sure + // they're returned in the right order. + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + EXPECT_TRUE(SetCookie(cm, GURL("http://d.c.b.a.google.com/aa/x.html"), + "c=1")); + EXPECT_TRUE(SetCookie(cm, GURL("http://b.a.google.com/aa/bb/cc/x.html"), + "d=1; domain=b.a.google.com")); + EXPECT_TRUE(SetCookie(cm, GURL("http://b.a.google.com/aa/bb/cc/x.html"), + "a=4; domain=b.a.google.com")); + EXPECT_TRUE(SetCookie(cm, GURL("http://c.b.a.google.com/aa/bb/cc/x.html"), + "e=1; domain=c.b.a.google.com")); + EXPECT_TRUE(SetCookie(cm, GURL("http://d.c.b.a.google.com/aa/bb/x.html"), + "b=1")); + EXPECT_TRUE(SetCookie(cm, GURL("http://news.bbc.co.uk/midpath/x.html"), + "g=10")); + { + unsigned int i = 0; + CookieList cookies(GetAllCookiesForURL( + cm, GURL("http://d.c.b.a.google.com/aa/bb/cc/dd"))); + ASSERT_EQ(5u, cookies.size()); + EXPECT_EQ("d", cookies[i++].Name()); + EXPECT_EQ("a", cookies[i++].Name()); + EXPECT_EQ("e", cookies[i++].Name()); + EXPECT_EQ("b", cookies[i++].Name()); + EXPECT_EQ("c", cookies[i++].Name()); + } + + { + unsigned int i = 0; + CookieList cookies(GetAllCookies(cm)); + ASSERT_EQ(6u, cookies.size()); + EXPECT_EQ("d", cookies[i++].Name()); + EXPECT_EQ("a", cookies[i++].Name()); + EXPECT_EQ("e", cookies[i++].Name()); + EXPECT_EQ("g", cookies[i++].Name()); + EXPECT_EQ("b", cookies[i++].Name()); + EXPECT_EQ("c", cookies[i++].Name()); + } +} + +// This test and CookieMonstertest.TestGCTimes (in cookie_monster_perftest.cc) +// are somewhat complementary twins. This test is probing for whether +// garbage collection always happens when it should (i.e. that we actually +// get rid of cookies when we should). The perftest is probing for +// whether garbage collection happens when it shouldn't. See comments +// before that test for more details. +TEST_F(CookieMonsterTest, GarbageCollectionTriggers) { + // First we check to make sure that a whole lot of recent cookies + // doesn't get rid of anything after garbage collection is checked for. + { + scoped_refptr<CookieMonster> cm( + CreateMonsterForGC(CookieMonster::kMaxCookies * 2)); + EXPECT_EQ(CookieMonster::kMaxCookies * 2, GetAllCookies(cm).size()); + SetCookie(cm, GURL("http://newdomain.com"), "b=2"); + EXPECT_EQ(CookieMonster::kMaxCookies * 2 + 1, GetAllCookies(cm).size()); + } + + // Now we explore a series of relationships between cookie last access + // time and size of store to make sure we only get rid of cookies when + // we really should. + const struct TestCase { + int num_cookies; + int num_old_cookies; + int expected_initial_cookies; + // Indexed by ExpiryAndKeyScheme + int expected_cookies_after_set; + } test_cases[] = { + { + // A whole lot of recent cookies; gc shouldn't happen. + CookieMonster::kMaxCookies * 2, + 0, + CookieMonster::kMaxCookies * 2, + CookieMonster::kMaxCookies * 2 + 1 + }, { + // Some old cookies, but still overflowing max. + CookieMonster::kMaxCookies * 2, + CookieMonster::kMaxCookies / 2, + CookieMonster::kMaxCookies * 2, + CookieMonster::kMaxCookies * 2 - CookieMonster::kMaxCookies / 2 + 1 + }, { + // Old cookies enough to bring us right down to our purge line. + CookieMonster::kMaxCookies * 2, + CookieMonster::kMaxCookies + CookieMonster::kPurgeCookies + 1, + CookieMonster::kMaxCookies * 2, + CookieMonster::kMaxCookies - CookieMonster::kPurgeCookies + }, { + // Old cookies enough to bring below our purge line (which we + // shouldn't do). + CookieMonster::kMaxCookies * 2, + CookieMonster::kMaxCookies * 3 / 2, + CookieMonster::kMaxCookies * 2, + CookieMonster::kMaxCookies - CookieMonster::kPurgeCookies + } + }; + + for (int ci = 0; ci < static_cast<int>(ARRAYSIZE_UNSAFE(test_cases)); ++ci) { + const TestCase *test_case = &test_cases[ci]; + scoped_refptr<CookieMonster> cm( + CreateMonsterFromStoreForGC( + test_case->num_cookies, test_case->num_old_cookies, + CookieMonster::kSafeFromGlobalPurgeDays * 2)); + EXPECT_EQ(test_case->expected_initial_cookies, + static_cast<int>(GetAllCookies(cm).size())) + << "For test case " << ci; + // Will trigger GC + SetCookie(cm, GURL("http://newdomain.com"), "b=2"); + EXPECT_EQ(test_case->expected_cookies_after_set, + static_cast<int>((GetAllCookies(cm).size()))) + << "For test case " << ci; + } +} + +// This test checks that setting a cookie forcing it to be a session only +// cookie works as expected. +TEST_F(CookieMonsterTest, ForceSessionOnly) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + CookieOptions options; + + // Set a persistent cookie, but force it to be a session cookie. + options.set_force_session(); + ASSERT_TRUE(SetCookieWithOptions( + cm, url_google_, + std::string(kValidCookieLine) + "; expires=Mon, 18-Apr-22 22:50:13 GMT", + options)); + + // Get the canonical cookie. + CookieList cookie_list = GetAllCookies(cm); + ASSERT_EQ(1U, cookie_list.size()); + ASSERT_FALSE(cookie_list[0].IsPersistent()); + + // Use a past expiry date to delete the cookie, but force it to session only. + ASSERT_TRUE(SetCookieWithOptions( + cm, url_google_, + std::string(kValidCookieLine) + "; expires=Mon, 18-Apr-1977 22:50:13 GMT", + options)); + + // Check that the cookie was deleted. + cookie_list = GetAllCookies(cm); + ASSERT_EQ(0U, cookie_list.size()); +} + +// This test checks that keep expired cookies flag is working. +TEST_F(CookieMonsterTest, KeepExpiredCookies) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + cm->SetKeepExpiredCookies(); + CookieOptions options; + + // Set a persistent cookie. + ASSERT_TRUE(SetCookieWithOptions( + cm, url_google_, + std::string(kValidCookieLine) + "; expires=Mon, 18-Apr-22 22:50:13 GMT", + options)); + + // Get the canonical cookie. + CookieList cookie_list = GetAllCookies(cm); + ASSERT_EQ(1U, cookie_list.size()); + + // Use a past expiry date to delete the cookie. + ASSERT_TRUE(SetCookieWithOptions( + cm, url_google_, + std::string(kValidCookieLine) + "; expires=Mon, 18-Apr-1977 22:50:13 GMT", + options)); + + // Check that the cookie with the past expiry date is still there. + // GetAllCookies() also triggers garbage collection. + cookie_list = GetAllCookies(cm); + ASSERT_EQ(1U, cookie_list.size()); + ASSERT_TRUE(cookie_list[0].IsExpired(Time::Now())); +} + +namespace { + +// Mock PersistentCookieStore that keeps track of the number of Flush() calls. +class FlushablePersistentStore : public CookieMonster::PersistentCookieStore { + public: + FlushablePersistentStore() : flush_count_(0) {} + + void Load(const LoadedCallback& loaded_callback) { + std::vector<CookieMonster::CanonicalCookie*> out_cookies; + MessageLoop::current()->PostTask(FROM_HERE, + base::Bind(&net::LoadedCallbackTask::Run, + new net::LoadedCallbackTask(loaded_callback, out_cookies))); + } + + void LoadCookiesForKey(const std::string& key, + const LoadedCallback& loaded_callback) { + Load(loaded_callback); + } + + void AddCookie(const CookieMonster::CanonicalCookie&) {} + void UpdateCookieAccessTime(const CookieMonster::CanonicalCookie&) {} + void DeleteCookie(const CookieMonster::CanonicalCookie&) {} + void SetClearLocalStateOnExit(bool clear_local_state) {} + + void Flush(const base::Closure& callback) { + ++flush_count_; + if (!callback.is_null()) + callback.Run(); + } + + int flush_count() { + return flush_count_; + } + + private: + volatile int flush_count_; +}; + +// Counts the number of times Callback() has been run. +class CallbackCounter : public base::RefCountedThreadSafe<CallbackCounter> { + public: + CallbackCounter() : callback_count_(0) {} + + void Callback() { + ++callback_count_; + } + + int callback_count() { + return callback_count_; + } + + private: + friend class base::RefCountedThreadSafe<CallbackCounter>; + volatile int callback_count_; +}; + +} // namespace + +// Test that FlushStore() is forwarded to the store and callbacks are posted. +TEST_F(CookieMonsterTest, FlushStore) { + scoped_refptr<CallbackCounter> counter(new CallbackCounter()); + scoped_refptr<FlushablePersistentStore> store(new FlushablePersistentStore()); + scoped_refptr<CookieMonster> cm(new CookieMonster(store, NULL)); + + ASSERT_EQ(0, store->flush_count()); + ASSERT_EQ(0, counter->callback_count()); + + // Before initialization, FlushStore() should just run the callback. + cm->FlushStore(base::Bind(&CallbackCounter::Callback, counter.get())); + MessageLoop::current()->RunAllPending(); + + ASSERT_EQ(0, store->flush_count()); + ASSERT_EQ(1, counter->callback_count()); + + // NULL callback is safe. + cm->FlushStore(base::Closure()); + MessageLoop::current()->RunAllPending(); + + ASSERT_EQ(0, store->flush_count()); + ASSERT_EQ(1, counter->callback_count()); + + // After initialization, FlushStore() should delegate to the store. + GetAllCookies(cm); // Force init. + cm->FlushStore(base::Bind(&CallbackCounter::Callback, counter.get())); + MessageLoop::current()->RunAllPending(); + + ASSERT_EQ(1, store->flush_count()); + ASSERT_EQ(2, counter->callback_count()); + + // NULL callback is still safe. + cm->FlushStore(base::Closure()); + MessageLoop::current()->RunAllPending(); + + ASSERT_EQ(2, store->flush_count()); + ASSERT_EQ(2, counter->callback_count()); + + // If there's no backing store, FlushStore() is always a safe no-op. + cm = new CookieMonster(NULL, NULL); + GetAllCookies(cm); // Force init. + cm->FlushStore(base::Closure()); + MessageLoop::current()->RunAllPending(); + + ASSERT_EQ(2, counter->callback_count()); + + cm->FlushStore(base::Bind(&CallbackCounter::Callback, counter.get())); + MessageLoop::current()->RunAllPending(); + + ASSERT_EQ(3, counter->callback_count()); +} + +TEST_F(CookieMonsterTest, GetCookieSourceFromURL) { + EXPECT_EQ("http://example.com/", + CookieMonster::CanonicalCookie::GetCookieSourceFromURL( + GURL("http://example.com"))); + EXPECT_EQ("http://example.com/", + CookieMonster::CanonicalCookie::GetCookieSourceFromURL( + GURL("http://example.com/"))); + EXPECT_EQ("http://example.com/", + CookieMonster::CanonicalCookie::GetCookieSourceFromURL( + GURL("http://example.com/test"))); + EXPECT_EQ("file:///tmp/test.html", + CookieMonster::CanonicalCookie::GetCookieSourceFromURL( + GURL("file:///tmp/test.html"))); + EXPECT_EQ("http://example.com/", + CookieMonster::CanonicalCookie::GetCookieSourceFromURL( + GURL("http://example.com:1234/"))); + EXPECT_EQ("http://example.com/", + CookieMonster::CanonicalCookie::GetCookieSourceFromURL( + GURL("https://example.com/"))); + EXPECT_EQ("http://example.com/", + CookieMonster::CanonicalCookie::GetCookieSourceFromURL( + GURL("http://user:pwd@example.com/"))); + EXPECT_EQ("http://example.com/", + CookieMonster::CanonicalCookie::GetCookieSourceFromURL( + GURL("http://example.com/test?foo"))); + EXPECT_EQ("http://example.com/", + CookieMonster::CanonicalCookie::GetCookieSourceFromURL( + GURL("http://example.com/test#foo"))); +} + +TEST_F(CookieMonsterTest, HistogramCheck) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + // Should match call in InitializeHistograms, but doesn't really matter + // since the histogram should have been initialized by the CM construction + // above. + base::Histogram* expired_histogram = + base::Histogram::FactoryGet( + "Cookie.ExpirationDurationMinutes", 1, 10 * 365 * 24 * 60, 50, + base::Histogram::kUmaTargetedHistogramFlag); + + base::Histogram::SampleSet histogram_set_1; + expired_histogram->SnapshotSample(&histogram_set_1); + ASSERT_TRUE(SetCookieWithDetails( + cm, GURL("http://fake.a.url"), "a", "b", "a.url", "/", + base::Time::Now() + base::TimeDelta::FromMinutes(59), + false, false)); + + base::Histogram::SampleSet histogram_set_2; + expired_histogram->SnapshotSample(&histogram_set_2); + EXPECT_EQ(histogram_set_1.TotalCount() + 1, + histogram_set_2.TotalCount()); + + // kValidCookieLine creates a session cookie. + ASSERT_TRUE(SetCookie(cm, url_google_, kValidCookieLine)); + expired_histogram->SnapshotSample(&histogram_set_1); + EXPECT_EQ(histogram_set_2.TotalCount(), + histogram_set_1.TotalCount()); +} + +namespace { + +class MultiThreadedCookieMonsterTest : public CookieMonsterTest { + public: + MultiThreadedCookieMonsterTest() : other_thread_("CMTthread") {} + + // Helper methods for calling the asynchronous CookieMonster methods + // from a different thread. + + void GetAllCookiesTask(CookieMonster* cm, + GetCookieListCallback* callback) { + cm->GetAllCookiesAsync( + base::Bind(&GetCookieListCallback::Run, base::Unretained(callback))); + } + + void GetAllCookiesForURLTask(CookieMonster* cm, + const GURL& url, + GetCookieListCallback* callback) { + cm->GetAllCookiesForURLAsync( + url, + base::Bind(&GetCookieListCallback::Run, base::Unretained(callback))); + } + + void GetAllCookiesForURLWithOptionsTask(CookieMonster* cm, + const GURL& url, + const CookieOptions& options, + GetCookieListCallback* callback) { + cm->GetAllCookiesForURLWithOptionsAsync( + url, options, + base::Bind(&GetCookieListCallback::Run, base::Unretained(callback))); + } + + void SetCookieWithDetailsTask(CookieMonster* cm, const GURL& url, + SetCookieCallback* callback) { + // Define the parameters here instead of in the calling fucntion. + // The maximum number of parameters for Bind function is 6. + std::string name = "A"; + std::string value = "B"; + std::string domain = std::string(); + std::string path = "/foo"; + base::Time expiration_time = base::Time(); + bool secure = false; + bool http_only = false; + cm->SetCookieWithDetailsAsync( + url, name, value, domain, path, expiration_time, secure, http_only, + base::Bind(&SetCookieCallback::Run, base::Unretained(callback))); + } + + void DeleteAllCreatedBetweenTask(CookieMonster* cm, + const base::Time& delete_begin, + const base::Time& delete_end, + DeleteCallback* callback) { + cm->DeleteAllCreatedBetweenAsync( + delete_begin, delete_end, + base::Bind(&DeleteCallback::Run, + base::Unretained(callback))); + } + + void DeleteAllForHostTask(CookieMonster* cm, + const GURL& url, + DeleteCallback* callback) { + cm->DeleteAllForHostAsync( + url, + base::Bind(&DeleteCallback::Run, base::Unretained(callback))); + } + + void DeleteCanonicalCookieTask(CookieMonster* cm, + const CookieMonster::CanonicalCookie& cookie, + SetCookieCallback* callback) { + cm->DeleteCanonicalCookieAsync( + cookie, + base::Bind(&SetCookieCallback::Run, base::Unretained(callback))); + } + + protected: + void RunOnOtherThread(const base::Closure& task) { + other_thread_.Start(); + other_thread_.message_loop()->PostTask(FROM_HERE, task); + RunFor(kTimeout); + other_thread_.Stop(); + } + + Thread other_thread_; +}; + +} // namespace + +TEST_F(MultiThreadedCookieMonsterTest, ThreadCheckGetAllCookies) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + EXPECT_TRUE(SetCookie(cm, url_google_, "A=B")); + CookieList cookies = GetAllCookies(cm); + CookieList::const_iterator it = cookies.begin(); + ASSERT_TRUE(it != cookies.end()); + EXPECT_EQ("www.google.izzle", it->Domain()); + EXPECT_EQ("A", it->Name()); + ASSERT_TRUE(++it == cookies.end()); + GetCookieListCallback callback(&other_thread_); + base::Closure task = + base::Bind(&net::MultiThreadedCookieMonsterTest::GetAllCookiesTask, + base::Unretained(this), + cm, &callback); + RunOnOtherThread(task); + EXPECT_TRUE(callback.did_run()); + it = callback.cookies().begin(); + ASSERT_TRUE(it != callback.cookies().end()); + EXPECT_EQ("www.google.izzle", it->Domain()); + EXPECT_EQ("A", it->Name()); + ASSERT_TRUE(++it == callback.cookies().end()); +} + +TEST_F(MultiThreadedCookieMonsterTest, ThreadCheckGetAllCookiesForURL) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + EXPECT_TRUE(SetCookie(cm, url_google_, "A=B")); + CookieList cookies = GetAllCookiesForURL(cm, url_google_); + CookieList::const_iterator it = cookies.begin(); + ASSERT_TRUE(it != cookies.end()); + EXPECT_EQ("www.google.izzle", it->Domain()); + EXPECT_EQ("A", it->Name()); + ASSERT_TRUE(++it == cookies.end()); + GetCookieListCallback callback(&other_thread_); + base::Closure task = + base::Bind(&net::MultiThreadedCookieMonsterTest::GetAllCookiesForURLTask, + base::Unretained(this), + cm, url_google_, &callback); + RunOnOtherThread(task); + EXPECT_TRUE(callback.did_run()); + it = callback.cookies().begin(); + ASSERT_TRUE(it != callback.cookies().end()); + EXPECT_EQ("www.google.izzle", it->Domain()); + EXPECT_EQ("A", it->Name()); + ASSERT_TRUE(++it == callback.cookies().end()); +} + +TEST_F(MultiThreadedCookieMonsterTest, ThreadCheckGetAllCookiesForURLWithOpt) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + EXPECT_TRUE(SetCookie(cm, url_google_, "A=B")); + CookieOptions options; + CookieList cookies = + GetAllCookiesForURLWithOptions(cm, url_google_, options); + CookieList::const_iterator it = cookies.begin(); + ASSERT_TRUE(it != cookies.end()); + EXPECT_EQ("www.google.izzle", it->Domain()); + EXPECT_EQ("A", it->Name()); + ASSERT_TRUE(++it == cookies.end()); + GetCookieListCallback callback(&other_thread_); + base::Closure task = base::Bind( + &net::MultiThreadedCookieMonsterTest::GetAllCookiesForURLWithOptionsTask, + base::Unretained(this), + cm, url_google_, options, &callback); + RunOnOtherThread(task); + EXPECT_TRUE(callback.did_run()); + it = callback.cookies().begin(); + ASSERT_TRUE(it != callback.cookies().end()); + EXPECT_EQ("www.google.izzle", it->Domain()); + EXPECT_EQ("A", it->Name()); + ASSERT_TRUE(++it == callback.cookies().end()); +} + +TEST_F(MultiThreadedCookieMonsterTest, ThreadCheckSetCookieWithDetails) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + EXPECT_TRUE(SetCookieWithDetails( + cm, url_google_foo_, + "A", "B", std::string(), "/foo", base::Time(), + false, false)); + SetCookieCallback callback(&other_thread_); + base::Closure task = base::Bind( + &net::MultiThreadedCookieMonsterTest::SetCookieWithDetailsTask, + base::Unretained(this), + cm, url_google_foo_, &callback); + RunOnOtherThread(task); + EXPECT_TRUE(callback.did_run()); + EXPECT_TRUE(callback.result()); +} + + +TEST_F(MultiThreadedCookieMonsterTest, ThreadCheckDeleteAllCreatedBetween) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + CookieOptions options; + Time now = Time::Now(); + EXPECT_TRUE(SetCookieWithOptions(cm, url_google_, "A=B", options)); + EXPECT_EQ(1, DeleteAllCreatedBetween(cm, now - TimeDelta::FromDays(99), + Time())); + EXPECT_TRUE(SetCookieWithOptions(cm, url_google_, "A=B", options)); + DeleteCallback callback(&other_thread_); + base::Closure task = base::Bind( + &net::MultiThreadedCookieMonsterTest::DeleteAllCreatedBetweenTask, + base::Unretained(this), + cm, now - TimeDelta::FromDays(99), + Time(), &callback); + RunOnOtherThread(task); + EXPECT_TRUE(callback.did_run()); + EXPECT_EQ(1, callback.num_deleted()); +} + +TEST_F(MultiThreadedCookieMonsterTest, ThreadCheckDeleteAllForHost) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + CookieOptions options; + EXPECT_TRUE(SetCookieWithOptions(cm, url_google_, "A=B", options)); + EXPECT_EQ(1, DeleteAllForHost(cm, url_google_)); + EXPECT_TRUE(SetCookieWithOptions(cm, url_google_, "A=B", options)); + DeleteCallback callback(&other_thread_); + base::Closure task = base::Bind( + &net::MultiThreadedCookieMonsterTest::DeleteAllForHostTask, + base::Unretained(this), + cm, url_google_, &callback); + RunOnOtherThread(task); + EXPECT_TRUE(callback.did_run()); + EXPECT_EQ(1, callback.num_deleted()); +} + +TEST_F(MultiThreadedCookieMonsterTest, ThreadCheckDeleteCanonicalCookie) { + scoped_refptr<CookieMonster> cm(new CookieMonster(NULL, NULL)); + CookieOptions options; + EXPECT_TRUE(SetCookieWithOptions(cm, url_google_, "A=B", options)); + CookieList cookies = GetAllCookies(cm); + CookieList::iterator it = cookies.begin(); + EXPECT_TRUE(DeleteCanonicalCookie(cm, *it)); + + EXPECT_TRUE(SetCookieWithOptions(cm, url_google_, "A=B", options)); + SetCookieCallback callback(&other_thread_); + cookies = GetAllCookies(cm); + it = cookies.begin(); + base::Closure task = base::Bind( + &net::MultiThreadedCookieMonsterTest::DeleteCanonicalCookieTask, + base::Unretained(this), + cm, *it, &callback); + RunOnOtherThread(task); + EXPECT_TRUE(callback.did_run()); + EXPECT_TRUE(callback.result()); +} + +TEST_F(CookieMonsterTest, ShortLivedSessionCookies) { + scoped_refptr<MockPersistentCookieStore> store( + new MockPersistentCookieStore); + scoped_refptr<CookieMonster> cm(new CookieMonster(store, NULL)); + + // Create a short-lived session cookie. + CookieOptions options; + options.set_force_session(); + Time current = Time::Now(); + EXPECT_TRUE(SetCookieWithOptions(cm, url_google_, + std::string(kValidCookieLine) + + "; max-age=10", + options)); + + // FindCookiesForKey asserts that its caller holds this lock. + base::AutoLock auto_lock(cm->lock_); + + // Get cookies before the cookie has expired. + std::vector<CookieMonster::CanonicalCookie*> cookies; + cm->FindCookiesForKey(cm->GetKey(url_google_.host()), url_google_, + CookieOptions(), current, false, &cookies); + EXPECT_EQ(1U, cookies.size()); + + // Get cookies after the cookie has expired. + cookies.clear(); + cm->FindCookiesForKey(cm->GetKey(url_google_.host()), url_google_, + CookieOptions(), current + TimeDelta::FromSeconds(20), + false, &cookies); + EXPECT_EQ(0U, cookies.size()); +} + +TEST_F(CookieMonsterTest, InvalidExpiryTime) { + CookieMonster::ParsedCookie pc( + std::string(kValidCookieLine) + "; expires=Blarg arg arg"); + scoped_ptr<CookieMonster::CanonicalCookie> cookie( + CookieMonster::CanonicalCookie::Create(url_google_, pc)); + + ASSERT_FALSE(cookie->DoesExpire()); +} + +// Test that CookieMonster writes session cookies into the underlying +// CookieStore if the "persist session cookies" option is on. +TEST_F(CookieMonsterTest, PersistSessionCookies) { + scoped_refptr<MockPersistentCookieStore> store( + new MockPersistentCookieStore); + scoped_refptr<CookieMonster> cm(new CookieMonster(store, NULL)); + cm->SetPersistSessionCookies(true); + + // All cookies set with SetCookie are session cookies. + EXPECT_TRUE(SetCookie(cm, url_google_, "A=B")); + EXPECT_EQ("A=B", GetCookies(cm, url_google_)); + + // The cookie was written to the backing store. + EXPECT_EQ(1u, store->commands().size()); + EXPECT_EQ(CookieStoreCommand::ADD, store->commands()[0].type); + EXPECT_EQ("A", store->commands()[0].cookie.Name()); + EXPECT_EQ("B", store->commands()[0].cookie.Value()); + + // Modify the cookie. + EXPECT_TRUE(SetCookie(cm, url_google_, "A=C")); + EXPECT_EQ("A=C", GetCookies(cm, url_google_)); + EXPECT_EQ(3u, store->commands().size()); + EXPECT_EQ(CookieStoreCommand::REMOVE, store->commands()[1].type); + EXPECT_EQ("A", store->commands()[1].cookie.Name()); + EXPECT_EQ("B", store->commands()[1].cookie.Value()); + EXPECT_EQ(CookieStoreCommand::ADD, store->commands()[2].type); + EXPECT_EQ("A", store->commands()[2].cookie.Name()); + EXPECT_EQ("C", store->commands()[2].cookie.Value()); + + // Delete the cookie. + DeleteCookie(cm, url_google_, "A"); + EXPECT_EQ("", GetCookies(cm, url_google_)); + EXPECT_EQ(4u, store->commands().size()); + EXPECT_EQ(CookieStoreCommand::REMOVE, store->commands()[3].type); + EXPECT_EQ("A", store->commands()[3].cookie.Name()); + EXPECT_EQ("C", store->commands()[3].cookie.Value()); +} + +// Test the commands sent to the persistent cookie store. +TEST_F(CookieMonsterTest, PersisentCookieStorageTest) { + scoped_refptr<MockPersistentCookieStore> store( + new MockPersistentCookieStore); + scoped_refptr<CookieMonster> cm(new CookieMonster(store, NULL)); + + // Add a cookie. + EXPECT_TRUE(SetCookie(cm, url_google_, + "A=B; expires=Mon, 18-Apr-22 22:50:13 GMT")); + this->MatchCookieLines("A=B", GetCookies(cm, url_google_)); + ASSERT_EQ(1u, store->commands().size()); + EXPECT_EQ(CookieStoreCommand::ADD, store->commands()[0].type); + // Remove it. + EXPECT_TRUE(SetCookie(cm, url_google_,"A=B; max-age=0")); + this->MatchCookieLines("", GetCookies(cm, url_google_)); + ASSERT_EQ(2u, store->commands().size()); + EXPECT_EQ(CookieStoreCommand::REMOVE, store->commands()[1].type); + + // Add a cookie. + EXPECT_TRUE(SetCookie(cm, url_google_, + "A=B; expires=Mon, 18-Apr-22 22:50:13 GMT")); + this->MatchCookieLines("A=B", GetCookies(cm, url_google_)); + ASSERT_EQ(3u, store->commands().size()); + EXPECT_EQ(CookieStoreCommand::ADD, store->commands()[2].type); + // Overwrite it. + EXPECT_TRUE(SetCookie(cm, url_google_, + "A=Foo; expires=Mon, 18-Apr-22 22:50:14 GMT")); + this->MatchCookieLines("A=Foo", GetCookies(cm, url_google_)); + ASSERT_EQ(5u, store->commands().size()); + EXPECT_EQ(CookieStoreCommand::REMOVE, store->commands()[3].type); + EXPECT_EQ(CookieStoreCommand::ADD, store->commands()[4].type); + + // Create some non-persistent cookies and check that they don't go to the + // persistent storage. + EXPECT_TRUE(SetCookie(cm, url_google_, "B=Bar")); + this->MatchCookieLines("A=Foo; B=Bar", GetCookies(cm, url_google_)); + EXPECT_EQ(5u, store->commands().size()); +} + +} // namespace net diff --git a/net/cookies/cookie_options.h b/net/cookies/cookie_options.h new file mode 100644 index 0000000..a59054a --- /dev/null +++ b/net/cookies/cookie_options.h @@ -0,0 +1,40 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Brought to you by number 42. + +#ifndef NET_COOKIES_COOKIE_OPTIONS_H_ +#define NET_COOKIES_COOKIE_OPTIONS_H_ +#pragma once + +namespace net { + +class CookieOptions { + public: + // Default is to exclude httponly, which means: + // - reading operations will not return httponly cookies. + // - writing operations will not write httponly cookies. + CookieOptions() + : exclude_httponly_(true), + force_session_(false) { + } + + void set_exclude_httponly() { exclude_httponly_ = true; } + void set_include_httponly() { exclude_httponly_ = false; } + bool exclude_httponly() const { return exclude_httponly_; } + + // Forces a cookie to be saved as a session cookie. If the expiration time of + // the cookie is in the past, i.e. the cookie would end up being deleted, this + // option is ignored. See CookieMonsterTest.ForceSessionOnly. + void set_force_session() { force_session_ = true; } + bool force_session() const { return force_session_; } + + private: + bool exclude_httponly_; + bool force_session_; +}; +} // namespace net + +#endif // NET_COOKIES_COOKIE_OPTIONS_H_ + diff --git a/net/cookies/cookie_store.cc b/net/cookies/cookie_store.cc new file mode 100644 index 0000000..25060904 --- /dev/null +++ b/net/cookies/cookie_store.cc @@ -0,0 +1,19 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/cookies/cookie_store.h" + +#include "net/cookies/cookie_options.h" + +namespace net { + +CookieStore::CookieStore() {} + +CookieStore::~CookieStore() {} + +CookieStore::CookieInfo::CookieInfo() {} + +CookieStore::CookieInfo::~CookieInfo() {} + +} // namespace net diff --git a/net/cookies/cookie_store.h b/net/cookies/cookie_store.h new file mode 100644 index 0000000..b9772d5 --- /dev/null +++ b/net/cookies/cookie_store.h @@ -0,0 +1,113 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Brought to you by number 42. + +#ifndef NET_COOKIES_COOKIE_STORE_H_ +#define NET_COOKIES_COOKIE_STORE_H_ +#pragma once + +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/callback.h" +#include "base/memory/ref_counted.h" +#include "base/time.h" +#include "net/cookies/cookie_options.h" +#include "net/base/net_export.h" + +class GURL; + +namespace net { + +class CookieMonster; + +// An interface for storing and retrieving cookies. Implementations need to +// be thread safe as its methods can be accessed from IO as well as UI threads. +class NET_EXPORT CookieStore : public base::RefCountedThreadSafe<CookieStore> { + public: + // This struct contains additional consumer-specific information that might + // be stored with cookies; currently just MAC information, see: + // http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac + struct NET_EXPORT CookieInfo { + CookieInfo(); + ~CookieInfo(); + + // The name of the cookie. + std::string name; + // TODO(abarth): Add value if any clients need it. + + // The time at which the cookie was created. + base::Time creation_date; + + // The value of the MAC-Key and MAC-Algorithm attributes, if present. + std::string mac_key; + std::string mac_algorithm; + }; + + // Callback definitions. + typedef base::Callback <void( + const std::string& cookie_line, + const std::vector<CookieInfo>& cookie_infos)> GetCookieInfoCallback; + typedef base::Callback<void(const std::string& cookie)> + GetCookiesCallback; + typedef base::Callback<void(bool success)> SetCookiesCallback; + typedef base::Callback<void(int num_deleted)> DeleteCallback; + + + // Sets a single cookie. Expects a cookie line, like "a=1; domain=b.com". + // + // Fails either if the cookie is invalid or if this is a non-HTTPONLY cookie + // and it would overwrite an existing HTTPONLY cookie. + // Returns true if the cookie is successfully set. + virtual void SetCookieWithOptionsAsync( + const GURL& url, + const std::string& cookie_line, + const CookieOptions& options, + const SetCookiesCallback& callback) = 0; + + // TODO(???): what if the total size of all the cookies >4k, can we have a + // header that big or do we need multiple Cookie: headers? + // Note: Some sites, such as Facebook, occasionally use Cookie headers >4k. + // + // Simple interface, gets a cookie string "a=b; c=d" for the given URL. + // Use options to access httponly cookies. + virtual void GetCookiesWithOptionsAsync( + const GURL& url, const CookieOptions& options, + const GetCookiesCallback& callback) = 0; + + // This function is similar to GetCookiesWithOptions same functionality as + // GetCookiesWithOptions except that it additionally provides detailed + // information about the cookie contained in the cookie line. See |struct + // CookieInfo| above for details. + virtual void GetCookiesWithInfoAsync( + const GURL& url, + const CookieOptions& options, + const GetCookieInfoCallback& callback) = 0; + + // Deletes the passed in cookie for the specified URL. + virtual void DeleteCookieAsync(const GURL& url, + const std::string& cookie_name, + const base::Closure& callback) = 0; + + // Deletes all of the cookies that have a creation_date greater than or equal + // to |delete_begin| and less than |delete_end| + // Returns the number of cookies that have been deleted. + virtual void DeleteAllCreatedBetweenAsync(const base::Time& delete_begin, + const base::Time& delete_end, + const DeleteCallback& callback) = 0; + + // Returns the underlying CookieMonster. + virtual CookieMonster* GetCookieMonster() = 0; + + protected: + friend class base::RefCountedThreadSafe<CookieStore>; + CookieStore(); + virtual ~CookieStore(); +}; + +} // namespace net + +#endif // NET_COOKIES_COOKIE_STORE_H_ diff --git a/net/cookies/cookie_store_test_callbacks.cc b/net/cookies/cookie_store_test_callbacks.cc new file mode 100644 index 0000000..0d4cb5c --- /dev/null +++ b/net/cookies/cookie_store_test_callbacks.cc @@ -0,0 +1,65 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/cookies/cookie_store_test_callbacks.h" + +#include "base/message_loop.h" +#include "base/threading/thread.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { + +CookieCallback::CookieCallback(base::Thread* run_in_thread) + : did_run_(false), + run_in_thread_(run_in_thread), + run_in_loop_(NULL), + parent_loop_(MessageLoop::current()), + loop_to_quit_(MessageLoop::current()) {} + +CookieCallback::CookieCallback() + : did_run_(false), + run_in_thread_(NULL), + run_in_loop_(MessageLoop::current()), + parent_loop_(NULL), + loop_to_quit_(MessageLoop::current()) {} + +void CookieCallback::CallbackEpilogue() { + MessageLoop* expected_loop = NULL; + if (run_in_thread_) { + DCHECK(!run_in_loop_); + expected_loop = run_in_thread_->message_loop(); + } else if (run_in_loop_) { + expected_loop = run_in_loop_; + } + ASSERT_TRUE(expected_loop != NULL); + + did_run_ = true; + EXPECT_EQ(expected_loop, MessageLoop::current()); + loop_to_quit_->PostTask(FROM_HERE, MessageLoop::QuitClosure()); +} + +SetCookieCallback::SetCookieCallback() : result_(false) {} +SetCookieCallback::SetCookieCallback(base::Thread* run_in_thread) + : CookieCallback(run_in_thread), + result_(false) {} + +GetCookieStringCallback::GetCookieStringCallback() {} +GetCookieStringCallback::GetCookieStringCallback(base::Thread* run_in_thread) + : CookieCallback(run_in_thread) {} + +GetCookiesWithInfoCallback::GetCookiesWithInfoCallback() {} +GetCookiesWithInfoCallback::GetCookiesWithInfoCallback( + base::Thread* run_in_thread) : CookieCallback(run_in_thread) {} +GetCookiesWithInfoCallback::~GetCookiesWithInfoCallback() {} + +DeleteCallback::DeleteCallback() : num_deleted_(0) {} +DeleteCallback::DeleteCallback(base::Thread* run_in_thread) + : CookieCallback(run_in_thread), + num_deleted_(0) {} + +DeleteCookieCallback::DeleteCookieCallback() {} +DeleteCookieCallback::DeleteCookieCallback(base::Thread* run_in_thread) + : CookieCallback(run_in_thread) {} + +} // namespace net diff --git a/net/cookies/cookie_store_test_callbacks.h b/net/cookies/cookie_store_test_callbacks.h new file mode 100644 index 0000000..d201d32 --- /dev/null +++ b/net/cookies/cookie_store_test_callbacks.h @@ -0,0 +1,137 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef NET_COOKIES_COOKIE_STORE_TEST_CALLBACKS_H_ +#define NET_COOKIES_COOKIE_STORE_TEST_CALLBACKS_H_ +#pragma once + +#include <string> +#include <vector> + +#include "net/cookies/cookie_store.h" + +class MessageLoop; + +namespace base { +class Thread; +} + +namespace net { + +// Defines common behaviour for the callbacks from GetCookies, SetCookies, etc. +// Asserts that the current thread is the expected invocation thread, sends a +// quit to the thread in which it was constructed. +class CookieCallback { + public: + // Indicates whether the callback has been called. + bool did_run() { return did_run_; } + + protected: + // Constructs a callback that expects to be called in the given thread and + // will, upon execution, send a QUIT to the constructing thread. + explicit CookieCallback(base::Thread* run_in_thread); + + // Constructs a callback that expects to be called in current thread and will + // send a QUIT to the constructing thread. + CookieCallback(); + + // Tests whether the current thread was the caller's thread. + // Sends a QUIT to the constructing thread. + void CallbackEpilogue(); + + private: + bool did_run_; + base::Thread* run_in_thread_; + MessageLoop* run_in_loop_; + MessageLoop* parent_loop_; + MessageLoop* loop_to_quit_; +}; + +// Callback implementations for the asynchronous CookieStore methods. + +class SetCookieCallback : public CookieCallback { + public: + SetCookieCallback(); + explicit SetCookieCallback(base::Thread* run_in_thread); + + void Run(bool result) { + result_ = result; + CallbackEpilogue(); + } + + bool result() { return result_; } + + private: + bool result_; +}; + +class GetCookieStringCallback : public CookieCallback { + public: + GetCookieStringCallback(); + explicit GetCookieStringCallback(base::Thread* run_in_thread); + + void Run(const std::string& cookie) { + cookie_ = cookie; + CallbackEpilogue(); + } + + const std::string& cookie() { return cookie_; } + + private: + std::string cookie_; +}; + +class GetCookiesWithInfoCallback : public CookieCallback { + public: + GetCookiesWithInfoCallback(); + explicit GetCookiesWithInfoCallback(base::Thread* run_in_thread); + ~GetCookiesWithInfoCallback(); + + void Run( + const std::string& cookie_line, + const std::vector<CookieStore::CookieInfo>& cookie_info) { + cookie_line_ = cookie_line; + cookie_info_ = cookie_info; + CallbackEpilogue(); + } + + const std::string& cookie_line() { return cookie_line_; } + const std::vector<CookieStore::CookieInfo>& cookie_info() { + return cookie_info_; + } + + private: + std::string cookie_line_; + std::vector<CookieStore::CookieInfo> cookie_info_; +}; + +class DeleteCallback : public CookieCallback { + public: + DeleteCallback(); + explicit DeleteCallback(base::Thread* run_in_thread); + + void Run(int num_deleted) { + num_deleted_ = num_deleted; + CallbackEpilogue(); + } + + int num_deleted() { return num_deleted_; } + + private: + int num_deleted_; +}; + +class DeleteCookieCallback : public CookieCallback { + public: + DeleteCookieCallback(); + explicit DeleteCookieCallback(base::Thread* run_in_thread); + + void Run() { + CallbackEpilogue(); + } +}; + +} // namespace net + +#endif // NET_COOKIES_COOKIE_STORE_TEST_CALLBACKS_H_ diff --git a/net/cookies/cookie_store_test_helpers.cc b/net/cookies/cookie_store_test_helpers.cc new file mode 100644 index 0000000..e764c9c --- /dev/null +++ b/net/cookies/cookie_store_test_helpers.cc @@ -0,0 +1,155 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/cookies/cookie_store_test_helpers.h" + +#include "base/bind.h" +#include "base/message_loop.h" + +namespace net { + +const int kDelayedTime = 0; + +DelayedCookieMonster::DelayedCookieMonster() + : cookie_monster_(new CookieMonster(NULL, NULL)), + did_run_(false), + result_(false) { +} + +DelayedCookieMonster::~DelayedCookieMonster() { +} + +void DelayedCookieMonster::GetCookiesInternalCallback( + const std::string& cookie_line, + const std::vector<CookieStore::CookieInfo>& cookie_info) { + cookie_line_ = cookie_line; + cookie_info_ = cookie_info; + did_run_ = true; +} + +void DelayedCookieMonster::SetCookiesInternalCallback(bool result) { + result_ = result; + did_run_ = true; +} + +void DelayedCookieMonster::GetCookiesWithOptionsInternalCallback( + const std::string& cookie) { + cookie_ = cookie; + did_run_ = true; +} + +void DelayedCookieMonster::GetCookiesWithInfoAsync( + const GURL& url, + const CookieOptions& options, + const CookieMonster::GetCookieInfoCallback& callback) { + did_run_ = false; + cookie_monster_->GetCookiesWithInfoAsync( + url, options, + base::Bind(&DelayedCookieMonster::GetCookiesInternalCallback, + base::Unretained(this))); + DCHECK_EQ(did_run_, true); + MessageLoop::current()->PostDelayedTask( + FROM_HERE, + base::Bind(&DelayedCookieMonster::InvokeGetCookiesCallback, + base::Unretained(this), callback), + base::TimeDelta::FromMilliseconds(kDelayedTime)); +} + +void DelayedCookieMonster::SetCookieWithOptionsAsync( + const GURL& url, const std::string& cookie_line, + const CookieOptions& options, + const CookieMonster::SetCookiesCallback& callback) { + did_run_ = false; + cookie_monster_->SetCookieWithOptionsAsync( + url, cookie_line, options, + base::Bind(&DelayedCookieMonster::SetCookiesInternalCallback, + base::Unretained(this))); + DCHECK_EQ(did_run_, true); + MessageLoop::current()->PostDelayedTask( + FROM_HERE, + base::Bind(&DelayedCookieMonster::InvokeSetCookiesCallback, + base::Unretained(this), callback), + base::TimeDelta::FromMilliseconds(kDelayedTime)); +} + +void DelayedCookieMonster::GetCookiesWithOptionsAsync( + const GURL& url, const CookieOptions& options, + const CookieMonster::GetCookiesCallback& callback) { + did_run_ = false; + cookie_monster_->GetCookiesWithOptionsAsync( + url, options, + base::Bind(&DelayedCookieMonster::GetCookiesWithOptionsInternalCallback, + base::Unretained(this))); + DCHECK_EQ(did_run_, true); + MessageLoop::current()->PostDelayedTask( + FROM_HERE, + base::Bind(&DelayedCookieMonster::InvokeGetCookieStringCallback, + base::Unretained(this), callback), + base::TimeDelta::FromMilliseconds(kDelayedTime)); +} + +void DelayedCookieMonster::InvokeGetCookiesCallback( + const CookieMonster::GetCookieInfoCallback& callback) { + if (!callback.is_null()) + callback.Run(cookie_line_, cookie_info_); +} + +void DelayedCookieMonster::InvokeSetCookiesCallback( + const CookieMonster::SetCookiesCallback& callback) { + if (!callback.is_null()) + callback.Run(result_); +} + +void DelayedCookieMonster::InvokeGetCookieStringCallback( + const CookieMonster::GetCookiesCallback& callback) { + if (!callback.is_null()) + callback.Run(cookie_); +} + +bool DelayedCookieMonster::SetCookieWithOptions( + const GURL& url, + const std::string& cookie_line, + const CookieOptions& options) { + ADD_FAILURE(); + return false; +} + +std::string DelayedCookieMonster::GetCookiesWithOptions( + const GURL& url, + const CookieOptions& options) { + ADD_FAILURE(); + return ""; +} + +void DelayedCookieMonster::GetCookiesWithInfo( + const GURL& url, + const CookieOptions& options, + std::string* cookie_line, + std::vector<CookieInfo>* cookie_infos) { + ADD_FAILURE(); +} + +void DelayedCookieMonster::DeleteCookie(const GURL& url, + const std::string& cookie_name) { + ADD_FAILURE(); +} + +void DelayedCookieMonster::DeleteCookieAsync(const GURL& url, + const std::string& cookie_name, + const base::Closure& callback) { + ADD_FAILURE(); +} + +void DelayedCookieMonster::DeleteAllCreatedBetweenAsync( + const base::Time& delete_begin, + const base::Time& delete_end, + const DeleteCallback& callback) { + ADD_FAILURE(); +} + +CookieMonster* DelayedCookieMonster::GetCookieMonster() { + return cookie_monster_; +} + +} // namespace net diff --git a/net/cookies/cookie_store_test_helpers.h b/net/cookies/cookie_store_test_helpers.h new file mode 100644 index 0000000..4e7ce27 --- /dev/null +++ b/net/cookies/cookie_store_test_helpers.h @@ -0,0 +1,105 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef NET_COOKIES_COOKIE_STORE_TEST_HELPERS_H_ +#define NET_COOKIES_COOKIE_STORE_TEST_HELPERS_H_ +#pragma once + +#include "net/cookies/cookie_monster.h" + +#include <string> +#include <vector> + +#include "base/callback_forward.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace net { + +class DelayedCookieMonster : public CookieStore { + public: + DelayedCookieMonster(); + + // Call the asynchronous CookieMonster function, expect it to immediately + // invoke the internal callback. + // Post a delayed task to invoke the original callback with the results. + + virtual void SetCookieWithOptionsAsync( + const GURL& url, + const std::string& cookie_line, + const CookieOptions& options, + const CookieMonster::SetCookiesCallback& callback) OVERRIDE; + + virtual void GetCookiesWithOptionsAsync( + const GURL& url, const CookieOptions& options, + const CookieMonster::GetCookiesCallback& callback) OVERRIDE; + + virtual void GetCookiesWithInfoAsync( + const GURL& url, + const CookieOptions& options, + const CookieMonster::GetCookieInfoCallback& callback) OVERRIDE; + + virtual bool SetCookieWithOptions(const GURL& url, + const std::string& cookie_line, + const CookieOptions& options); + + virtual std::string GetCookiesWithOptions(const GURL& url, + const CookieOptions& options); + + virtual void GetCookiesWithInfo(const GURL& url, + const CookieOptions& options, + std::string* cookie_line, + std::vector<CookieInfo>* cookie_infos); + + virtual void DeleteCookie(const GURL& url, + const std::string& cookie_name); + + virtual void DeleteCookieAsync(const GURL& url, + const std::string& cookie_name, + const base::Closure& callback) OVERRIDE; + + virtual void DeleteAllCreatedBetweenAsync( + const base::Time& delete_begin, + const base::Time& delete_end, + const DeleteCallback& callback) OVERRIDE; + + virtual CookieMonster* GetCookieMonster() OVERRIDE; + + private: + + // Be called immediately from CookieMonster. + + void GetCookiesInternalCallback( + const std::string& cookie_line, + const std::vector<CookieStore::CookieInfo>& cookie_info); + + void SetCookiesInternalCallback(bool result); + + void GetCookiesWithOptionsInternalCallback(const std::string& cookie); + + // Invoke the original callbacks. + + void InvokeGetCookiesCallback( + const CookieMonster::GetCookieInfoCallback& callback); + + void InvokeSetCookiesCallback( + const CookieMonster::SetCookiesCallback& callback); + + void InvokeGetCookieStringCallback( + const CookieMonster::GetCookiesCallback& callback); + + friend class base::RefCountedThreadSafe<DelayedCookieMonster>; + virtual ~DelayedCookieMonster(); + + scoped_refptr<CookieMonster> cookie_monster_; + + bool did_run_; + bool result_; + std::string cookie_; + std::string cookie_line_; + std::vector<CookieStore::CookieInfo> cookie_info_; +}; + +} // namespace net + +#endif // NET_COOKIES_COOKIE_STORE_TEST_HELPERS_H_ diff --git a/net/cookies/cookie_store_unittest.h b/net/cookies/cookie_store_unittest.h new file mode 100644 index 0000000..e1d23f8 --- /dev/null +++ b/net/cookies/cookie_store_unittest.h @@ -0,0 +1,1055 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef NET_COOKIES_COOKIE_STORE_UNITTEST_H_ +#define NET_COOKIES_COOKIE_STORE_UNITTEST_H_ +#pragma once + +#include "base/bind.h" +#include "base/message_loop.h" +#include "base/string_tokenizer.h" +#include "base/threading/thread.h" +#include "googleurl/src/gurl.h" +#include "net/cookies/cookie_monster.h" +#include "net/cookies/cookie_store.h" +#include "net/cookies/cookie_store_test_callbacks.h" +#include "testing/gtest/include/gtest/gtest.h" + +// This file declares unittest templates that can be used to test common +// behavior of any CookieStore implementation. +// See cookie_monster_unittest.cc for an example of an implementation. + +namespace net { + +using base::Thread; + +const int kTimeout = 1000; + +const char kUrlFtp[] = "ftp://ftp.google.izzle/"; +const char kUrlGoogle[] = "http://www.google.izzle"; +const char kUrlGoogleFoo[] = "http://www.google.izzle/foo"; +const char kUrlGoogleBar[] = "http://www.google.izzle/bar"; +const char kUrlGoogleSecure[] = "https://www.google.izzle"; +const char kValidCookieLine[] = "A=B; path=/"; +const char kValidDomainCookieLine[] = "A=B; path=/; domain=google.izzle"; + +// The CookieStoreTestTraits must have the following members: +// struct CookieStoreTestTraits { +// // Factory function. +// static scoped_refptr<CookieStore> Create(); +// +// // The cookie store is a CookieMonster. Only used to test +// // GetCookieMonster(). +// static const bool is_cookie_monster; +// +// // The cookie store supports cookies with the exclude_httponly() option. +// static const bool supports_http_only; +// +// // The cookie store supports the GetCookiesWithInfoAsync() method. +// static const bool supports_cookies_with_info; +// +// // The cookie store is able to make the difference between the ".com" +// // and the "com" domains. +// static const bool supports_non_dotted_domains; +// +// // The cookie store handles the domains with trailing dots (such as "com.") +// // correctly. +// static const bool supports_trailing_dots; +// +// // The cookie store rejects cookies for invalid schemes such as ftp. +// static const bool filters_schemes; +// +// // The cookie store has a bug happening when a path is a substring of +// // another. +// static const bool has_path_prefix_bug; +// +// // Time to wait between two cookie insertions to ensure that cookies have +// // different creation times. +// static const int creation_time_granularity_in_ms; +// }; + +template <class CookieStoreTestTraits> +class CookieStoreTest : public testing::Test { + protected: + CookieStoreTest() + : url_google_(kUrlGoogle), + url_google_secure_(kUrlGoogleSecure), + url_google_foo_(kUrlGoogleFoo), + url_google_bar_(kUrlGoogleBar) { + // This test may be used outside of the net test suite, and thus may not + // have a message loop. + if (!MessageLoop::current()) + message_loop_.reset(new MessageLoop); + weak_factory_.reset( + new base::WeakPtrFactory<MessageLoop>(MessageLoop::current())); + } + + // Helper methods for the asynchronous Cookie Store API that call the + // asynchronous method and then pump the loop until the callback is invoked, + // finally returning the value. + + std::string GetCookies(CookieStore* cs, const GURL& url) { + DCHECK(cs); + CookieOptions options; + if (!CookieStoreTestTraits::supports_http_only) + options.set_include_httponly(); + GetCookieStringCallback callback; + cs->GetCookiesWithOptionsAsync( + url, options, + base::Bind(&GetCookieStringCallback::Run, base::Unretained(&callback))); + RunFor(kTimeout); + EXPECT_TRUE(callback.did_run()); + return callback.cookie(); + } + + std::string GetCookiesWithOptions(CookieStore* cs, + const GURL& url, + const CookieOptions& options) { + DCHECK(cs); + GetCookieStringCallback callback; + cs->GetCookiesWithOptionsAsync( + url, options, base::Bind(&GetCookieStringCallback::Run, + base::Unretained(&callback))); + RunFor(kTimeout); + EXPECT_TRUE(callback.did_run()); + return callback.cookie(); + } + + void GetCookiesWithInfo(CookieStore* cs, + const GURL& url, + const CookieOptions& options, + std::string* cookie_line, + std::vector<CookieStore::CookieInfo>* cookie_info) { + DCHECK(cs); + DCHECK(cookie_line); + DCHECK(cookie_info); + GetCookiesWithInfoCallback callback; + cs->GetCookiesWithInfoAsync( + url, options, base::Bind(&GetCookiesWithInfoCallback::Run, + base::Unretained(&callback))); + RunFor(kTimeout); + EXPECT_TRUE(callback.did_run()); + *cookie_line = callback.cookie_line(); + *cookie_info = callback.cookie_info(); + } + + bool SetCookieWithOptions(CookieStore* cs, + const GURL& url, + const std::string& cookie_line, + const CookieOptions& options) { + DCHECK(cs); + SetCookieCallback callback; + cs->SetCookieWithOptionsAsync( + url, cookie_line, options, + base::Bind(&SetCookieCallback::Run, base::Unretained(&callback))); + RunFor(kTimeout); + EXPECT_TRUE(callback.did_run()); + return callback.result(); + } + + bool SetCookie(CookieStore* cs, + const GURL& url, + const std::string& cookie_line) { + CookieOptions options; + if (!CookieStoreTestTraits::supports_http_only) + options.set_include_httponly(); + return SetCookieWithOptions(cs, url, cookie_line, options); + } + + void DeleteCookie(CookieStore* cs, + const GURL& url, + const std::string& cookie_name) { + DCHECK(cs); + DeleteCookieCallback callback; + cs->DeleteCookieAsync( + url, cookie_name, + base::Bind(&DeleteCookieCallback::Run, base::Unretained(&callback))); + RunFor(kTimeout); + EXPECT_TRUE(callback.did_run()); + } + + int DeleteCreatedBetween(CookieStore* cs, + const base::Time& delete_begin, + const base::Time& delete_end) { + DCHECK(cs); + DeleteCallback callback; + cs->DeleteAllCreatedBetweenAsync( + delete_begin, delete_end, + base::Bind(&DeleteCallback::Run, base::Unretained(&callback))); + RunFor(kTimeout); + EXPECT_TRUE(callback.did_run()); + return callback.num_deleted(); + } + + void RunFor(int ms) { + // Runs the test thread message loop for up to |ms| milliseconds. + MessageLoop::current()->PostDelayedTask( + FROM_HERE, base::Bind(&MessageLoop::Quit, weak_factory_->GetWeakPtr()), + base::TimeDelta::FromMilliseconds(ms)); + MessageLoop::current()->Run(); + weak_factory_->InvalidateWeakPtrs(); + } + + scoped_refptr<CookieStore> GetCookieStore() { + return CookieStoreTestTraits::Create(); + } + + // Compares two cookie lines. + void MatchCookieLines(const std::string& line1, const std::string& line2) { + EXPECT_EQ(TokenizeCookieLine(line1), TokenizeCookieLine(line2)); + } + + // Check the cookie line by polling until equality or a timeout is reached. + void MatchCookieLineWithTimeout(CookieStore* cs, + const GURL& url, + const std::string& line) { + std::string cookies = GetCookies(cs, url); + bool matched = (TokenizeCookieLine(line) == TokenizeCookieLine(cookies)); + base::Time polling_end_date = base::Time::Now() + + base::TimeDelta::FromMilliseconds( + CookieStoreTestTraits::creation_time_granularity_in_ms); + + while (!matched && base::Time::Now() <= polling_end_date) { + base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(10)); + cookies = GetCookies(cs, url); + matched = (TokenizeCookieLine(line) == TokenizeCookieLine(cookies)); + } + + EXPECT_TRUE(matched) << "\"" << cookies + << "\" does not match \"" << line << "\""; + } + + GURL url_google_; + GURL url_google_secure_; + GURL url_google_foo_; + GURL url_google_bar_; + + scoped_ptr<base::WeakPtrFactory<MessageLoop> > weak_factory_; + scoped_ptr<MessageLoop> message_loop_; + + private: + // Returns a set of strings of type "name=value". Fails in case of duplicate. + std::set<std::string> TokenizeCookieLine(const std::string& line) { + std::set<std::string> tokens; + StringTokenizer tokenizer(line, " ;"); + while (tokenizer.GetNext()) + EXPECT_TRUE(tokens.insert(tokenizer.token()).second); + return tokens; + } +}; + +TYPED_TEST_CASE_P(CookieStoreTest); + +TYPED_TEST_P(CookieStoreTest, TypeTest) { + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + EXPECT_EQ(cs->GetCookieMonster(), + (TypeParam::is_cookie_monster) ? + static_cast<CookieMonster*>(cs.get()) : NULL); +} + +TYPED_TEST_P(CookieStoreTest, DomainTest) { + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + EXPECT_TRUE(this->SetCookie(cs, this->url_google_, "A=B")); + this->MatchCookieLines("A=B", this->GetCookies(cs, this->url_google_)); + EXPECT_TRUE(this->SetCookie(cs, this->url_google_, + "C=D; domain=.google.izzle")); + this->MatchCookieLines("A=B; C=D", this->GetCookies(cs, this->url_google_)); + + // Verify that A=B was set as a host cookie rather than a domain + // cookie -- should not be accessible from a sub sub-domain. + this->MatchCookieLines("C=D", + this->GetCookies(cs, GURL("http://foo.www.google.izzle"))); + + // Test and make sure we find domain cookies on the same domain. + EXPECT_TRUE(this->SetCookie(cs, this->url_google_, + "E=F; domain=.www.google.izzle")); + this->MatchCookieLines("A=B; C=D; E=F", + this->GetCookies(cs, this->url_google_)); + + // Test setting a domain= that doesn't start w/ a dot, should + // treat it as a domain cookie, as if there was a pre-pended dot. + EXPECT_TRUE(this->SetCookie(cs, this->url_google_, + "G=H; domain=www.google.izzle")); + this->MatchCookieLines("A=B; C=D; E=F; G=H", + this->GetCookies(cs, this->url_google_)); + + // Test domain enforcement, should fail on a sub-domain or something too deep. + EXPECT_FALSE(this->SetCookie(cs, this->url_google_, "I=J; domain=.izzle")); + this->MatchCookieLines("", this->GetCookies(cs, GURL("http://a.izzle"))); + EXPECT_FALSE(this->SetCookie(cs, this->url_google_, + "K=L; domain=.bla.www.google.izzle")); + this->MatchCookieLines("C=D; E=F; G=H", + this->GetCookies(cs, GURL("http://bla.www.google.izzle"))); + this->MatchCookieLines("A=B; C=D; E=F; G=H", + this->GetCookies(cs, this->url_google_)); +} + +// FireFox recognizes domains containing trailing periods as valid. +// IE and Safari do not. Assert the expected policy here. +TYPED_TEST_P(CookieStoreTest, DomainWithTrailingDotTest) { + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + EXPECT_FALSE(this->SetCookie(cs, this->url_google_, + "a=1; domain=.www.google.com.")); + EXPECT_FALSE(this->SetCookie(cs, this->url_google_, + "b=2; domain=.www.google.com..")); + this->MatchCookieLines("", this->GetCookies(cs, this->url_google_)); +} + +// Test that cookies can bet set on higher level domains. +// http://b/issue?id=896491 +TYPED_TEST_P(CookieStoreTest, ValidSubdomainTest) { + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + GURL url_abcd("http://a.b.c.d.com"); + GURL url_bcd("http://b.c.d.com"); + GURL url_cd("http://c.d.com"); + GURL url_d("http://d.com"); + + EXPECT_TRUE(this->SetCookie(cs, url_abcd, "a=1; domain=.a.b.c.d.com")); + EXPECT_TRUE(this->SetCookie(cs, url_abcd, "b=2; domain=.b.c.d.com")); + EXPECT_TRUE(this->SetCookie(cs, url_abcd, "c=3; domain=.c.d.com")); + EXPECT_TRUE(this->SetCookie(cs, url_abcd, "d=4; domain=.d.com")); + + this->MatchCookieLines("a=1; b=2; c=3; d=4", this->GetCookies(cs, url_abcd)); + this->MatchCookieLines("b=2; c=3; d=4", this->GetCookies(cs, url_bcd)); + this->MatchCookieLines("c=3; d=4", this->GetCookies(cs, url_cd)); + this->MatchCookieLines("d=4", this->GetCookies(cs, url_d)); + + // Check that the same cookie can exist on different sub-domains. + EXPECT_TRUE(this->SetCookie(cs, url_bcd, "X=bcd; domain=.b.c.d.com")); + EXPECT_TRUE(this->SetCookie(cs, url_bcd, "X=cd; domain=.c.d.com")); + this->MatchCookieLines("b=2; c=3; d=4; X=bcd; X=cd", + this->GetCookies(cs, url_bcd)); + this->MatchCookieLines("c=3; d=4; X=cd", this->GetCookies(cs, url_cd)); +} + +// Test that setting a cookie which specifies an invalid domain has +// no side-effect. An invalid domain in this context is one which does +// not match the originating domain. +// http://b/issue?id=896472 +TYPED_TEST_P(CookieStoreTest, InvalidDomainTest) { + { + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + GURL url_foobar("http://foo.bar.com"); + + // More specific sub-domain than allowed. + EXPECT_FALSE(this->SetCookie(cs, url_foobar, + "a=1; domain=.yo.foo.bar.com")); + + EXPECT_FALSE(this->SetCookie(cs, url_foobar, "b=2; domain=.foo.com")); + EXPECT_FALSE(this->SetCookie(cs, url_foobar, "c=3; domain=.bar.foo.com")); + + // Different TLD, but the rest is a substring. + EXPECT_FALSE(this->SetCookie(cs, url_foobar, + "d=4; domain=.foo.bar.com.net")); + + // A substring that isn't really a parent domain. + EXPECT_FALSE(this->SetCookie(cs, url_foobar, "e=5; domain=ar.com")); + + // Completely invalid domains: + EXPECT_FALSE(this->SetCookie(cs, url_foobar, "f=6; domain=.")); + EXPECT_FALSE(this->SetCookie(cs, url_foobar, "g=7; domain=/")); + EXPECT_FALSE(this->SetCookie(cs, url_foobar, + "h=8; domain=http://foo.bar.com")); + EXPECT_FALSE(this->SetCookie(cs, url_foobar, "i=9; domain=..foo.bar.com")); + EXPECT_FALSE(this->SetCookie(cs, url_foobar, "j=10; domain=..bar.com")); + + // Make sure there isn't something quirky in the domain canonicalization + // that supports full URL semantics. + EXPECT_FALSE(this->SetCookie(cs, url_foobar, + "k=11; domain=.foo.bar.com?blah")); + EXPECT_FALSE(this->SetCookie(cs, url_foobar, + "l=12; domain=.foo.bar.com/blah")); + EXPECT_FALSE(this->SetCookie(cs, url_foobar, + "m=13; domain=.foo.bar.com:80")); + EXPECT_FALSE(this->SetCookie(cs, url_foobar, + "n=14; domain=.foo.bar.com:")); + EXPECT_FALSE(this->SetCookie(cs, url_foobar, + "o=15; domain=.foo.bar.com#sup")); + + this->MatchCookieLines("", this->GetCookies(cs, url_foobar)); + } + + { + // Make sure the cookie code hasn't gotten its subdomain string handling + // reversed, missed a suffix check, etc. It's important here that the two + // hosts below have the same domain + registry. + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + GURL url_foocom("http://foo.com.com"); + EXPECT_FALSE(this->SetCookie(cs, url_foocom, + "a=1; domain=.foo.com.com.com")); + this->MatchCookieLines("", this->GetCookies(cs, url_foocom)); + } +} + +// Test the behavior of omitting dot prefix from domain, should +// function the same as FireFox. +// http://b/issue?id=889898 +TYPED_TEST_P(CookieStoreTest, DomainWithoutLeadingDotTest) { + { // The omission of dot results in setting a domain cookie. + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + GURL url_hosted("http://manage.hosted.filefront.com"); + GURL url_filefront("http://www.filefront.com"); + EXPECT_TRUE(this->SetCookie(cs, url_hosted, + "sawAd=1; domain=filefront.com")); + this->MatchCookieLines("sawAd=1", this->GetCookies(cs, url_hosted)); + this->MatchCookieLines("sawAd=1", this->GetCookies(cs, url_filefront)); + } + + { // Even when the domains match exactly, don't consider it host cookie. + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + GURL url("http://www.google.com"); + EXPECT_TRUE(this->SetCookie(cs, url, "a=1; domain=www.google.com")); + this->MatchCookieLines("a=1", this->GetCookies(cs, url)); + this->MatchCookieLines("a=1", + this->GetCookies(cs, GURL("http://sub.www.google.com"))); + this->MatchCookieLines("", + this->GetCookies(cs, GURL("http://something-else.com"))); + } +} + +// Test that the domain specified in cookie string is treated case-insensitive +// http://b/issue?id=896475. +TYPED_TEST_P(CookieStoreTest, CaseInsensitiveDomainTest) { + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + GURL url("http://www.google.com"); + EXPECT_TRUE(this->SetCookie(cs, url, "a=1; domain=.GOOGLE.COM")); + EXPECT_TRUE(this->SetCookie(cs, url, "b=2; domain=.wWw.gOOgLE.coM")); + this->MatchCookieLines("a=1; b=2", this->GetCookies(cs, url)); +} + +TYPED_TEST_P(CookieStoreTest, TestIpAddress) { + GURL url_ip("http://1.2.3.4/weee"); + { + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + EXPECT_TRUE(this->SetCookie(cs, url_ip, kValidCookieLine)); + this->MatchCookieLines("A=B", this->GetCookies(cs, url_ip)); + } + + { // IP addresses should not be able to set domain cookies. + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + EXPECT_FALSE(this->SetCookie(cs, url_ip, "b=2; domain=.1.2.3.4")); + EXPECT_FALSE(this->SetCookie(cs, url_ip, "c=3; domain=.3.4")); + this->MatchCookieLines("", this->GetCookies(cs, url_ip)); + // It should be allowed to set a cookie if domain= matches the IP address + // exactly. This matches IE/Firefox, even though it seems a bit wrong. + EXPECT_FALSE(this->SetCookie(cs, url_ip, "b=2; domain=1.2.3.3")); + this->MatchCookieLines("", this->GetCookies(cs, url_ip)); + EXPECT_TRUE(this->SetCookie(cs, url_ip, "b=2; domain=1.2.3.4")); + this->MatchCookieLines("b=2", this->GetCookies(cs, url_ip)); + } +} + +// Test host cookies, and setting of cookies on TLD. +TYPED_TEST_P(CookieStoreTest, TestNonDottedAndTLD) { + { + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + GURL url("http://com/"); + // Allow setting on "com", (but only as a host cookie). + EXPECT_TRUE(this->SetCookie(cs, url, "a=1")); + EXPECT_FALSE(this->SetCookie(cs, url, "b=2; domain=.com")); + EXPECT_FALSE(this->SetCookie(cs, url, "c=3; domain=com")); + this->MatchCookieLines("a=1", this->GetCookies(cs, url)); + // Make sure it doesn't show up for a normal .com, it should be a host + // not a domain cookie. + this->MatchCookieLines("", + this->GetCookies(cs, GURL("http://hopefully-no-cookies.com/"))); + if (TypeParam::supports_non_dotted_domains) { + this->MatchCookieLines("", this->GetCookies(cs, GURL("http://.com/"))); + } + } + + { + // http://com. should be treated the same as http://com. + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + GURL url("http://com./index.html"); + if (TypeParam::supports_trailing_dots) { + EXPECT_TRUE(this->SetCookie(cs, url, "a=1")); + this->MatchCookieLines("a=1", this->GetCookies(cs, url)); + this->MatchCookieLines("", + this->GetCookies(cs, GURL("http://hopefully-no-cookies.com./"))); + } else { + EXPECT_FALSE(this->SetCookie(cs, url, "a=1")); + } + } + + { // Should not be able to set host cookie from a subdomain. + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + GURL url("http://a.b"); + EXPECT_FALSE(this->SetCookie(cs, url, "a=1; domain=.b")); + EXPECT_FALSE(this->SetCookie(cs, url, "b=2; domain=b")); + this->MatchCookieLines("", this->GetCookies(cs, url)); + } + + { // Same test as above, but explicitly on a known TLD (com). + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + GURL url("http://google.com"); + EXPECT_FALSE(this->SetCookie(cs, url, "a=1; domain=.com")); + EXPECT_FALSE(this->SetCookie(cs, url, "b=2; domain=com")); + this->MatchCookieLines("", this->GetCookies(cs, url)); + } + + { // Make sure can't set cookie on TLD which is dotted. + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + GURL url("http://google.co.uk"); + EXPECT_FALSE(this->SetCookie(cs, url, "a=1; domain=.co.uk")); + EXPECT_FALSE(this->SetCookie(cs, url, "b=2; domain=.uk")); + this->MatchCookieLines("", this->GetCookies(cs, url)); + this->MatchCookieLines("", + this->GetCookies(cs, GURL("http://something-else.co.uk"))); + this->MatchCookieLines("", + this->GetCookies(cs, GURL("http://something-else.uk"))); + } + + { // Intranet URLs should only be able to set host cookies. + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + GURL url("http://b"); + EXPECT_TRUE(this->SetCookie(cs, url, "a=1")); + EXPECT_FALSE(this->SetCookie(cs, url, "b=2; domain=.b")); + EXPECT_FALSE(this->SetCookie(cs, url, "c=3; domain=b")); + this->MatchCookieLines("a=1", this->GetCookies(cs, url)); + } +} + +// Test reading/writing cookies when the domain ends with a period, +// as in "www.google.com." +TYPED_TEST_P(CookieStoreTest, TestHostEndsWithDot) { + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + GURL url("http://www.google.com"); + GURL url_with_dot("http://www.google.com."); + EXPECT_TRUE(this->SetCookie(cs, url, "a=1")); + this->MatchCookieLines("a=1", this->GetCookies(cs, url)); + + if (TypeParam::supports_trailing_dots) { + // Do not share cookie space with the dot version of domain. + // Note: this is not what FireFox does, but it _is_ what IE+Safari do. + EXPECT_FALSE(this->SetCookie(cs, url, "b=2; domain=.www.google.com.")); + this->MatchCookieLines("a=1", this->GetCookies(cs, url)); + + EXPECT_TRUE(this->SetCookie(cs, url_with_dot, "b=2; domain=.google.com.")); + this->MatchCookieLines("b=2", this->GetCookies(cs, url_with_dot)); + } else { + EXPECT_TRUE(this->SetCookie(cs, url, "b=2; domain=.www.google.com.")); + EXPECT_FALSE(this->SetCookie(cs, url_with_dot, "b=2; domain=.google.com.")); + } + + // Make sure there weren't any side effects. + this->MatchCookieLines("", + this->GetCookies(cs, GURL("http://hopefully-no-cookies.com/"))); + this->MatchCookieLines("", this->GetCookies(cs, GURL("http://.com/"))); +} + +TYPED_TEST_P(CookieStoreTest, InvalidScheme) { + if (!TypeParam::filters_schemes) + return; + + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + EXPECT_FALSE(this->SetCookie(cs, GURL(kUrlFtp), kValidCookieLine)); +} + +TYPED_TEST_P(CookieStoreTest, InvalidScheme_Read) { + if (!TypeParam::filters_schemes) + return; + + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + EXPECT_TRUE(this->SetCookie(cs, GURL(kUrlGoogle), kValidDomainCookieLine)); + this->MatchCookieLines("", this->GetCookies(cs, GURL(kUrlFtp))); +} + +TYPED_TEST_P(CookieStoreTest, PathTest) { + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + std::string url("http://www.google.izzle"); + EXPECT_TRUE(this->SetCookie(cs, GURL(url), "A=B; path=/wee")); + this->MatchCookieLines("A=B", this->GetCookies(cs, GURL(url + "/wee"))); + this->MatchCookieLines("A=B", this->GetCookies(cs, GURL(url + "/wee/"))); + this->MatchCookieLines("A=B", this->GetCookies(cs, GURL(url + "/wee/war"))); + this->MatchCookieLines("A=B", + this->GetCookies(cs, GURL(url + "/wee/war/more/more"))); + if (!TypeParam::has_path_prefix_bug) + this->MatchCookieLines("", this->GetCookies(cs, GURL(url + "/weehee"))); + this->MatchCookieLines("", this->GetCookies(cs, GURL(url + "/"))); + + // If we add a 0 length path, it should default to / + EXPECT_TRUE(this->SetCookie(cs, GURL(url), "A=C; path=")); + this->MatchCookieLines("A=B; A=C", this->GetCookies(cs, GURL(url + "/wee"))); + this->MatchCookieLines("A=C", this->GetCookies(cs, GURL(url + "/"))); +} + +TYPED_TEST_P(CookieStoreTest, HttpOnlyTest) { + if (!TypeParam::supports_http_only) + return; + + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + CookieOptions options; + options.set_include_httponly(); + + // Create a httponly cookie. + EXPECT_TRUE(this->SetCookieWithOptions(cs, this->url_google_, "A=B; httponly", + options)); + + // Check httponly read protection. + this->MatchCookieLines("", this->GetCookies(cs, this->url_google_)); + this->MatchCookieLines("A=B", + this->GetCookiesWithOptions(cs, this->url_google_, options)); + + // Check httponly overwrite protection. + EXPECT_FALSE(this->SetCookie(cs, this->url_google_, "A=C")); + this->MatchCookieLines("", this->GetCookies(cs, this->url_google_)); + this->MatchCookieLines("A=B", + this->GetCookiesWithOptions(cs, this->url_google_, options)); + EXPECT_TRUE(this->SetCookieWithOptions(cs, this->url_google_, "A=C", + options)); + this->MatchCookieLines("A=C", this->GetCookies(cs, this->url_google_)); + + // Check httponly create protection. + EXPECT_FALSE(this->SetCookie(cs, this->url_google_, "B=A; httponly")); + this->MatchCookieLines("A=C", + this->GetCookiesWithOptions(cs, this->url_google_, options)); + EXPECT_TRUE(this->SetCookieWithOptions(cs, this->url_google_, "B=A; httponly", + options)); + this->MatchCookieLines("A=C; B=A", + this->GetCookiesWithOptions(cs, this->url_google_, options)); + this->MatchCookieLines("A=C", this->GetCookies(cs, this->url_google_)); +} + +TYPED_TEST_P(CookieStoreTest, TestGetCookiesWithInfo) { + if (!TypeParam::supports_cookies_with_info) + return; + + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + CookieOptions options; + + EXPECT_TRUE(this->SetCookieWithOptions(cs, this->url_google_, "A=B", + options)); + EXPECT_TRUE(this->SetCookieWithOptions(cs, this->url_google_, + "C=D; Mac-Key=390jfn0awf3; Mac-Algorithm=hmac-sha-1", options)); + + this->MatchCookieLines("A=B; C=D", + this->GetCookiesWithOptions(cs, this->url_google_, options)); + + std::string cookie_line; + std::vector<CookieStore::CookieInfo> cookie_infos; + + this->GetCookiesWithInfo(cs, this->url_google_, options, &cookie_line, + &cookie_infos); + + EXPECT_EQ("A=B; C=D", cookie_line); + + EXPECT_EQ(2U, cookie_infos.size()); + + EXPECT_EQ("A", cookie_infos[0].name); + EXPECT_EQ("", cookie_infos[0].mac_key); + EXPECT_EQ("", cookie_infos[0].mac_algorithm); + + EXPECT_EQ("C", cookie_infos[1].name); + EXPECT_EQ("390jfn0awf3", cookie_infos[1].mac_key); + EXPECT_EQ("hmac-sha-1", cookie_infos[1].mac_algorithm); +} + +TYPED_TEST_P(CookieStoreTest, TestCookieDeletion) { + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + + // Create a session cookie. + EXPECT_TRUE(this->SetCookie(cs, this->url_google_, kValidCookieLine)); + this->MatchCookieLines("A=B", this->GetCookies(cs, this->url_google_)); + // Delete it via Max-Age. + EXPECT_TRUE(this->SetCookie(cs, this->url_google_, + std::string(kValidCookieLine) + "; max-age=0")); + this->MatchCookieLineWithTimeout(cs, this->url_google_, ""); + + // Create a session cookie. + EXPECT_TRUE(this->SetCookie(cs, this->url_google_, kValidCookieLine)); + this->MatchCookieLines("A=B", this->GetCookies(cs, this->url_google_)); + // Delete it via Expires. + EXPECT_TRUE(this->SetCookie(cs, this->url_google_, + std::string(kValidCookieLine) + + "; expires=Mon, 18-Apr-1977 22:50:13 GMT")); + this->MatchCookieLines("", this->GetCookies(cs, this->url_google_)); + + // Create a persistent cookie. + EXPECT_TRUE(this->SetCookie(cs, this->url_google_, + std::string(kValidCookieLine) + + "; expires=Mon, 18-Apr-22 22:50:13 GMT")); + + this->MatchCookieLines("A=B", this->GetCookies(cs, this->url_google_)); + // Delete it via Max-Age. + EXPECT_TRUE(this->SetCookie(cs, this->url_google_, + std::string(kValidCookieLine) + "; max-age=0")); + this->MatchCookieLineWithTimeout(cs, this->url_google_, ""); + + // Create a persistent cookie. + EXPECT_TRUE(this->SetCookie(cs, this->url_google_, + std::string(kValidCookieLine) + + "; expires=Mon, 18-Apr-22 22:50:13 GMT")); + this->MatchCookieLines("A=B", this->GetCookies(cs, this->url_google_)); + // Delete it via Expires. + EXPECT_TRUE(this->SetCookie(cs, this->url_google_, + std::string(kValidCookieLine) + + "; expires=Mon, 18-Apr-1977 22:50:13 GMT")); + this->MatchCookieLines("", this->GetCookies(cs, this->url_google_)); + + // Create a persistent cookie. + EXPECT_TRUE(this->SetCookie(cs, this->url_google_, + std::string(kValidCookieLine) + + "; expires=Mon, 18-Apr-22 22:50:13 GMT")); + this->MatchCookieLines("A=B", this->GetCookies(cs, this->url_google_)); + // Delete it via Expires, with a unix epoch of 0. + EXPECT_TRUE(this->SetCookie(cs, this->url_google_, + std::string(kValidCookieLine) + + "; expires=Thu, 1-Jan-1970 00:00:00 GMT")); + this->MatchCookieLines("", this->GetCookies(cs, this->url_google_)); +} + +TYPED_TEST_P(CookieStoreTest, TestDeleteAllCreatedBetween) { + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + const base::Time last_month = base::Time::Now() - + base::TimeDelta::FromDays(30); + const base::Time last_minute = base::Time::Now() - + base::TimeDelta::FromMinutes(1); + const base::Time next_minute = base::Time::Now() + + base::TimeDelta::FromMinutes(1); + const base::Time next_month = base::Time::Now() + + base::TimeDelta::FromDays(30); + + // Add a cookie. + EXPECT_TRUE(this->SetCookie(cs, this->url_google_, "A=B")); + // Check that the cookie is in the store. + this->MatchCookieLines("A=B", this->GetCookies(cs, this->url_google_)); + + // Remove cookies in empty intervals. + EXPECT_EQ(0, this->DeleteCreatedBetween(cs, last_month, last_minute)); + EXPECT_EQ(0, this->DeleteCreatedBetween(cs, next_minute, next_month)); + // Check that the cookie is still there. + this->MatchCookieLines("A=B", this->GetCookies(cs, this->url_google_)); + + // Remove the cookie with an interval defined by two dates. + EXPECT_EQ(1, this->DeleteCreatedBetween(cs, last_minute, next_minute)); + // Check that the cookie disappeared. + this->MatchCookieLines("", this->GetCookies(cs, this->url_google_)); + + // Add another cookie. + EXPECT_TRUE(this->SetCookie(cs, this->url_google_, "C=D")); + // Check that the cookie is in the store. + this->MatchCookieLines("C=D", this->GetCookies(cs, this->url_google_)); + + // Remove the cookie with a null ending time. + EXPECT_EQ(1, this->DeleteCreatedBetween(cs, last_minute, base::Time())); + // Check that the cookie disappeared. + this->MatchCookieLines("", this->GetCookies(cs, this->url_google_)); +} + +TYPED_TEST_P(CookieStoreTest, TestSecure) { + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + + EXPECT_TRUE(this->SetCookie(cs, this->url_google_, "A=B")); + this->MatchCookieLines("A=B", this->GetCookies(cs, this->url_google_)); + this->MatchCookieLines("A=B", this->GetCookies(cs, this->url_google_secure_)); + + EXPECT_TRUE(this->SetCookie(cs, this->url_google_secure_, "A=B; secure")); + // The secure should overwrite the non-secure. + this->MatchCookieLines("", this->GetCookies(cs, this->url_google_)); + this->MatchCookieLines("A=B", this->GetCookies(cs, this->url_google_secure_)); + + EXPECT_TRUE(this->SetCookie(cs, this->url_google_secure_, "D=E; secure")); + this->MatchCookieLines("", this->GetCookies(cs, this->url_google_)); + this->MatchCookieLines("A=B; D=E", + this->GetCookies(cs, this->url_google_secure_)); + + EXPECT_TRUE(this->SetCookie(cs, this->url_google_secure_, "A=B")); + // The non-secure should overwrite the secure. + this->MatchCookieLines("A=B", this->GetCookies(cs, this->url_google_)); + this->MatchCookieLines("D=E; A=B", + this->GetCookies(cs, this->url_google_secure_)); +} + +static const int kLastAccessThresholdMilliseconds = 200; + +// Formerly NetUtilTest.CookieTest back when we used wininet's cookie handling. +TYPED_TEST_P(CookieStoreTest, NetUtilCookieTest) { + const GURL test_url("http://mojo.jojo.google.izzle/"); + + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + + EXPECT_TRUE(this->SetCookie(cs, test_url, "foo=bar")); + std::string value = this->GetCookies(cs, test_url); + this->MatchCookieLines("foo=bar", value); + + // test that we can retrieve all cookies: + EXPECT_TRUE(this->SetCookie(cs, test_url, "x=1")); + EXPECT_TRUE(this->SetCookie(cs, test_url, "y=2")); + + std::string result = this->GetCookies(cs, test_url); + EXPECT_FALSE(result.empty()); + EXPECT_NE(result.find("x=1"), std::string::npos) << result; + EXPECT_NE(result.find("y=2"), std::string::npos) << result; +} + +TYPED_TEST_P(CookieStoreTest, OverwritePersistentCookie) { + GURL url_google("http://www.google.com/"); + GURL url_chromium("http://chromium.org"); + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + + // Insert a cookie "a" for path "/path1" + EXPECT_TRUE( + this->SetCookie(cs, url_google, "a=val1; path=/path1; " + "expires=Mon, 18-Apr-22 22:50:13 GMT")); + + // Insert a cookie "b" for path "/path1" + EXPECT_TRUE( + this->SetCookie(cs, url_google, "b=val1; path=/path1; " + "expires=Mon, 18-Apr-22 22:50:14 GMT")); + + // Insert a cookie "b" for path "/path1", that is httponly. This should + // overwrite the non-http-only version. + CookieOptions allow_httponly; + allow_httponly.set_include_httponly(); + EXPECT_TRUE( + this->SetCookieWithOptions(cs, url_google, + "b=val2; path=/path1; httponly; " + "expires=Mon, 18-Apr-22 22:50:14 GMT", + allow_httponly)); + + // Insert a cookie "a" for path "/path1". This should overwrite. + EXPECT_TRUE(this->SetCookie(cs, url_google, + "a=val33; path=/path1; " + "expires=Mon, 18-Apr-22 22:50:14 GMT")); + + // Insert a cookie "a" for path "/path2". This should NOT overwrite + // cookie "a", since the path is different. + EXPECT_TRUE(this->SetCookie(cs, url_google, + "a=val9; path=/path2; " + "expires=Mon, 18-Apr-22 22:50:14 GMT")); + + // Insert a cookie "a" for path "/path1", but this time for "chromium.org". + // Although the name and path match, the hostnames do not, so shouldn't + // overwrite. + EXPECT_TRUE(this->SetCookie(cs, url_chromium, + "a=val99; path=/path1; " + "expires=Mon, 18-Apr-22 22:50:14 GMT")); + + if (TypeParam::supports_http_only) { + this->MatchCookieLines("a=val33", + this->GetCookies(cs, GURL("http://www.google.com/path1"))); + } else { + this->MatchCookieLines("a=val33; b=val2", + this->GetCookies(cs, GURL("http://www.google.com/path1"))); + } + this->MatchCookieLines("a=val9", + this->GetCookies(cs, GURL("http://www.google.com/path2"))); + this->MatchCookieLines("a=val99", + this->GetCookies(cs, GURL("http://chromium.org/path1"))); +} + +TYPED_TEST_P(CookieStoreTest, CookieOrdering) { + // Put a random set of cookies into a store and make sure they're returned in + // the right order. + // Cookies should be sorted by path length and creation time, as per RFC6265. + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + EXPECT_TRUE(this->SetCookie(cs, GURL("http://d.c.b.a.google.com/aa/x.html"), + "c=1")); + EXPECT_TRUE(this->SetCookie(cs, GURL("http://b.a.google.com/aa/bb/cc/x.html"), + "d=1; domain=b.a.google.com")); + base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds( + TypeParam::creation_time_granularity_in_ms)); + EXPECT_TRUE(this->SetCookie(cs, GURL("http://b.a.google.com/aa/bb/cc/x.html"), + "a=4; domain=b.a.google.com")); + base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds( + TypeParam::creation_time_granularity_in_ms)); + EXPECT_TRUE(this->SetCookie(cs, + GURL("http://c.b.a.google.com/aa/bb/cc/x.html"), + "e=1; domain=c.b.a.google.com")); + EXPECT_TRUE(this->SetCookie(cs, + GURL("http://d.c.b.a.google.com/aa/bb/x.html"), + "b=1")); + EXPECT_TRUE(this->SetCookie(cs, GURL("http://news.bbc.co.uk/midpath/x.html"), + "g=10")); + EXPECT_EQ("d=1; a=4; e=1; b=1; c=1", + this->GetCookies(cs, GURL("http://d.c.b.a.google.com/aa/bb/cc/dd"))); +} + +REGISTER_TYPED_TEST_CASE_P(CookieStoreTest, + TypeTest, DomainTest, DomainWithTrailingDotTest, ValidSubdomainTest, + InvalidDomainTest, DomainWithoutLeadingDotTest, CaseInsensitiveDomainTest, + TestIpAddress, TestNonDottedAndTLD, TestHostEndsWithDot, InvalidScheme, + InvalidScheme_Read, PathTest, HttpOnlyTest, TestGetCookiesWithInfo, + TestCookieDeletion, TestDeleteAllCreatedBetween, TestSecure, + NetUtilCookieTest, OverwritePersistentCookie, CookieOrdering); + +template<class CookieStoreTestTraits> +class MultiThreadedCookieStoreTest : + public CookieStoreTest<CookieStoreTestTraits> { + public: + MultiThreadedCookieStoreTest() : other_thread_("CMTthread") {} + + // Helper methods for calling the asynchronous CookieStore methods + // from a different thread. + + void GetCookiesTask(CookieStore* cs, + const GURL& url, + GetCookieStringCallback* callback) { + CookieOptions options; + if (!CookieStoreTestTraits::supports_http_only) + options.set_include_httponly(); + cs->GetCookiesWithOptionsAsync( + url, options, + base::Bind(&GetCookieStringCallback::Run, base::Unretained(callback))); + } + + void GetCookiesWithOptionsTask(CookieStore* cs, + const GURL& url, + const CookieOptions& options, + GetCookieStringCallback* callback) { + cs->GetCookiesWithOptionsAsync( + url, options, + base::Bind(&GetCookieStringCallback::Run, base::Unretained(callback))); + } + + void GetCookiesWithInfoTask(CookieStore* cs, + const GURL& url, + const CookieOptions& options, + GetCookiesWithInfoCallback* callback) { + cs->GetCookiesWithInfoAsync( + url, options, + base::Bind(&GetCookiesWithInfoCallback::Run, + base::Unretained(callback))); + } + + void SetCookieWithOptionsTask(CookieStore* cs, + const GURL& url, + const std::string& cookie_line, + const CookieOptions& options, + SetCookieCallback* callback) { + cs->SetCookieWithOptionsAsync( + url, cookie_line, options, + base::Bind(&SetCookieCallback::Run, base::Unretained(callback))); + } + + void DeleteCookieTask(CookieStore* cs, + const GURL& url, + const std::string& cookie_name, + DeleteCookieCallback* callback) { + cs->DeleteCookieAsync( + url, cookie_name, + base::Bind(&DeleteCookieCallback::Run, base::Unretained(callback))); + } + + protected: + void RunOnOtherThread(const base::Closure& task) { + other_thread_.Start(); + other_thread_.message_loop()->PostTask(FROM_HERE, task); + CookieStoreTest<CookieStoreTestTraits>::RunFor(kTimeout); + other_thread_.Stop(); + } + + Thread other_thread_; +}; + +TYPED_TEST_CASE_P(MultiThreadedCookieStoreTest); + +// TODO(ycxiao): Eventually, we will need to create a separate thread, create +// the cookie store on that thread (or at least its store, i.e., the DB +// thread). +TYPED_TEST_P(MultiThreadedCookieStoreTest, ThreadCheckGetCookies) { + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + EXPECT_TRUE(this->SetCookie(cs, this->url_google_, "A=B")); + this->MatchCookieLines("A=B", this->GetCookies(cs, this->url_google_)); + GetCookieStringCallback callback(&this->other_thread_); + base::Closure task = base::Bind( + &net::MultiThreadedCookieStoreTest<TypeParam>::GetCookiesTask, + base::Unretained(this), + cs, this->url_google_, &callback); + this->RunOnOtherThread(task); + EXPECT_TRUE(callback.did_run()); + EXPECT_EQ("A=B", callback.cookie()); +} + +TYPED_TEST_P(MultiThreadedCookieStoreTest, ThreadCheckGetCookiesWithOptions) { + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + CookieOptions options; + if (!TypeParam::supports_http_only) + options.set_include_httponly(); + EXPECT_TRUE(this->SetCookie(cs, this->url_google_, "A=B")); + this->MatchCookieLines("A=B", + this->GetCookiesWithOptions(cs, this->url_google_, options)); + GetCookieStringCallback callback(&this->other_thread_); + base::Closure task = base::Bind( + &net::MultiThreadedCookieStoreTest<TypeParam>::GetCookiesWithOptionsTask, + base::Unretained(this), + cs, this->url_google_, options, &callback); + this->RunOnOtherThread(task); + EXPECT_TRUE(callback.did_run()); + EXPECT_EQ("A=B", callback.cookie()); +} + +TYPED_TEST_P(MultiThreadedCookieStoreTest, ThreadCheckGetCookiesWithInfo) { + if (!TypeParam::supports_cookies_with_info) + return; + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + CookieOptions options; + std::string cookie_line; + std::vector<CookieStore::CookieInfo> cookie_infos; + EXPECT_TRUE(this->SetCookie(cs, this->url_google_, "A=B")); + this->GetCookiesWithInfo(cs, this->url_google_, options, &cookie_line, + &cookie_infos); + this->MatchCookieLines("A=B", cookie_line); + EXPECT_EQ(1U, cookie_infos.size()); + EXPECT_EQ("A", cookie_infos[0].name); + EXPECT_EQ("", cookie_infos[0].mac_key); + EXPECT_EQ("", cookie_infos[0].mac_algorithm); + GetCookiesWithInfoCallback callback(&this->other_thread_); + base::Closure task = base::Bind( + &net::MultiThreadedCookieStoreTest<TypeParam>::GetCookiesWithInfoTask, + base::Unretained(this), cs, this->url_google_, options, &callback); + this->RunOnOtherThread(task); + EXPECT_TRUE(callback.did_run()); + this->MatchCookieLines("A=B", callback.cookie_line()); + EXPECT_EQ(1U, callback.cookie_info().size()); + EXPECT_EQ("A", callback.cookie_info()[0].name); + EXPECT_EQ("", callback.cookie_info()[0].mac_key); + EXPECT_EQ("", callback.cookie_info()[0].mac_algorithm); +} + +TYPED_TEST_P(MultiThreadedCookieStoreTest, ThreadCheckSetCookieWithOptions) { + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + CookieOptions options; + if (!TypeParam::supports_http_only) + options.set_include_httponly(); + EXPECT_TRUE(this->SetCookieWithOptions(cs, this->url_google_, "A=B", + options)); + SetCookieCallback callback(&this->other_thread_); + base::Closure task = base::Bind( + &net::MultiThreadedCookieStoreTest<TypeParam>::SetCookieWithOptionsTask, + base::Unretained(this), + cs, this->url_google_, "A=B", options, &callback); + this->RunOnOtherThread(task); + EXPECT_TRUE(callback.did_run()); + EXPECT_TRUE(callback.result()); +} + +TYPED_TEST_P(MultiThreadedCookieStoreTest, ThreadCheckDeleteCookie) { + scoped_refptr<CookieStore> cs(this->GetCookieStore()); + CookieOptions options; + if (!TypeParam::supports_http_only) + options.set_include_httponly(); + EXPECT_TRUE(this->SetCookieWithOptions(cs, this->url_google_, "A=B", + options)); + this->DeleteCookie(cs, this->url_google_, "A"); + EXPECT_TRUE(this->SetCookieWithOptions(cs, this->url_google_, "A=B", + options)); + DeleteCookieCallback callback(&this->other_thread_); + base::Closure task = base::Bind( + &net::MultiThreadedCookieStoreTest<TypeParam>::DeleteCookieTask, + base::Unretained(this), + cs, this->url_google_, "A", &callback); + this->RunOnOtherThread(task); + EXPECT_TRUE(callback.did_run()); +} + +REGISTER_TYPED_TEST_CASE_P(MultiThreadedCookieStoreTest, + ThreadCheckGetCookies, ThreadCheckGetCookiesWithOptions, + ThreadCheckGetCookiesWithInfo, ThreadCheckSetCookieWithOptions, + ThreadCheckDeleteCookie); + +} // namespace net + +#endif // NET_COOKIES_COOKIE_STORE_UNITTEST_H_ diff --git a/net/cookies/cookie_util.cc b/net/cookies/cookie_util.cc new file mode 100644 index 0000000..6e1833f --- /dev/null +++ b/net/cookies/cookie_util.cc @@ -0,0 +1,79 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/cookies/cookie_util.h" + +#include "base/logging.h" +#include "googleurl/src/gurl.h" +#include "net/base/net_util.h" +#include "net/base/registry_controlled_domain.h" + +namespace net { +namespace cookie_util { + +bool DomainIsHostOnly(const std::string& domain_string) { + return (domain_string.empty() || domain_string[0] != '.'); +} + +std::string GetEffectiveDomain(const std::string& scheme, + const std::string& host) { + if (scheme == "http" || scheme == "https") + return RegistryControlledDomainService::GetDomainAndRegistry(host); + + if (!DomainIsHostOnly(host)) + return host.substr(1); + return host; +} + +bool GetCookieDomainWithString(const GURL& url, + const std::string& domain_string, + std::string* result) { + const std::string url_host(url.host()); + + // If no domain was specified in the domain string, default to a host cookie. + // We match IE/Firefox in allowing a domain=IPADDR if it matches the url + // ip address hostname exactly. It should be treated as a host cookie. + if (domain_string.empty() || + (url.HostIsIPAddress() && url_host == domain_string)) { + *result = url_host; + DCHECK(DomainIsHostOnly(*result)); + return true; + } + + // Get the normalized domain specified in cookie line. + url_canon::CanonHostInfo ignored; + std::string cookie_domain(CanonicalizeHost(domain_string, &ignored)); + if (cookie_domain.empty()) + return false; + if (cookie_domain[0] != '.') + cookie_domain = "." + cookie_domain; + + // Ensure |url| and |cookie_domain| have the same domain+registry. + const std::string url_scheme(url.scheme()); + const std::string url_domain_and_registry( + GetEffectiveDomain(url_scheme, url_host)); + if (url_domain_and_registry.empty()) + return false; // IP addresses/intranet hosts can't set domain cookies. + const std::string cookie_domain_and_registry( + GetEffectiveDomain(url_scheme, cookie_domain)); + if (url_domain_and_registry != cookie_domain_and_registry) + return false; // Can't set a cookie on a different domain + registry. + + // Ensure |url_host| is |cookie_domain| or one of its subdomains. Given that + // we know the domain+registry are the same from the above checks, this is + // basically a simple string suffix check. + const bool is_suffix = (url_host.length() < cookie_domain.length()) ? + (cookie_domain != ("." + url_host)) : + (url_host.compare(url_host.length() - cookie_domain.length(), + cookie_domain.length(), cookie_domain) != 0); + if (is_suffix) + return false; + + *result = cookie_domain; + return true; +} + +} // namespace cookie_utils +} // namespace net + diff --git a/net/cookies/cookie_util.h b/net/cookies/cookie_util.h new file mode 100644 index 0000000..4c6e72f --- /dev/null +++ b/net/cookies/cookie_util.h @@ -0,0 +1,40 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef NET_COOKIES_COOKIE_UTIL_H_ +#define NET_COOKIES_COOKIE_UTIL_H_ +#pragma once + +#include <string> + +#include "net/base/net_export.h" + +class GURL; + +namespace net { +namespace cookie_util { + +// Returns the effective TLD+1 for a given host. This only makes sense for http +// and https schemes. For other schemes, the host will be returned unchanged +// (minus any leading period). +NET_EXPORT std::string GetEffectiveDomain(const std::string& scheme, + const std::string& host); + +// Determine the actual cookie domain based on the domain string passed +// (if any) and the URL from which the cookie came. +// On success returns true, and sets cookie_domain to either a +// -host cookie domain (ex: "google.com") +// -domain cookie domain (ex: ".google.com") +NET_EXPORT bool GetCookieDomainWithString(const GURL& url, + const std::string& domain_string, + std::string* result); + +// Returns true if a domain string represents a host-only cookie, +// i.e. it doesn't begin with a leading '.' character. +NET_EXPORT bool DomainIsHostOnly(const std::string& domain_string); + +} // namspace cookie_util +} // namespace net + +#endif // NET_COOKIES_COOKIE_UTIL_H_ diff --git a/net/cookies/cookie_util_unittest.cc b/net/cookies/cookie_util_unittest.cc new file mode 100644 index 0000000..5d2a420 --- /dev/null +++ b/net/cookies/cookie_util_unittest.cc @@ -0,0 +1,23 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/basictypes.h" +#include "net/cookies/cookie_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +TEST(CookieUtilTest, TestDomainIsHostOnly) { + const struct { + const char* str; + const bool is_host_only; + } tests[] = { + { "", true }, + { "www.google.com", true }, + { ".google.com", false } + }; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(tests); ++i) { + EXPECT_EQ(tests[i].is_host_only, + net::cookie_util::DomainIsHostOnly(tests[i].str)); + } +} |