Move URL from WebCore to WTF
[WebKit-https.git] / Source / WebCore / platform / network / curl / CookieJarDB.cpp
1 /*
2  * Copyright (C) 2018 Sony Interactive Entertainment Inc.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  * notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  * notice, this list of conditions and the following disclaimer in the
11  * documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16  * DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY
17  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
20  * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
23  */
24
25 #include "config.h"
26 #include "CookieJarDB.h"
27
28 #include "CookieUtil.h"
29 #include "FileSystem.h"
30 #include "Logging.h"
31 #include "SQLiteFileSystem.h"
32
33 #include <wtf/MonotonicTime.h>
34 #include <wtf/URL.h>
35
36 #if ENABLE(PUBLIC_SUFFIX_LIST)
37 #include "PublicSuffix.h"
38 #endif
39
40 namespace WebCore {
41
42 #define CORRUPT_MARKER_SUFFIX "-corrupted"
43
44 // At least 50 cookies per domain (RFC6265 6.1. Limits)
45 #define MAX_COOKIE_PER_DOMAIN 80
46
47 #define CREATE_COOKIE_TABLE_SQL \
48     "CREATE TABLE IF NOT EXISTS Cookie ("\
49     "  name TEXT NOT NULL,"\
50     "  value TEXT,"\
51     "  domain TEXT NOT NULL,"\
52     "  path TEXT NOT NULL,"\
53     "  expires INTEGER NOT NULL,"\
54     "  size INTEGER NOT NULL,"\
55     "  session INTEGER NOT NULL,"\
56     "  httponly INTEGER NOT NULL DEFAULT 0,"\
57     "  secure INTEGER NOT NULL DEFAULT 0,"\
58     "  lastupdated INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, "\
59     "  UNIQUE(name, domain, path));"
60 #define CREATE_DOMAIN_INDEX_SQL \
61     "CREATE INDEX IF NOT EXISTS domain_index ON Cookie(domain);"
62 #define CREATE_PATH_INDEX_SQL \
63     "CREATE INDEX IF NOT EXISTS path_index ON Cookie(path);"
64 #define CHECK_EXISTS_HTTPONLY_COOKIE_SQL \
65     "SELECT name FROM Cookie WHERE (name = ?) AND (domain = ?) AND (path = ?) AND (httponly = 1);"
66 #define SET_COOKIE_SQL \
67     "INSERT OR REPLACE INTO Cookie (name, value, domain, path, expires, size, session, httponly, secure) "\
68     "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);"
69 #define DELETE_COOKIE_BY_NAME_DOMAIN_PATH_SQL \
70     "DELETE FROM Cookie WHERE name = ? AND domain = ? AND path = ?;"
71 #define DELETE_COOKIE_BY_NAME_DOMAIN_SQL \
72     "DELETE FROM Cookie WHERE name = ? AND domain = ?;"
73 #define DELETE_ALL_SESSION_COOKIE_SQL \
74     "DELETE FROM Cookie WHERE session = 1;"
75 #define DELETE_ALL_COOKIE_SQL \
76     "DELETE FROM Cookie;"
77
78
79 // If the database schema is updated:
80 // - Increment schemaVersion
81 // - Add upgrade logic in verifySchemaVersion to migrate databases from the previous schema version
82 static constexpr int schemaVersion = 1;
83
84
85 void CookieJarDB::setEnabled(bool enable)
86 {
87     m_isEnabled = enable;
88 }
89
90 CookieJarDB::CookieJarDB(const String& databasePath)
91     : m_databasePath(databasePath)
92 {
93 }
94
95 CookieJarDB::~CookieJarDB()
96 {
97     closeDatabase();
98 }
99
100 void CookieJarDB::open()
101 {
102     if (!m_database.isOpen()) {
103         checkDatabaseCorruptionAndRemoveIfNeeded();
104         openDatabase();
105     }
106 }
107
108 bool CookieJarDB::openDatabase()
109 {
110     if (m_database.isOpen())
111         return true;
112
113     bool existsDatabaseFile = false;
114     if (!isOnMemory())
115         existsDatabaseFile = SQLiteFileSystem::ensureDatabaseFileExists(m_databasePath, false);
116
117     if (existsDatabaseFile) {
118         if (m_database.open(m_databasePath, false)) {
119             if (checkDatabaseValidity())
120                 executeSql(DELETE_ALL_SESSION_COOKIE_SQL);
121             else {
122                 // delete database and try to re-create again
123                 LOG_ERROR("Cookie database validity check failed, attempting to recreate the database");
124                 m_database.close();
125                 deleteAllDatabaseFiles();
126                 existsDatabaseFile = false;
127             }
128         } else {
129             LOG_ERROR("Failed to open cookie database: %s, attempting to recreate the database", m_databasePath.utf8().data());
130             deleteAllDatabaseFiles();
131             existsDatabaseFile = false;
132         }
133     }
134
135     if (!existsDatabaseFile) {
136         if (!FileSystem::makeAllDirectories(FileSystem::directoryName(m_databasePath)))
137             LOG_ERROR("Unable to create the Cookie Database path %s", m_databasePath.utf8().data());
138
139         m_database.open(m_databasePath, false);
140     }
141
142     if (!m_database.isOpen())
143         return false;
144
145     if (!isOnMemory() && !m_database.turnOnIncrementalAutoVacuum())
146         LOG_ERROR("Unable to turn on incremental auto-vacuum (%d %s)", m_database.lastError(), m_database.lastErrorMsg());
147
148     verifySchemaVersion();
149
150     if (!existsDatabaseFile || !m_database.tableExists("Cookie")) {
151         bool ok = executeSql(CREATE_COOKIE_TABLE_SQL) && executeSql(CREATE_DOMAIN_INDEX_SQL) && executeSql(CREATE_PATH_INDEX_SQL);
152
153         if (!ok) {
154             // give up create database at this time (all cookies on request/response are ignored)
155             m_database.close();
156             deleteAllDatabaseFiles();
157             return false;
158         }
159     }
160
161     m_database.setSynchronous(SQLiteDatabase::SyncNormal);
162
163     // create prepared statements
164     createPrepareStatement(SET_COOKIE_SQL);
165     createPrepareStatement(CHECK_EXISTS_HTTPONLY_COOKIE_SQL);
166     createPrepareStatement(DELETE_COOKIE_BY_NAME_DOMAIN_PATH_SQL);
167     createPrepareStatement(DELETE_COOKIE_BY_NAME_DOMAIN_SQL);
168
169     return true;
170 }
171
172 void CookieJarDB::closeDatabase()
173 {
174     if (m_database.isOpen()) {
175         for (const auto& statement : m_statements)
176             statement.value.get()->finalize();
177         m_statements.clear();
178         m_database.close();
179     }
180 }
181
182 void CookieJarDB::verifySchemaVersion()
183 {
184     if (isOnMemory())
185         return;
186
187     int version = SQLiteStatement(m_database, "PRAGMA user_version").getColumnInt(0);
188     if (version == schemaVersion)
189         return;
190
191     switch (version) {
192         // Placeholder for schema version upgrade logic
193         // Ensure cases fall through to the next version's upgrade logic
194
195     case 0:
196         deleteAllTables();
197         break;
198     default:
199         // This case can be reached when downgrading versions
200         LOG_ERROR("Unknown cookie database version: %d", version);
201         deleteAllTables();
202         break;
203     }
204
205     // Update version
206     executeSql(String::format("PRAGMA user_version=%d", schemaVersion));
207 }
208
209 void CookieJarDB::deleteAllTables()
210 {
211     if (!m_database.isOpen())
212         return;
213
214     m_database.clearAllTables();
215 }
216
217 String CookieJarDB::getCorruptionMarkerPath() const
218 {
219     ASSERT(!isOnMemory());
220
221     return m_databasePath + CORRUPT_MARKER_SUFFIX;
222 }
223
224 void CookieJarDB::flagDatabaseCorruption()
225 {
226     if (isOnMemory())
227         return;
228
229     auto handle = FileSystem::openFile(getCorruptionMarkerPath(), FileSystem::FileOpenMode::Write);
230     if (FileSystem::isHandleValid(handle))
231         FileSystem::closeFile(handle);
232 }
233
234 bool CookieJarDB::checkDatabaseCorruptionAndRemoveIfNeeded()
235 {
236     if (!isOnMemory() && FileSystem::fileExists(getCorruptionMarkerPath())) {
237         LOG_ERROR("Detected cookie database corruption, attempting to recreate the database");
238         deleteAllDatabaseFiles();
239         return true;
240     }
241
242     return false;
243 }
244
245 bool CookieJarDB::checkSQLiteReturnCode(int code)
246 {
247     if (!m_detectedDatabaseCorruption) {
248         switch (code) {
249         case SQLITE_CORRUPT:
250         case SQLITE_SCHEMA:
251         case SQLITE_FORMAT:
252         case SQLITE_NOTADB:
253             flagDatabaseCorruption();
254             m_detectedDatabaseCorruption = true;
255         }
256     }
257     return code == SQLITE_OK || code == SQLITE_DONE || code == SQLITE_ROW;
258 }
259
260 bool CookieJarDB::checkDatabaseValidity()
261 {
262     ASSERT(m_database.isOpen());
263
264     if (!m_database.tableExists("Cookie"))
265         return false;
266
267     SQLiteStatement integrity(m_database, "PRAGMA quick_check;");
268     if (integrity.prepare() != SQLITE_OK) {
269         LOG_ERROR("Failed to execute database integrity check");
270         return false;
271     }
272
273     int resultCode = integrity.step();
274     if (resultCode != SQLITE_ROW) {
275         LOG_ERROR("Integrity quick_check step returned %d", resultCode);
276         return false;
277     }
278
279     int columns = integrity.columnCount();
280     if (columns != 1) {
281         LOG_ERROR("Received %i columns performing integrity check, should be 1", columns);
282         return false;
283     }
284
285     String resultText = integrity.getColumnText(0);
286
287     if (resultText != "ok") {
288         LOG_ERROR("Cookie database integrity check failed - %s", resultText.ascii().data());
289         return false;
290     }
291
292     return true;
293 }
294
295 void CookieJarDB::deleteAllDatabaseFiles()
296 {
297     closeDatabase();
298     if (isOnMemory())
299         return;
300
301     FileSystem::deleteFile(m_databasePath);
302     FileSystem::deleteFile(getCorruptionMarkerPath());
303     FileSystem::deleteFile(m_databasePath + "-shm");
304     FileSystem::deleteFile(m_databasePath + "-wal");
305 }
306
307 bool CookieJarDB::isEnabled() const
308 {
309     if (m_databasePath.isEmpty())
310         return false;
311
312     return m_isEnabled;
313 }
314
315 std::optional<Vector<Cookie>> CookieJarDB::searchCookies(const String& requestUrl, const std::optional<bool>& httpOnly, const std::optional<bool>& secure, const std::optional<bool>& session)
316 {
317     if (!isEnabled() || !m_database.isOpen())
318         return std::nullopt;
319
320     URL requestUrlObj({ }, requestUrl);
321     String requestHost(requestUrlObj.host().toString().convertToASCIILowercase());
322     String requestPath(requestUrlObj.path().convertToASCIILowercase());
323
324     if (requestHost.isEmpty())
325         return std::nullopt;
326
327     if (requestPath.isEmpty())
328         requestPath = "/";
329
330     const String sql =
331         "SELECT name, value, domain, path, expires, httponly, secure, session FROM Cookie WHERE "\
332         "(NOT ((session = 0) AND (datetime(expires, 'unixepoch') < datetime('now')))) "\
333         "AND (httponly = COALESCE(NULLIF(?, -1), httponly)) "\
334         "AND (secure = COALESCE(NULLIF(?, -1), secure)) "\
335         "AND (session = COALESCE(NULLIF(?, -1), session)) "\
336         "AND ((domain = ?) OR (domain GLOB ?)) "\
337         "ORDER BY length(path) DESC, lastupdated";
338
339     auto pstmt = std::make_unique<SQLiteStatement>(m_database, sql);
340     if (!pstmt)
341         return std::nullopt;
342
343     pstmt->prepare();
344     pstmt->bindInt(1, httpOnly ? *httpOnly : -1);
345     pstmt->bindInt(2, secure ? *secure : -1);
346     pstmt->bindInt(3, session ? *session : -1);
347     pstmt->bindText(4, requestHost);
348
349     if (CookieUtil::isIPAddress(requestHost) || !requestHost.contains('.'))
350         pstmt->bindNull(5);
351     else {
352 #if ENABLE(PUBLIC_SUFFIX_LIST)
353         String topPrivateDomain = topPrivatelyControlledDomain(requestHost);
354         if (!topPrivateDomain.isEmpty())
355             pstmt->bindText(5, String("*.") + topPrivateDomain);
356         else
357             pstmt->bindNull(5);
358 #else
359         // Fallback to glob for cookies under the second level domain e.g. *.domain.com
360         // This will return too many cookies under multilevel tlds such as *.co.uk, but they will get filtered out later.
361         size_t topLevelSeparator = requestHost.reverseFind('.');
362         size_t secondLevelSeparator = requestHost.reverseFind('.', topLevelSeparator-1);
363         String localDomain = secondLevelSeparator == notFound ? requestHost : requestHost.substring(secondLevelSeparator+1);
364
365         ASSERT(!localDomain.isEmpty());
366         pstmt->bindText(5, String("*.") + localDomain);
367 #endif
368     }
369
370     if (!pstmt)
371         return std::nullopt;
372
373     Vector<Cookie> results;
374
375     while (pstmt->step() == SQLITE_ROW) {
376
377         if (results.size() > MAX_COOKIE_PER_DOMAIN)
378             break;
379
380         String cookieName = pstmt->getColumnText(0);
381         String cookieValue = pstmt->getColumnText(1);
382         String cookieDomain = pstmt->getColumnText(2).convertToASCIILowercase();
383         String cookiePath = pstmt->getColumnText(3).convertToASCIILowercase();
384         double cookieExpires = (double)pstmt->getColumnInt64(4) * 1000;
385         bool cookieHttpOnly = (pstmt->getColumnInt(5) == 1);
386         bool cookieSecure = (pstmt->getColumnInt(6) == 1);
387         bool cookieSession = (pstmt->getColumnInt(7) == 1);
388
389         if (!CookieUtil::domainMatch(cookieDomain, requestHost))
390             continue;
391
392         // https://tools.ietf.org/html/rfc6265#section-5.1.4 "Paths and Path-Match"
393         bool isPathMatched = cookiePath == requestPath
394             || (requestPath.startsWith(cookiePath) && cookiePath.endsWith('/'))
395             || (requestPath.startsWith(cookiePath) && (requestPath.characterAt(cookiePath.length()) == '/'));
396
397         if (!isPathMatched)
398             continue;
399
400         Cookie cookie;
401         cookie.name = cookieName;
402         cookie.value = cookieValue;
403         cookie.domain = cookieDomain;
404         cookie.path = cookiePath;
405         cookie.expires = cookieExpires;
406         cookie.httpOnly = cookieHttpOnly;
407         cookie.secure = cookieSecure;
408         cookie.session = cookieSession;
409         results.append(WTFMove(cookie));
410     }
411     pstmt->finalize();
412
413     return results;
414 }
415
416 bool CookieJarDB::hasHttpOnlyCookie(const String& name, const String& domain, const String& path)
417 {
418     auto& statement = preparedStatement(CHECK_EXISTS_HTTPONLY_COOKIE_SQL);
419
420     statement.bindText(1, name);
421     statement.bindText(2, domain);
422     statement.bindText(3, path);
423
424     return statement.step() == SQLITE_ROW;
425 }
426
427 bool CookieJarDB::canAcceptCookie(const Cookie& cookie, const String& host, CookieJarDB::Source source)
428 {
429 #if ENABLE(PUBLIC_SUFFIX_LIST)
430     if (isPublicSuffix(cookie.domain))
431         return false;
432 #endif
433
434     bool fromJavaScript = source == CookieJarDB::Source::Script;
435     if (fromJavaScript && (cookie.httpOnly || hasHttpOnlyCookie(cookie.name, cookie.domain, cookie.path)))
436         return false;
437
438     if (!CookieUtil::domainMatch(cookie.domain, host))
439         return false;
440
441     return true;
442 }
443
444 bool CookieJarDB::setCookie(const Cookie& cookie)
445 {
446     if (!cookie.session && MonotonicTime::fromRawSeconds(cookie.expires) <= MonotonicTime::now())
447         return deleteCookieInternal(cookie.name, cookie.domain, cookie.path);
448
449     auto& statement = preparedStatement(SET_COOKIE_SQL);
450
451     // FIXME: We should have some eviction policy when a domain goes over MAX_COOKIE_PER_DOMAIN
452     statement.bindText(1, cookie.name);
453     statement.bindText(2, cookie.value);
454     statement.bindText(3, cookie.domain);
455     statement.bindText(4, cookie.path);
456     statement.bindInt64(5, cookie.session ? 0 : static_cast<int64_t>(cookie.expires));
457     statement.bindInt(6, cookie.value.length());
458     statement.bindInt(7, cookie.session ? 1 : 0);
459     statement.bindInt(8, cookie.httpOnly ? 1 : 0);
460     statement.bindInt(9, cookie.secure ? 1 : 0);
461     return checkSQLiteReturnCode(statement.step());
462 }
463
464 bool CookieJarDB::setCookie(const String& url, const String& body, CookieJarDB::Source source)
465 {
466     if (!isEnabled() || !m_database.isOpen())
467         return false;
468
469     if (url.isEmpty() || body.isEmpty())
470         return false;
471
472     URL urlObj({ }, url);
473     String host(urlObj.host().toString());
474     String path(urlObj.path());
475
476     auto cookie = CookieUtil::parseCookieHeader(body);
477     if (!cookie)
478         return false;
479
480     if (cookie->domain.isEmpty())
481         cookie->domain = String(host);
482
483     if (cookie->path.isEmpty())
484         cookie->path = CookieUtil::defaultPathForURL(urlObj);
485
486     if (!canAcceptCookie(*cookie, host, source))
487         return false;
488
489     return setCookie(*cookie);
490 }
491
492 bool CookieJarDB::deleteCookie(const String& url, const String& name)
493 {
494     if (!isEnabled() || !m_database.isOpen())
495         return false;
496
497     String urlCopied = String(url);
498     if (urlCopied.startsWith('.'))
499         urlCopied.remove(0, 1);
500
501     URL urlObj({ }, urlCopied);
502     if (urlObj.isValid()) {
503         String hostStr(urlObj.host().toString());
504         String pathStr(urlObj.path());
505         return deleteCookieInternal(name, hostStr, pathStr);
506     }
507
508     return false;
509 }
510
511 bool CookieJarDB::deleteCookieInternal(const String& name, const String& domain, const String& path)
512 {
513     auto& statement = preparedStatement(path.isEmpty() ? DELETE_COOKIE_BY_NAME_DOMAIN_SQL : DELETE_COOKIE_BY_NAME_DOMAIN_PATH_SQL);
514     statement.bindText(1, name);
515     statement.bindText(2, domain);
516     if (!path.isEmpty())
517         statement.bindText(3, path);
518     return checkSQLiteReturnCode(statement.step());
519 }
520
521 bool CookieJarDB::deleteCookies(const String&)
522 {
523     // NOT IMPLEMENTED
524     // TODO: this function will be called if application calls WKCookieManagerDeleteCookiesForHostname() in WKCookieManager.h.
525     return false;
526 }
527
528 bool CookieJarDB::deleteAllCookies()
529 {
530     if (!isEnabled() || !m_database.isOpen())
531         return false;
532
533     return executeSql(DELETE_ALL_COOKIE_SQL);
534 }
535
536 void CookieJarDB::createPrepareStatement(const String& sql)
537 {
538     auto statement = std::make_unique<SQLiteStatement>(m_database, sql);
539     int ret = statement->prepare();
540     ASSERT(ret == SQLITE_OK);
541     m_statements.add(sql, WTFMove(statement));
542 }
543
544 SQLiteStatement& CookieJarDB::preparedStatement(const String& sql)
545 {
546     const auto& statement = m_statements.get(sql);
547     ASSERT(statement);
548     statement->reset();
549     return *statement;
550 }
551
552 bool CookieJarDB::executeSql(const String& sql)
553 {
554     SQLiteStatement statement(m_database, sql);
555     int ret = statement.prepareAndStep();
556     statement.finalize();
557
558     if (!checkSQLiteReturnCode(ret)) {
559         LOG_ERROR("Failed to execute %s error: %s", sql.ascii().data(), m_database.lastErrorMsg());
560         return false;
561     }
562
563     return true;
564 }
565
566 } // namespace WebCore