1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 #include "ServiceWorkerUpdateJob.h"
8
9 #include "mozilla/Telemetry.h"
10 #include "nsIScriptError.h"
11 #include "nsIURL.h"
12 #include "nsNetUtil.h"
13 #include "nsProxyRelease.h"
14 #include "ServiceWorkerManager.h"
15 #include "ServiceWorkerPrivate.h"
16 #include "ServiceWorkerRegistrationInfo.h"
17 #include "ServiceWorkerScriptCache.h"
18 #include "mozilla/dom/WorkerCommon.h"
19
20 namespace mozilla {
21 namespace dom {
22
23 using serviceWorkerScriptCache::OnFailure;
24
25 namespace {
26
27 /**
28 * The spec mandates slightly different behaviors for computing the scope
29 * prefix string in case a Service-Worker-Allowed header is specified versus
30 * when it's not available.
31 *
32 * With the header:
33 * "Set maxScopeString to "/" concatenated with the strings in maxScope's
34 * path (including empty strings), separated from each other by "/"."
35 * Without the header:
36 * "Set maxScopeString to "/" concatenated with the strings, except the last
37 * string that denotes the script's file name, in registration's registering
38 * script url's path (including empty strings), separated from each other by
39 * "/"."
40 *
41 * In simpler terms, if the header is not present, we should only use the
42 * "directory" part of the pathname, and otherwise the entire pathname should be
43 * used. ScopeStringPrefixMode allows the caller to specify the desired
44 * behavior.
45 */
46 enum ScopeStringPrefixMode { eUseDirectory, eUsePath };
47
GetRequiredScopeStringPrefix(nsIURI * aScriptURI,nsACString & aPrefix,ScopeStringPrefixMode aPrefixMode)48 nsresult GetRequiredScopeStringPrefix(nsIURI* aScriptURI, nsACString& aPrefix,
49 ScopeStringPrefixMode aPrefixMode) {
50 nsresult rv;
51 if (aPrefixMode == eUseDirectory) {
52 nsCOMPtr<nsIURL> scriptURL(do_QueryInterface(aScriptURI));
53 if (NS_WARN_IF(!scriptURL)) {
54 return NS_ERROR_FAILURE;
55 }
56
57 rv = scriptURL->GetDirectory(aPrefix);
58 if (NS_WARN_IF(NS_FAILED(rv))) {
59 return rv;
60 }
61 } else if (aPrefixMode == eUsePath) {
62 rv = aScriptURI->GetPathQueryRef(aPrefix);
63 if (NS_WARN_IF(NS_FAILED(rv))) {
64 return rv;
65 }
66 } else {
67 MOZ_ASSERT_UNREACHABLE("Invalid value for aPrefixMode");
68 }
69 return NS_OK;
70 }
71
72 } // anonymous namespace
73
74 class ServiceWorkerUpdateJob::CompareCallback final
75 : public serviceWorkerScriptCache::CompareCallback {
76 RefPtr<ServiceWorkerUpdateJob> mJob;
77
78 ~CompareCallback() = default;
79
80 public:
CompareCallback(ServiceWorkerUpdateJob * aJob)81 explicit CompareCallback(ServiceWorkerUpdateJob* aJob) : mJob(aJob) {
82 MOZ_ASSERT(mJob);
83 }
84
ComparisonResult(nsresult aStatus,bool aInCacheAndEqual,OnFailure aOnFailure,const nsAString & aNewCacheName,const nsACString & aMaxScope,nsLoadFlags aLoadFlags)85 virtual void ComparisonResult(nsresult aStatus, bool aInCacheAndEqual,
86 OnFailure aOnFailure,
87 const nsAString& aNewCacheName,
88 const nsACString& aMaxScope,
89 nsLoadFlags aLoadFlags) override {
90 mJob->ComparisonResult(aStatus, aInCacheAndEqual, aOnFailure, aNewCacheName,
91 aMaxScope, aLoadFlags);
92 }
93
94 NS_INLINE_DECL_REFCOUNTING(ServiceWorkerUpdateJob::CompareCallback, override)
95 };
96
97 class ServiceWorkerUpdateJob::ContinueUpdateRunnable final
98 : public LifeCycleEventCallback {
99 nsMainThreadPtrHandle<ServiceWorkerUpdateJob> mJob;
100 bool mSuccess;
101
102 public:
ContinueUpdateRunnable(const nsMainThreadPtrHandle<ServiceWorkerUpdateJob> & aJob)103 explicit ContinueUpdateRunnable(
104 const nsMainThreadPtrHandle<ServiceWorkerUpdateJob>& aJob)
105 : mJob(aJob), mSuccess(false) {
106 MOZ_ASSERT(NS_IsMainThread());
107 }
108
SetResult(bool aResult)109 void SetResult(bool aResult) override { mSuccess = aResult; }
110
111 NS_IMETHOD
Run()112 Run() override {
113 MOZ_ASSERT(NS_IsMainThread());
114 mJob->ContinueUpdateAfterScriptEval(mSuccess);
115 mJob = nullptr;
116 return NS_OK;
117 }
118 };
119
120 class ServiceWorkerUpdateJob::ContinueInstallRunnable final
121 : public LifeCycleEventCallback {
122 nsMainThreadPtrHandle<ServiceWorkerUpdateJob> mJob;
123 bool mSuccess;
124
125 public:
ContinueInstallRunnable(const nsMainThreadPtrHandle<ServiceWorkerUpdateJob> & aJob)126 explicit ContinueInstallRunnable(
127 const nsMainThreadPtrHandle<ServiceWorkerUpdateJob>& aJob)
128 : mJob(aJob), mSuccess(false) {
129 MOZ_ASSERT(NS_IsMainThread());
130 }
131
SetResult(bool aResult)132 void SetResult(bool aResult) override { mSuccess = aResult; }
133
134 NS_IMETHOD
Run()135 Run() override {
136 MOZ_ASSERT(NS_IsMainThread());
137 mJob->ContinueAfterInstallEvent(mSuccess);
138 mJob = nullptr;
139 return NS_OK;
140 }
141 };
142
ServiceWorkerUpdateJob(nsIPrincipal * aPrincipal,const nsACString & aScope,nsCString aScriptSpec,ServiceWorkerUpdateViaCache aUpdateViaCache)143 ServiceWorkerUpdateJob::ServiceWorkerUpdateJob(
144 nsIPrincipal* aPrincipal, const nsACString& aScope, nsCString aScriptSpec,
145 ServiceWorkerUpdateViaCache aUpdateViaCache)
146 : ServiceWorkerUpdateJob(Type::Update, aPrincipal, aScope,
147 std::move(aScriptSpec), aUpdateViaCache) {}
148
149 already_AddRefed<ServiceWorkerRegistrationInfo>
GetRegistration() const150 ServiceWorkerUpdateJob::GetRegistration() const {
151 MOZ_ASSERT(NS_IsMainThread());
152 RefPtr<ServiceWorkerRegistrationInfo> ref = mRegistration;
153 return ref.forget();
154 }
155
ServiceWorkerUpdateJob(Type aType,nsIPrincipal * aPrincipal,const nsACString & aScope,nsCString aScriptSpec,ServiceWorkerUpdateViaCache aUpdateViaCache)156 ServiceWorkerUpdateJob::ServiceWorkerUpdateJob(
157 Type aType, nsIPrincipal* aPrincipal, const nsACString& aScope,
158 nsCString aScriptSpec, ServiceWorkerUpdateViaCache aUpdateViaCache)
159 : ServiceWorkerJob(aType, aPrincipal, aScope, std::move(aScriptSpec)),
160 mUpdateViaCache(aUpdateViaCache),
161 mOnFailure(serviceWorkerScriptCache::OnFailure::DoNothing) {}
162
163 ServiceWorkerUpdateJob::~ServiceWorkerUpdateJob() = default;
164
FailUpdateJob(ErrorResult & aRv)165 void ServiceWorkerUpdateJob::FailUpdateJob(ErrorResult& aRv) {
166 MOZ_ASSERT(NS_IsMainThread());
167 MOZ_ASSERT(aRv.Failed());
168
169 // Cleanup after a failed installation. This essentially implements
170 // step 13 of the Install algorithm.
171 //
172 // https://w3c.github.io/ServiceWorker/#installation-algorithm
173 //
174 // The spec currently only runs this after an install event fails,
175 // but we must handle many more internal errors. So we check for
176 // cleanup on every non-successful exit.
177 if (mRegistration) {
178 // Some kinds of failures indicate there is something broken in the
179 // currently installed registration. In these cases we want to fully
180 // unregister.
181 if (mOnFailure == OnFailure::Uninstall) {
182 mRegistration->ClearAsCorrupt();
183 }
184
185 // Otherwise just clear the workers we may have created as part of the
186 // update process.
187 else {
188 mRegistration->ClearEvaluating();
189 mRegistration->ClearInstalling();
190 }
191
192 RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
193 if (swm) {
194 swm->MaybeRemoveRegistration(mRegistration);
195
196 // Also clear the registration on disk if we are forcing uninstall
197 // due to a particularly bad failure.
198 if (mOnFailure == OnFailure::Uninstall) {
199 swm->MaybeSendUnregister(mRegistration->Principal(),
200 mRegistration->Scope());
201 }
202 }
203 }
204
205 mRegistration = nullptr;
206
207 Finish(aRv);
208 }
209
FailUpdateJob(nsresult aRv)210 void ServiceWorkerUpdateJob::FailUpdateJob(nsresult aRv) {
211 ErrorResult rv(aRv);
212 FailUpdateJob(rv);
213 }
214
AsyncExecute()215 void ServiceWorkerUpdateJob::AsyncExecute() {
216 MOZ_ASSERT(NS_IsMainThread());
217 MOZ_ASSERT(GetType() == Type::Update);
218
219 RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
220 if (Canceled() || !swm) {
221 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
222 return;
223 }
224
225 // Invoke Update algorithm:
226 // https://w3c.github.io/ServiceWorker/#update-algorithm
227 //
228 // "Let registration be the result of running the Get Registration algorithm
229 // passing job’s scope url as the argument."
230 RefPtr<ServiceWorkerRegistrationInfo> registration =
231 swm->GetRegistration(mPrincipal, mScope);
232
233 if (!registration) {
234 ErrorResult rv;
235 rv.ThrowTypeError<MSG_SW_UPDATE_BAD_REGISTRATION>(mScope, "uninstalled");
236 FailUpdateJob(rv);
237 return;
238 }
239
240 // "Let newestWorker be the result of running Get Newest Worker algorithm
241 // passing registration as the argument."
242 RefPtr<ServiceWorkerInfo> newest = registration->Newest();
243
244 // "If job’s job type is update, and newestWorker is not null and its script
245 // url does not equal job’s script url, then:
246 // 1. Invoke Reject Job Promise with job and TypeError.
247 // 2. Invoke Finish Job with job and abort these steps."
248 if (newest && !newest->ScriptSpec().Equals(mScriptSpec)) {
249 ErrorResult rv;
250 rv.ThrowTypeError<MSG_SW_UPDATE_BAD_REGISTRATION>(mScope, "changed");
251 FailUpdateJob(rv);
252 return;
253 }
254
255 SetRegistration(registration);
256 Update();
257 }
258
SetRegistration(ServiceWorkerRegistrationInfo * aRegistration)259 void ServiceWorkerUpdateJob::SetRegistration(
260 ServiceWorkerRegistrationInfo* aRegistration) {
261 MOZ_ASSERT(NS_IsMainThread());
262
263 MOZ_ASSERT(!mRegistration);
264 MOZ_ASSERT(aRegistration);
265 mRegistration = aRegistration;
266 }
267
Update()268 void ServiceWorkerUpdateJob::Update() {
269 MOZ_ASSERT(NS_IsMainThread());
270 MOZ_ASSERT(!Canceled());
271
272 // SetRegistration() must be called before Update().
273 MOZ_ASSERT(mRegistration);
274 MOZ_ASSERT(!mRegistration->GetInstalling());
275
276 // Begin the script download and comparison steps starting at step 5
277 // of the Update algorithm.
278
279 RefPtr<ServiceWorkerInfo> workerInfo = mRegistration->Newest();
280 nsAutoString cacheName;
281
282 // If the script has not changed, we need to perform a byte-for-byte
283 // comparison.
284 if (workerInfo && workerInfo->ScriptSpec().Equals(mScriptSpec)) {
285 cacheName = workerInfo->CacheName();
286 }
287
288 RefPtr<CompareCallback> callback = new CompareCallback(this);
289
290 nsresult rv = serviceWorkerScriptCache::Compare(
291 mRegistration, mPrincipal, cacheName, NS_ConvertUTF8toUTF16(mScriptSpec),
292 callback);
293 if (NS_WARN_IF(NS_FAILED(rv))) {
294 FailUpdateJob(rv);
295 return;
296 }
297 }
298
GetUpdateViaCache() const299 ServiceWorkerUpdateViaCache ServiceWorkerUpdateJob::GetUpdateViaCache() const {
300 return mUpdateViaCache;
301 }
302
ComparisonResult(nsresult aStatus,bool aInCacheAndEqual,OnFailure aOnFailure,const nsAString & aNewCacheName,const nsACString & aMaxScope,nsLoadFlags aLoadFlags)303 void ServiceWorkerUpdateJob::ComparisonResult(nsresult aStatus,
304 bool aInCacheAndEqual,
305 OnFailure aOnFailure,
306 const nsAString& aNewCacheName,
307 const nsACString& aMaxScope,
308 nsLoadFlags aLoadFlags) {
309 MOZ_ASSERT(NS_IsMainThread());
310
311 mOnFailure = aOnFailure;
312
313 RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
314 if (NS_WARN_IF(Canceled() || !swm)) {
315 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
316 return;
317 }
318
319 // Handle failure of the download or comparison. This is part of Update
320 // step 5 as "If the algorithm asynchronously completes with null, then:".
321 if (NS_WARN_IF(NS_FAILED(aStatus))) {
322 FailUpdateJob(aStatus);
323 return;
324 }
325
326 // The spec validates the response before performing the byte-for-byte check.
327 // Here we perform the comparison in another module and then validate the
328 // script URL and scope. Make sure to do this validation before accepting
329 // an byte-for-byte match since the service-worker-allowed header might have
330 // changed since the last time it was installed.
331
332 // This is step 2 the "validate response" section of Update algorithm step 5.
333 // Step 1 is performed in the serviceWorkerScriptCache code.
334
335 nsCOMPtr<nsIURI> scriptURI;
336 nsresult rv = NS_NewURI(getter_AddRefs(scriptURI), mScriptSpec);
337 if (NS_WARN_IF(NS_FAILED(rv))) {
338 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR);
339 return;
340 }
341
342 nsCOMPtr<nsIURI> maxScopeURI;
343 if (!aMaxScope.IsEmpty()) {
344 rv = NS_NewURI(getter_AddRefs(maxScopeURI), aMaxScope, nullptr, scriptURI);
345 if (NS_WARN_IF(NS_FAILED(rv))) {
346 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR);
347 return;
348 }
349 }
350
351 nsAutoCString defaultAllowedPrefix;
352 rv = GetRequiredScopeStringPrefix(scriptURI, defaultAllowedPrefix,
353 eUseDirectory);
354 if (NS_WARN_IF(NS_FAILED(rv))) {
355 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR);
356 return;
357 }
358
359 nsAutoCString maxPrefix(defaultAllowedPrefix);
360 if (maxScopeURI) {
361 rv = GetRequiredScopeStringPrefix(maxScopeURI, maxPrefix, eUsePath);
362 if (NS_WARN_IF(NS_FAILED(rv))) {
363 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR);
364 return;
365 }
366 }
367
368 nsCOMPtr<nsIURI> scopeURI;
369 rv = NS_NewURI(getter_AddRefs(scopeURI), mRegistration->Scope(), nullptr,
370 scriptURI);
371 if (NS_WARN_IF(NS_FAILED(rv))) {
372 FailUpdateJob(NS_ERROR_FAILURE);
373 return;
374 }
375
376 nsAutoCString scopeString;
377 rv = scopeURI->GetPathQueryRef(scopeString);
378 if (NS_WARN_IF(NS_FAILED(rv))) {
379 FailUpdateJob(NS_ERROR_FAILURE);
380 return;
381 }
382
383 if (!StringBeginsWith(scopeString, maxPrefix)) {
384 nsAutoString message;
385 NS_ConvertUTF8toUTF16 reportScope(mRegistration->Scope());
386 NS_ConvertUTF8toUTF16 reportMaxPrefix(maxPrefix);
387
388 rv = nsContentUtils::FormatLocalizedString(
389 message, nsContentUtils::eDOM_PROPERTIES,
390 "ServiceWorkerScopePathMismatch", reportScope, reportMaxPrefix);
391 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to format localized string");
392 swm->ReportToAllClients(mScope, message, u""_ns, u""_ns, 0, 0,
393 nsIScriptError::errorFlag);
394 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR);
395 return;
396 }
397
398 // The response has been validated, so now we can consider if its a
399 // byte-for-byte match. This is step 6 of the Update algorithm.
400 if (aInCacheAndEqual) {
401 Finish(NS_OK);
402 return;
403 }
404
405 Telemetry::Accumulate(Telemetry::SERVICE_WORKER_UPDATED, 1);
406
407 // Begin step 7 of the Update algorithm to evaluate the new script.
408 nsLoadFlags flags = aLoadFlags;
409 if (GetUpdateViaCache() == ServiceWorkerUpdateViaCache::None) {
410 flags |= nsIRequest::VALIDATE_ALWAYS;
411 }
412
413 RefPtr<ServiceWorkerInfo> sw = new ServiceWorkerInfo(
414 mRegistration->Principal(), mRegistration->Scope(), mRegistration->Id(),
415 mRegistration->Version(), mScriptSpec, aNewCacheName, flags);
416
417 // If the registration is corrupt enough to force an uninstall if the
418 // upgrade fails, then we want to make sure the upgrade takes effect
419 // if it succeeds. Therefore force the skip-waiting flag on to replace
420 // the broken worker after install.
421 if (aOnFailure == OnFailure::Uninstall) {
422 sw->SetSkipWaitingFlag();
423 }
424
425 mRegistration->SetEvaluating(sw);
426
427 nsMainThreadPtrHandle<ServiceWorkerUpdateJob> handle(
428 new nsMainThreadPtrHolder<ServiceWorkerUpdateJob>(
429 "ServiceWorkerUpdateJob", this));
430 RefPtr<LifeCycleEventCallback> callback = new ContinueUpdateRunnable(handle);
431
432 ServiceWorkerPrivate* workerPrivate = sw->WorkerPrivate();
433 MOZ_ASSERT(workerPrivate);
434 rv = workerPrivate->CheckScriptEvaluation(callback);
435
436 if (NS_WARN_IF(NS_FAILED(rv))) {
437 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
438 return;
439 }
440 }
441
ContinueUpdateAfterScriptEval(bool aScriptEvaluationResult)442 void ServiceWorkerUpdateJob::ContinueUpdateAfterScriptEval(
443 bool aScriptEvaluationResult) {
444 MOZ_ASSERT(NS_IsMainThread());
445
446 RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
447 if (Canceled() || !swm) {
448 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
449 return;
450 }
451
452 // Step 7.5 of the Update algorithm verifying that the script evaluated
453 // successfully.
454
455 if (NS_WARN_IF(!aScriptEvaluationResult)) {
456 ErrorResult error;
457 error.ThrowTypeError<MSG_SW_SCRIPT_THREW>(mScriptSpec,
458 mRegistration->Scope());
459 FailUpdateJob(error);
460 return;
461 }
462
463 Install();
464 }
465
Install()466 void ServiceWorkerUpdateJob::Install() {
467 MOZ_ASSERT(NS_IsMainThread());
468 MOZ_DIAGNOSTIC_ASSERT(!Canceled());
469
470 MOZ_ASSERT(!mRegistration->GetInstalling());
471
472 // Begin step 2 of the Install algorithm.
473 //
474 // https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#installation-algorithm
475
476 mRegistration->TransitionEvaluatingToInstalling();
477
478 // Step 6 of the Install algorithm resolving the job promise.
479 InvokeResultCallbacks(NS_OK);
480
481 // Queue a task to fire an event named updatefound at all the
482 // ServiceWorkerRegistration.
483 mRegistration->FireUpdateFound();
484
485 nsMainThreadPtrHandle<ServiceWorkerUpdateJob> handle(
486 new nsMainThreadPtrHolder<ServiceWorkerUpdateJob>(
487 "ServiceWorkerUpdateJob", this));
488 RefPtr<LifeCycleEventCallback> callback = new ContinueInstallRunnable(handle);
489
490 // Send the install event to the worker thread
491 ServiceWorkerPrivate* workerPrivate =
492 mRegistration->GetInstalling()->WorkerPrivate();
493 nsresult rv = workerPrivate->SendLifeCycleEvent(u"install"_ns, callback);
494 if (NS_WARN_IF(NS_FAILED(rv))) {
495 ContinueAfterInstallEvent(false /* aSuccess */);
496 }
497 }
498
ContinueAfterInstallEvent(bool aInstallEventSuccess)499 void ServiceWorkerUpdateJob::ContinueAfterInstallEvent(
500 bool aInstallEventSuccess) {
501 if (Canceled()) {
502 return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
503 }
504
505 // If we haven't been canceled we should have a registration. There appears
506 // to be a path where it gets cleared before we call into here. Assert
507 // to try to catch this condition, but don't crash in release.
508 MOZ_DIAGNOSTIC_ASSERT(mRegistration);
509 if (!mRegistration) {
510 return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
511 }
512
513 // Continue executing the Install algorithm at step 12.
514
515 // "If installFailed is true"
516 if (NS_WARN_IF(!aInstallEventSuccess)) {
517 // The installing worker is cleaned up by FailUpdateJob().
518 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
519 return;
520 }
521
522 // Abort the update Job if the installWorker is null (e.g. when an extension
523 // is shutting down and all its workers have been terminated).
524 if (!mRegistration->GetInstalling()) {
525 return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR);
526 }
527
528 mRegistration->TransitionInstallingToWaiting();
529
530 Finish(NS_OK);
531
532 // Step 20 calls for explicitly waiting for queued event tasks to fire.
533 // Instead, we simply queue a runnable to execute Activate. This ensures the
534 // events are flushed from the queue before proceeding.
535
536 // Step 22 of the Install algorithm. Activate is executed after the
537 // completion of this job. The controlling client and skipWaiting checks are
538 // performed in TryToActivate().
539 mRegistration->TryToActivateAsync();
540 }
541
542 } // namespace dom
543 } // namespace mozilla
544