/* * Copyright (C) 2008, 2009 Julien Chaffraix * Copyright (C) 2010, 2011, 2012 Research In Motion Limited. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #define ENABLE_COOKIE_DEBUG 0 #define ENABLE_COOKIE_SUPER_VERBOSE_DEBUG 0 #define ENABLE_COOKIE_LIMIT_DEBUG 0 #include "config.h" #include "CookieManager.h" #include "CookieDatabaseBackingStore.h" #include "CookieParser.h" #include "FileSystem.h" #include "Logging.h" #include "WebSettings.h" #include #include #include #include #include #include #include #include #include #include #if ENABLE_COOKIE_DEBUG #include #endif #if ENABLE_COOKIE_SUPER_VERBOSE_DEBUG #define CookieLog(format, ...) BlackBerry::Platform::logAlways(BlackBerry::Platform::LogLevelInfo, format, ## __VA_ARGS__) #else #define CookieLog(format, ...) #endif // ENABLE_COOKIE_SUPER_VERBOSE_DEBUG #if ENABLE_COOKIE_LIMIT_DEBUG #define CookieLimitLog(format, ...) BlackBerry::Platform::logAlways(BlackBerry::Platform::LogLevelInfo, format, ## __VA_ARGS__) #else #define CookieLimitLog(format, ...) #endif // ENABLE_COOKIE_LIMIT_DEBUG namespace WebCore { // Max count constants. static const unsigned s_globalMaxCookieCount = 6000; static const unsigned s_maxCookieCountPerHost = 60; static const unsigned s_cookiesToDeleteWhenLimitReached = 60; static const unsigned s_delayToStartCookieCleanup = 10; CookieManager& cookieManager() { static CookieManager *cookieManager = 0; if (!cookieManager) { // Open the cookieJar now and get the backing store cookies to fill the manager. cookieManager = new CookieManager; cookieManager->m_cookieBackingStore->open(cookieManager->cookieJar()); cookieManager->getBackingStoreCookies(); CookieLog("CookieManager - Backingstore load complete.\n"); } return *cookieManager; } CookieManager::CookieManager() : m_count(0) , m_privateMode(false) , m_shouldDumpAllCookies(false) , m_cookieJarFileName(pathByAppendingComponent(BlackBerry::Platform::Settings::instance()->applicationDataDirectory().c_str(), "/cookieCollection.db")) , m_policy(CookieStorageAcceptPolicyAlways) , m_cookieBackingStore(CookieDatabaseBackingStore::create()) , m_limitTimer(this, &CookieManager::cookieLimitCleanUp) { } CookieManager::~CookieManager() { removeAllCookies(DoNotRemoveFromBackingStore); // FIXME: m_managerMap and the top layer protocolMaps are not properly deleted. // Do not delete any protocol maps to avoid double-deletion of the maps that are // being used for both secure and non-secure protocols; this leak is OK since // there's nothing important in the hashtable destructors, and the memory will be reclaimed on exit // FIXME: CookieDatabaseBackingStore is not deleted, only flushed // (currently the destructor is never called since this class is a // singleton; on exit, the db is flushed manually. This call is only here // as a fallback in case this class is made a non-singleton. m_cookieBackingStore->sendChangesToDatabaseSynchronously(); } // Sorting logic is based on Cookie Spec RFC6265, section 5.4.2 static bool cookieSorter(ParsedCookie* a, ParsedCookie* b) { if (a->path().length() == b->path().length()) return a->creationTime() < b->creationTime(); return a->path().length() > b->path().length(); } // Returns whether the protocol supports domains static bool shouldIgnoreDomain(const String protocol) { // ignore domain security for file and local return protocol == "file" || protocol == "local"; } void CookieManager::setCookies(const KURL& url, const String& value, CookieFilter filter) { CookieLog("CookieManager - Setting cookies"); CookieParser parser(url); Vector cookies = parser.parse(value); for (size_t i = 0; i < cookies.size(); ++i) { BackingStoreRemovalPolicy treatment = m_privateMode ? DoNotRemoveFromBackingStore : RemoveFromBackingStore; checkAndTreatCookie(cookies[i], treatment, filter); } } void CookieManager::setCookies(const KURL& url, const Vector& cookies, CookieFilter filter) { CookieLog("CookieManager - Setting cookies"); CookieParser parser(url); for (size_t i = 0; i < cookies.size(); ++i) { BackingStoreRemovalPolicy treatment = m_privateMode ? DoNotRemoveFromBackingStore : RemoveFromBackingStore; if (ParsedCookie* parsedCookie = parser.parseOneCookie(cookies[i])) checkAndTreatCookie(parsedCookie, treatment, filter); } } String CookieManager::getCookie(const KURL& url, CookieFilter filter) const { Vector rawCookies; rawCookies.reserveInitialCapacity(s_maxCookieCountPerHost); // Retrieve cookies related to this url getRawCookies(rawCookies, url, filter); CookieLog("CookieManager - there are %d cookies in raw cookies\n", rawCookies.size()); // Generate the cookie header string using the retrieved cookies StringBuilder cookieStringBuilder; cookieStringBuilder.reserveCapacity(512); size_t cookieSize = rawCookies.size(); for (size_t i = 0; i < cookieSize; i++) { cookieStringBuilder.append(rawCookies[i]->toNameValuePair()); if (i != cookieSize-1) cookieStringBuilder.append("; "); } CookieLog("CookieManager - cookieString is - %s\n", cookieStringBuilder.toString().utf8().data()); return cookieStringBuilder.toString(); } String CookieManager::generateHtmlFragmentForCookies() { CookieLog("CookieManager - generateHtmlFragmentForCookies\n"); Vector cookieCandidates; for (HashMap::iterator it = m_managerMap.begin(); it != m_managerMap.end(); ++it) it->second->getAllChildCookies(&cookieCandidates); String result; ParsedCookie* cookie = 0; result.append(String("")); for (size_t i = 0; i < cookieCandidates.size(); ++i) { cookie = cookieCandidates[i]; result.append(String("")); } result.append(String("
DomainPathProtocolNameValueSecureHttpOnlySession
")); result.append(cookie->domain()); result.append(String("")); result.append(cookie->path()); result.append(String("")); result.append(cookie->protocol()); result.append(String("")); result.append(cookie->name()); result.append(String("")); result.append(cookie->value()); result.append(String("")); result.append(String(cookie->isSecure() ? "Yes" : "No")); result.append(String("")); result.append(String(cookie->isHttpOnly() ? "Yes" : "No")); result.append(String("")); result.append(String(cookie->isSession() ? "Yes" : "No")); result.append(String("
")); return result; } void CookieManager::getRawCookies(Vector &stackOfCookies, const KURL& requestURL, CookieFilter filter) const { CookieLog("CookieManager - getRawCookies - processing url with domain - %s & protocol: %s & path: %s\n", requestURL.host().utf8().data(), requestURL.protocol().utf8().data(), requestURL.path().utf8().data()); bool specialCaseForLocal = (requestURL.protocolIs("local") || requestURL.protocolIs("file")) && m_shouldDumpAllCookies; bool isConnectionSecure = requestURL.protocolIs("https") || requestURL.protocolIs("wss") || specialCaseForLocal; Vector cookieCandidates; Vector protocolsToSearch; // Special Case: If a server sets a "secure" cookie over a non-secure channel and tries to access the cookie // over a secure channel, it will not succeed because the secure protocol isn't mapped to the insecure protocol yet. // Set the map to the non-secure version, so it'll search the mapping for a secure cookie. CookieMap* targetMap = m_managerMap.get(requestURL.protocol()); if (!targetMap && isConnectionSecure) { CookieLog("CookieManager - special case: secure protocol are not linked yet."); if (requestURL.protocolIs("https")) targetMap = m_managerMap.get("http"); else if (requestURL.protocolIs("wss")) targetMap = m_managerMap.get("ws"); } if (specialCaseForLocal) copyValuesToVector(m_managerMap, protocolsToSearch); else { protocolsToSearch.append(targetMap); // FIXME: this is a hack for webworks apps; RFC 6265 says "Cookies do not provide isolation by scheme" // so we should not be checking protocols at all. See PR 135595 if (m_shouldDumpAllCookies) { protocolsToSearch.append(m_managerMap.get("file")); protocolsToSearch.append(m_managerMap.get("local")); } } Vector delimitedHost; // IP addresses are stored in a particular format (due to ipv6). Reduce the ip address so we can match // it with the one in memory. string canonicalIP = BlackBerry::Platform::getCanonicalIPFormat(requestURL.host().utf8().data()); if (!canonicalIP.empty()) delimitedHost.append(String(canonicalIP.c_str())); else requestURL.host().lower().split(".", true, delimitedHost); // Go through all the protocol trees that we need to search for // and get all cookies that are valid for this domain for (size_t k = 0; k < protocolsToSearch.size(); k++) { CookieMap* currentMap = protocolsToSearch[k]; // if no cookies exist for this protocol, break right away if (!currentMap) continue; CookieLog("CookieManager - looking at protocol map %s \n", currentMap->getName().utf8().data()); // Special case for local and files - because WebApps expect to get ALL cookies from the backing-store on local protocol if (specialCaseForLocal) { CookieLog("CookieManager - special case find in protocol map - %s\n", currentMap->getName().utf8().data()); currentMap->getAllChildCookies(&cookieCandidates); } else { // Get cookies from the null domain map currentMap->getAllCookies(&cookieCandidates); // Get cookies from the valid domain maps int i = delimitedHost.size() - 1; while (i >= 0) { CookieLog("CookieManager - finding %s in currentmap\n", delimitedHost[i].utf8().data()); currentMap = currentMap->getSubdomainMap(delimitedHost[i]); // if this subdomain/domain does not exist in our mapping then we simply exit if (!currentMap) { CookieLog("CookieManager - cannot find next map exiting the while loop.\n"); break; } CookieLog("CookieManager - found the map, grabbing cookies from this map\n"); currentMap->getAllCookies(&cookieCandidates); i--; } } } CookieLog("CookieManager - there are %d cookies in candidate\n", cookieCandidates.size()); for (size_t i = 0; i < cookieCandidates.size(); ++i) { ParsedCookie* cookie = cookieCandidates[i]; // According to the path-matches rules in RFC6265, section 5.1.4, // we should add a '/' at the end of cookie-path for comparison if the cookie-path is not end with '/'. String path = cookie->path(); CookieLog("CookieManager - comparing cookie path %s (len %d) to request path %s (len %d)", path.utf8().data(), path.length(), requestURL.path().utf8().data(), path.length()); if (!equalIgnoringCase(path, requestURL.path()) && !path.endsWith("/", false)) path = path + "/"; // Only secure connections have access to secure cookies. Unless specialCaseForLocal is true // Get the cookies filtering out HttpOnly cookies if requested. if (requestURL.path().startsWith(path, false) && (isConnectionSecure || !cookie->isSecure()) && (filter == WithHttpOnlyCookies || !cookie->isHttpOnly())) { CookieLog("CookieManager - cookie chosen - %s\n", cookie->toString().utf8().data()); cookie->setLastAccessed(currentTime()); stackOfCookies.append(cookie); } } std::stable_sort(stackOfCookies.begin(), stackOfCookies.end(), cookieSorter); } void CookieManager::removeAllCookies(BackingStoreRemovalPolicy backingStoreRemoval) { HashMap::iterator first = m_managerMap.begin(); HashMap::iterator end = m_managerMap.end(); for (HashMap::iterator it = first; it != end; ++it) it->second->deleteAllCookiesAndDomains(); if (backingStoreRemoval == RemoveFromBackingStore) m_cookieBackingStore->removeAll(); m_count = 0; } void CookieManager::setCookieJar(const char* fileName) { m_cookieJarFileName = String(fileName); m_cookieBackingStore->open(m_cookieJarFileName); } void CookieManager::checkAndTreatCookie(ParsedCookie* candidateCookie, BackingStoreRemovalPolicy postToBackingStore, CookieFilter filter) { CookieLog("CookieManager - checkAndTreatCookie - processing url with domain - %s & protocol %s\n", candidateCookie->domain().utf8().data(), candidateCookie->protocol().utf8().data()); // A cookie which is not from http shouldn't have a httpOnly property. if (filter == NoHttpOnlyCookie && candidateCookie->isHttpOnly()) { delete candidateCookie; return; } const bool ignoreDomain = shouldIgnoreDomain(candidateCookie->protocol()); // Determine which protocol tree to add the cookie to. Create one if necessary. CookieMap* curMap = 0; if (m_managerMap.contains(candidateCookie->protocol())) curMap = m_managerMap.get(candidateCookie->protocol()); else { // Check if it is a secure version, if it is, link it to the non-secure version // Link curMap to the new protocol as well as the old one if it doesn't exist if (candidateCookie->protocol() == "https") { curMap = m_managerMap.get("http"); if (!curMap) { curMap = new CookieMap("http"); m_managerMap.add("http", curMap); } } else if (candidateCookie->protocol() == "wss") { curMap = m_managerMap.get("ws"); if (!curMap) { curMap = new CookieMap("ws"); m_managerMap.add("ws", curMap); } } else curMap = new CookieMap(candidateCookie->protocol()); CookieLog("CookieManager - adding protocol cookiemap - %s\n", curMap->getName().utf8().data()); m_managerMap.add(candidateCookie->protocol(), curMap); } // If protocol support domain, we have to traverse the domain tree to find the right // cookieMap to handle with if (!ignoreDomain) curMap = findOrCreateCookieMap(curMap, *candidateCookie); // Now that we have the proper map for this cookie, we can modify it // If cookie does not exist and has expired, delete it // If cookie exists and it has expired, so we must remove it from the map, if not update it // If cookie expired and came from the BackingStore (therefore does not exist), we have to remove from database // If cookie does not exist & it's valid, add it to the current map if (candidateCookie->hasExpired() || candidateCookie->isForceExpired()) { // Special case for getBackingStoreCookies() to catch expired cookies if (postToBackingStore == BackingStoreCookieEntry) m_cookieBackingStore->remove(candidateCookie); else if (curMap) { // RemoveCookie will return 0 if the cookie doesn't exist. ParsedCookie* expired = curMap->removeCookie(candidateCookie, filter); // Cookie is useless, Remove the cookie from the backingstore if it exists. // Backup check for BackingStoreCookieEntry incase someone incorrectly uses this enum. if (expired && postToBackingStore != BackingStoreCookieEntry && !expired->isSession()) { CookieLog("CookieManager - expired cookie is nonsession, deleting from db"); m_cookieBackingStore->remove(expired); } delete expired; } else delete candidateCookie; } else { ASSERT(curMap); addCookieToMap(curMap, candidateCookie, postToBackingStore, filter); } } void CookieManager::addCookieToMap(CookieMap* targetMap, ParsedCookie* candidateCookie, BackingStoreRemovalPolicy postToBackingStore, CookieFilter filter) { ParsedCookie* replacedCookie = 0; if (!targetMap->addOrReplaceCookie(candidateCookie, &replacedCookie, filter)) { CookieLog("CookieManager - rejecting new cookie - %s.\n", candidateCookie->toString().utf8().data()); delete candidateCookie; return; } if (replacedCookie) { CookieLog("CookieManager - updating new cookie - %s.\n", candidateCookie->toString().utf8().data()); // A cookie was replaced in targetMap. // If old cookie is non-session and new one is, we have to delete it from backingstore // If new cookie is non-session and old one is, we have to add it to backingstore // If both sessions are non-session, then we update it in the backingstore bool newIsSession = candidateCookie->isSession(); bool oldIsSession = replacedCookie->isSession(); if (postToBackingStore == RemoveFromBackingStore) { if (!newIsSession && !oldIsSession) m_cookieBackingStore->update(candidateCookie); else if (newIsSession && !oldIsSession) { // Must manually decrease the counter because it was not counted when // the cookie was removed in cookieVector. removedCookie(); m_cookieBackingStore->remove(replacedCookie); } else if (!newIsSession && oldIsSession) { // Must manually increase the counter because it was not counted when // the cookie was added in cookieVector. addedCookie(); m_cookieBackingStore->insert(candidateCookie); } } delete replacedCookie; return; } CookieLog("CookieManager - adding new cookie - %s.\n", candidateCookie->toString().utf8().data()); ParsedCookie* oldestCookie = 0; // Check if we have not reached the per cookie domain limit. // If that is not true, we check if the global limit has been reached if backingstore mode is on // Two points: // 1) We only do a global check if backingstore mode is on because the global cookie limit only // counts session cookies that are saved in the database. If the user goes over the limit // when they are in private mode, we know that the total cookie limit will be under the limit // once the user goes back to normal mode (memory deleted and reloaded from the database) // 2) We use else if for this statement because if we remove a cookie in the 1st statement // then it means the global count will never exceed the limit CookieLimitLog("CookieManager - local count: %d global count: %d", targetMap->count(), m_count); if (targetMap->count() > s_maxCookieCountPerHost) { CookieLog("CookieManager - deleting oldest cookie from this map due to domain count.\n"); oldestCookie = targetMap->removeOldestCookie(); } else if (m_count > s_globalMaxCookieCount && (postToBackingStore != DoNotRemoveFromBackingStore)) { CookieLimitLog("CookieManager - Global limit reached, initiate cookie limit clean up."); initiateCookieLimitCleanUp(); } // Only add non session cookie to the backing store. if (postToBackingStore == RemoveFromBackingStore) { if (oldestCookie && !oldestCookie->isSession()) { CookieLog("CookieManager - oldestCookie exists, deleting it from backingstore and destructing.\n"); m_cookieBackingStore->remove(oldestCookie); } if (!candidateCookie->isSession()) m_cookieBackingStore->insert(candidateCookie); } if (oldestCookie) delete oldestCookie; } void CookieManager::getBackingStoreCookies() { // This method should be called just after having created the cookieManager // NEVER afterwards! ASSERT(!m_count); Vector cookies; m_cookieBackingStore->getCookiesFromDatabase(cookies); CookieLog("CookieManager - Backingstore has %d cookies, loading them in memory now", cookies.size()); for (size_t i = 0; i < cookies.size(); ++i) { ParsedCookie* newCookie = cookies[i]; checkAndTreatCookie(newCookie, BackingStoreCookieEntry); } } void CookieManager::setPrivateMode(bool mode) { if (m_privateMode == mode) return; m_privateMode = mode; if (!mode) { removeAllCookies(DoNotRemoveFromBackingStore); getBackingStoreCookies(); } } CookieMap* CookieManager::findOrCreateCookieMap(CookieMap* protocolMap, const ParsedCookie& candidateCookie) { // Explode the domain with the '.' delimiter Vector delimitedHost; // If the domain is an IP address, don't split it. if (candidateCookie.domainIsIPAddress()) delimitedHost.append(candidateCookie.domain()); else candidateCookie.domain().split(".", delimitedHost); CookieMap* curMap = protocolMap; size_t hostSize = delimitedHost.size(); CookieLog("CookieManager - looking at protocol map %s \n", protocolMap->getName().utf8().data()); // Find & create necessary CookieMaps by traversing down the domain tree // Each CookieMap represent a subsection of the domain, delimited by "." int i = hostSize - 1; while (i >= 0) { CookieLog("CookieManager - finding %s in currentmap\n", delimitedHost[i].utf8().data()); CookieMap* nextMap = curMap->getSubdomainMap(delimitedHost[i]); if (!nextMap) { CookieLog("CookieManager - cannot find map\n"); if (candidateCookie.hasExpired()) return 0; CookieLog("CookieManager - creating %s in currentmap %s\n", delimitedHost[i].utf8().data(), curMap->getName().utf8().data()); nextMap = new CookieMap(delimitedHost[i]); CookieLog("CookieManager - adding subdomain to map\n"); curMap->addSubdomainMap(delimitedHost[i], nextMap); } curMap = nextMap; i--; } return curMap; } void CookieManager::removeCookieWithName(const KURL& url, const String& cookieName) { // We get all cookies from all domains that domain matches the request domain // and delete any cookies with the specified name that path matches the request path Vector results; getRawCookies(results, url, WithHttpOnlyCookies); // Delete the cookies that path matches the request path for (size_t i = 0; i < results.size(); i++) { ParsedCookie* cookie = results[i]; if (!equalIgnoringCase(cookie->name(), cookieName)) continue; if (url.path().startsWith(cookie->path(), false)) { cookie->forceExpire(); checkAndTreatCookie(cookie, RemoveFromBackingStore); } } } void CookieManager::initiateCookieLimitCleanUp() { if (!m_limitTimer.isActive()) { CookieLog("CookieManager - Starting a timer for cookie cleanup"); m_limitTimer.startOneShot(s_delayToStartCookieCleanup); } else { #ifndef NDEBUG CookieLog("CookieManager - Cookie cleanup timer already running"); #endif } } void CookieManager::cookieLimitCleanUp(Timer* timer) { ASSERT_UNUSED(timer, timer == &m_limitTimer); CookieLimitLog("CookieManager - Starting cookie clean up"); size_t numberOfCookiesOverLimit = (m_count > s_globalMaxCookieCount) ? m_count - s_globalMaxCookieCount : 0; size_t amountToDelete = s_cookiesToDeleteWhenLimitReached + numberOfCookiesOverLimit; CookieLimitLog("CookieManager - Excess: %d Amount to Delete: %d", numberOfCookiesOverLimit, amountToDelete); // Call the database to delete 'amountToDelete' of cookies Vector cookiesToDelete; cookiesToDelete.reserveInitialCapacity(amountToDelete); CookieLimitLog("CookieManager - Calling database to clean up"); m_cookieBackingStore->getCookiesFromDatabase(cookiesToDelete, amountToDelete); // Cookies are ordered in ASC order by lastAccessed for (size_t i = 0; i < amountToDelete; ++i) { // Expire them and call checkandtreat to delete them from memory and database ParsedCookie* newCookie = cookiesToDelete[i]; CookieLimitLog("CookieManager - Expire cookie: %s and delete", newCookie->toString().utf8().data()); newCookie->forceExpire(); checkAndTreatCookie(newCookie, RemoveFromBackingStore); } CookieLimitLog("CookieManager - Cookie clean up complete."); } } // namespace WebCore