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
, NS_ConvertUTF8toUTF16(mScriptSpec
),
292 if (NS_WARN_IF(NS_FAILED(rv
))) {
298 ServiceWorkerUpdateViaCache
ServiceWorkerUpdateJob::GetUpdateViaCache() const {
299 return mUpdateViaCache
;
302 void ServiceWorkerUpdateJob::ComparisonResult(nsresult aStatus
,
303 bool aInCacheAndEqual
,
304 OnFailure aOnFailure
,
305 const nsAString
& aNewCacheName
,
306 const nsACString
& aMaxScope
,
307 nsLoadFlags aLoadFlags
) {
308 MOZ_ASSERT(NS_IsMainThread());
310 mOnFailure
= aOnFailure
;
312 RefPtr
<ServiceWorkerManager
> swm
= ServiceWorkerManager::GetInstance();
313 if (NS_WARN_IF(Canceled() || !swm
)) {
314 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR
);
318 // Handle failure of the download or comparison. This is part of Update
319 // step 5 as "If the algorithm asynchronously completes with null, then:".
320 if (NS_WARN_IF(NS_FAILED(aStatus
))) {
321 FailUpdateJob(aStatus
);
325 // The spec validates the response before performing the byte-for-byte check.
326 // Here we perform the comparison in another module and then validate the
327 // script URL and scope. Make sure to do this validation before accepting
328 // an byte-for-byte match since the service-worker-allowed header might have
329 // changed since the last time it was installed.
331 // This is step 2 the "validate response" section of Update algorithm step 5.
332 // Step 1 is performed in the serviceWorkerScriptCache code.
334 nsCOMPtr
<nsIURI
> scriptURI
;
335 nsresult rv
= NS_NewURI(getter_AddRefs(scriptURI
), mScriptSpec
);
336 if (NS_WARN_IF(NS_FAILED(rv
))) {
337 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR
);
341 nsCOMPtr
<nsIURI
> maxScopeURI
;
342 if (!aMaxScope
.IsEmpty()) {
343 rv
= NS_NewURI(getter_AddRefs(maxScopeURI
), aMaxScope
, nullptr, scriptURI
);
344 if (NS_WARN_IF(NS_FAILED(rv
))) {
345 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR
);
350 nsAutoCString defaultAllowedPrefix
;
351 rv
= GetRequiredScopeStringPrefix(scriptURI
, defaultAllowedPrefix
,
353 if (NS_WARN_IF(NS_FAILED(rv
))) {
354 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR
);
358 nsAutoCString
maxPrefix(defaultAllowedPrefix
);
360 rv
= GetRequiredScopeStringPrefix(maxScopeURI
, maxPrefix
, eUsePath
);
361 if (NS_WARN_IF(NS_FAILED(rv
))) {
362 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR
);
367 nsCOMPtr
<nsIURI
> scopeURI
;
368 rv
= NS_NewURI(getter_AddRefs(scopeURI
), mRegistration
->Scope(), nullptr,
370 if (NS_WARN_IF(NS_FAILED(rv
))) {
371 FailUpdateJob(NS_ERROR_FAILURE
);
375 nsAutoCString scopeString
;
376 rv
= scopeURI
->GetPathQueryRef(scopeString
);
377 if (NS_WARN_IF(NS_FAILED(rv
))) {
378 FailUpdateJob(NS_ERROR_FAILURE
);
382 if (!StringBeginsWith(scopeString
, maxPrefix
)) {
383 nsAutoString message
;
384 NS_ConvertUTF8toUTF16
reportScope(mRegistration
->Scope());
385 NS_ConvertUTF8toUTF16
reportMaxPrefix(maxPrefix
);
387 rv
= nsContentUtils::FormatLocalizedString(
388 message
, nsContentUtils::eDOM_PROPERTIES
,
389 "ServiceWorkerScopePathMismatch", reportScope
, reportMaxPrefix
);
390 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv
), "Failed to format localized string");
391 swm
->ReportToAllClients(mScope
, message
, u
""_ns
, u
""_ns
, 0, 0,
392 nsIScriptError::errorFlag
);
393 FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR
);
397 // The response has been validated, so now we can consider if its a
398 // byte-for-byte match. This is step 6 of the Update algorithm.
399 if (aInCacheAndEqual
) {
404 Telemetry::Accumulate(Telemetry::SERVICE_WORKER_UPDATED
, 1);
406 // Begin step 7 of the Update algorithm to evaluate the new script.
407 nsLoadFlags flags
= aLoadFlags
;
408 if (GetUpdateViaCache() == ServiceWorkerUpdateViaCache::None
) {
409 flags
|= nsIRequest::VALIDATE_ALWAYS
;
412 RefPtr
<ServiceWorkerInfo
> sw
= new ServiceWorkerInfo(
413 mRegistration
->Principal(), mRegistration
->Scope(), mRegistration
->Id(),
414 mRegistration
->Version(), mScriptSpec
, aNewCacheName
, flags
);
416 // If the registration is corrupt enough to force an uninstall if the
417 // upgrade fails, then we want to make sure the upgrade takes effect
418 // if it succeeds. Therefore force the skip-waiting flag on to replace
419 // the broken worker after install.
420 if (aOnFailure
== OnFailure::Uninstall
) {
421 sw
->SetSkipWaitingFlag();
424 mRegistration
->SetEvaluating(sw
);
426 nsMainThreadPtrHandle
<ServiceWorkerUpdateJob
> handle(
427 new nsMainThreadPtrHolder
<ServiceWorkerUpdateJob
>(
428 "ServiceWorkerUpdateJob", this));
429 RefPtr
<LifeCycleEventCallback
> callback
= new ContinueUpdateRunnable(handle
);
431 ServiceWorkerPrivate
* workerPrivate
= sw
->WorkerPrivate();
432 MOZ_ASSERT(workerPrivate
);
433 rv
= workerPrivate
->CheckScriptEvaluation(callback
);
435 if (NS_WARN_IF(NS_FAILED(rv
))) {
436 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR
);
441 void ServiceWorkerUpdateJob::ContinueUpdateAfterScriptEval(
442 bool aScriptEvaluationResult
) {
443 MOZ_ASSERT(NS_IsMainThread());
445 RefPtr
<ServiceWorkerManager
> swm
= ServiceWorkerManager::GetInstance();
446 if (Canceled() || !swm
) {
447 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR
);
451 // Step 7.5 of the Update algorithm verifying that the script evaluated
454 if (NS_WARN_IF(!aScriptEvaluationResult
)) {
456 error
.ThrowTypeError
<MSG_SW_SCRIPT_THREW
>(mScriptSpec
,
457 mRegistration
->Scope());
458 FailUpdateJob(error
);
465 void ServiceWorkerUpdateJob::Install() {
466 MOZ_ASSERT(NS_IsMainThread());
467 MOZ_DIAGNOSTIC_ASSERT(!Canceled());
469 MOZ_ASSERT(!mRegistration
->GetInstalling());
471 // Begin step 2 of the Install algorithm.
473 // https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#installation-algorithm
475 mRegistration
->TransitionEvaluatingToInstalling();
477 // Step 6 of the Install algorithm resolving the job promise.
478 InvokeResultCallbacks(NS_OK
);
480 // Queue a task to fire an event named updatefound at all the
481 // ServiceWorkerRegistration.
482 mRegistration
->FireUpdateFound();
484 nsMainThreadPtrHandle
<ServiceWorkerUpdateJob
> handle(
485 new nsMainThreadPtrHolder
<ServiceWorkerUpdateJob
>(
486 "ServiceWorkerUpdateJob", this));
487 RefPtr
<LifeCycleEventCallback
> callback
= new ContinueInstallRunnable(handle
);
489 // Send the install event to the worker thread
490 ServiceWorkerPrivate
* workerPrivate
=
491 mRegistration
->GetInstalling()->WorkerPrivate();
492 nsresult rv
= workerPrivate
->SendLifeCycleEvent(u
"install"_ns
, callback
);
493 if (NS_WARN_IF(NS_FAILED(rv
))) {
494 ContinueAfterInstallEvent(false /* aSuccess */);
498 void ServiceWorkerUpdateJob::ContinueAfterInstallEvent(
499 bool aInstallEventSuccess
) {
501 return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR
);
504 // If we haven't been canceled we should have a registration. There appears
505 // to be a path where it gets cleared before we call into here. Assert
506 // to try to catch this condition, but don't crash in release.
507 MOZ_DIAGNOSTIC_ASSERT(mRegistration
);
508 if (!mRegistration
) {
509 return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR
);
512 // Continue executing the Install algorithm at step 12.
514 // "If installFailed is true"
515 if (NS_WARN_IF(!aInstallEventSuccess
)) {
516 // The installing worker is cleaned up by FailUpdateJob().
517 FailUpdateJob(NS_ERROR_DOM_ABORT_ERR
);
521 // Abort the update Job if the installWorker is null (e.g. when an extension
522 // is shutting down and all its workers have been terminated).
523 if (!mRegistration
->GetInstalling()) {
524 return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR
);
527 mRegistration
->TransitionInstallingToWaiting();
531 // Step 20 calls for explicitly waiting for queued event tasks to fire.
532 // Instead, we simply queue a runnable to execute Activate. This ensures the
533 // events are flushed from the queue before proceeding.
535 // Step 22 of the Install algorithm. Activate is executed after the
536 // completion of this job. The controlling client and skipWaiting checks are
537 // performed in TryToActivate().
538 mRegistration
->TryToActivateAsync();
541 } // namespace mozilla::dom