616bc96d392a7b62749ce976c514c91e00e317d5
[WebKit-https.git] / Source / WebCore / workers / service / server / SWServerJobQueue.cpp
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 "SWServerJobQueue.h"
28
29 #if ENABLE(SERVICE_WORKER)
30
31 #include "ExceptionData.h"
32 #include "SWServer.h"
33 #include "SWServerRegistration.h"
34 #include "SWServerWorker.h"
35 #include "SchemeRegistry.h"
36 #include "SecurityOrigin.h"
37 #include "ServiceWorkerFetchResult.h"
38 #include "ServiceWorkerRegistrationData.h"
39 #include "ServiceWorkerUpdateViaCache.h"
40 #include "WorkerType.h"
41
42 namespace WebCore {
43
44 SWServerJobQueue::SWServerJobQueue(SWServer& server, const ServiceWorkerRegistrationKey& key)
45     : m_jobTimer(*this, &SWServerJobQueue::runNextJobSynchronously)
46     , m_server(server)
47     , m_registrationKey(key)
48 {
49 }
50
51 SWServerJobQueue::~SWServerJobQueue()
52 {
53 }
54
55 bool SWServerJobQueue::isCurrentlyProcessingJob(const ServiceWorkerJobDataIdentifier& jobDataIdentifier) const
56 {
57     return !m_jobQueue.isEmpty() && firstJob().identifier() == jobDataIdentifier;
58 }
59
60 void SWServerJobQueue::scriptFetchFinished(SWServer::Connection& connection, const ServiceWorkerFetchResult& result)
61 {
62     if (!isCurrentlyProcessingJob(result.jobDataIdentifier))
63         return;
64
65     auto& job = firstJob();
66
67     auto* registration = m_server.getRegistration(m_registrationKey);
68     if (!registration)
69         return;
70
71     auto* newestWorker = registration->getNewestWorker();
72
73     if (!result.scriptError.isNull()) {
74         // Invoke Reject Job Promise with job and TypeError.
75         m_server.rejectJob(job, ExceptionData { TypeError, makeString("Script URL ", job.scriptURL.string(), " fetch resulted in error: ", result.scriptError.localizedDescription()) });
76
77         // If newestWorker is null, invoke Clear Registration algorithm passing registration as its argument.
78         if (!newestWorker)
79             registration->clear();
80
81         // Invoke Finish Job with job and abort these steps.
82         finishCurrentJob();
83         return;
84     }
85
86     registration->setLastUpdateTime(WallTime::now());
87
88     // If newestWorker is not null, newestWorker's script url equals job's script url with the exclude fragments
89     // flag set, and script's source text is a byte-for-byte match with newestWorker's script resource's source
90     // text, then:
91     if (newestWorker && equalIgnoringFragmentIdentifier(newestWorker->scriptURL(), job.scriptURL) && result.script == newestWorker->script()) {
92         // FIXME: for non classic scripts, check the script’s module record's [[ECMAScriptCode]].
93
94         // Invoke Resolve Job Promise with job and registration.
95         m_server.resolveRegistrationJob(job, registration->data(), ShouldNotifyWhenResolved::No);
96
97         // Invoke Finish Job with job and abort these steps.
98         finishCurrentJob();
99         return;
100     }
101
102     // FIXME: Update all the imported scripts as per spec. For now, we just do as if there is none.
103
104     // FIXME: Support the proper worker type (classic vs module)
105     m_server.updateWorker(connection, job.identifier(), *registration, job.scriptURL, result.script, result.contentSecurityPolicy, result.referrerPolicy, WorkerType::Classic, { });
106 }
107
108 // https://w3c.github.io/ServiceWorker/#update-algorithm
109 void SWServerJobQueue::scriptContextFailedToStart(const ServiceWorkerJobDataIdentifier& jobDataIdentifier, ServiceWorkerIdentifier, const String& message)
110 {
111     if (!isCurrentlyProcessingJob(jobDataIdentifier))
112         return;
113
114     // If an uncaught runtime script error occurs during the above step, then:
115     auto* registration = m_server.getRegistration(m_registrationKey);
116     ASSERT(registration);
117
118     ASSERT(registration->preInstallationWorker());
119     registration->preInstallationWorker()->terminate();
120     registration->setPreInstallationWorker(nullptr);
121
122     // Invoke Reject Job Promise with job and TypeError.
123     m_server.rejectJob(firstJob(), { TypeError, message });
124
125     // If newestWorker is null, invoke Clear Registration algorithm passing registration as its argument.
126     if (!registration->getNewestWorker())
127         registration->clear();
128
129     // Invoke Finish Job with job and abort these steps.
130     finishCurrentJob();
131 }
132
133 void SWServerJobQueue::scriptContextStarted(const ServiceWorkerJobDataIdentifier& jobDataIdentifier, ServiceWorkerIdentifier identifier)
134 {
135     if (!isCurrentlyProcessingJob(jobDataIdentifier))
136         return;
137
138     auto* registration = m_server.getRegistration(m_registrationKey);
139     ASSERT(registration);
140
141     install(*registration, identifier);
142 }
143
144 // https://w3c.github.io/ServiceWorker/#install
145 void SWServerJobQueue::install(SWServerRegistration& registration, ServiceWorkerIdentifier installingWorker)
146 {
147     // The Install algorithm should never be invoked with a null worker.
148     auto* worker = m_server.workerByID(installingWorker);
149     RELEASE_ASSERT(worker);
150
151     ASSERT(registration.preInstallationWorker() == worker);
152     registration.setPreInstallationWorker(nullptr);
153
154     registration.updateRegistrationState(ServiceWorkerRegistrationState::Installing, worker);
155     registration.updateWorkerState(*worker, ServiceWorkerState::Installing);
156
157     // Invoke Resolve Job Promise with job and registration.
158     m_server.resolveRegistrationJob(firstJob(), registration.data(), ShouldNotifyWhenResolved::Yes);
159 }
160
161 // https://w3c.github.io/ServiceWorker/#install (after resolving promise).
162 void SWServerJobQueue::didResolveRegistrationPromise()
163 {
164     auto* registration = m_server.getRegistration(m_registrationKey);
165     ASSERT(registration);
166     ASSERT(registration->installingWorker());
167
168     RELEASE_LOG(ServiceWorker, "%p - SWServerJobQueue::didResolveRegistrationPromise: Registration ID: %llu. Now proceeding with install", this, registration->identifier().toUInt64());
169
170     // Queue a task to fire an event named updatefound at all the ServiceWorkerRegistration objects
171     // for all the service worker clients whose creation URL matches registration's scope url and
172     // all the service workers whose containing service worker registration is registration.
173     registration->fireUpdateFoundEvent();
174
175     // Queue a task to fire the InstallEvent.
176     ASSERT(registration->installingWorker());
177     m_server.fireInstallEvent(*registration->installingWorker());
178 }
179
180 // https://w3c.github.io/ServiceWorker/#install
181 void SWServerJobQueue::didFinishInstall(const ServiceWorkerJobDataIdentifier& jobDataIdentifier, ServiceWorkerIdentifier identifier, bool wasSuccessful)
182 {
183     if (!isCurrentlyProcessingJob(jobDataIdentifier))
184         return;
185
186     auto* registration = m_server.getRegistration(m_registrationKey);
187     ASSERT(registration);
188     ASSERT(registration->installingWorker());
189     ASSERT(registration->installingWorker()->identifier() == identifier);
190
191     if (!wasSuccessful) {
192         RefPtr<SWServerWorker> worker = m_server.workerByID(identifier);
193         RELEASE_ASSERT(worker);
194
195         worker->terminate();
196         // Run the Update Registration State algorithm passing registration, "installing" and null as the arguments.
197         registration->updateRegistrationState(ServiceWorkerRegistrationState::Installing, nullptr);
198         // Run the Update Worker State algorithm passing registration's installing worker and redundant as the arguments.
199         registration->updateWorkerState(*worker, ServiceWorkerState::Redundant);
200
201         // If newestWorker is null, invoke Clear Registration algorithm passing registration as its argument.
202         if (!registration->getNewestWorker())
203             registration->clear();
204
205         // Invoke Finish Job with job and abort these steps.
206         finishCurrentJob();
207         return;
208     }
209
210     if (auto* waitingWorker = registration->waitingWorker()) {
211         waitingWorker->terminate();
212         registration->updateWorkerState(*waitingWorker, ServiceWorkerState::Redundant);
213     }
214
215     auto* installing = registration->installingWorker();
216     ASSERT(installing);
217
218     registration->updateRegistrationState(ServiceWorkerRegistrationState::Waiting, installing);
219     registration->updateRegistrationState(ServiceWorkerRegistrationState::Installing, nullptr);
220     registration->updateWorkerState(*installing, ServiceWorkerState::Installed);
221
222     finishCurrentJob();
223
224     // FIXME: Wait for all the tasks queued by Update Worker State invoked in this algorithm have executed.
225     registration->tryActivate();
226 }
227
228 // https://w3c.github.io/ServiceWorker/#run-job
229 void SWServerJobQueue::runNextJob()
230 {
231     ASSERT(!m_jobQueue.isEmpty());
232     ASSERT(!m_jobTimer.isActive());
233     m_jobTimer.startOneShot(0_s);
234 }
235
236 void SWServerJobQueue::runNextJobSynchronously()
237 {
238     ASSERT(!m_jobQueue.isEmpty());
239     if (m_jobQueue.isEmpty())
240         return;
241
242     auto& job = firstJob();
243     switch (job.type) {
244     case ServiceWorkerJobType::Register:
245         runRegisterJob(job);
246         return;
247     case ServiceWorkerJobType::Unregister:
248         runUnregisterJob(job);
249         return;
250     case ServiceWorkerJobType::Update:
251         runUpdateJob(job);
252         return;
253     }
254
255     ASSERT_NOT_REACHED();
256 }
257
258 // https://w3c.github.io/ServiceWorker/#register-algorithm
259 void SWServerJobQueue::runRegisterJob(const ServiceWorkerJobData& job)
260 {
261     ASSERT(job.type == ServiceWorkerJobType::Register);
262
263     if (!shouldTreatAsPotentiallyTrustworthy(job.scriptURL) && !SchemeRegistry::isServiceWorkerContainerCustomScheme(job.scriptURL.protocol().toStringWithoutCopying()))
264         return rejectCurrentJob(ExceptionData { SecurityError, "Script URL is not potentially trustworthy"_s });
265
266     // If the origin of job's script url is not job's referrer's origin, then:
267     if (!protocolHostAndPortAreEqual(job.scriptURL, job.clientCreationURL))
268         return rejectCurrentJob(ExceptionData { SecurityError, "Script origin does not match the registering client's origin"_s });
269
270     // If the origin of job's scope url is not job's referrer's origin, then:
271     if (!protocolHostAndPortAreEqual(job.scopeURL, job.clientCreationURL))
272         return rejectCurrentJob(ExceptionData { SecurityError, "Scope origin does not match the registering client's origin"_s });
273
274     // If registration is not null (in our parlance "empty"), then:
275     if (auto* registration = m_server.getRegistration(m_registrationKey)) {
276         auto* newestWorker = registration->getNewestWorker();
277         if (newestWorker && equalIgnoringFragmentIdentifier(job.scriptURL, newestWorker->scriptURL()) && job.registrationOptions.updateViaCache == registration->updateViaCache()) {
278             RELEASE_LOG(ServiceWorker, "%p - SWServerJobQueue::runRegisterJob: Found directly reusable registration %llu for job %s (DONE)", this, registration->identifier().toUInt64(), job.identifier().loggingString().utf8().data());
279             m_server.resolveRegistrationJob(job, registration->data(), ShouldNotifyWhenResolved::No);
280             finishCurrentJob();
281             return;
282         }
283         // This is not specified yet (https://github.com/w3c/ServiceWorker/issues/1189).
284         if (registration->updateViaCache() != job.registrationOptions.updateViaCache)
285             registration->setUpdateViaCache(job.registrationOptions.updateViaCache);
286         RELEASE_LOG(ServiceWorker, "%p - SWServerJobQueue::runRegisterJob: Found registration %llu for job %s but it needs updating", this, registration->identifier().toUInt64(), job.identifier().loggingString().utf8().data());
287     } else {
288         auto newRegistration = makeUnique<SWServerRegistration>(m_server, m_registrationKey, job.registrationOptions.updateViaCache, job.scopeURL, job.scriptURL);
289         m_server.addRegistration(WTFMove(newRegistration));
290
291         RELEASE_LOG(ServiceWorker, "%p - SWServerJobQueue::runRegisterJob: No existing registration for job %s, constructing a new one.", this, job.identifier().loggingString().utf8().data());
292     }
293
294     runUpdateJob(job);
295 }
296
297 // https://w3c.github.io/ServiceWorker/#unregister-algorithm
298 void SWServerJobQueue::runUnregisterJob(const ServiceWorkerJobData& job)
299 {
300     // If the origin of job's scope url is not job's client's origin, then:
301     if (!protocolHostAndPortAreEqual(job.scopeURL, job.clientCreationURL))
302         return rejectCurrentJob(ExceptionData { SecurityError, "Origin of scope URL does not match the client's origin"_s });
303
304     // Let registration be the result of running "Get Registration" algorithm passing job's scope url as the argument.
305     auto* registration = m_server.getRegistration(m_registrationKey);
306
307     // If registration is null, then:
308     if (!registration) {
309         // Invoke Resolve Job Promise with job and false.
310         m_server.resolveUnregistrationJob(job, m_registrationKey, false);
311         finishCurrentJob();
312         return;
313     }
314     
315     // Remove scope to registration map[job’s scope url].
316     m_server.removeFromScopeToRegistrationMap(m_registrationKey);
317
318     // Invoke Resolve Job Promise with job and true.
319     m_server.resolveUnregistrationJob(job, m_registrationKey, true);
320
321     // Invoke Try Clear Registration with registration.
322     registration->tryClear();
323     finishCurrentJob();
324 }
325
326 // https://w3c.github.io/ServiceWorker/#update-algorithm
327 void SWServerJobQueue::runUpdateJob(const ServiceWorkerJobData& job)
328 {
329     // Let registration be the result of running the Get Registration algorithm passing job's scope url as the argument.
330     auto* registration = m_server.getRegistration(m_registrationKey);
331
332     // If registration is null (in our parlance "empty") or registration's uninstalling flag is set, then:
333     if (!registration)
334         return rejectCurrentJob(ExceptionData { TypeError, "Cannot update a null/nonexistent service worker registration"_s });
335
336     // Let newestWorker be the result of running Get Newest Worker algorithm passing registration as the argument.
337     auto* newestWorker = registration->getNewestWorker();
338
339     // If job's type is update, and newestWorker's script url does not equal job's script url with the exclude fragments flag set, then:
340     if (job.type == ServiceWorkerJobType::Update && newestWorker && !equalIgnoringFragmentIdentifier(job.scriptURL, newestWorker->scriptURL()))
341         return rejectCurrentJob(ExceptionData { TypeError, "Cannot update a service worker with a requested script URL whose newest worker has a different script URL"_s });
342
343     FetchOptions::Cache cachePolicy = FetchOptions::Cache::Default;
344     // Set request's cache mode to "no-cache" if any of the following are true:
345     // - registration's update via cache mode is not "all".
346     // - job's force bypass cache flag is set.
347     // - newestWorker is not null, and registration's last update check time is not null and the time difference in seconds calculated by the
348     //   current time minus registration's last update check time is greater than 86400.
349     if (registration->updateViaCache() != ServiceWorkerUpdateViaCache::All
350         || (newestWorker && registration->isStale())) {
351         cachePolicy = FetchOptions::Cache::NoCache;
352     }
353     m_server.startScriptFetch(job, cachePolicy);
354 }
355
356 void SWServerJobQueue::rejectCurrentJob(const ExceptionData& exceptionData)
357 {
358     m_server.rejectJob(firstJob(), exceptionData);
359
360     finishCurrentJob();
361 }
362
363 // https://w3c.github.io/ServiceWorker/#finish-job
364 void SWServerJobQueue::finishCurrentJob()
365 {
366     ASSERT(!m_jobTimer.isActive());
367
368     m_jobQueue.removeFirst();
369     if (!m_jobQueue.isEmpty())
370         runNextJob();
371 }
372
373 void SWServerJobQueue::removeAllJobsMatching(const WTF::Function<bool(ServiceWorkerJobData&)>& matches)
374 {
375     bool isFirst = true;
376     bool didRemoveFirstJob = false;
377     m_jobQueue.removeAllMatching([&](auto& job) {
378         bool shouldRemove = matches(job);
379         if (isFirst) {
380             isFirst = false;
381             if (shouldRemove)
382                 didRemoveFirstJob = true;
383         }
384         return shouldRemove;
385     });
386
387     if (m_jobTimer.isActive()) {
388         if (m_jobQueue.isEmpty())
389             m_jobTimer.stop();
390     } else if (didRemoveFirstJob && !m_jobQueue.isEmpty())
391         runNextJob();
392 }
393
394 void SWServerJobQueue::cancelJobsFromConnection(SWServerConnectionIdentifier connectionIdentifier)
395 {
396     removeAllJobsMatching([connectionIdentifier](auto& job) {
397         return job.identifier().connectionIdentifier == connectionIdentifier;
398     });
399 }
400
401 void SWServerJobQueue::cancelJobsFromServiceWorker(ServiceWorkerIdentifier serviceWorkerIdentifier)
402 {
403     removeAllJobsMatching([serviceWorkerIdentifier](auto& job) {
404         return WTF::holds_alternative<ServiceWorkerIdentifier>(job.sourceContext) && WTF::get<ServiceWorkerIdentifier>(job.sourceContext) == serviceWorkerIdentifier;
405     });
406 }
407
408 } // namespace WebCore
409
410 #endif // ENABLE(SERVICE_WORKER)