blob: 101624f2900a94cc7d1a89989676d6b45ab7fa3d [file] [log] [blame]
/*
* Copyright (C) 2018 Sony Interactive Entertainment Inc.
*
* 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. AND ITS CONTRIBUTORS ``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 INC. OR ITS 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.
*/
#include "config.h"
#include "CookieJarDB.h"
#include "CookieUtil.h"
#include "Logging.h"
#include "PublicSuffix.h"
#include "RegistrableDomain.h"
#include "SQLiteFileSystem.h"
#include <wtf/FileSystem.h>
#include <wtf/MonotonicTime.h>
#include <wtf/Optional.h>
#include <wtf/URL.h>
#include <wtf/Vector.h>
#include <wtf/text/StringConcatenateNumbers.h>
namespace WebCore {
#define CORRUPT_MARKER_SUFFIX "-corrupted"
// At least 50 cookies per domain (RFC6265 6.1. Limits)
#define MAX_COOKIE_PER_DOMAIN 80
#define CREATE_COOKIE_TABLE_SQL \
"CREATE TABLE IF NOT EXISTS Cookie ("\
" name TEXT NOT NULL,"\
" value TEXT,"\
" domain TEXT NOT NULL,"\
" path TEXT NOT NULL,"\
" expires INTEGER NOT NULL,"\
" size INTEGER NOT NULL,"\
" session INTEGER NOT NULL,"\
" httponly INTEGER NOT NULL DEFAULT 0,"\
" secure INTEGER NOT NULL DEFAULT 0,"\
" lastupdated INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, "\
" UNIQUE(name, domain, path));"
#define CREATE_DOMAIN_INDEX_SQL \
"CREATE INDEX IF NOT EXISTS domain_index ON Cookie(domain);"
#define CREATE_PATH_INDEX_SQL \
"CREATE INDEX IF NOT EXISTS path_index ON Cookie(path);"
#define CHECK_EXISTS_COOKIE_SQL \
"SELECT domain FROM Cookie WHERE ((domain = ?) OR (domain GLOB ?));"
#define CHECK_EXISTS_HTTPONLY_COOKIE_SQL \
"SELECT name FROM Cookie WHERE (name = ?) AND (domain = ?) AND (path = ?) AND (httponly = 1);"
#define SET_COOKIE_SQL \
"INSERT OR REPLACE INTO Cookie (name, value, domain, path, expires, size, session, httponly, secure) "\
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);"
#define DELETE_COOKIE_BY_NAME_DOMAIN_PATH_SQL \
"DELETE FROM Cookie WHERE name = ? AND domain = ? AND path = ?;"
#define DELETE_COOKIE_BY_NAME_DOMAIN_SQL \
"DELETE FROM Cookie WHERE name = ? AND domain = ?;"
#define DELETE_ALL_SESSION_COOKIE_SQL \
"DELETE FROM Cookie WHERE session = 1;"
#define DELETE_ALL_COOKIE_SQL \
"DELETE FROM Cookie;"
// If the database schema is updated:
// - Increment schemaVersion
// - Add upgrade logic in verifySchemaVersion to migrate databases from the previous schema version
static constexpr int schemaVersion = 1;
CookieJarDB::CookieJarDB(const String& databasePath)
: m_databasePath(databasePath)
{
}
CookieJarDB::~CookieJarDB()
{
closeDatabase();
}
void CookieJarDB::open()
{
if (!m_database.isOpen()) {
checkDatabaseCorruptionAndRemoveIfNeeded();
openDatabase();
}
}
bool CookieJarDB::openDatabase()
{
if (m_database.isOpen())
return true;
bool existsDatabaseFile = false;
if (!isOnMemory())
existsDatabaseFile = SQLiteFileSystem::ensureDatabaseFileExists(m_databasePath, false);
if (existsDatabaseFile) {
if (m_database.open(m_databasePath)) {
if (checkDatabaseValidity())
executeSql(DELETE_ALL_SESSION_COOKIE_SQL);
else {
// delete database and try to re-create again
LOG_ERROR("Cookie database validity check failed, attempting to recreate the database");
m_database.close();
deleteAllDatabaseFiles();
existsDatabaseFile = false;
}
} else {
LOG_ERROR("Failed to open cookie database: %s, attempting to recreate the database", m_databasePath.utf8().data());
deleteAllDatabaseFiles();
existsDatabaseFile = false;
}
}
if (!existsDatabaseFile) {
if (!FileSystem::makeAllDirectories(FileSystem::directoryName(m_databasePath)))
LOG_ERROR("Unable to create the Cookie Database path %s", m_databasePath.utf8().data());
m_database.open(m_databasePath);
}
if (!m_database.isOpen())
return false;
if (!isOnMemory() && !m_database.turnOnIncrementalAutoVacuum())
LOG_ERROR("Unable to turn on incremental auto-vacuum (%d %s)", m_database.lastError(), m_database.lastErrorMsg());
verifySchemaVersion();
if (!existsDatabaseFile || !m_database.tableExists("Cookie")) {
bool ok = executeSql(CREATE_COOKIE_TABLE_SQL) && executeSql(CREATE_DOMAIN_INDEX_SQL) && executeSql(CREATE_PATH_INDEX_SQL);
if (!ok) {
// give up create database at this time (all cookies on request/response are ignored)
m_database.close();
deleteAllDatabaseFiles();
return false;
}
}
m_database.setSynchronous(SQLiteDatabase::SyncNormal);
// create prepared statements
createPrepareStatement(SET_COOKIE_SQL);
createPrepareStatement(CHECK_EXISTS_COOKIE_SQL);
createPrepareStatement(CHECK_EXISTS_HTTPONLY_COOKIE_SQL);
createPrepareStatement(DELETE_COOKIE_BY_NAME_DOMAIN_PATH_SQL);
createPrepareStatement(DELETE_COOKIE_BY_NAME_DOMAIN_SQL);
return true;
}
void CookieJarDB::closeDatabase()
{
if (m_database.isOpen()) {
for (const auto& statement : m_statements)
statement.value.get()->finalize();
m_statements.clear();
m_database.close();
}
}
void CookieJarDB::verifySchemaVersion()
{
if (isOnMemory())
return;
int version = SQLiteStatement(m_database, "PRAGMA user_version").getColumnInt(0);
if (version == schemaVersion)
return;
switch (version) {
// Placeholder for schema version upgrade logic
// Ensure cases fall through to the next version's upgrade logic
case 0:
deleteAllTables();
break;
default:
// This case can be reached when downgrading versions
LOG_ERROR("Unknown cookie database version: %d", version);
deleteAllTables();
break;
}
// Update version
executeSql(makeString("PRAGMA user_version=", schemaVersion));
}
void CookieJarDB::deleteAllTables()
{
if (!m_database.isOpen())
return;
m_database.clearAllTables();
}
String CookieJarDB::getCorruptionMarkerPath() const
{
ASSERT(!isOnMemory());
return m_databasePath + CORRUPT_MARKER_SUFFIX;
}
void CookieJarDB::flagDatabaseCorruption()
{
if (isOnMemory())
return;
auto handle = FileSystem::openFile(getCorruptionMarkerPath(), FileSystem::FileOpenMode::Write);
if (FileSystem::isHandleValid(handle))
FileSystem::closeFile(handle);
}
bool CookieJarDB::checkDatabaseCorruptionAndRemoveIfNeeded()
{
if (!isOnMemory() && FileSystem::fileExists(getCorruptionMarkerPath())) {
LOG_ERROR("Detected cookie database corruption, attempting to recreate the database");
deleteAllDatabaseFiles();
return true;
}
return false;
}
bool CookieJarDB::checkSQLiteReturnCode(int code)
{
if (!m_detectedDatabaseCorruption) {
switch (code) {
case SQLITE_CORRUPT:
case SQLITE_SCHEMA:
case SQLITE_FORMAT:
case SQLITE_NOTADB:
flagDatabaseCorruption();
m_detectedDatabaseCorruption = true;
}
}
return code == SQLITE_OK || code == SQLITE_DONE || code == SQLITE_ROW;
}
bool CookieJarDB::checkDatabaseValidity()
{
ASSERT(m_database.isOpen());
if (!m_database.tableExists("Cookie"))
return false;
SQLiteStatement integrity(m_database, "PRAGMA quick_check;");
if (integrity.prepare() != SQLITE_OK) {
LOG_ERROR("Failed to execute database integrity check");
return false;
}
int resultCode = integrity.step();
if (resultCode != SQLITE_ROW) {
LOG_ERROR("Integrity quick_check step returned %d", resultCode);
return false;
}
int columns = integrity.columnCount();
if (columns != 1) {
LOG_ERROR("Received %i columns performing integrity check, should be 1", columns);
return false;
}
String resultText = integrity.getColumnText(0);
if (resultText != "ok") {
LOG_ERROR("Cookie database integrity check failed - %s", resultText.ascii().data());
return false;
}
return true;
}
void CookieJarDB::deleteAllDatabaseFiles()
{
closeDatabase();
if (isOnMemory())
return;
FileSystem::deleteFile(m_databasePath);
FileSystem::deleteFile(getCorruptionMarkerPath());
FileSystem::deleteFile(m_databasePath + "-shm");
FileSystem::deleteFile(m_databasePath + "-wal");
}
bool CookieJarDB::isEnabled() const
{
if (m_databasePath.isEmpty())
return false;
return (m_acceptPolicy == CookieAcceptPolicy::Always || m_acceptPolicy == CookieAcceptPolicy::OnlyFromMainDocumentDomain || m_acceptPolicy == CookieAcceptPolicy::ExclusivelyFromMainDocumentDomain);
}
bool CookieJarDB::checkCookieAcceptPolicy(const URL& firstParty, const URL& url)
{
if (m_acceptPolicy == CookieAcceptPolicy::Always)
return true;
// See https://bugs.webkit.org/show_bug.cgi?id=193458#c0
if (m_acceptPolicy != CookieAcceptPolicy::OnlyFromMainDocumentDomain && m_acceptPolicy != CookieAcceptPolicy::ExclusivelyFromMainDocumentDomain)
return false;
if (firstParty.host() == url.host())
return true;
if (RegistrableDomain(firstParty).matches(url))
return true;
// third-party resources can read or write cookies if they have pre-existing cookies.
if (m_acceptPolicy == CookieAcceptPolicy::OnlyFromMainDocumentDomain && hasCookies(url))
return true;
return false;
}
bool CookieJarDB::hasCookies(const URL& url)
{
String host = url.host().convertToASCIILowercase();
if (host.isEmpty())
return false;
#if ENABLE(PUBLIC_SUFFIX_LIST)
if (isPublicSuffix(host))
return false;
#endif
RegistrableDomain registrableDomain { url };
auto& statement = preparedStatement(CHECK_EXISTS_COOKIE_SQL);
if (CookieUtil::isIPAddress(host) || !host.contains('.') || registrableDomain.isEmpty()) {
statement.bindText(1, host);
statement.bindNull(2);
} else {
statement.bindText(1, registrableDomain.string());
statement.bindText(2, String("*.") + registrableDomain.string());
}
return statement.step() == SQLITE_ROW;
}
Optional<Vector<Cookie>> CookieJarDB::searchCookies(const URL& firstParty, const URL& requestUrl, const Optional<bool>& httpOnly, const Optional<bool>& secure, const Optional<bool>& session)
{
if (!isEnabled() || !m_database.isOpen())
return WTF::nullopt;
String requestHost = requestUrl.host().convertToASCIILowercase();
if (requestHost.isEmpty())
return WTF::nullopt;
if (!checkCookieAcceptPolicy(firstParty, requestUrl))
return WTF::nullopt;
String requestPath = requestUrl.path();
if (requestPath.isEmpty())
requestPath = "/";
RegistrableDomain registrableDomain { requestUrl };
const String sql =
"SELECT name, value, domain, path, expires, httponly, secure, session FROM Cookie WHERE "\
"(NOT ((session = 0) AND (datetime(expires, 'unixepoch') < datetime('now')))) "\
"AND (httponly = COALESCE(NULLIF(?, -1), httponly)) "\
"AND (secure = COALESCE(NULLIF(?, -1), secure)) "\
"AND (session = COALESCE(NULLIF(?, -1), session)) "\
"AND ((domain = ?) OR (domain GLOB ?)) "\
"ORDER BY length(path) DESC, lastupdated";
auto pstmt = makeUnique<SQLiteStatement>(m_database, sql);
if (!pstmt)
return WTF::nullopt;
pstmt->prepare();
pstmt->bindInt(1, httpOnly ? *httpOnly : -1);
pstmt->bindInt(2, secure ? *secure : -1);
pstmt->bindInt(3, session ? *session : -1);
pstmt->bindText(4, requestHost);
if (CookieUtil::isIPAddress(requestHost) || !requestHost.contains('.') || registrableDomain.isEmpty())
pstmt->bindNull(5);
else
pstmt->bindText(5, String("*.") + registrableDomain.string());
if (!pstmt)
return WTF::nullopt;
Vector<Cookie> results;
while (pstmt->step() == SQLITE_ROW) {
if (results.size() > MAX_COOKIE_PER_DOMAIN)
break;
String cookieName = pstmt->getColumnText(0);
String cookieValue = pstmt->getColumnText(1);
String cookieDomain = pstmt->getColumnText(2).convertToASCIILowercase();
String cookiePath = pstmt->getColumnText(3);
double cookieExpires = (double)pstmt->getColumnInt64(4) * 1000;
bool cookieHttpOnly = (pstmt->getColumnInt(5) == 1);
bool cookieSecure = (pstmt->getColumnInt(6) == 1);
bool cookieSession = (pstmt->getColumnInt(7) == 1);
if (!CookieUtil::domainMatch(cookieDomain, requestHost))
continue;
// https://tools.ietf.org/html/rfc6265#section-5.1.4 "Paths and Path-Match"
bool isPathMatched = cookiePath == requestPath
|| (requestPath.startsWith(cookiePath) && cookiePath.endsWith('/'))
|| (requestPath.startsWith(cookiePath) && (requestPath.characterAt(cookiePath.length()) == '/'));
if (!isPathMatched)
continue;
Cookie cookie;
cookie.name = cookieName;
cookie.value = cookieValue;
cookie.domain = cookieDomain;
cookie.path = cookiePath;
cookie.expires = cookieExpires;
cookie.httpOnly = cookieHttpOnly;
cookie.secure = cookieSecure;
cookie.session = cookieSession;
results.append(WTFMove(cookie));
}
pstmt->finalize();
return results;
}
bool CookieJarDB::hasHttpOnlyCookie(const String& name, const String& domain, const String& path)
{
auto& statement = preparedStatement(CHECK_EXISTS_HTTPONLY_COOKIE_SQL);
statement.bindText(1, name);
statement.bindText(2, domain);
statement.bindText(3, path);
return statement.step() == SQLITE_ROW;
}
bool CookieJarDB::canAcceptCookie(const Cookie& cookie, const URL& firstParty, const URL& url, CookieJarDB::Source source)
{
#if ENABLE(PUBLIC_SUFFIX_LIST)
if (isPublicSuffix(cookie.domain))
return false;
#endif
bool fromJavaScript = source == CookieJarDB::Source::Script;
if (fromJavaScript && (cookie.httpOnly || hasHttpOnlyCookie(cookie.name, cookie.domain, cookie.path)))
return false;
if (!CookieUtil::domainMatch(cookie.domain, url.host().convertToASCIILowercase()))
return false;
if (!checkCookieAcceptPolicy(firstParty, url))
return false;
return true;
}
bool CookieJarDB::setCookie(const Cookie& cookie)
{
if (!cookie.session && MonotonicTime::fromRawSeconds(cookie.expires) <= MonotonicTime::now())
return deleteCookieInternal(cookie.name, cookie.domain, cookie.path);
auto& statement = preparedStatement(SET_COOKIE_SQL);
// FIXME: We should have some eviction policy when a domain goes over MAX_COOKIE_PER_DOMAIN
statement.bindText(1, cookie.name);
statement.bindText(2, cookie.value);
statement.bindText(3, cookie.domain);
statement.bindText(4, cookie.path);
statement.bindInt64(5, cookie.session ? 0 : static_cast<int64_t>(cookie.expires));
statement.bindInt(6, cookie.value.length());
statement.bindInt(7, cookie.session ? 1 : 0);
statement.bindInt(8, cookie.httpOnly ? 1 : 0);
statement.bindInt(9, cookie.secure ? 1 : 0);
return checkSQLiteReturnCode(statement.step());
}
bool CookieJarDB::setCookie(const URL& firstParty, const URL& url, const String& body, CookieJarDB::Source source)
{
if (!isEnabled() || !m_database.isOpen())
return false;
if (url.isEmpty() || body.isEmpty())
return false;
auto cookie = CookieUtil::parseCookieHeader(body);
if (!cookie)
return false;
if (cookie->domain.isEmpty())
cookie->domain = url.host().convertToASCIILowercase();
if (cookie->path.isEmpty())
cookie->path = CookieUtil::defaultPathForURL(url);
if (!canAcceptCookie(*cookie, firstParty, url, source))
return false;
return setCookie(*cookie);
}
bool CookieJarDB::deleteCookie(const String& url, const String& name)
{
if (!isEnabled() || !m_database.isOpen())
return false;
String urlCopied = String(url);
if (urlCopied.startsWith('.'))
urlCopied.remove(0, 1);
URL urlObj({ }, urlCopied);
if (urlObj.isValid()) {
String hostStr(urlObj.host().toString());
String pathStr(urlObj.path());
return deleteCookieInternal(name, hostStr, pathStr);
}
return false;
}
bool CookieJarDB::deleteCookieInternal(const String& name, const String& domain, const String& path)
{
auto& statement = preparedStatement(path.isEmpty() ? DELETE_COOKIE_BY_NAME_DOMAIN_SQL : DELETE_COOKIE_BY_NAME_DOMAIN_PATH_SQL);
statement.bindText(1, name);
statement.bindText(2, domain);
if (!path.isEmpty())
statement.bindText(3, path);
return checkSQLiteReturnCode(statement.step());
}
bool CookieJarDB::deleteCookies(const String&)
{
// NOT IMPLEMENTED
// TODO: this function will be called if application calls WKCookieManagerDeleteCookiesForHostname() in WKCookieManager.h.
return false;
}
bool CookieJarDB::deleteAllCookies()
{
if (!isEnabled() || !m_database.isOpen())
return false;
return executeSql(DELETE_ALL_COOKIE_SQL);
}
void CookieJarDB::createPrepareStatement(const String& sql)
{
auto statement = makeUnique<SQLiteStatement>(m_database, sql);
int ret = statement->prepare();
ASSERT(ret == SQLITE_OK);
m_statements.add(sql, WTFMove(statement));
}
SQLiteStatement& CookieJarDB::preparedStatement(const String& sql)
{
const auto& statement = m_statements.get(sql);
ASSERT(statement);
statement->reset();
return *statement;
}
bool CookieJarDB::executeSql(const String& sql)
{
SQLiteStatement statement(m_database, sql);
int ret = statement.prepareAndStep();
statement.finalize();
if (!checkSQLiteReturnCode(ret)) {
LOG_ERROR("Failed to execute %s error: %s", sql.ascii().data(), m_database.lastErrorMsg());
return false;
}
return true;
}
} // namespace WebCore