[Curl] Use SQLite database in cookie jar implementation for Curl port
[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 "SQLiteFileSystem.h"
30 #include "URL.h"
31
32 namespace WebCore {
33
34 #define CORRUPT_MARKER_SUFFIX "-corrupted"
35
36 // At least 50 cookies per domain (RFC6265 6.1. Limits)
37 #define MAX_COOKIE_PER_DOMAIN 80
38
39 #define CREATE_COOKIE_TABLE_SQL \
40     "CREATE TABLE IF NOT EXISTS Cookie ("\
41     "  name TEXT NOT NULL,"\
42     "  value TEXT,"\
43     "  domain TEXT NOT NULL,"\
44     "  path TEXT NOT NULL,"\
45     "  expires INTEGER NOT NULL,"\
46     "  size INTEGER NOT NULL,"\
47     "  session INTEGER NOT NULL,"\
48     "  httponly INTEGER NOT NULL DEFAULT 0,"\
49     "  secure INTEGER NOT NULL DEFAULT 0,"\
50     "  lastupdated INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP, "\
51     "  UNIQUE(name, domain, path));"
52 #define CREATE_DOMAIN_INDEX_SQL \
53     "CREATE INDEX IF NOT EXISTS domain_index ON Cookie(domain);"
54 #define CREATE_PATH_INDEX_SQL \
55     "CREATE INDEX IF NOT EXISTS path_index ON Cookie(path);"
56 #define CHECK_EXISTS_HTTPONLY_COOKIE_SQL \
57     "SELECT name FROM Cookie WHERE (name = ?) AND (domain = ?) AND (path = ?) AND (httponly = 1);"
58 #define SET_COOKIE_SQL \
59     "INSERT OR REPLACE INTO Cookie (name, value, domain, path, expires, size, session, httponly, secure) "\
60     "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);"
61 #define DELETE_COOKIE_BY_NAME_DOMAIN_PATH_SQL \
62     "DELETE FROM Cookie WHERE name = ? AND domain = ? AND path = ?;"
63 #define DELETE_COOKIE_BY_NAME_DOMAIN_SQL \
64     "DELETE FROM Cookie WHERE name = ? AND domain = ?;"
65 #define DELETE_ALL_SESSION_COOKIE_SQL \
66     "DELETE FROM Cookie WHERE session = 1;"
67 #define DELETE_ALL_COOKIE_SQL \
68     "DELETE FROM Cookie;"
69
70 void CookieJarDB::setEnabled(bool enable)
71 {
72     CookieJarDB::m_cookieEnable = enable;
73 }
74
75 CookieJarDB::CookieJarDB(const String& databasePath)
76     : m_databasePath(databasePath)
77 {
78     checkDatabaseCorruptionAndRemoveIfNeeded();
79
80     if (!openDatabase())
81         return;
82 }
83
84 CookieJarDB::~CookieJarDB()
85 {
86     closeDatabase();
87 }
88
89 bool CookieJarDB::openDatabase()
90 {
91     if (m_database.isOpen())
92         return true;
93
94     bool existsDatabaseFile = false;
95     if (!isOnMemory())
96         existsDatabaseFile = SQLiteFileSystem::ensureDatabaseFileExists(m_databasePath, false);
97
98     if (existsDatabaseFile) {
99         if (m_database.open(m_databasePath, false)) {
100             if (checkDatabaseValidity())
101                 executeSimpleSql(DELETE_ALL_SESSION_COOKIE_SQL);
102             else {
103                 // delete database and try to re-create again
104                 m_database.close();
105                 deleteAllDatabaseFiles();
106                 existsDatabaseFile = false;
107             }
108         } else {
109             deleteAllDatabaseFiles();
110             existsDatabaseFile = false;
111         }
112     }
113
114     if (!existsDatabaseFile) {
115         if (m_database.open(m_databasePath, false)) {
116             bool databaseValidity = true;
117             databaseValidity &= (executeSimpleSql(CREATE_COOKIE_TABLE_SQL) == SQLITE_DONE);
118             databaseValidity &= (executeSimpleSql(CREATE_DOMAIN_INDEX_SQL) == SQLITE_DONE);
119             databaseValidity &= (executeSimpleSql(CREATE_PATH_INDEX_SQL) == SQLITE_DONE);
120             if (!databaseValidity) {
121                 // give up create database at this time (all cookies on request/response are ignored)
122                 m_database.close();
123                 deleteAllDatabaseFiles();
124             }
125         }
126     }
127
128     if (!m_database.isOpen())
129         return false;
130
131     executeSimpleSql("PRAGMA temp_store = MEMORY;");
132     executeSimpleSql("PRAGMA synchronous = NORMAL;");
133     executeSimpleSql("PRAGMA journal_mode = WAL;");
134
135     // create prepared statements
136     createPrepareStatement(SET_COOKIE_SQL);
137     createPrepareStatement(CHECK_EXISTS_HTTPONLY_COOKIE_SQL);
138     createPrepareStatement(DELETE_COOKIE_BY_NAME_DOMAIN_PATH_SQL);
139     createPrepareStatement(DELETE_COOKIE_BY_NAME_DOMAIN_SQL);
140
141     return true;
142 }
143
144 void CookieJarDB::closeDatabase()
145 {
146     if (m_database.isOpen()) {
147         for (const auto& statement : m_statements)
148             statement.value.get()->finalize();
149         m_statements.clear();
150         m_database.close();
151     }
152 }
153
154 String CookieJarDB::getCorruptionMarkerPath() const
155 {
156     ASSERT(!isOnMemory());
157
158     return m_databasePath + CORRUPT_MARKER_SUFFIX;
159 }
160
161 void CookieJarDB::flagDatabaseCorruption()
162 {
163     if (isOnMemory())
164         return;
165
166     FILE* f = fopen(getCorruptionMarkerPath().utf8().data(), "wb");
167     fclose(f);
168 }
169
170 bool CookieJarDB::checkDatabaseCorruptionAndRemoveIfNeeded()
171 {
172     if (isOnMemory())
173         return false;
174
175     struct stat st = { 0 };
176     int ret = stat(getCorruptionMarkerPath().utf8().data(), &st);
177     if (!ret)
178         return false;
179
180     deleteAllDatabaseFiles();
181     return true;
182 }
183
184 bool CookieJarDB::checkSQLiteReturnCode(int actual, int expected)
185 {
186     if (!m_detectedDatabaseCorruption) {
187         switch (actual) {
188         case SQLITE_CORRUPT:
189         case SQLITE_SCHEMA:
190         case SQLITE_FORMAT:
191         case SQLITE_NOTADB:
192             flagDatabaseCorruption();
193             m_detectedDatabaseCorruption = true;
194         }
195     }
196
197     return (actual == expected);
198 }
199
200 bool CookieJarDB::checkDatabaseValidity()
201 {
202     ASSERT(m_database.isOpen());
203
204     return m_database.tableExists("Cookie");
205 }
206
207 void CookieJarDB::deleteAllDatabaseFiles()
208 {
209     closeDatabase();
210     if (isOnMemory())
211         return;
212
213     int ret = 0;
214     String removePath;
215
216     // remove marker file (cookie.jar.db-corrupted)
217     ret = remove(removePath.utf8().data());
218
219     // remove shm file (cookie.jar.db-shm)
220     removePath = String(m_databasePath + "-shm");
221     ret = remove(removePath.utf8().data());
222
223     // remove wal file (cookie.jar.db-wal)
224     removePath = String(m_databasePath + "-wal");
225     ret = remove(removePath.utf8().data());
226 }
227
228 bool CookieJarDB::isEnabled()
229 {
230     if (m_databasePath.isEmpty())
231         return false;
232
233     return m_cookieEnable;
234 }
235
236 bool CookieJarDB::searchCookies(const String& requestUrl, const std::optional<bool>& httpOnly, const std::optional<bool>& secure, const std::optional<bool>& session, Vector<Cookie>& results)
237 {
238     if (!isEnabled() || !m_database.isOpen())
239         return false;
240
241     URL requestUrlObj(ParsedURLString, requestUrl);
242     String requestHost(requestUrlObj.host().convertToASCIILowercase());
243     String requestPath(requestUrlObj.path().convertToASCIILowercase());
244
245     if (requestHost.isEmpty())
246         return false;
247
248     if (requestPath.isEmpty())
249         requestPath = "/";
250
251     const String sql =
252         "SELECT name, value, domain, path, expires, httponly, secure, session FROM Cookie WHERE "\
253         "(NOT ((session = 0) AND (datetime(expires, 'unixepoch') < datetime('now')))) "\
254         "AND (httponly = COALESCE(NULLIF(?, -1), httponly)) "\
255         "AND (secure = COALESCE(NULLIF(?, -1), secure)) "\
256         "AND (session = COALESCE(NULLIF(?, -1), session)) "\
257         "AND ((domain = ?) OR (domain GLOB ?))";
258
259     std::unique_ptr<SQLiteStatement> pstmt = std::make_unique<SQLiteStatement>(m_database, sql);
260     pstmt->prepare();
261     pstmt->bindInt(1, httpOnly ? *httpOnly : -1);
262     pstmt->bindInt(2, secure ? *secure : -1);
263     pstmt->bindInt(3, session ? *session : -1);
264     pstmt->bindText(4, requestHost);
265
266     size_t topLevelSeparator = requestHost.reverseFind('.');
267     if (CookieUtil::isIPAddress(requestHost) || topLevelSeparator == notFound)
268         pstmt->bindNull(5);
269     else {
270         /* FIXME: currently we currently do not have a public suffic list in wincairo
271            so right now we glob using just the second level domain e.g. *.DOMAIN.com
272            This will return too many cookies under multilevel tlds such as *.co.uk
273            but we filter those out later
274         */
275
276         size_t secondLevelSeparator = requestHost.reverseFind('.', topLevelSeparator-1);
277         String localDomain = secondLevelSeparator == notFound ? requestHost : requestHost.substring(secondLevelSeparator+1);
278
279         ASSERT(!localDomain.isEmpty());
280         pstmt->bindText(5, String("*.") + localDomain);
281     }
282
283     if (!pstmt)
284         return false;
285
286     results.clear();
287
288     while (pstmt->step() == SQLITE_ROW) {
289
290         if (results.size() > MAX_COOKIE_PER_DOMAIN)
291             break;
292
293         String cookieName = pstmt->getColumnText(0);
294         String cookieValue = pstmt->getColumnText(1);
295         String cookieDomain = pstmt->getColumnText(2).convertToASCIILowercase();
296         String cookiePath = pstmt->getColumnText(3).convertToASCIILowercase();
297         double cookieExpires = (double)pstmt->getColumnInt64(4) * 1000;
298         bool cookieHttpOnly = (pstmt->getColumnInt(5) == 1);
299         bool cookieSecure = (pstmt->getColumnInt(6) == 1);
300         bool cookieSession = (pstmt->getColumnInt(7) == 1);
301
302         if (!CookieUtil::domainMatch(cookieDomain, requestHost))
303             continue;
304
305         // https://tools.ietf.org/html/rfc6265#section-5.1.4 "Paths and Path-Match"
306         bool isPathMatched = cookiePath == requestPath
307             || (requestPath.startsWith(cookiePath) && cookiePath.endsWith('/'))
308             || (requestPath.startsWith(cookiePath) && (requestPath.characterAt(cookiePath.length()) == '/'));
309
310         if (!isPathMatched)
311             continue;
312
313         Cookie result(cookieName,
314             cookieValue,
315             cookieDomain,
316             cookiePath,
317             0,
318             cookieExpires,
319             cookieHttpOnly,
320             cookieSecure,
321             cookieSession,
322             String(),
323             URL(),
324             Vector<uint16_t>());
325
326         results.append(result);
327     }
328     pstmt->finalize();
329
330     return true;
331 }
332
333 bool CookieJarDB::hasHttpOnlyCookie(const String& name, const String& domain, const String& path)
334 {
335     SQLiteStatement* statement = getPrepareStatement(CHECK_EXISTS_HTTPONLY_COOKIE_SQL);
336     ASSERT(statement);
337
338     statement->bindText(1, name);
339     statement->bindText(2, domain);
340     statement->bindText(3, path);
341
342     return statement->step() == SQLITE_ROW;
343 }
344
345
346 int CookieJarDB::setCookie(const Cookie& cookie)
347 {
348     int ret = 0;
349     if (!cookie.session && (cookie.expires < ::time(0)))
350         ret = deleteCookieInternal(cookie.name, cookie.domain, cookie.path);
351     else {
352         SQLiteStatement* statement = getPrepareStatement(SET_COOKIE_SQL);
353         ASSERT(statement);
354
355         // FIXME: We should have some eviction policy when a domain goes over MAX_COOKIE_PER_DOMAIN
356
357         statement->bindText(1, cookie.name);
358         statement->bindText(2, cookie.value);
359         statement->bindText(3, cookie.domain);
360         statement->bindText(4, cookie.path);
361         statement->bindInt64(5, cookie.session ? 0 : (int64_t)cookie.expires);
362         statement->bindInt(6, cookie.value.length());
363         statement->bindInt(7, cookie.session ? 1 : 0);
364         statement->bindInt(8, cookie.httpOnly ? 1 : 0);
365         statement->bindInt(9, cookie.secure ? 1 : 0);
366
367         ret = statement->step();
368     }
369     ASSERT(checkSQLiteReturnCode(ret, SQLITE_DONE));
370
371     return ret;
372 }
373
374 int CookieJarDB::setCookie(const String& url, const String& cookie, bool fromJavaScript)
375 {
376     if (!isEnabled() || !m_database.isOpen())
377         return -1;
378
379     if (url.isEmpty() || cookie.isEmpty())
380         return -1;
381
382     URL urlObj(ParsedURLString, url);
383     String host(urlObj.host());
384     String path(urlObj.path());
385
386     Cookie cookieObj;
387     if (!CookieUtil::parseCookieHeader(cookie, host, cookieObj))
388         return -1;
389
390     if (cookieObj.domain.isEmpty())
391         cookieObj.domain = String(host);
392
393     if (cookieObj.path.isEmpty())
394         cookieObj.path = String(path);
395
396     // FIXME: Need to check that a domain doesn't a set cookie for a tld when wincairo supports PSL
397
398     if (fromJavaScript && cookieObj.httpOnly)
399         return -1;
400
401     if (fromJavaScript && hasHttpOnlyCookie(cookieObj.name, cookieObj.domain, cookieObj.path))
402         return -1;
403
404     return setCookie(cookieObj);
405 }
406
407 int CookieJarDB::deleteCookie(const String& url, const String& name)
408 {
409     if (!isEnabled() || !m_database.isOpen())
410         return -1;
411
412     String urlCopied = String(url);
413     if (urlCopied.startsWith('.'))
414         urlCopied.remove(0, 1);
415
416     URL urlObj(ParsedURLString, urlCopied);
417     if (urlObj.isValid()) {
418         String hostStr(urlObj.host());
419         String pathStr(urlObj.path());
420         int ret = deleteCookieInternal(name, hostStr, pathStr);
421         ASSERT(checkSQLiteReturnCode(ret, SQLITE_DONE));
422
423         return ret;
424     }
425
426     return -1;
427 }
428
429 int CookieJarDB::deleteCookieInternal(const String& name, const String& domain, const String& path)
430 {
431     SQLiteStatement* statement;
432     if (!path.isEmpty()) {
433         statement = getPrepareStatement(DELETE_COOKIE_BY_NAME_DOMAIN_PATH_SQL);
434         ASSERT(statement);
435         statement->bindText(1, name);
436         statement->bindText(2, domain);
437         statement->bindText(3, path);
438     } else {
439         statement = getPrepareStatement(DELETE_COOKIE_BY_NAME_DOMAIN_SQL);
440         ASSERT(statement);
441         statement->bindText(1, name);
442         statement->bindText(2, domain);
443     }
444
445     return statement->step();
446 }
447
448 int CookieJarDB::deleteCookies(const String& url)
449 {
450     // NOT IMPLEMENTED
451     // TODO: this function will be called if application calls WKCookieManagerDeleteCookiesForHostname() in WKCookieManager.h.
452     return 0;
453 }
454
455 int CookieJarDB::deleteAllCookies()
456 {
457     if (!isEnabled() || !m_database.isOpen())
458         return -1;
459
460     return executeSimpleSql(DELETE_ALL_COOKIE_SQL);
461 }
462
463 void CookieJarDB::createPrepareStatement(const char* sql)
464 {
465     auto statement = std::make_unique<SQLiteStatement>(m_database, sql);
466     int ret = statement->prepare();
467     ASSERT(ret == SQLITE_OK);
468     m_statements.add(String(sql), WTFMove(statement));
469 }
470
471 SQLiteStatement* CookieJarDB::getPrepareStatement(const char* sql)
472 {
473     const auto& statement = m_statements.get(String(sql));
474     ASSERT(statement);
475     statement->reset();
476     return statement;
477 }
478
479 int CookieJarDB::executeSimpleSql(const char* sql, bool ignoreError)
480 {
481     SQLiteStatement statement(m_database, sql);
482     int ret = statement.prepareAndStep();
483     statement.finalize();
484
485     ASSERT(checkSQLiteReturnCode(ret, SQLITE_OK)
486         || checkSQLiteReturnCode(ret, SQLITE_DONE)
487         || checkSQLiteReturnCode(ret, SQLITE_ROW)
488         || ignoreError);
489     return ret;
490 }
491
492 } // namespace WebCore