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/. */
7 #include "ServiceWorkerUpdateJob.h"
9 #include "mozilla/Telemetry.h"
10 #include "nsIScriptError.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"
20 namespace mozilla::dom
{
22 using serviceWorkerScriptCache::OnFailure
;
27 * The spec mandates slightly different behaviors for computing the scope
28 * prefix string in case a Service-Worker-Allowed header is specified versus
29 * when it's not available.
32 * "Set maxScopeString to "/" concatenated with the strings in maxScope's
33 * path (including empty strings), separated from each other by "/"."
35 * "Set maxScopeString to "/" concatenated with the strings, except the last
36 * string that denotes the script's file name, in registration's registering
37 * script url's path (including empty strings), separated from each other by
40 * In simpler terms, if the header is not present, we should only use the
41 * "directory" part of the pathname, and otherwise the entire pathname should be
42 * used. ScopeStringPrefixMode allows the caller to specify the desired
45 enum ScopeStringPrefixMode
{ eUseDirectory
, eUsePath
};
47 nsresult
GetRequiredScopeStringPrefix(nsIURI
* aScriptURI
, nsACString
& aPrefix
,
48 ScopeStringPrefixMode aPrefixMode
) {
50 if (aPrefixMode
== eUseDirectory
) {
51 nsCOMPtr
<nsIURL
> scriptURL(do_QueryInterface(aScriptURI
));
52 if (NS_WARN_IF(!scriptURL
)) {
53 return NS_ERROR_FAILURE
;
56 rv
= scriptURL
->GetDirectory(aPrefix
);
57 if (NS_WARN_IF(NS_FAILED(rv
))) {
60 } else if (aPrefixMode
== eUsePath
) {
61 rv
= aScriptURI
->GetPathQueryRef(aPrefix
);
62 if (NS_WARN_IF(NS_FAILED(rv
))) {
66 MOZ_ASSERT_UNREACHABLE("Invalid value for aPrefixMode");
71 } // anonymous namespace
73 class ServiceWorkerUpdateJob::CompareCallback final
74 : public serviceWorkerScriptCache::CompareCallback
{
75 RefPtr
<ServiceWorkerUpdateJob
> mJob
;
77 ~CompareCallback() = default;
80 explicit CompareCallback(ServiceWorkerUpdateJob
* aJob
) : mJob(aJob
) {
84 virtual void ComparisonResult(nsresult aStatus
, bool aInCacheAndEqual
,
86 const nsAString
& aNewCacheName
,
87 const nsACString
& aMaxScope
,
88 nsLoadFlags aLoadFlags
) override
{
89 mJob
->ComparisonResult(aStatus
, aInCacheAndEqual
, aOnFailure
, aNewCacheName
,
90 aMaxScope
, aLoadFlags
);
93 NS_INLINE_DECL_REFCOUNTING(ServiceWorkerUpdateJob::CompareCallback
, override
)
96 class ServiceWorkerUpdateJob::ContinueUpdateRunnable final
97 : public LifeCycleEventCallback
{
98 nsMainThreadPtrHandle
<ServiceWorkerUpdateJob
> mJob
;
102 explicit ContinueUpdateRunnable(
103 const nsMainThreadPtrHandle
<ServiceWorkerUpdateJob
>& aJob
)
104 : mJob(aJob
), mSuccess(false) {
105 MOZ_ASSERT(NS_IsMainThread());
108 void SetResult(bool aResult
) override
{ mSuccess
= aResult
; }
112 MOZ_ASSERT(NS_IsMainThread());
113 mJob
->ContinueUpdateAfterScriptEval(mSuccess
);
119 class ServiceWorkerUpdateJob::ContinueInstallRunnable final
120 : public LifeCycleEventCallback
{
121 nsMainThreadPtrHandle
<ServiceWorkerUpdateJob
> mJob
;
125 explicit ContinueInstallRunnable(
126 const nsMainThreadPtrHandle
<ServiceWorkerUpdateJob
>& aJob
)
127 : mJob(aJob
), mSuccess(false) {
128 MOZ_ASSERT(NS_IsMainThread());
131 void SetResult(bool aResult
) override
{ mSuccess
= aResult
; }
135 MOZ_ASSERT(NS_IsMainThread());
136 mJob
->ContinueAfterInstallEvent(mSuccess
);
142 ServiceWorkerUpdateJob::ServiceWorkerUpdateJob(
143 nsIPrincipal
* aPrincipal
, const nsACString
& aScope
, nsCString aScriptSpec
,
144 ServiceWorkerUpdateViaCache aUpdateViaCache
)
145 : ServiceWorkerUpdateJob(Type::Update
, aPrincipal
, aScope
,
146 std::move(aScriptSpec
), aUpdateViaCache
) {}
148 already_AddRefed
<ServiceWorkerRegistrationInfo
>
149 ServiceWorkerUpdateJob::GetRegistration() const {
150 MOZ_ASSERT(NS_IsMainThread());
151 RefPtr
<ServiceWorkerRegistrationInfo
> ref
= mRegistration
;
155 ServiceWorkerUpdateJob::ServiceWorkerUpdateJob(
156 Type aType
, nsIPrincipal
* aPrincipal
, const nsACString
& aScope
,
157 nsCString aScriptSpec
, ServiceWorkerUpdateViaCache aUpdateViaCache
)
158 : ServiceWorkerJob(aType
, aPrincipal
, aScope
, std::move(aScriptSpec
)),
159 mUpdateViaCache(aUpdateViaCache
),
160 mOnFailure(serviceWorkerScriptCache::OnFailure::DoNothing
) {}
162 ServiceWorkerUpdateJob::~ServiceWorkerUpdateJob() = default;
164 void ServiceWorkerUpdateJob::FailUpdateJob(ErrorResult
& aRv
) {
165 MOZ_ASSERT(NS_IsMainThread());
166 MOZ_ASSERT(aRv
.Failed());
168 // Cleanup after a failed installation. This essentially implements
169 // step 13 of the Install algorithm.
171 // https://w3c.github.io/ServiceWorker/#installation-algorithm
173 // The spec currently only runs this after an install event fails,
174 // but we must handle many more internal errors. So we check for
175 // cleanup on every non-successful exit.
177 // Some kinds of failures indicate there is something broken in the
178 // currently installed registration. In these cases we want to fully
180 if (mOnFailure
== OnFailure::Uninstall
) {
181 mRegistration
->ClearAsCorrupt();
184 // Otherwise just clear the workers we may have created as part of the
187 mRegistration
->ClearEvaluating();
188 mRegistration
->ClearInstalling();
191 RefPtr
<ServiceWorkerManager
> swm
= ServiceWorkerManager::GetInstance();
193 swm
->MaybeRemoveRegistration(mRegistration
);
195 // Also clear the registration on disk if we are forcing uninstall
196 // due to a particularly bad failure.
197 if (mOnFailure
== OnFailure::Uninstall
) {
198 swm
->MaybeSendUnregister(mRegistration
->Principal(),
199 mRegistration
->Scope());
204 mRegistration
= nullptr;
209 void ServiceWorkerUpdateJob::FailUpdateJob(nsresult aRv
) {
214 void ServiceWorkerUpdateJob::AsyncExecute() {
215 MOZ_ASSERT(NS_IsMainThread());
216 MOZ_ASSERT(GetType() == Type::Update
);
218 RefPtr
<ServiceWorkerManager
> swm
= ServiceWorkerManager::GetInstance();
219 if (Canceled() || !swm
) {
220 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR
);
224 // Invoke Update algorithm:
225 // https://w3c.github.io/ServiceWorker/#update-algorithm
227 // "Let registration be the result of running the Get Registration algorithm
228 // passing job’s scope url as the argument."
229 RefPtr
<ServiceWorkerRegistrationInfo
> registration
=
230 swm
->GetRegistration(mPrincipal
, mScope
);
234 rv
.ThrowTypeError
<MSG_SW_UPDATE_BAD_REGISTRATION
>(mScope
, "uninstalled");
239 // "Let newestWorker be the result of running Get Newest Worker algorithm
240 // passing registration as the argument."
241 RefPtr
<ServiceWorkerInfo
> newest
= registration
->Newest();
243 // "If job’s job type is update, and newestWorker is not null and its script
244 // url does not equal job’s script url, then:
245 // 1. Invoke Reject Job Promise with job and TypeError.
246 // 2. Invoke Finish Job with job and abort these steps."
247 if (newest
&& !newest
->ScriptSpec().Equals(mScriptSpec
)) {
249 rv
.ThrowTypeError
<MSG_SW_UPDATE_BAD_REGISTRATION
>(mScope
, "changed");
254 SetRegistration(registration
);
258 void ServiceWorkerUpdateJob::SetRegistration(
259 ServiceWorkerRegistrationInfo
* aRegistration
) {
260 MOZ_ASSERT(NS_IsMainThread());
262 MOZ_ASSERT(!mRegistration
);
263 MOZ_ASSERT(aRegistration
);
264 mRegistration
= aRegistration
;
267 void ServiceWorkerUpdateJob::Update() {
268 MOZ_ASSERT(NS_IsMainThread());
269 MOZ_ASSERT(!Canceled());
271 // SetRegistration() must be called before Update().
272 MOZ_ASSERT(mRegistration
);
273 MOZ_ASSERT(!mRegistration
->GetInstalling());
275 // Begin the script download and comparison steps starting at step 5
276 // of the Update algorithm.
278 RefPtr
<ServiceWorkerInfo
> workerInfo
= mRegistration
->Newest();
279 nsAutoString cacheName
;
281 // If the script has not changed, we need to perform a byte-for-byte
283 if (workerInfo
&& workerInfo
->ScriptSpec().Equals(mScriptSpec
)) {
284 cacheName
= workerInfo
->CacheName();
287 RefPtr
<CompareCallback
> callback
= new CompareCallback(this);
289 nsresult rv
= serviceWorkerScriptCache::Compare(
290 mRegistration
, mPrincipal
, cacheName
, mScriptSpec
, callback
);
291 if (NS_WARN_IF(NS_FAILED(rv
))) {
297 ServiceWorkerUpdateViaCache
ServiceWorkerUpdateJob::GetUpdateViaCache() const {
298 return mUpdateViaCache
;
301 void ServiceWorkerUpdateJob::ComparisonResult(nsresult aStatus
,
302 bool aInCacheAndEqual
,
303 OnFailure aOnFailure
,
304 const nsAString
& aNewCacheName
,
305 const nsACString
& aMaxScope
,
306 nsLoadFlags aLoadFlags
) {
307 MOZ_ASSERT(NS_IsMainThread());
309 mOnFailure
= aOnFailure
;
311 RefPtr
<ServiceWorkerManager
> swm
= ServiceWorkerManager::GetInstance();
312 if (NS_WARN_IF(Canceled() || !swm
)) {
313 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR
);
317 // Handle failure of the download or comparison. This is part of Update
318 // step 5 as "If the algorithm asynchronously completes with null, then:".
319 if (NS_WARN_IF(NS_FAILED(aStatus
))) {
320 FailUpdateJob(aStatus
);
324 // The spec validates the response before performing the byte-for-byte check.
325 // Here we perform the comparison in another module and then validate the
326 // script URL and scope. Make sure to do this validation before accepting
327 // an byte-for-byte match since the service-worker-allowed header might have
328 // changed since the last time it was installed.
330 // This is step 2 the "validate response" section of Update algorithm step 5.
331 // Step 1 is performed in the serviceWorkerScriptCache code.
333 nsCOMPtr
<nsIURI
> scriptURI
;
334 nsresult rv
= NS_NewURI(getter_AddRefs(scriptURI
), mScriptSpec
);
335 if (NS_WARN_IF(NS_FAILED(rv
))) {
336 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR
);
340 nsCOMPtr
<nsIURI
> maxScopeURI
;
341 if (!aMaxScope
.IsEmpty()) {
342 rv
= NS_NewURI(getter_AddRefs(maxScopeURI
), aMaxScope
, nullptr, scriptURI
);
343 if (NS_WARN_IF(NS_FAILED(rv
))) {
344 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR
);
349 nsAutoCString defaultAllowedPrefix
;
350 rv
= GetRequiredScopeStringPrefix(scriptURI
, defaultAllowedPrefix
,
352 if (NS_WARN_IF(NS_FAILED(rv
))) {
353 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR
);
357 nsAutoCString
maxPrefix(defaultAllowedPrefix
);
359 rv
= GetRequiredScopeStringPrefix(maxScopeURI
, maxPrefix
, eUsePath
);
360 if (NS_WARN_IF(NS_FAILED(rv
))) {
361 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR
);
366 nsCOMPtr
<nsIURI
> scopeURI
;
367 rv
= NS_NewURI(getter_AddRefs(scopeURI
), mRegistration
->Scope(), nullptr,
369 if (NS_WARN_IF(NS_FAILED(rv
))) {
370 FailUpdateJob(NS_ERROR_FAILURE
);
374 nsAutoCString scopeString
;
375 rv
= scopeURI
->GetPathQueryRef(scopeString
);
376 if (NS_WARN_IF(NS_FAILED(rv
))) {
377 FailUpdateJob(NS_ERROR_FAILURE
);
381 if (!StringBeginsWith(scopeString
, maxPrefix
)) {
382 nsAutoString message
;
383 NS_ConvertUTF8toUTF16
reportScope(mRegistration
->Scope());
384 NS_ConvertUTF8toUTF16
reportMaxPrefix(maxPrefix
);
386 rv
= nsContentUtils::FormatLocalizedString(
387 message
, nsContentUtils::eDOM_PROPERTIES
,
388 "ServiceWorkerScopePathMismatch", reportScope
, reportMaxPrefix
);
389 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv
), "Failed to format localized string");
390 swm
->ReportToAllClients(mScope
, message
, u
""_ns
, u
""_ns
, 0, 0,
391 nsIScriptError::errorFlag
);
392 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR
);
396 // The response has been validated, so now we can consider if its a
397 // byte-for-byte match. This is step 6 of the Update algorithm.
398 if (aInCacheAndEqual
) {
403 Telemetry::Accumulate(Telemetry::SERVICE_WORKER_UPDATED
, 1);
405 // Begin step 7 of the Update algorithm to evaluate the new script.
406 nsLoadFlags flags
= aLoadFlags
;
407 if (GetUpdateViaCache() == ServiceWorkerUpdateViaCache::None
) {
408 flags
|= nsIRequest::VALIDATE_ALWAYS
;
411 RefPtr
<ServiceWorkerInfo
> sw
= new ServiceWorkerInfo(
412 mRegistration
->Principal(), mRegistration
->Scope(), mRegistration
->Id(),
413 mRegistration
->Version(), mScriptSpec
, aNewCacheName
, flags
);
415 // If the registration is corrupt enough to force an uninstall if the
416 // upgrade fails, then we want to make sure the upgrade takes effect
417 // if it succeeds. Therefore force the skip-waiting flag on to replace
418 // the broken worker after install.
419 if (aOnFailure
== OnFailure::Uninstall
) {
420 sw
->SetSkipWaitingFlag();
423 mRegistration
->SetEvaluating(sw
);
425 nsMainThreadPtrHandle
<ServiceWorkerUpdateJob
> handle(
426 new nsMainThreadPtrHolder
<ServiceWorkerUpdateJob
>(
427 "ServiceWorkerUpdateJob", this));
428 RefPtr
<LifeCycleEventCallback
> callback
= new ContinueUpdateRunnable(handle
);
430 ServiceWorkerPrivate
* workerPrivate
= sw
->WorkerPrivate();
431 MOZ_ASSERT(workerPrivate
);
432 rv
= workerPrivate
->CheckScriptEvaluation(callback
);
434 if (NS_WARN_IF(NS_FAILED(rv
))) {
435 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR
);
440 void ServiceWorkerUpdateJob::ContinueUpdateAfterScriptEval(
441 bool aScriptEvaluationResult
) {
442 MOZ_ASSERT(NS_IsMainThread());
444 RefPtr
<ServiceWorkerManager
> swm
= ServiceWorkerManager::GetInstance();
445 if (Canceled() || !swm
) {
446 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR
);
450 // Step 7.5 of the Update algorithm verifying that the script evaluated
453 if (NS_WARN_IF(!aScriptEvaluationResult
)) {
455 error
.ThrowTypeError
<MSG_SW_SCRIPT_THREW
>(mScriptSpec
,
456 mRegistration
->Scope());
457 FailUpdateJob(error
);
464 void ServiceWorkerUpdateJob::Install() {
465 MOZ_ASSERT(NS_IsMainThread());
466 MOZ_DIAGNOSTIC_ASSERT(!Canceled());
468 MOZ_ASSERT(!mRegistration
->GetInstalling());
470 // Begin step 2 of the Install algorithm.
472 // https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#installation-algorithm
474 mRegistration
->TransitionEvaluatingToInstalling();
476 // Step 6 of the Install algorithm resolving the job promise.
477 InvokeResultCallbacks(NS_OK
);
479 // Queue a task to fire an event named updatefound at all the
480 // ServiceWorkerRegistration.
481 mRegistration
->FireUpdateFound();
483 nsMainThreadPtrHandle
<ServiceWorkerUpdateJob
> handle(
484 new nsMainThreadPtrHolder
<ServiceWorkerUpdateJob
>(
485 "ServiceWorkerUpdateJob", this));
486 RefPtr
<LifeCycleEventCallback
> callback
= new ContinueInstallRunnable(handle
);
488 // Send the install event to the worker thread
489 ServiceWorkerPrivate
* workerPrivate
=
490 mRegistration
->GetInstalling()->WorkerPrivate();
491 nsresult rv
= workerPrivate
->SendLifeCycleEvent(u
"install"_ns
, callback
);
492 if (NS_WARN_IF(NS_FAILED(rv
))) {
493 ContinueAfterInstallEvent(false /* aSuccess */);
497 void ServiceWorkerUpdateJob::ContinueAfterInstallEvent(
498 bool aInstallEventSuccess
) {
500 return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR
);
503 // If we haven't been canceled we should have a registration. There appears
504 // to be a path where it gets cleared before we call into here. Assert
505 // to try to catch this condition, but don't crash in release.
506 MOZ_DIAGNOSTIC_ASSERT(mRegistration
);
507 if (!mRegistration
) {
508 return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR
);
511 // Continue executing the Install algorithm at step 12.
513 // "If installFailed is true"
514 if (NS_WARN_IF(!aInstallEventSuccess
)) {
515 // The installing worker is cleaned up by FailUpdateJob().
516 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR
);
520 // Abort the update Job if the installWorker is null (e.g. when an extension
521 // is shutting down and all its workers have been terminated).
522 if (!mRegistration
->GetInstalling()) {
523 return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR
);
526 mRegistration
->TransitionInstallingToWaiting();
530 // Step 20 calls for explicitly waiting for queued event tasks to fire.
531 // Instead, we simply queue a runnable to execute Activate. This ensures the
532 // events are flushed from the queue before proceeding.
534 // Step 22 of the Install algorithm. Activate is executed after the
535 // completion of this job. The controlling client and skipWaiting checks are
536 // performed in TryToActivate().
537 mRegistration
->TryToActivateAsync();
540 } // namespace mozilla::dom