/* -*- 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 "mozilla/Unused.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 { namespace dom { namespace 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) override; virtual void RejectedCallback(JSContext* aCx, JS::Handle aValue) 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) override; void RejectedCallback(JSContext* aCx, JS::Handle aValue) 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; } RefPtr ir = new InternalResponse(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(), 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(); 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->Cancel(NS_BINDING_ABORTED); 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); } char16_t* buffer = nullptr; 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, 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")) { char16_t* buffer = nullptr; 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, 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); } char16_t* buffer = nullptr; 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, 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->Cancel(NS_BINDING_ABORTED); 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; } char16_t* buffer = nullptr; 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, len); Finish(NS_OK, true); return NS_OK; } void CompareCache::ResolvedCallback(JSContext* aCx, JS::Handle aValue) { 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) { 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); rv = rr->RetargetDeliveryTo(sts); 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) { 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) { 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(); } } class NoopPromiseHandler final : public PromiseNativeHandler { public: NS_DECL_ISUPPORTS NoopPromiseHandler() { AssertIsOnMainThread(); } void ResolvedCallback(JSContext* aCx, JS::Handle aValue) override { } void RejectedCallback(JSContext* aCx, JS::Handle aValue) override { } private: ~NoopPromiseHandler() { AssertIsOnMainThread(); } }; NS_IMPL_ISUPPORTS0(NoopPromiseHandler) } // 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(); } // Add a no-op promise handler to ensure that if this promise gets rejected, // we don't end up reporting a rejected promise to the console. RefPtr promiseHandler = new NoopPromiseHandler(); promise->AppendNativeHandler(promiseHandler); // 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 serviceWorkerScriptCache } // namespace dom } // namespace mozilla