1/*
2 * Copyright (C) 2017 Apple Inc. All rights reserved.
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''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "RegistrationDatabase.h"
28
29#if ENABLE(SERVICE_WORKER)
30
31#include "Logging.h"
32#include "RegistrationStore.h"
33#include "SQLiteDatabase.h"
34#include "SQLiteFileSystem.h"
35#include "SQLiteStatement.h"
36#include "SQLiteTransaction.h"
37#include "SWServer.h"
38#include "SecurityOrigin.h"
39#include <wtf/CompletionHandler.h>
40#include <wtf/CrossThreadCopier.h>
41#include <wtf/FileSystem.h>
42#include <wtf/MainThread.h>
43#include <wtf/NeverDestroyed.h>
44#include <wtf/Scope.h>
45#include <wtf/persistence/PersistentCoders.h>
46#include <wtf/persistence/PersistentDecoder.h>
47#include <wtf/persistence/PersistentEncoder.h>
48#include <wtf/text/StringConcatenateNumbers.h>
49
50namespace WebCore {
51
52static const uint64_t schemaVersion = 4;
53
54static const String recordsTableSchema(const String& tableName)
55{
56 return makeString("CREATE TABLE ", tableName, " (key TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE, origin TEXT NOT NULL ON CONFLICT FAIL, scopeURL TEXT NOT NULL ON CONFLICT FAIL, topOrigin TEXT NOT NULL ON CONFLICT FAIL, lastUpdateCheckTime DOUBLE NOT NULL ON CONFLICT FAIL, updateViaCache TEXT NOT NULL ON CONFLICT FAIL, scriptURL TEXT NOT NULL ON CONFLICT FAIL, script TEXT NOT NULL ON CONFLICT FAIL, workerType TEXT NOT NULL ON CONFLICT FAIL, contentSecurityPolicy BLOB NOT NULL ON CONFLICT FAIL, referrerPolicy TEXT NOT NULL ON CONFLICT FAIL, scriptResourceMap BLOB NOT NULL ON CONFLICT FAIL)");
57}
58
59static const String recordsTableSchema()
60{
61 ASSERT(!isMainThread());
62 static NeverDestroyed<String> schema(recordsTableSchema("Records"));
63 return schema;
64}
65
66static const String recordsTableSchemaAlternate()
67{
68 ASSERT(!isMainThread());
69 static NeverDestroyed<String> schema(recordsTableSchema("\"Records\""));
70 return schema;
71}
72
73static inline String databaseFilenameFromVersion(uint64_t version)
74{
75 return makeString("ServiceWorkerRegistrations-", version, ".sqlite3");
76}
77
78static const String& databaseFilename()
79{
80 ASSERT(isMainThread());
81 static NeverDestroyed<String> filename = databaseFilenameFromVersion(schemaVersion);
82 return filename;
83}
84
85String serviceWorkerRegistrationDatabaseFilename(const String& databaseDirectory)
86{
87 return FileSystem::pathByAppendingComponent(databaseDirectory, databaseFilename());
88}
89
90static inline void cleanOldDatabases(const String& databaseDirectory)
91{
92 for (uint64_t version = 1; version < schemaVersion; ++version)
93 SQLiteFileSystem::deleteDatabaseFile(FileSystem::pathByAppendingComponent(databaseDirectory, databaseFilenameFromVersion(version)));
94}
95
96RegistrationDatabase::RegistrationDatabase(RegistrationStore& store, String&& databaseDirectory)
97 : m_workQueue(WorkQueue::create("ServiceWorker I/O Thread", WorkQueue::Type::Serial))
98 , m_store(makeWeakPtr(store))
99 , m_sessionID(m_store->server().sessionID())
100 , m_databaseDirectory(WTFMove(databaseDirectory))
101 , m_databaseFilePath(FileSystem::pathByAppendingComponent(m_databaseDirectory, databaseFilename()))
102{
103 ASSERT(isMainThread());
104
105 postTaskToWorkQueue([this] {
106 importRecordsIfNecessary();
107 });
108}
109
110RegistrationDatabase::~RegistrationDatabase()
111{
112 ASSERT(isMainThread());
113
114 // The database needs to be destroyed on the background thread.
115 if (m_database)
116 m_workQueue->dispatch([database = WTFMove(m_database)] { });
117}
118
119void RegistrationDatabase::postTaskToWorkQueue(Function<void()>&& task)
120{
121 ASSERT(isMainThread());
122
123 m_workQueue->dispatch([protectedThis = makeRef(*this), task = WTFMove(task)]() mutable {
124 task();
125 });
126}
127
128void RegistrationDatabase::openSQLiteDatabase(const String& fullFilename)
129{
130 ASSERT(!isMainThread());
131 ASSERT(!m_database);
132
133 cleanOldDatabases(m_databaseDirectory);
134
135 LOG(ServiceWorker, "ServiceWorker RegistrationDatabase opening file %s", fullFilename.utf8().data());
136
137 String errorMessage;
138 auto scopeExit = makeScopeExit([this, protectedThis = makeRef(*this), errorMessage = &errorMessage] {
139 ASSERT_UNUSED(errorMessage, !errorMessage->isNull());
140
141#if RELEASE_LOG_DISABLED
142 LOG_ERROR("Failed to open Service Worker registration database: %s", errorMessage->utf8().data());
143#else
144 RELEASE_LOG_ERROR(ServiceWorker, "Failed to open Service Worker registration database: %{public}s", errorMessage->utf8().data());
145#endif
146
147 m_database = nullptr;
148 callOnMainThread([protectedThis = protectedThis.copyRef()] {
149 protectedThis->databaseFailedToOpen();
150 });
151 });
152
153 SQLiteFileSystem::ensureDatabaseDirectoryExists(m_databaseDirectory);
154
155 m_database = std::make_unique<SQLiteDatabase>();
156 if (!m_database->open(fullFilename)) {
157 errorMessage = "Failed to open registration database";
158 return;
159 }
160
161 // Disable threading checks. We always access the database from our serial WorkQueue. Such accesses
162 // are safe since work queue tasks are guaranteed to run one after another. However, tasks will not
163 // necessary run on the same thread every time (as per GCD documentation).
164 m_database->disableThreadingChecks();
165
166 errorMessage = ensureValidRecordsTable();
167 if (!errorMessage.isNull())
168 return;
169
170 errorMessage = importRecords();
171 if (!errorMessage.isNull())
172 return;
173
174 scopeExit.release();
175}
176
177void RegistrationDatabase::importRecordsIfNecessary()
178{
179 ASSERT(!isMainThread());
180
181 if (FileSystem::fileExists(m_databaseFilePath))
182 openSQLiteDatabase(m_databaseFilePath);
183
184 callOnMainThread([protectedThis = makeRef(*this)] {
185 protectedThis->databaseOpenedAndRecordsImported();
186 });
187}
188
189String RegistrationDatabase::ensureValidRecordsTable()
190{
191 ASSERT(!isMainThread());
192 ASSERT(m_database);
193 ASSERT(m_database->isOpen());
194
195 String currentSchema;
196 {
197 // Fetch the schema for an existing records table.
198 SQLiteStatement statement(*m_database, "SELECT type, sql FROM sqlite_master WHERE tbl_name='Records'");
199 if (statement.prepare() != SQLITE_OK)
200 return "Unable to prepare statement to fetch schema for the Records table.";
201
202 int sqliteResult = statement.step();
203
204 // If there is no Records table at all, create it and then bail.
205 if (sqliteResult == SQLITE_DONE) {
206 if (!m_database->executeCommand(recordsTableSchema()))
207 return makeString("Could not create Records table in database (", m_database->lastError(), ") - ", m_database->lastErrorMsg());
208 return { };
209 }
210
211 if (sqliteResult != SQLITE_ROW)
212 return "Error executing statement to fetch schema for the Records table.";
213
214 currentSchema = statement.getColumnText(1);
215 }
216
217 ASSERT(!currentSchema.isEmpty());
218
219 if (currentSchema == recordsTableSchema() || currentSchema == recordsTableSchemaAlternate())
220 return { };
221
222 // This database has a Records table but it is not a schema we expect.
223 // Trying to recover by deleting the data contained within is dangerous so
224 // we should consider this an unrecoverable error.
225 RELEASE_ASSERT_NOT_REACHED();
226}
227
228static String updateViaCacheToString(ServiceWorkerUpdateViaCache update)
229{
230 switch (update) {
231 case ServiceWorkerUpdateViaCache::Imports:
232 return "Imports";
233 case ServiceWorkerUpdateViaCache::All:
234 return "All";
235 case ServiceWorkerUpdateViaCache::None:
236 return "None";
237 }
238
239 RELEASE_ASSERT_NOT_REACHED();
240}
241
242static Optional<ServiceWorkerUpdateViaCache> stringToUpdateViaCache(const String& update)
243{
244 if (update == "Imports")
245 return ServiceWorkerUpdateViaCache::Imports;
246 if (update == "All")
247 return ServiceWorkerUpdateViaCache::All;
248 if (update == "None")
249 return ServiceWorkerUpdateViaCache::None;
250
251 return WTF::nullopt;
252}
253
254static String workerTypeToString(WorkerType workerType)
255{
256 switch (workerType) {
257 case WorkerType::Classic:
258 return "Classic";
259 case WorkerType::Module:
260 return "Module";
261 }
262
263 RELEASE_ASSERT_NOT_REACHED();
264}
265
266static Optional<WorkerType> stringToWorkerType(const String& type)
267{
268 if (type == "Classic")
269 return WorkerType::Classic;
270 if (type == "Module")
271 return WorkerType::Module;
272
273 return WTF::nullopt;
274}
275
276void RegistrationDatabase::pushChanges(Vector<ServiceWorkerContextData>&& datas, CompletionHandler<void()>&& completionHandler)
277{
278 postTaskToWorkQueue([this, datas = crossThreadCopy(datas), completionHandler = WTFMove(completionHandler)]() mutable {
279 doPushChanges(WTFMove(datas));
280
281 if (!completionHandler)
282 return;
283
284 callOnMainThread(WTFMove(completionHandler));
285 });
286}
287
288void RegistrationDatabase::close(CompletionHandler<void()>&& completionHandler)
289{
290 postTaskToWorkQueue([this, completionHandler = WTFMove(completionHandler)]() mutable {
291 m_database = nullptr;
292 callOnMainThread(WTFMove(completionHandler));
293 });
294}
295
296void RegistrationDatabase::clearAll(CompletionHandler<void()>&& completionHandler)
297{
298 postTaskToWorkQueue([this, completionHandler = WTFMove(completionHandler)]() mutable {
299 m_database = nullptr;
300
301 SQLiteFileSystem::deleteDatabaseFile(m_databaseFilePath);
302 SQLiteFileSystem::deleteEmptyDatabaseDirectory(m_databaseDirectory);
303
304 callOnMainThread(WTFMove(completionHandler));
305 });
306}
307
308void RegistrationDatabase::doPushChanges(Vector<ServiceWorkerContextData>&& datas)
309{
310 if (!m_database) {
311 openSQLiteDatabase(m_databaseFilePath);
312 if (!m_database)
313 return;
314 }
315
316 SQLiteTransaction transaction(*m_database);
317 transaction.begin();
318
319 SQLiteStatement sql(*m_database, "INSERT INTO Records VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"_s);
320 if (sql.prepare() != SQLITE_OK) {
321 RELEASE_LOG_ERROR(ServiceWorker, "Failed to prepare statement to store registration data into records table (%i) - %s", m_database->lastError(), m_database->lastErrorMsg());
322 return;
323 }
324
325 for (auto& data : datas) {
326 if (data.registration.identifier == ServiceWorkerRegistrationIdentifier()) {
327 SQLiteStatement sql(*m_database, "DELETE FROM Records WHERE key = ?");
328 if (sql.prepare() != SQLITE_OK
329 || sql.bindText(1, data.registration.key.toDatabaseKey()) != SQLITE_OK
330 || sql.step() != SQLITE_DONE) {
331 RELEASE_LOG_ERROR(ServiceWorker, "Failed to remove registration data from records table (%i) - %s", m_database->lastError(), m_database->lastErrorMsg());
332 return;
333 }
334
335 continue;
336 }
337
338 WTF::Persistence::Encoder cspEncoder;
339 data.contentSecurityPolicy.encode(cspEncoder);
340
341 WTF::Persistence::Encoder scriptResourceMapEncoder;
342 scriptResourceMapEncoder.encode(data.scriptResourceMap);
343
344 if (sql.bindText(1, data.registration.key.toDatabaseKey()) != SQLITE_OK
345 || sql.bindText(2, data.registration.scopeURL.protocolHostAndPort()) != SQLITE_OK
346 || sql.bindText(3, data.registration.scopeURL.path()) != SQLITE_OK
347 || sql.bindText(4, data.registration.key.topOrigin().databaseIdentifier()) != SQLITE_OK
348 || sql.bindDouble(5, data.registration.lastUpdateTime.secondsSinceEpoch().value()) != SQLITE_OK
349 || sql.bindText(6, updateViaCacheToString(data.registration.updateViaCache)) != SQLITE_OK
350 || sql.bindText(7, data.scriptURL.string()) != SQLITE_OK
351 || sql.bindText(8, data.script) != SQLITE_OK
352 || sql.bindText(9, workerTypeToString(data.workerType)) != SQLITE_OK
353 || sql.bindBlob(10, cspEncoder.buffer(), cspEncoder.bufferSize()) != SQLITE_OK
354 || sql.bindText(11, data.referrerPolicy) != SQLITE_OK
355 || sql.bindBlob(12, scriptResourceMapEncoder.buffer(), scriptResourceMapEncoder.bufferSize()) != SQLITE_OK
356 || sql.step() != SQLITE_DONE) {
357 RELEASE_LOG_ERROR(ServiceWorker, "Failed to store registration data into records table (%i) - %s", m_database->lastError(), m_database->lastErrorMsg());
358 return;
359 }
360 }
361
362 transaction.commit();
363
364 LOG(ServiceWorker, "Pushed %zu changes to ServiceWorker registration database", datas.size());
365}
366
367String RegistrationDatabase::importRecords()
368{
369 ASSERT(!isMainThread());
370
371 SQLiteStatement sql(*m_database, "SELECT * FROM Records;"_s);
372 if (sql.prepare() != SQLITE_OK)
373 return makeString("Failed to prepare statement to retrieve registrations from records table (", m_database->lastError(), ") - ", m_database->lastErrorMsg());
374
375 int result = sql.step();
376
377 for (; result == SQLITE_ROW; result = sql.step()) {
378 auto key = ServiceWorkerRegistrationKey::fromDatabaseKey(sql.getColumnText(0));
379 auto originURL = URL { URL(), sql.getColumnText(1) };
380 auto scopePath = sql.getColumnText(2);
381 auto topOrigin = SecurityOriginData::fromDatabaseIdentifier(sql.getColumnText(3));
382 auto lastUpdateCheckTime = WallTime::fromRawSeconds(sql.getColumnDouble(4));
383 auto updateViaCache = stringToUpdateViaCache(sql.getColumnText(5));
384 auto scriptURL = URL { URL(), sql.getColumnText(6) };
385 auto script = sql.getColumnText(7);
386 auto workerType = stringToWorkerType(sql.getColumnText(8));
387
388 Vector<uint8_t> contentSecurityPolicyData;
389 sql.getColumnBlobAsVector(9, contentSecurityPolicyData);
390 WTF::Persistence::Decoder cspDecoder(contentSecurityPolicyData.data(), contentSecurityPolicyData.size());
391 ContentSecurityPolicyResponseHeaders contentSecurityPolicy;
392 if (contentSecurityPolicyData.size() && !ContentSecurityPolicyResponseHeaders::decode(cspDecoder, contentSecurityPolicy))
393 continue;
394
395 auto referrerPolicy = sql.getColumnText(10);
396
397 Vector<uint8_t> scriptResourceMapData;
398 sql.getColumnBlobAsVector(11, scriptResourceMapData);
399 HashMap<URL, ServiceWorkerContextData::ImportedScript> scriptResourceMap;
400
401 WTF::Persistence::Decoder scriptResourceMapDecoder(scriptResourceMapData.data(), scriptResourceMapData.size());
402 if (scriptResourceMapData.size()) {
403 if (!scriptResourceMapDecoder.decode(scriptResourceMap))
404 continue;
405 }
406
407 // Validate the input for this registration.
408 // If any part of this input is invalid, let's skip this registration.
409 // FIXME: Should we return an error skipping *all* registrations?
410 if (!key || !originURL.isValid() || !topOrigin || !updateViaCache || !scriptURL.isValid() || !workerType)
411 continue;
412
413 auto workerIdentifier = ServiceWorkerIdentifier::generate();
414 auto registrationIdentifier = ServiceWorkerRegistrationIdentifier::generate();
415 auto serviceWorkerData = ServiceWorkerData { workerIdentifier, scriptURL, ServiceWorkerState::Activated, *workerType, registrationIdentifier };
416 auto registration = ServiceWorkerRegistrationData { WTFMove(*key), registrationIdentifier, URL(originURL, scopePath), *updateViaCache, lastUpdateCheckTime, WTF::nullopt, WTF::nullopt, WTFMove(serviceWorkerData) };
417 auto contextData = ServiceWorkerContextData { WTF::nullopt, WTFMove(registration), workerIdentifier, WTFMove(script), WTFMove(contentSecurityPolicy), WTFMove(referrerPolicy), WTFMove(scriptURL), *workerType, m_sessionID, true, WTFMove(scriptResourceMap) };
418
419 callOnMainThread([protectedThis = makeRef(*this), contextData = contextData.isolatedCopy()]() mutable {
420 protectedThis->addRegistrationToStore(WTFMove(contextData));
421 });
422 }
423
424 if (result != SQLITE_DONE)
425 return makeString("Failed to import at least one registration from records table (", m_database->lastError(), ") - ", m_database->lastErrorMsg());
426
427 return { };
428}
429
430void RegistrationDatabase::addRegistrationToStore(ServiceWorkerContextData&& context)
431{
432 if (m_store)
433 m_store->addRegistrationFromDatabase(WTFMove(context));
434}
435
436void RegistrationDatabase::databaseFailedToOpen()
437{
438 if (m_store)
439 m_store->databaseFailedToOpen();
440}
441
442void RegistrationDatabase::databaseOpenedAndRecordsImported()
443{
444 if (m_store)
445 m_store->databaseOpenedAndRecordsImported();
446}
447
448} // namespace WebCore
449
450#endif // ENABLE(SERVICE_WORKER)
451