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
42namespace WebCore {
43
44SWServerJobQueue::SWServerJobQueue(SWServer& server, const ServiceWorkerRegistrationKey& key)
45 : m_jobTimer(*this, &SWServerJobQueue::runNextJobSynchronously)
46 , m_server(server)
47 , m_registrationKey(key)
48{
49}
50
51SWServerJobQueue::~SWServerJobQueue()
52{
53}
54
55bool SWServerJobQueue::isCurrentlyProcessingJob(const ServiceWorkerJobDataIdentifier& jobDataIdentifier) const
56{
57 return !m_jobQueue.isEmpty() && firstJob().identifier() == jobDataIdentifier;
58}
59
60void 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
109void 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
133void 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
145void 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).
162void 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
181void 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
229void SWServerJobQueue::runNextJob()
230{
231 ASSERT(!m_jobQueue.isEmpty());
232 ASSERT(!m_jobTimer.isActive());
233 m_jobTimer.startOneShot(0_s);
234}
235
236void 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
259void 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 registration->setIsUninstalling(false);
277 auto* newestWorker = registration->getNewestWorker();
278 if (newestWorker && equalIgnoringFragmentIdentifier(job.scriptURL, newestWorker->scriptURL()) && job.registrationOptions.updateViaCache == registration->updateViaCache()) {
279 RELEASE_LOG(ServiceWorker, "%p - SWServerJobQueue::runRegisterJob: Found directly reusable registration %llu for job %s (DONE)", this, registration->identifier().toUInt64(), job.identifier().loggingString().utf8().data());
280 m_server.resolveRegistrationJob(job, registration->data(), ShouldNotifyWhenResolved::No);
281 finishCurrentJob();
282 return;
283 }
284 // This is not specified yet (https://github.com/w3c/ServiceWorker/issues/1189).
285 if (registration->updateViaCache() != job.registrationOptions.updateViaCache)
286 registration->setUpdateViaCache(job.registrationOptions.updateViaCache);
287 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());
288 } else {
289 auto newRegistration = std::make_unique<SWServerRegistration>(m_server, m_registrationKey, job.registrationOptions.updateViaCache, job.scopeURL, job.scriptURL);
290 m_server.addRegistration(WTFMove(newRegistration));
291
292 RELEASE_LOG(ServiceWorker, "%p - SWServerJobQueue::runRegisterJob: No existing registration for job %s, constructing a new one.", this, job.identifier().loggingString().utf8().data());
293 }
294
295 runUpdateJob(job);
296}
297
298// https://w3c.github.io/ServiceWorker/#unregister-algorithm
299void SWServerJobQueue::runUnregisterJob(const ServiceWorkerJobData& job)
300{
301 // If the origin of job's scope url is not job's client's origin, then:
302 if (!protocolHostAndPortAreEqual(job.scopeURL, job.clientCreationURL))
303 return rejectCurrentJob(ExceptionData { SecurityError, "Origin of scope URL does not match the client's origin"_s });
304
305 // Let registration be the result of running "Get Registration" algorithm passing job's scope url as the argument.
306 auto* registration = m_server.getRegistration(m_registrationKey);
307
308 // If registration is null, then:
309 if (!registration || registration->isUninstalling()) {
310 // Invoke Resolve Job Promise with job and false.
311 m_server.resolveUnregistrationJob(job, m_registrationKey, false);
312 finishCurrentJob();
313 return;
314 }
315
316 // Set registration's uninstalling flag.
317 registration->setIsUninstalling(true);
318
319 // Invoke Resolve Job Promise with job and true.
320 m_server.resolveUnregistrationJob(job, m_registrationKey, true);
321
322 // Invoke Try Clear Registration with registration.
323 registration->tryClear();
324 finishCurrentJob();
325}
326
327// https://w3c.github.io/ServiceWorker/#update-algorithm
328void SWServerJobQueue::runUpdateJob(const ServiceWorkerJobData& job)
329{
330 // Let registration be the result of running the Get Registration algorithm passing job's scope url as the argument.
331 auto* registration = m_server.getRegistration(m_registrationKey);
332
333 // If registration is null (in our parlance "empty") or registration's uninstalling flag is set, then:
334 if (!registration)
335 return rejectCurrentJob(ExceptionData { TypeError, "Cannot update a null/nonexistent service worker registration"_s });
336 if (registration->isUninstalling())
337 return rejectCurrentJob(ExceptionData { TypeError, "Cannot update a service worker registration that is uninstalling"_s });
338
339 // Let newestWorker be the result of running Get Newest Worker algorithm passing registration as the argument.
340 auto* newestWorker = registration->getNewestWorker();
341
342 // 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:
343 if (job.type == ServiceWorkerJobType::Update && newestWorker && !equalIgnoringFragmentIdentifier(job.scriptURL, newestWorker->scriptURL()))
344 return rejectCurrentJob(ExceptionData { TypeError, "Cannot update a service worker with a requested script URL whose newest worker has a different script URL"_s });
345
346 FetchOptions::Cache cachePolicy = FetchOptions::Cache::Default;
347 // Set request's cache mode to "no-cache" if any of the following are true:
348 // - registration's update via cache mode is not "all".
349 // - job's force bypass cache flag is set.
350 // - newestWorker is not null, and registration's last update check time is not null and the time difference in seconds calculated by the
351 // current time minus registration's last update check time is greater than 86400.
352 if (registration->updateViaCache() != ServiceWorkerUpdateViaCache::All
353 || (newestWorker && registration->lastUpdateTime() && (WallTime::now() - registration->lastUpdateTime()) > 86400_s)) {
354 cachePolicy = FetchOptions::Cache::NoCache;
355 }
356 m_server.startScriptFetch(job, cachePolicy);
357}
358
359void SWServerJobQueue::rejectCurrentJob(const ExceptionData& exceptionData)
360{
361 m_server.rejectJob(firstJob(), exceptionData);
362
363 finishCurrentJob();
364}
365
366// https://w3c.github.io/ServiceWorker/#finish-job
367void SWServerJobQueue::finishCurrentJob()
368{
369 ASSERT(!m_jobTimer.isActive());
370
371 m_jobQueue.removeFirst();
372 if (!m_jobQueue.isEmpty())
373 runNextJob();
374}
375
376void SWServerJobQueue::removeAllJobsMatching(const WTF::Function<bool(ServiceWorkerJobData&)>& matches)
377{
378 bool isFirst = true;
379 bool didRemoveFirstJob = false;
380 m_jobQueue.removeAllMatching([&](auto& job) {
381 bool shouldRemove = matches(job);
382 if (isFirst) {
383 isFirst = false;
384 if (shouldRemove)
385 didRemoveFirstJob = true;
386 }
387 return shouldRemove;
388 });
389
390 if (m_jobTimer.isActive()) {
391 if (m_jobQueue.isEmpty())
392 m_jobTimer.stop();
393 } else if (didRemoveFirstJob && !m_jobQueue.isEmpty())
394 runNextJob();
395}
396
397void SWServerJobQueue::cancelJobsFromConnection(SWServerConnectionIdentifier connectionIdentifier)
398{
399 removeAllJobsMatching([connectionIdentifier](auto& job) {
400 return job.identifier().connectionIdentifier == connectionIdentifier;
401 });
402}
403
404void SWServerJobQueue::cancelJobsFromServiceWorker(ServiceWorkerIdentifier serviceWorkerIdentifier)
405{
406 removeAllJobsMatching([serviceWorkerIdentifier](auto& job) {
407 return WTF::holds_alternative<ServiceWorkerIdentifier>(job.sourceContext) && WTF::get<ServiceWorkerIdentifier>(job.sourceContext) == serviceWorkerIdentifier;
408 });
409}
410
411} // namespace WebCore
412
413#endif // ENABLE(SERVICE_WORKER)
414