From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- dom/serviceworkers/ServiceWorkerScriptCache.cpp | 1508 +++++++++++++++++++++++ 1 file changed, 1508 insertions(+) create mode 100644 dom/serviceworkers/ServiceWorkerScriptCache.cpp (limited to 'dom/serviceworkers/ServiceWorkerScriptCache.cpp') diff --git a/dom/serviceworkers/ServiceWorkerScriptCache.cpp b/dom/serviceworkers/ServiceWorkerScriptCache.cpp new file mode 100644 index 0000000000..f1569ffe5f --- /dev/null +++ b/dom/serviceworkers/ServiceWorkerScriptCache.cpp @@ -0,0 +1,1508 @@ +/* -*- 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 "ServiceWorkerScriptCache.h" + +#include "js/Array.h" // JS::GetArrayLength +#include "js/PropertyAndElement.h" // JS_GetElement +#include "js/Utility.h" // JS::FreePolicy +#include "mozilla/TaskQueue.h" +#include "mozilla/Unused.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/CacheBinding.h" +#include "mozilla/dom/cache/CacheStorage.h" +#include "mozilla/dom/cache/Cache.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseWorkerProxy.h" +#include "mozilla/dom/ScriptLoader.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/net/CookieJarSettings.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_extensions.h" +#include "nsICacheInfoChannel.h" +#include "nsIHttpChannel.h" +#include "nsIStreamLoader.h" +#include "nsIThreadRetargetableRequest.h" +#include "nsIUUIDGenerator.h" +#include "nsIXPConnect.h" + +#include "nsIInputStreamPump.h" +#include "nsIPrincipal.h" +#include "nsIScriptSecurityManager.h" +#include "nsContentUtils.h" +#include "nsNetUtil.h" +#include "ServiceWorkerManager.h" +#include "nsStringStream.h" + +using mozilla::dom::cache::Cache; +using mozilla::dom::cache::CacheStorage; +using mozilla::ipc::PrincipalInfo; + +namespace mozilla::dom::serviceWorkerScriptCache { + +namespace { + +already_AddRefed CreateCacheStorage(JSContext* aCx, + nsIPrincipal* aPrincipal, + ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + nsIXPConnect* xpc = nsContentUtils::XPConnect(); + MOZ_ASSERT(xpc, "This should never be null!"); + JS::Rooted sandbox(aCx); + aRv = xpc->CreateSandbox(aCx, aPrincipal, sandbox.address()); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + // This is called when the JSContext is not in a realm, so CreateSandbox + // returned an unwrapped global. + MOZ_ASSERT(JS_IsGlobalObject(sandbox)); + + nsCOMPtr sandboxGlobalObject = xpc::NativeGlobal(sandbox); + if (!sandboxGlobalObject) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + // We assume private browsing is not enabled here. The ScriptLoader + // explicitly fails for private browsing so there should never be + // a service worker running in private browsing mode. Therefore if + // we are purging scripts or running a comparison algorithm we cannot + // be in private browsing. + // + // Also, bypass the CacheStorage trusted origin checks. The ServiceWorker + // has validated the origin prior to this point. All the information + // to revalidate is not available now. + return CacheStorage::CreateOnMainThread(cache::CHROME_ONLY_NAMESPACE, + sandboxGlobalObject, aPrincipal, + true /* force trusted origin */, aRv); +} + +class CompareManager; +class CompareCache; + +// This class downloads a URL from the network, compare the downloaded script +// with an existing cache if provided, and report to CompareManager via calling +// ComparisonFinished(). +class CompareNetwork final : public nsIStreamLoaderObserver, + public nsIRequestObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLOADEROBSERVER + NS_DECL_NSIREQUESTOBSERVER + + CompareNetwork(CompareManager* aManager, + ServiceWorkerRegistrationInfo* aRegistration, + bool aIsMainScript) + : mManager(aManager), + mRegistration(aRegistration), + mInternalHeaders(new InternalHeaders()), + mLoadFlags(nsIChannel::LOAD_BYPASS_SERVICE_WORKER), + mState(WaitingForInitialization), + mNetworkResult(NS_OK), + mCacheResult(NS_OK), + mIsMainScript(aIsMainScript), + mIsFromCache(false) { + MOZ_ASSERT(aManager); + MOZ_ASSERT(NS_IsMainThread()); + } + + nsresult Initialize(nsIPrincipal* aPrincipal, const nsAString& aURL, + Cache* const aCache); + + void Abort(); + + void NetworkFinish(nsresult aRv); + + void CacheFinish(nsresult aRv); + + const nsString& URL() const { + MOZ_ASSERT(NS_IsMainThread()); + return mURL; + } + + const nsString& Buffer() const { + MOZ_ASSERT(NS_IsMainThread()); + return mBuffer; + } + + const ChannelInfo& GetChannelInfo() const { return mChannelInfo; } + + already_AddRefed GetInternalHeaders() const { + RefPtr internalHeaders = mInternalHeaders; + return internalHeaders.forget(); + } + + UniquePtr TakePrincipalInfo() { + return std::move(mPrincipalInfo); + } + + bool Succeeded() const { return NS_SUCCEEDED(mNetworkResult); } + + const nsTArray& URLList() const { return mURLList; } + + private: + ~CompareNetwork() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mCC); + } + + void Finish(); + + nsresult SetPrincipalInfo(nsIChannel* aChannel); + + RefPtr mManager; + RefPtr mCC; + RefPtr mRegistration; + + nsCOMPtr mChannel; + nsString mBuffer; + nsString mURL; + ChannelInfo mChannelInfo; + RefPtr mInternalHeaders; + UniquePtr mPrincipalInfo; + nsTArray mURLList; + + nsCString mMaxScope; + nsLoadFlags mLoadFlags; + + enum { + WaitingForInitialization, + WaitingForBothFinished, + WaitingForNetworkFinished, + WaitingForCacheFinished, + Finished + } mState; + + nsresult mNetworkResult; + nsresult mCacheResult; + + const bool mIsMainScript; + bool mIsFromCache; +}; + +NS_IMPL_ISUPPORTS(CompareNetwork, nsIStreamLoaderObserver, nsIRequestObserver) + +// This class gets a cached Response from the CacheStorage and then it calls +// CacheFinish() in the CompareNetwork. +class CompareCache final : public PromiseNativeHandler, + public nsIStreamLoaderObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLOADEROBSERVER + + explicit CompareCache(CompareNetwork* aCN) + : mCN(aCN), mState(WaitingForInitialization), mInCache(false) { + MOZ_ASSERT(aCN); + MOZ_ASSERT(NS_IsMainThread()); + } + + nsresult Initialize(Cache* const aCache, const nsAString& aURL); + + void Finish(nsresult aStatus, bool aInCache); + + void Abort(); + + virtual void ResolvedCallback(JSContext* aCx, JS::Handle aValue, + ErrorResult& aRv) override; + + virtual void RejectedCallback(JSContext* aCx, JS::Handle aValue, + ErrorResult& aRv) override; + + const nsString& Buffer() const { + MOZ_ASSERT(NS_IsMainThread()); + return mBuffer; + } + + bool InCache() { return mInCache; } + + private: + ~CompareCache() { MOZ_ASSERT(NS_IsMainThread()); } + + void ManageValueResult(JSContext* aCx, JS::Handle aValue); + + RefPtr mCN; + nsCOMPtr mPump; + + nsString mURL; + nsString mBuffer; + + enum { + WaitingForInitialization, + WaitingForScript, + Finished, + } mState; + + bool mInCache; +}; + +NS_IMPL_ISUPPORTS(CompareCache, nsIStreamLoaderObserver) + +class CompareManager final : public PromiseNativeHandler { + public: + NS_DECL_ISUPPORTS + + explicit CompareManager(ServiceWorkerRegistrationInfo* aRegistration, + CompareCallback* aCallback) + : mRegistration(aRegistration), + mCallback(aCallback), + mLoadFlags(nsIChannel::LOAD_BYPASS_SERVICE_WORKER), + mState(WaitingForInitialization), + mPendingCount(0), + mOnFailure(OnFailure::DoNothing), + mAreScriptsEqual(true) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aRegistration); + } + + nsresult Initialize(nsIPrincipal* aPrincipal, const nsAString& aURL, + const nsAString& aCacheName); + + void ResolvedCallback(JSContext* aCx, JS::Handle aValue, + ErrorResult& aRv) override; + + void RejectedCallback(JSContext* aCx, JS::Handle aValue, + ErrorResult& aRv) override; + + CacheStorage* CacheStorage_() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mCacheStorage); + return mCacheStorage; + } + + void ComparisonFinished(nsresult aStatus, bool aIsMainScript, bool aIsEqual, + const nsACString& aMaxScope, nsLoadFlags aLoadFlags) { + MOZ_ASSERT(NS_IsMainThread()); + if (mState == Finished) { + return; + } + + MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForScriptOrComparisonResult); + + if (NS_WARN_IF(NS_FAILED(aStatus))) { + Fail(aStatus); + return; + } + + mAreScriptsEqual = mAreScriptsEqual && aIsEqual; + + if (aIsMainScript) { + mMaxScope = aMaxScope; + mLoadFlags = aLoadFlags; + } + + // Check whether all CompareNetworks finished their jobs. + MOZ_DIAGNOSTIC_ASSERT(mPendingCount > 0); + if (--mPendingCount) { + return; + } + + if (mAreScriptsEqual) { + MOZ_ASSERT(mCallback); + mCallback->ComparisonResult(aStatus, true /* aSameScripts */, mOnFailure, + u""_ns, mMaxScope, mLoadFlags); + Cleanup(); + return; + } + + // Write to Cache so ScriptLoader reads succeed. + WriteNetworkBufferToNewCache(); + } + + private: + ~CompareManager() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mCNList.Length() == 0); + } + + void Fail(nsresult aStatus); + + void Cleanup(); + + nsresult FetchScript(const nsAString& aURL, bool aIsMainScript, + Cache* const aCache = nullptr) { + MOZ_ASSERT(NS_IsMainThread()); + + MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForInitialization || + mState == WaitingForScriptOrComparisonResult); + + RefPtr cn = + new CompareNetwork(this, mRegistration, aIsMainScript); + mCNList.AppendElement(cn); + mPendingCount += 1; + + nsresult rv = cn->Initialize(mPrincipal, aURL, aCache); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; + } + + void ManageOldCache(JSContext* aCx, JS::Handle aValue) { + MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForExistingOpen); + + // RAII Cleanup when fails. + nsresult rv = NS_ERROR_FAILURE; + auto guard = MakeScopeExit([&] { Fail(rv); }); + + if (NS_WARN_IF(!aValue.isObject())) { + return; + } + + MOZ_ASSERT(!mOldCache); + JS::Rooted obj(aCx, &aValue.toObject()); + if (NS_WARN_IF(!obj) || + NS_WARN_IF(NS_FAILED(UNWRAP_OBJECT(Cache, obj, mOldCache)))) { + return; + } + + Optional request; + CacheQueryOptions options; + ErrorResult error; + RefPtr promise = mOldCache->Keys(aCx, request, options, error); + if (NS_WARN_IF(error.Failed())) { + // No exception here because there are no ReadableStreams involved here. + MOZ_ASSERT(!error.IsJSException()); + rv = error.StealNSResult(); + return; + } + + mState = WaitingForExistingKeys; + promise->AppendNativeHandler(this); + guard.release(); + } + + void ManageOldKeys(JSContext* aCx, JS::Handle aValue) { + MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForExistingKeys); + + // RAII Cleanup when fails. + nsresult rv = NS_ERROR_FAILURE; + auto guard = MakeScopeExit([&] { Fail(rv); }); + + if (NS_WARN_IF(!aValue.isObject())) { + return; + } + + JS::Rooted obj(aCx, &aValue.toObject()); + if (NS_WARN_IF(!obj)) { + return; + } + + uint32_t len = 0; + if (!JS::GetArrayLength(aCx, obj, &len)) { + return; + } + + // Fetch and compare the source scripts. + MOZ_ASSERT(mPendingCount == 0); + + mState = WaitingForScriptOrComparisonResult; + + bool hasMainScript = false; + AutoTArray urlList; + + // Extract the list of URLs in the old cache. + for (uint32_t i = 0; i < len; ++i) { + JS::Rooted val(aCx); + if (NS_WARN_IF(!JS_GetElement(aCx, obj, i, &val)) || + NS_WARN_IF(!val.isObject())) { + return; + } + + Request* request; + JS::Rooted requestObj(aCx, &val.toObject()); + if (NS_WARN_IF(NS_FAILED(UNWRAP_OBJECT(Request, &requestObj, request)))) { + return; + }; + + nsString url; + request->GetUrl(url); + + if (!hasMainScript && url == mURL) { + hasMainScript = true; + } + + urlList.AppendElement(url); + } + + // If the main script is missing, then something has gone wrong. We + // will try to continue with the update process to trigger a new + // installation. If that fails, however, then uninstall the registration + // because it is broken in a way that cannot be fixed. + if (!hasMainScript) { + mOnFailure = OnFailure::Uninstall; + } + + // Always make sure to fetch the main script. If the old cache has + // no entries or the main script entry is missing, then the loop below + // may not trigger it. This should not really happen, but we handle it + // gracefully if it does occur. Its possible the bad cache state is due + // to a crash or shutdown during an update, etc. + rv = FetchScript(mURL, true /* aIsMainScript */, mOldCache); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + for (const auto& url : urlList) { + // We explicitly start the fetch for the main script above. + if (mURL == url) { + continue; + } + + rv = FetchScript(url, false /* aIsMainScript */, mOldCache); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } + + guard.release(); + } + + void ManageNewCache(JSContext* aCx, JS::Handle aValue) { + MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForOpen); + + // RAII Cleanup when fails. + nsresult rv = NS_ERROR_FAILURE; + auto guard = MakeScopeExit([&] { Fail(rv); }); + + if (NS_WARN_IF(!aValue.isObject())) { + return; + } + + JS::Rooted obj(aCx, &aValue.toObject()); + if (NS_WARN_IF(!obj)) { + return; + } + + Cache* cache = nullptr; + rv = UNWRAP_OBJECT(Cache, &obj, cache); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + // Just to be safe. + RefPtr kungfuDeathGrip = cache; + + MOZ_ASSERT(mPendingCount == 0); + for (uint32_t i = 0; i < mCNList.Length(); ++i) { + // We bail out immediately when something goes wrong. + rv = WriteToCache(aCx, cache, mCNList[i]); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } + + mState = WaitingForPut; + guard.release(); + } + + void WriteNetworkBufferToNewCache() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mCNList.Length() != 0); + MOZ_ASSERT(mCacheStorage); + MOZ_ASSERT(mNewCacheName.IsEmpty()); + + ErrorResult result; + result = serviceWorkerScriptCache::GenerateCacheName(mNewCacheName); + if (NS_WARN_IF(result.Failed())) { + MOZ_ASSERT(!result.IsErrorWithMessage()); + Fail(result.StealNSResult()); + return; + } + + RefPtr cacheOpenPromise = + mCacheStorage->Open(mNewCacheName, result); + if (NS_WARN_IF(result.Failed())) { + MOZ_ASSERT(!result.IsErrorWithMessage()); + Fail(result.StealNSResult()); + return; + } + + mState = WaitingForOpen; + cacheOpenPromise->AppendNativeHandler(this); + } + + nsresult WriteToCache(JSContext* aCx, Cache* aCache, CompareNetwork* aCN) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aCache); + MOZ_ASSERT(aCN); + MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForOpen); + + // We don't have to save any information from a failed CompareNetwork. + if (!aCN->Succeeded()) { + return NS_OK; + } + + nsCOMPtr body; + nsresult rv = NS_NewCStringInputStream( + getter_AddRefs(body), NS_ConvertUTF16toUTF8(aCN->Buffer())); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + SafeRefPtr ir = + MakeSafeRefPtr(200, "OK"_ns); + ir->SetBody(body, aCN->Buffer().Length()); + ir->SetURLList(aCN->URLList()); + + ir->InitChannelInfo(aCN->GetChannelInfo()); + UniquePtr principalInfo = aCN->TakePrincipalInfo(); + if (principalInfo) { + ir->SetPrincipalInfo(std::move(principalInfo)); + } + + RefPtr internalHeaders = aCN->GetInternalHeaders(); + ir->Headers()->Fill(*(internalHeaders.get()), IgnoreErrors()); + + RefPtr response = + new Response(aCache->GetGlobalObject(), std::move(ir), nullptr); + + RequestOrUSVString request; + request.SetAsUSVString().ShareOrDependUpon(aCN->URL()); + + // For now we have to wait until the Put Promise is fulfilled before we can + // continue since Cache does not yet support starting a read that is being + // written to. + ErrorResult result; + RefPtr cachePromise = aCache->Put(aCx, request, *response, result); + result.WouldReportJSException(); + if (NS_WARN_IF(result.Failed())) { + // No exception here because there are no ReadableStreams involved here. + MOZ_ASSERT(!result.IsJSException()); + MOZ_ASSERT(!result.IsErrorWithMessage()); + return result.StealNSResult(); + } + + mPendingCount += 1; + cachePromise->AppendNativeHandler(this); + return NS_OK; + } + + RefPtr mRegistration; + RefPtr mCallback; + RefPtr mCacheStorage; + + nsTArray> mCNList; + + nsString mURL; + RefPtr mPrincipal; + + // Used for the old cache where saves the old source scripts. + RefPtr mOldCache; + + // Only used if the network script has changed and needs to be cached. + nsString mNewCacheName; + + nsCString mMaxScope; + nsLoadFlags mLoadFlags; + + enum { + WaitingForInitialization, + WaitingForExistingOpen, + WaitingForExistingKeys, + WaitingForScriptOrComparisonResult, + WaitingForOpen, + WaitingForPut, + Finished + } mState; + + uint32_t mPendingCount; + OnFailure mOnFailure; + bool mAreScriptsEqual; +}; + +NS_IMPL_ISUPPORTS0(CompareManager) + +nsresult CompareNetwork::Initialize(nsIPrincipal* aPrincipal, + const nsAString& aURL, + Cache* const aCache) { + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mURL = aURL; + mURLList.AppendElement(NS_ConvertUTF16toUTF8(mURL)); + + nsCOMPtr loadGroup; + rv = NS_NewLoadGroup(getter_AddRefs(loadGroup), aPrincipal); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Update LoadFlags for propagating to ServiceWorkerInfo. + mLoadFlags = nsIChannel::LOAD_BYPASS_SERVICE_WORKER; + + ServiceWorkerUpdateViaCache uvc = mRegistration->GetUpdateViaCache(); + if (uvc == ServiceWorkerUpdateViaCache::None || + (uvc == ServiceWorkerUpdateViaCache::Imports && mIsMainScript)) { + mLoadFlags |= nsIRequest::VALIDATE_ALWAYS; + } + + if (mRegistration->IsLastUpdateCheckTimeOverOneDay()) { + mLoadFlags |= nsIRequest::LOAD_BYPASS_CACHE; + } + + // Different settings are needed for fetching imported scripts, since they + // might be cross-origin scripts. + uint32_t secFlags = + mIsMainScript ? nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED + : nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT; + + nsContentPolicyType contentPolicyType = + mIsMainScript ? nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER + : nsIContentPolicy::TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS; + + // Create a new cookieJarSettings. + nsCOMPtr cookieJarSettings = + mozilla::net::CookieJarSettings::Create(aPrincipal); + + // Populate the partitionKey by using the given prinicpal. The ServiceWorkers + // are using the foreign partitioned principal, so we can get the partitionKey + // from it and the partitionKey will only exist if it's in the third-party + // context. In first-party context, we can still use the uri to set the + // partitionKey. + if (!aPrincipal->OriginAttributesRef().mPartitionKey.IsEmpty()) { + net::CookieJarSettings::Cast(cookieJarSettings) + ->SetPartitionKey(aPrincipal->OriginAttributesRef().mPartitionKey); + } else { + net::CookieJarSettings::Cast(cookieJarSettings)->SetPartitionKey(uri); + } + + // Note that because there is no "serviceworker" RequestContext type, we can + // use the TYPE_INTERNAL_SCRIPT content policy types when loading a service + // worker. + rv = NS_NewChannel(getter_AddRefs(mChannel), uri, aPrincipal, secFlags, + contentPolicyType, cookieJarSettings, + nullptr /* aPerformanceStorage */, loadGroup, + nullptr /* aCallbacks */, mLoadFlags); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr httpChannel = do_QueryInterface(mChannel); + if (httpChannel) { + // Spec says no redirects allowed for top-level SW scripts. + if (mIsMainScript) { + rv = httpChannel->SetRedirectionLimit(0); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + rv = httpChannel->SetRequestHeader("Service-Worker"_ns, "script"_ns, + /* merge */ false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + nsCOMPtr loader; + rv = NS_NewStreamLoader(getter_AddRefs(loader), this, this); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mChannel->AsyncOpen(loader); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // If we do have an existing cache to compare with. + if (aCache) { + mCC = new CompareCache(this); + rv = mCC->Initialize(aCache, aURL); + if (NS_WARN_IF(NS_FAILED(rv))) { + Abort(); + return rv; + } + + mState = WaitingForBothFinished; + return NS_OK; + } + + mState = WaitingForNetworkFinished; + return NS_OK; +} + +void CompareNetwork::Finish() { + if (mState == Finished) { + return; + } + + bool same = true; + nsresult rv = NS_OK; + + // mNetworkResult is prior to mCacheResult, since it's needed for reporting + // various errors to web content. + if (NS_FAILED(mNetworkResult)) { + // An imported script could become offline, since it might no longer be + // needed by the new importing script. In that case, the importing script + // must be different, and thus, it's okay to report same script found here. + rv = mIsMainScript ? mNetworkResult : NS_OK; + same = true; + } else if (mCC && NS_FAILED(mCacheResult)) { + rv = mCacheResult; + } else { // Both passed. + same = mCC && mCC->InCache() && mCC->Buffer().Equals(mBuffer); + } + + mManager->ComparisonFinished(rv, mIsMainScript, same, mMaxScope, mLoadFlags); + + // We have done with the CompareCache. + mCC = nullptr; +} + +void CompareNetwork::NetworkFinish(nsresult aRv) { + MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForBothFinished || + mState == WaitingForNetworkFinished); + + mNetworkResult = aRv; + + if (mState == WaitingForBothFinished) { + mState = WaitingForCacheFinished; + return; + } + + if (mState == WaitingForNetworkFinished) { + Finish(); + return; + } +} + +void CompareNetwork::CacheFinish(nsresult aRv) { + MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForBothFinished || + mState == WaitingForCacheFinished); + + mCacheResult = aRv; + + if (mState == WaitingForBothFinished) { + mState = WaitingForNetworkFinished; + return; + } + + if (mState == WaitingForCacheFinished) { + Finish(); + return; + } +} + +void CompareNetwork::Abort() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mState != Finished) { + mState = Finished; + + MOZ_ASSERT(mChannel); + mChannel->CancelWithReason(NS_BINDING_ABORTED, "CompareNetwork::Abort"_ns); + mChannel = nullptr; + + if (mCC) { + mCC->Abort(); + mCC = nullptr; + } + } +} + +NS_IMETHODIMP +CompareNetwork::OnStartRequest(nsIRequest* aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + + if (mState == Finished) { + return NS_OK; + } + + nsCOMPtr channel = do_QueryInterface(aRequest); + MOZ_ASSERT_IF(mIsMainScript, channel == mChannel); + mChannel = channel; + + MOZ_ASSERT(!mChannelInfo.IsInitialized()); + mChannelInfo.InitFromChannel(mChannel); + + nsresult rv = SetPrincipalInfo(mChannel); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mInternalHeaders->FillResponseHeaders(mChannel); + + nsCOMPtr cacheChannel(do_QueryInterface(channel)); + if (cacheChannel) { + cacheChannel->IsFromCache(&mIsFromCache); + } + + return NS_OK; +} + +nsresult CompareNetwork::SetPrincipalInfo(nsIChannel* aChannel) { + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + if (!ssm) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr channelPrincipal; + nsresult rv = ssm->GetChannelResultPrincipal( + aChannel, getter_AddRefs(channelPrincipal)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + UniquePtr principalInfo = MakeUnique(); + rv = PrincipalToPrincipalInfo(channelPrincipal, principalInfo.get()); + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mPrincipalInfo = std::move(principalInfo); + return NS_OK; +} + +NS_IMETHODIMP +CompareNetwork::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) { + // Nothing to do here! + return NS_OK; +} + +NS_IMETHODIMP +CompareNetwork::OnStreamComplete(nsIStreamLoader* aLoader, + nsISupports* aContext, nsresult aStatus, + uint32_t aLen, const uint8_t* aString) { + MOZ_ASSERT(NS_IsMainThread()); + + if (mState == Finished) { + return NS_OK; + } + + nsresult rv = NS_ERROR_FAILURE; + auto guard = MakeScopeExit([&] { NetworkFinish(rv); }); + + if (NS_WARN_IF(NS_FAILED(aStatus))) { + rv = (aStatus == NS_ERROR_REDIRECT_LOOP) ? NS_ERROR_DOM_SECURITY_ERR + : aStatus; + return NS_OK; + } + + nsCOMPtr request; + rv = aLoader->GetRequest(getter_AddRefs(request)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_OK; + } + + nsCOMPtr channel = do_QueryInterface(request); + MOZ_ASSERT(channel, "How come we don't have any channel?"); + + nsCOMPtr uri; + channel->GetOriginalURI(getter_AddRefs(uri)); + bool isExtension = uri->SchemeIs("moz-extension"); + + if (isExtension && + !StaticPrefs::extensions_backgroundServiceWorker_enabled_AtStartup()) { + // Return earlier with error is the worker script is a moz-extension url + // but the feature isn't enabled by prefs. + return NS_ERROR_FAILURE; + } + + if (isExtension) { + // NOTE: trying to register any moz-extension use that doesn't ends + // with .js/.jsm/.mjs seems to be already completing with an error + // in aStatus and they never reach this point. + + // TODO: look into avoid duplicated parts that could be shared with the HTTP + // channel scenario. + nsCOMPtr channelURL; + rv = channel->GetURI(getter_AddRefs(channelURL)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCString channelURLSpec; + MOZ_ALWAYS_SUCCEEDS(channelURL->GetSpec(channelURLSpec)); + + // Append the final URL (which for an extension worker script is going to + // be a file or jar url). + MOZ_DIAGNOSTIC_ASSERT(!mURLList.IsEmpty()); + if (channelURLSpec != mURLList[0]) { + mURLList.AppendElement(channelURLSpec); + } + + UniquePtr buffer; + size_t len = 0; + + rv = ScriptLoader::ConvertToUTF16(channel, aString, aLen, u"UTF-8"_ns, + nullptr, buffer, len); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mBuffer.Adopt(buffer.release(), len); + + rv = NS_OK; + return NS_OK; + } + + nsCOMPtr httpChannel = do_QueryInterface(request); + + // Main scripts cannot be redirected successfully, however extensions + // may successfuly redirect imported scripts to a moz-extension url + // (if listed in the web_accessible_resources manifest property). + // + // When the service worker is initially registered the imported scripts + // will be loaded from the child process (see dom/workers/ScriptLoader.cpp) + // and in that case this method will only be called for the main script. + // + // When a registered worker is loaded again (e.g. when the webpage calls + // the ServiceWorkerRegistration's update method): + // + // - both the main and imported scripts are loaded by the + // CompareManager::FetchScript + // - the update requests for the imported scripts will also be calling this + // method and we should expect scripts redirected to an extension script + // to have a null httpChannel. + // + // The request that triggers this method is: + // + // - the one that is coming from the network (which may be intercepted by + // WebRequest listeners in extensions and redirected to a web_accessible + // moz-extension url) + // - it will then be compared with a previous response that we may have + // in the cache + // + // When the next service worker update occurs, if the request (for an imported + // script) is not redirected by an extension the cache entry is invalidated + // and a network request is triggered for the import. + if (!httpChannel) { + // Redirecting a service worker main script should fail before reaching this + // method. + // If a main script is somehow redirected, the diagnostic assert will crash + // in non-release builds. Release builds will return an explicit error. + MOZ_DIAGNOSTIC_ASSERT(!mIsMainScript, + "Unexpected ServiceWorker main script redirected"); + if (mIsMainScript) { + return NS_ERROR_UNEXPECTED; + } + + nsCOMPtr channelPrincipal; + + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + if (!ssm) { + return NS_ERROR_UNEXPECTED; + } + + nsresult rv = ssm->GetChannelResultPrincipal( + channel, getter_AddRefs(channelPrincipal)); + + // An extension did redirect a non-MainScript request to a moz-extension url + // (in that case the originalURL is the resolved jar URI and so we have to + // look to the channel principal instead). + if (channelPrincipal->SchemeIs("moz-extension")) { + UniquePtr buffer; + size_t len = 0; + + rv = ScriptLoader::ConvertToUTF16(channel, aString, aLen, u"UTF-8"_ns, + nullptr, buffer, len); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mBuffer.Adopt(buffer.release(), len); + + return NS_OK; + } + + // Make non-release and debug builds to crash if this happens and fail + // explicitly on release builds. + MOZ_DIAGNOSTIC_ASSERT(false, + "ServiceWorker imported script redirected to an url " + "with an unexpected scheme"); + return NS_ERROR_UNEXPECTED; + } + + bool requestSucceeded; + rv = httpChannel->GetRequestSucceeded(&requestSucceeded); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_OK; + } + + if (NS_WARN_IF(!requestSucceeded)) { + // Get the stringified numeric status code, not statusText which could be + // something misleading like OK for a 404. + uint32_t status = 0; + Unused << httpChannel->GetResponseStatus( + &status); // don't care if this fails, use 0. + nsAutoString statusAsText; + statusAsText.AppendInt(status); + + ServiceWorkerManager::LocalizeAndReportToAllClients( + mRegistration->Scope(), "ServiceWorkerRegisterNetworkError", + nsTArray{NS_ConvertUTF8toUTF16(mRegistration->Scope()), + statusAsText, mURL}); + + rv = NS_ERROR_FAILURE; + return NS_OK; + } + + // Note: we explicitly don't check for the return value here, because the + // absence of the header is not an error condition. + Unused << httpChannel->GetResponseHeader("Service-Worker-Allowed"_ns, + mMaxScope); + + // [9.2 Update]4.13, If response's cache state is not "local", + // set registration's last update check time to the current time + if (!mIsFromCache) { + mRegistration->RefreshLastUpdateCheckTime(); + } + + nsAutoCString mimeType; + rv = httpChannel->GetContentType(mimeType); + if (NS_WARN_IF(NS_FAILED(rv))) { + // We should only end up here if !mResponseHead in the channel. If headers + // were received but no content type was specified, we'll be given + // UNKNOWN_CONTENT_TYPE "application/x-unknown-content-type" and so fall + // into the next case with its better error message. + rv = NS_ERROR_DOM_SECURITY_ERR; + return rv; + } + + if (mimeType.IsEmpty() || + !nsContentUtils::IsJavascriptMIMEType(NS_ConvertUTF8toUTF16(mimeType))) { + ServiceWorkerManager::LocalizeAndReportToAllClients( + mRegistration->Scope(), "ServiceWorkerRegisterMimeTypeError2", + nsTArray{NS_ConvertUTF8toUTF16(mRegistration->Scope()), + NS_ConvertUTF8toUTF16(mimeType), mURL}); + rv = NS_ERROR_DOM_SECURITY_ERR; + return rv; + } + + nsCOMPtr channelURL; + rv = httpChannel->GetURI(getter_AddRefs(channelURL)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCString channelURLSpec; + MOZ_ALWAYS_SUCCEEDS(channelURL->GetSpec(channelURLSpec)); + + // Append the final URL if its different from the original + // request URL. This lets us note that a redirect occurred + // even though we don't track every redirect URL here. + MOZ_DIAGNOSTIC_ASSERT(!mURLList.IsEmpty()); + if (channelURLSpec != mURLList[0]) { + mURLList.AppendElement(channelURLSpec); + } + + UniquePtr buffer; + size_t len = 0; + + rv = ScriptLoader::ConvertToUTF16(httpChannel, aString, aLen, u"UTF-8"_ns, + nullptr, buffer, len); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mBuffer.Adopt(buffer.release(), len); + + rv = NS_OK; + return NS_OK; +} + +nsresult CompareCache::Initialize(Cache* const aCache, const nsAString& aURL) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aCache); + MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForInitialization); + + // This JSContext will not end up executing JS code because here there are + // no ReadableStreams involved. + AutoJSAPI jsapi; + jsapi.Init(); + + RequestOrUSVString request; + request.SetAsUSVString().ShareOrDependUpon(aURL); + ErrorResult error; + CacheQueryOptions params; + RefPtr promise = aCache->Match(jsapi.cx(), request, params, error); + if (NS_WARN_IF(error.Failed())) { + // No exception here because there are no ReadableStreams involved here. + MOZ_ASSERT(!error.IsJSException()); + mState = Finished; + return error.StealNSResult(); + } + + // Retrieve the script from aCache. + mState = WaitingForScript; + promise->AppendNativeHandler(this); + return NS_OK; +} + +void CompareCache::Finish(nsresult aStatus, bool aInCache) { + if (mState != Finished) { + mState = Finished; + mInCache = aInCache; + mCN->CacheFinish(aStatus); + } +} + +void CompareCache::Abort() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mState != Finished) { + mState = Finished; + + if (mPump) { + mPump->CancelWithReason(NS_BINDING_ABORTED, "CompareCache::Abort"_ns); + mPump = nullptr; + } + } +} + +NS_IMETHODIMP +CompareCache::OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aContext, + nsresult aStatus, uint32_t aLen, + const uint8_t* aString) { + MOZ_ASSERT(NS_IsMainThread()); + + if (mState == Finished) { + return aStatus; + } + + if (NS_WARN_IF(NS_FAILED(aStatus))) { + Finish(aStatus, false); + return aStatus; + } + + UniquePtr buffer; + size_t len = 0; + + nsresult rv = ScriptLoader::ConvertToUTF16(nullptr, aString, aLen, + u"UTF-8"_ns, nullptr, buffer, len); + if (NS_WARN_IF(NS_FAILED(rv))) { + Finish(rv, false); + return rv; + } + + mBuffer.Adopt(buffer.release(), len); + + Finish(NS_OK, true); + return NS_OK; +} + +void CompareCache::ResolvedCallback(JSContext* aCx, + JS::Handle aValue, + ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + + switch (mState) { + case Finished: + return; + case WaitingForScript: + ManageValueResult(aCx, aValue); + return; + default: + MOZ_CRASH("Unacceptable state."); + } +} + +void CompareCache::RejectedCallback(JSContext* aCx, + JS::Handle aValue, + ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + + if (mState != Finished) { + Finish(NS_ERROR_FAILURE, false); + return; + } +} + +void CompareCache::ManageValueResult(JSContext* aCx, + JS::Handle aValue) { + MOZ_ASSERT(NS_IsMainThread()); + + // The cache returns undefined if the object is not stored. + if (aValue.isUndefined()) { + Finish(NS_OK, false); + return; + } + + MOZ_ASSERT(aValue.isObject()); + + JS::Rooted obj(aCx, &aValue.toObject()); + if (NS_WARN_IF(!obj)) { + Finish(NS_ERROR_FAILURE, false); + return; + } + + Response* response = nullptr; + nsresult rv = UNWRAP_OBJECT(Response, &obj, response); + if (NS_WARN_IF(NS_FAILED(rv))) { + Finish(rv, false); + return; + } + + MOZ_ASSERT(response->Ok()); + + nsCOMPtr inputStream; + response->GetBody(getter_AddRefs(inputStream)); + MOZ_ASSERT(inputStream); + + MOZ_ASSERT(!mPump); + rv = NS_NewInputStreamPump(getter_AddRefs(mPump), inputStream.forget(), + 0, /* default segsize */ + 0, /* default segcount */ + false, /* default closeWhenDone */ + GetMainThreadSerialEventTarget()); + if (NS_WARN_IF(NS_FAILED(rv))) { + Finish(rv, false); + return; + } + + nsCOMPtr loader; + rv = NS_NewStreamLoader(getter_AddRefs(loader), this); + if (NS_WARN_IF(NS_FAILED(rv))) { + Finish(rv, false); + return; + } + + rv = mPump->AsyncRead(loader); + if (NS_WARN_IF(NS_FAILED(rv))) { + mPump = nullptr; + Finish(rv, false); + return; + } + + nsCOMPtr rr = do_QueryInterface(mPump); + if (rr) { + nsCOMPtr sts = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + RefPtr queue = + TaskQueue::Create(sts.forget(), "CompareCache STS Delivery Queue"); + rv = rr->RetargetDeliveryTo(queue); + if (NS_WARN_IF(NS_FAILED(rv))) { + mPump = nullptr; + Finish(rv, false); + return; + } + } +} + +nsresult CompareManager::Initialize(nsIPrincipal* aPrincipal, + const nsAString& aURL, + const nsAString& aCacheName) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(mPendingCount == 0); + MOZ_DIAGNOSTIC_ASSERT(mState == WaitingForInitialization); + + // RAII Cleanup when fails. + auto guard = MakeScopeExit([&] { Cleanup(); }); + + mURL = aURL; + mPrincipal = aPrincipal; + + // Always create a CacheStorage since we want to write the network entry to + // the cache even if there isn't an existing one. + AutoJSAPI jsapi; + jsapi.Init(); + ErrorResult result; + mCacheStorage = CreateCacheStorage(jsapi.cx(), aPrincipal, result); + if (NS_WARN_IF(result.Failed())) { + MOZ_ASSERT(!result.IsErrorWithMessage()); + return result.StealNSResult(); + } + + // If there is no existing cache, proceed to fetch the script directly. + if (aCacheName.IsEmpty()) { + mState = WaitingForScriptOrComparisonResult; + nsresult rv = FetchScript(aURL, true /* aIsMainScript */); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + guard.release(); + return NS_OK; + } + + // Open the cache saving the old source scripts. + RefPtr promise = mCacheStorage->Open(aCacheName, result); + if (NS_WARN_IF(result.Failed())) { + MOZ_ASSERT(!result.IsErrorWithMessage()); + return result.StealNSResult(); + } + + mState = WaitingForExistingOpen; + promise->AppendNativeHandler(this); + + guard.release(); + return NS_OK; +} + +// This class manages 4 promises if needed: +// 1. Retrieve the Cache object by a given CacheName of OldCache. +// 2. Retrieve the URLs saved in OldCache. +// 3. Retrieve the Cache object of the NewCache for the newly created SW. +// 4. Put the value in the cache. +// For this reason we have mState to know what callback we are handling. +void CompareManager::ResolvedCallback(JSContext* aCx, + JS::Handle aValue, + ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mCallback); + + switch (mState) { + case Finished: + return; + case WaitingForExistingOpen: + ManageOldCache(aCx, aValue); + return; + case WaitingForExistingKeys: + ManageOldKeys(aCx, aValue); + return; + case WaitingForOpen: + ManageNewCache(aCx, aValue); + return; + case WaitingForPut: + MOZ_DIAGNOSTIC_ASSERT(mPendingCount > 0); + if (--mPendingCount == 0) { + mCallback->ComparisonResult(NS_OK, false /* aIsEqual */, mOnFailure, + mNewCacheName, mMaxScope, mLoadFlags); + Cleanup(); + } + return; + default: + MOZ_DIAGNOSTIC_ASSERT(false); + } +} + +void CompareManager::RejectedCallback(JSContext* aCx, + JS::Handle aValue, + ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + switch (mState) { + case Finished: + return; + case WaitingForExistingOpen: + NS_WARNING("Could not open the existing cache."); + break; + case WaitingForExistingKeys: + NS_WARNING("Could not get the existing URLs."); + break; + case WaitingForOpen: + NS_WARNING("Could not open cache."); + break; + case WaitingForPut: + NS_WARNING("Could not write to cache."); + break; + default: + MOZ_DIAGNOSTIC_ASSERT(false); + } + + Fail(NS_ERROR_FAILURE); +} + +void CompareManager::Fail(nsresult aStatus) { + MOZ_ASSERT(NS_IsMainThread()); + mCallback->ComparisonResult(aStatus, false /* aIsEqual */, mOnFailure, u""_ns, + ""_ns, mLoadFlags); + Cleanup(); +} + +void CompareManager::Cleanup() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mState != Finished) { + mState = Finished; + + MOZ_ASSERT(mCallback); + mCallback = nullptr; + + // Abort and release CompareNetworks. + for (uint32_t i = 0; i < mCNList.Length(); ++i) { + mCNList[i]->Abort(); + } + mCNList.Clear(); + } +} + +} // namespace + +nsresult PurgeCache(nsIPrincipal* aPrincipal, const nsAString& aCacheName) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPrincipal); + + if (aCacheName.IsEmpty()) { + return NS_OK; + } + + AutoJSAPI jsapi; + jsapi.Init(); + ErrorResult rv; + RefPtr cacheStorage = + CreateCacheStorage(jsapi.cx(), aPrincipal, rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + // We use the ServiceWorker scope as key for the cacheStorage. + RefPtr promise = cacheStorage->Delete(aCacheName, rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + + // Set [[PromiseIsHandled]] to ensure that if this promise gets rejected, + // we don't end up reporting a rejected promise to the console. + MOZ_ALWAYS_TRUE(promise->SetAnyPromiseIsHandled()); + + // We don't actually care about the result of the delete operation. + return NS_OK; +} + +nsresult GenerateCacheName(nsAString& aName) { + nsresult rv; + nsCOMPtr uuidGenerator = + do_GetService("@mozilla.org/uuid-generator;1", &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsID id; + rv = uuidGenerator->GenerateUUIDInPlace(&id); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + char chars[NSID_LENGTH]; + id.ToProvidedString(chars); + + // NSID_LENGTH counts the null terminator. + aName.AssignASCII(chars, NSID_LENGTH - 1); + + return NS_OK; +} + +nsresult Compare(ServiceWorkerRegistrationInfo* aRegistration, + nsIPrincipal* aPrincipal, const nsAString& aCacheName, + const nsAString& aURL, CompareCallback* aCallback) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aRegistration); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(!aURL.IsEmpty()); + MOZ_ASSERT(aCallback); + + RefPtr cm = new CompareManager(aRegistration, aCallback); + + nsresult rv = cm->Initialize(aPrincipal, aURL, aCacheName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +} // namespace mozilla::dom::serviceWorkerScriptCache -- cgit v1.2.3