diff options
Diffstat (limited to 'dom/serviceworkers/ServiceWorkerUpdateJob.cpp')
-rw-r--r-- | dom/serviceworkers/ServiceWorkerUpdateJob.cpp | 541 |
1 files changed, 541 insertions, 0 deletions
diff --git a/dom/serviceworkers/ServiceWorkerUpdateJob.cpp b/dom/serviceworkers/ServiceWorkerUpdateJob.cpp new file mode 100644 index 0000000000..a6726fbf47 --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerUpdateJob.cpp @@ -0,0 +1,541 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ServiceWorkerUpdateJob.h" + +#include "mozilla/Telemetry.h" +#include "nsIScriptError.h" +#include "nsIURL.h" +#include "nsNetUtil.h" +#include "nsProxyRelease.h" +#include "ServiceWorkerManager.h" +#include "ServiceWorkerPrivate.h" +#include "ServiceWorkerRegistrationInfo.h" +#include "ServiceWorkerScriptCache.h" +#include "mozilla/dom/WorkerCommon.h" + +namespace mozilla::dom { + +using serviceWorkerScriptCache::OnFailure; + +namespace { + +/** + * The spec mandates slightly different behaviors for computing the scope + * prefix string in case a Service-Worker-Allowed header is specified versus + * when it's not available. + * + * With the header: + * "Set maxScopeString to "/" concatenated with the strings in maxScope's + * path (including empty strings), separated from each other by "/"." + * Without the header: + * "Set maxScopeString to "/" concatenated with the strings, except the last + * string that denotes the script's file name, in registration's registering + * script url's path (including empty strings), separated from each other by + * "/"." + * + * In simpler terms, if the header is not present, we should only use the + * "directory" part of the pathname, and otherwise the entire pathname should be + * used. ScopeStringPrefixMode allows the caller to specify the desired + * behavior. + */ +enum ScopeStringPrefixMode { eUseDirectory, eUsePath }; + +nsresult GetRequiredScopeStringPrefix(nsIURI* aScriptURI, nsACString& aPrefix, + ScopeStringPrefixMode aPrefixMode) { + nsresult rv; + if (aPrefixMode == eUseDirectory) { + nsCOMPtr<nsIURL> scriptURL(do_QueryInterface(aScriptURI)); + if (NS_WARN_IF(!scriptURL)) { + return NS_ERROR_FAILURE; + } + + rv = scriptURL->GetDirectory(aPrefix); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else if (aPrefixMode == eUsePath) { + rv = aScriptURI->GetPathQueryRef(aPrefix); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + MOZ_ASSERT_UNREACHABLE("Invalid value for aPrefixMode"); + } + return NS_OK; +} + +} // anonymous namespace + +class ServiceWorkerUpdateJob::CompareCallback final + : public serviceWorkerScriptCache::CompareCallback { + RefPtr<ServiceWorkerUpdateJob> mJob; + + ~CompareCallback() = default; + + public: + explicit CompareCallback(ServiceWorkerUpdateJob* aJob) : mJob(aJob) { + MOZ_ASSERT(mJob); + } + + virtual void ComparisonResult(nsresult aStatus, bool aInCacheAndEqual, + OnFailure aOnFailure, + const nsAString& aNewCacheName, + const nsACString& aMaxScope, + nsLoadFlags aLoadFlags) override { + mJob->ComparisonResult(aStatus, aInCacheAndEqual, aOnFailure, aNewCacheName, + aMaxScope, aLoadFlags); + } + + NS_INLINE_DECL_REFCOUNTING(ServiceWorkerUpdateJob::CompareCallback, override) +}; + +class ServiceWorkerUpdateJob::ContinueUpdateRunnable final + : public LifeCycleEventCallback { + nsMainThreadPtrHandle<ServiceWorkerUpdateJob> mJob; + bool mSuccess; + + public: + explicit ContinueUpdateRunnable( + const nsMainThreadPtrHandle<ServiceWorkerUpdateJob>& aJob) + : mJob(aJob), mSuccess(false) { + MOZ_ASSERT(NS_IsMainThread()); + } + + void SetResult(bool aResult) override { mSuccess = aResult; } + + NS_IMETHOD + Run() override { + MOZ_ASSERT(NS_IsMainThread()); + mJob->ContinueUpdateAfterScriptEval(mSuccess); + mJob = nullptr; + return NS_OK; + } +}; + +class ServiceWorkerUpdateJob::ContinueInstallRunnable final + : public LifeCycleEventCallback { + nsMainThreadPtrHandle<ServiceWorkerUpdateJob> mJob; + bool mSuccess; + + public: + explicit ContinueInstallRunnable( + const nsMainThreadPtrHandle<ServiceWorkerUpdateJob>& aJob) + : mJob(aJob), mSuccess(false) { + MOZ_ASSERT(NS_IsMainThread()); + } + + void SetResult(bool aResult) override { mSuccess = aResult; } + + NS_IMETHOD + Run() override { + MOZ_ASSERT(NS_IsMainThread()); + mJob->ContinueAfterInstallEvent(mSuccess); + mJob = nullptr; + return NS_OK; + } +}; + +ServiceWorkerUpdateJob::ServiceWorkerUpdateJob( + nsIPrincipal* aPrincipal, const nsACString& aScope, nsCString aScriptSpec, + ServiceWorkerUpdateViaCache aUpdateViaCache) + : ServiceWorkerUpdateJob(Type::Update, aPrincipal, aScope, + std::move(aScriptSpec), aUpdateViaCache) {} + +already_AddRefed<ServiceWorkerRegistrationInfo> +ServiceWorkerUpdateJob::GetRegistration() const { + MOZ_ASSERT(NS_IsMainThread()); + RefPtr<ServiceWorkerRegistrationInfo> ref = mRegistration; + return ref.forget(); +} + +ServiceWorkerUpdateJob::ServiceWorkerUpdateJob( + Type aType, nsIPrincipal* aPrincipal, const nsACString& aScope, + nsCString aScriptSpec, ServiceWorkerUpdateViaCache aUpdateViaCache) + : ServiceWorkerJob(aType, aPrincipal, aScope, std::move(aScriptSpec)), + mUpdateViaCache(aUpdateViaCache), + mOnFailure(serviceWorkerScriptCache::OnFailure::DoNothing) {} + +ServiceWorkerUpdateJob::~ServiceWorkerUpdateJob() = default; + +void ServiceWorkerUpdateJob::FailUpdateJob(ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aRv.Failed()); + + // Cleanup after a failed installation. This essentially implements + // step 13 of the Install algorithm. + // + // https://w3c.github.io/ServiceWorker/#installation-algorithm + // + // The spec currently only runs this after an install event fails, + // but we must handle many more internal errors. So we check for + // cleanup on every non-successful exit. + if (mRegistration) { + // Some kinds of failures indicate there is something broken in the + // currently installed registration. In these cases we want to fully + // unregister. + if (mOnFailure == OnFailure::Uninstall) { + mRegistration->ClearAsCorrupt(); + } + + // Otherwise just clear the workers we may have created as part of the + // update process. + else { + mRegistration->ClearEvaluating(); + mRegistration->ClearInstalling(); + } + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (swm) { + swm->MaybeRemoveRegistration(mRegistration); + + // Also clear the registration on disk if we are forcing uninstall + // due to a particularly bad failure. + if (mOnFailure == OnFailure::Uninstall) { + swm->MaybeSendUnregister(mRegistration->Principal(), + mRegistration->Scope()); + } + } + } + + mRegistration = nullptr; + + Finish(aRv); +} + +void ServiceWorkerUpdateJob::FailUpdateJob(nsresult aRv) { + ErrorResult rv(aRv); + FailUpdateJob(rv); +} + +void ServiceWorkerUpdateJob::AsyncExecute() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(GetType() == Type::Update); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (Canceled() || !swm) { + FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + return; + } + + // Invoke Update algorithm: + // https://w3c.github.io/ServiceWorker/#update-algorithm + // + // "Let registration be the result of running the Get Registration algorithm + // passing job’s scope url as the argument." + RefPtr<ServiceWorkerRegistrationInfo> registration = + swm->GetRegistration(mPrincipal, mScope); + + if (!registration) { + ErrorResult rv; + rv.ThrowTypeError<MSG_SW_UPDATE_BAD_REGISTRATION>(mScope, "uninstalled"); + FailUpdateJob(rv); + return; + } + + // "Let newestWorker be the result of running Get Newest Worker algorithm + // passing registration as the argument." + RefPtr<ServiceWorkerInfo> newest = registration->Newest(); + + // "If job’s job type is update, and newestWorker is not null and its script + // url does not equal job’s script url, then: + // 1. Invoke Reject Job Promise with job and TypeError. + // 2. Invoke Finish Job with job and abort these steps." + if (newest && !newest->ScriptSpec().Equals(mScriptSpec)) { + ErrorResult rv; + rv.ThrowTypeError<MSG_SW_UPDATE_BAD_REGISTRATION>(mScope, "changed"); + FailUpdateJob(rv); + return; + } + + SetRegistration(registration); + Update(); +} + +void ServiceWorkerUpdateJob::SetRegistration( + ServiceWorkerRegistrationInfo* aRegistration) { + MOZ_ASSERT(NS_IsMainThread()); + + MOZ_ASSERT(!mRegistration); + MOZ_ASSERT(aRegistration); + mRegistration = aRegistration; +} + +void ServiceWorkerUpdateJob::Update() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!Canceled()); + + // SetRegistration() must be called before Update(). + MOZ_ASSERT(mRegistration); + MOZ_ASSERT(!mRegistration->GetInstalling()); + + // Begin the script download and comparison steps starting at step 5 + // of the Update algorithm. + + RefPtr<ServiceWorkerInfo> workerInfo = mRegistration->Newest(); + nsAutoString cacheName; + + // If the script has not changed, we need to perform a byte-for-byte + // comparison. + if (workerInfo && workerInfo->ScriptSpec().Equals(mScriptSpec)) { + cacheName = workerInfo->CacheName(); + } + + RefPtr<CompareCallback> callback = new CompareCallback(this); + + nsresult rv = serviceWorkerScriptCache::Compare( + mRegistration, mPrincipal, cacheName, NS_ConvertUTF8toUTF16(mScriptSpec), + callback); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailUpdateJob(rv); + return; + } +} + +ServiceWorkerUpdateViaCache ServiceWorkerUpdateJob::GetUpdateViaCache() const { + return mUpdateViaCache; +} + +void ServiceWorkerUpdateJob::ComparisonResult(nsresult aStatus, + bool aInCacheAndEqual, + OnFailure aOnFailure, + const nsAString& aNewCacheName, + const nsACString& aMaxScope, + nsLoadFlags aLoadFlags) { + MOZ_ASSERT(NS_IsMainThread()); + + mOnFailure = aOnFailure; + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (NS_WARN_IF(Canceled() || !swm)) { + FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + return; + } + + // Handle failure of the download or comparison. This is part of Update + // step 5 as "If the algorithm asynchronously completes with null, then:". + if (NS_WARN_IF(NS_FAILED(aStatus))) { + FailUpdateJob(aStatus); + return; + } + + // The spec validates the response before performing the byte-for-byte check. + // Here we perform the comparison in another module and then validate the + // script URL and scope. Make sure to do this validation before accepting + // an byte-for-byte match since the service-worker-allowed header might have + // changed since the last time it was installed. + + // This is step 2 the "validate response" section of Update algorithm step 5. + // Step 1 is performed in the serviceWorkerScriptCache code. + + nsCOMPtr<nsIURI> scriptURI; + nsresult rv = NS_NewURI(getter_AddRefs(scriptURI), mScriptSpec); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsCOMPtr<nsIURI> maxScopeURI; + if (!aMaxScope.IsEmpty()) { + rv = NS_NewURI(getter_AddRefs(maxScopeURI), aMaxScope, nullptr, scriptURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR); + return; + } + } + + nsAutoCString defaultAllowedPrefix; + rv = GetRequiredScopeStringPrefix(scriptURI, defaultAllowedPrefix, + eUseDirectory); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsAutoCString maxPrefix(defaultAllowedPrefix); + if (maxScopeURI) { + rv = GetRequiredScopeStringPrefix(maxScopeURI, maxPrefix, eUsePath); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR); + return; + } + } + + nsCOMPtr<nsIURI> scopeURI; + rv = NS_NewURI(getter_AddRefs(scopeURI), mRegistration->Scope(), nullptr, + scriptURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailUpdateJob(NS_ERROR_FAILURE); + return; + } + + nsAutoCString scopeString; + rv = scopeURI->GetPathQueryRef(scopeString); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailUpdateJob(NS_ERROR_FAILURE); + return; + } + + if (!StringBeginsWith(scopeString, maxPrefix)) { + nsAutoString message; + NS_ConvertUTF8toUTF16 reportScope(mRegistration->Scope()); + NS_ConvertUTF8toUTF16 reportMaxPrefix(maxPrefix); + + rv = nsContentUtils::FormatLocalizedString( + message, nsContentUtils::eDOM_PROPERTIES, + "ServiceWorkerScopePathMismatch", reportScope, reportMaxPrefix); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to format localized string"); + swm->ReportToAllClients(mScope, message, u""_ns, u""_ns, 0, 0, + nsIScriptError::errorFlag); + FailUpdateJob(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + // The response has been validated, so now we can consider if its a + // byte-for-byte match. This is step 6 of the Update algorithm. + if (aInCacheAndEqual) { + Finish(NS_OK); + return; + } + + Telemetry::Accumulate(Telemetry::SERVICE_WORKER_UPDATED, 1); + + // Begin step 7 of the Update algorithm to evaluate the new script. + nsLoadFlags flags = aLoadFlags; + if (GetUpdateViaCache() == ServiceWorkerUpdateViaCache::None) { + flags |= nsIRequest::VALIDATE_ALWAYS; + } + + RefPtr<ServiceWorkerInfo> sw = new ServiceWorkerInfo( + mRegistration->Principal(), mRegistration->Scope(), mRegistration->Id(), + mRegistration->Version(), mScriptSpec, aNewCacheName, flags); + + // If the registration is corrupt enough to force an uninstall if the + // upgrade fails, then we want to make sure the upgrade takes effect + // if it succeeds. Therefore force the skip-waiting flag on to replace + // the broken worker after install. + if (aOnFailure == OnFailure::Uninstall) { + sw->SetSkipWaitingFlag(); + } + + mRegistration->SetEvaluating(sw); + + nsMainThreadPtrHandle<ServiceWorkerUpdateJob> handle( + new nsMainThreadPtrHolder<ServiceWorkerUpdateJob>( + "ServiceWorkerUpdateJob", this)); + RefPtr<LifeCycleEventCallback> callback = new ContinueUpdateRunnable(handle); + + ServiceWorkerPrivate* workerPrivate = sw->WorkerPrivate(); + MOZ_ASSERT(workerPrivate); + rv = workerPrivate->CheckScriptEvaluation(callback); + + if (NS_WARN_IF(NS_FAILED(rv))) { + FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + return; + } +} + +void ServiceWorkerUpdateJob::ContinueUpdateAfterScriptEval( + bool aScriptEvaluationResult) { + MOZ_ASSERT(NS_IsMainThread()); + + RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance(); + if (Canceled() || !swm) { + FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + return; + } + + // Step 7.5 of the Update algorithm verifying that the script evaluated + // successfully. + + if (NS_WARN_IF(!aScriptEvaluationResult)) { + ErrorResult error; + error.ThrowTypeError<MSG_SW_SCRIPT_THREW>(mScriptSpec, + mRegistration->Scope()); + FailUpdateJob(error); + return; + } + + Install(); +} + +void ServiceWorkerUpdateJob::Install() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(!Canceled()); + + MOZ_ASSERT(!mRegistration->GetInstalling()); + + // Begin step 2 of the Install algorithm. + // + // https://slightlyoff.github.io/ServiceWorker/spec/service_worker/index.html#installation-algorithm + + mRegistration->TransitionEvaluatingToInstalling(); + + // Step 6 of the Install algorithm resolving the job promise. + InvokeResultCallbacks(NS_OK); + + // Queue a task to fire an event named updatefound at all the + // ServiceWorkerRegistration. + mRegistration->FireUpdateFound(); + + nsMainThreadPtrHandle<ServiceWorkerUpdateJob> handle( + new nsMainThreadPtrHolder<ServiceWorkerUpdateJob>( + "ServiceWorkerUpdateJob", this)); + RefPtr<LifeCycleEventCallback> callback = new ContinueInstallRunnable(handle); + + // Send the install event to the worker thread + ServiceWorkerPrivate* workerPrivate = + mRegistration->GetInstalling()->WorkerPrivate(); + nsresult rv = workerPrivate->SendLifeCycleEvent(u"install"_ns, callback); + if (NS_WARN_IF(NS_FAILED(rv))) { + ContinueAfterInstallEvent(false /* aSuccess */); + } +} + +void ServiceWorkerUpdateJob::ContinueAfterInstallEvent( + bool aInstallEventSuccess) { + if (Canceled()) { + return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + } + + // If we haven't been canceled we should have a registration. There appears + // to be a path where it gets cleared before we call into here. Assert + // to try to catch this condition, but don't crash in release. + MOZ_DIAGNOSTIC_ASSERT(mRegistration); + if (!mRegistration) { + return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + } + + // Continue executing the Install algorithm at step 12. + + // "If installFailed is true" + if (NS_WARN_IF(!aInstallEventSuccess)) { + // The installing worker is cleaned up by FailUpdateJob(). + FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + return; + } + + // Abort the update Job if the installWorker is null (e.g. when an extension + // is shutting down and all its workers have been terminated). + if (!mRegistration->GetInstalling()) { + return FailUpdateJob(NS_ERROR_DOM_ABORT_ERR); + } + + mRegistration->TransitionInstallingToWaiting(); + + Finish(NS_OK); + + // Step 20 calls for explicitly waiting for queued event tasks to fire. + // Instead, we simply queue a runnable to execute Activate. This ensures the + // events are flushed from the queue before proceeding. + + // Step 22 of the Install algorithm. Activate is executed after the + // completion of this job. The controlling client and skipWaiting checks are + // performed in TryToActivate(). + mRegistration->TryToActivateAsync(); +} + +} // namespace mozilla::dom |