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