/* -*- 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 "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/ipc/BackgroundUtils.h" #include "mozilla/ipc/PBackgroundSharedTypes.h" #include "nsICacheInfoChannel.h" #include "nsIHttpChannelInternal.h" #include "nsIStreamLoader.h" #include "nsIThreadRetargetableRequest.h" #include "nsIPrincipal.h" #include "nsIScriptError.h" #include "nsIScriptSecurityManager.h" #include "nsContentUtils.h" #include "nsNetUtil.h" #include "ServiceWorkerManager.h" #include "Workers.h" #include "nsStringStream.h" using mozilla::dom::cache::Cache; using mozilla::dom::cache::CacheStorage; using mozilla::ipc::PrincipalInfo; BEGIN_WORKERS_NAMESPACE namespace serviceWorkerScriptCache { namespace { // XXX A sandbox nsIGlobalObject does not preserve its reflector, so |aSandbox| // must be kept alive as long as the CacheStorage if you want to ensure that // the CacheStorage will continue to work. Failures will manifest as errors // like "JavaScript error: , line 0: TypeError: The expression cannot be // converted to return the specified type." already_AddRefed CreateCacheStorage(JSContext* aCx, nsIPrincipal* aPrincipal, ErrorResult& aRv, JS::MutableHandle aSandbox) { AssertIsOnMainThread(); MOZ_ASSERT(aPrincipal); nsIXPConnect* xpc = nsContentUtils::XPConnect(); MOZ_ASSERT(xpc, "This should never be null!"); aRv = xpc->CreateSandbox(aCx, aPrincipal, aSandbox.address()); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } nsCOMPtr sandboxGlobalObject = xpc::NativeGlobal(aSandbox); 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 browing. // // 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, false /* private browsing */, 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, nsIPrincipal* aPrincipal, const nsAString& aURL, bool aIsMainScript, nsILoadGroup* aLoadGroup, Cache* const aCache) : mManager(aManager) , mIsMainScript(true) , mInternalHeaders(new InternalHeaders()) , mState(WaitingForInitialization) , mNetworkResult(NS_OK) , mCacheResult(NS_OK) , mLoadFlags(nsIChannel::LOAD_BYPASS_SERVICE_WORKER) { MOZ_ASSERT(aManager); AssertIsOnMainThread(); Initialize(aPrincipal, aURL, aIsMainScript, aLoadGroup, aCache); } void Abort(); void NetworkFinished(nsresult aRv); void CacheFinished(nsresult aRv); const nsString& Buffer() const { AssertIsOnMainThread(); return mBuffer; } const nsString& URL() const { AssertIsOnMainThread(); return mURL; } const ChannelInfo& GetChannelInfo() const { return mChannelInfo; } already_AddRefed GetInternalHeaders() const { RefPtr internalHeaders = mInternalHeaders; return internalHeaders.forget(); } UniquePtr TakePrincipalInfo() { return Move(mPrincipalInfo); } bool Succeeded() const { return NS_SUCCEEDED(mNetworkResult); } private: ~CompareNetwork() { AssertIsOnMainThread(); MOZ_ASSERT(!mCC); } void Initialize(nsIPrincipal* aPrincipal, const nsAString& aURL, bool aIsMainScript, nsILoadGroup* aLoadGroup, Cache* const aCache); void Finished(); nsresult SetPrincipalInfo(nsIChannel* aChannel); RefPtr mManager; RefPtr mCC; nsCOMPtr mChannel; nsString mBuffer; nsString mURL; bool mIsMainScript; ChannelInfo mChannelInfo; RefPtr mInternalHeaders; UniquePtr mPrincipalInfo; enum { WaitingForInitialization, WaitingForBothFinished, WaitingForNetworkFinished, WaitingForCacheFinished, Redundant } mState; nsresult mNetworkResult; nsresult mCacheResult; nsCString mMaxScope; nsLoadFlags mLoadFlags; }; NS_IMPL_ISUPPORTS(CompareNetwork, nsIStreamLoaderObserver, nsIRequestObserver) // This class gets a cached Response from the CacheStorage and then it calls // CacheFinished() in the CompareManager. class CompareCache final : public PromiseNativeHandler , public nsIStreamLoaderObserver { public: NS_DECL_ISUPPORTS NS_DECL_NSISTREAMLOADEROBSERVER CompareCache(CompareNetwork* aCN, const nsAString& aURL, Cache* const aCache) : mCN(aCN) , mState(WaitingForInitialization) , mInCache(false) { MOZ_ASSERT(aCN); AssertIsOnMainThread(); Initialize(aURL, aCache); } void Abort(); virtual void ResolvedCallback(JSContext* aCx, JS::Handle aValue) override; virtual void RejectedCallback(JSContext* aCx, JS::Handle aValue) override; const nsString& Buffer() const { AssertIsOnMainThread(); return mBuffer; } const nsString& URL() const { AssertIsOnMainThread(); return mURL; } bool InCache() { return mInCache; } private: ~CompareCache() { AssertIsOnMainThread(); } void Initialize(const nsAString& aURL, Cache* const aCache); void Finished(nsresult aStatus, bool aInCache = false); void ManageValueResult(JSContext* aCx, JS::Handle aValue); RefPtr mCN; nsCOMPtr mPump; nsString mURL; nsString mBuffer; enum { WaitingForInitialization, WaitingForValue, Redundant } mState; bool mInCache; }; NS_IMPL_ISUPPORTS(CompareCache, nsIStreamLoaderObserver) class CompareManager final : public PromiseNativeHandler { public: NS_DECL_ISUPPORTS CompareManager(ServiceWorkerRegistrationInfo* aRegistration, CompareCallback* aCallback, nsIPrincipal* aPrincipal, const nsAString& aURL, const nsAString& aCacheName, nsILoadGroup* aLoadGroup) : mRegistration(aRegistration) , mCallback(aCallback) , mState(WaitingForInitialization) , mPendingCount(0) , mAreScriptsEqual(true) , mLoadFlags(nsIChannel::LOAD_BYPASS_SERVICE_WORKER) { AssertIsOnMainThread(); MOZ_ASSERT(aRegistration); Initialize(aPrincipal, aURL, aCacheName, aLoadGroup); } const nsString& URL() const { AssertIsOnMainThread(); return mURL; } already_AddRefed GetRegistration() { RefPtr copy = mRegistration.get(); return copy.forget(); } void ResolvedCallback(JSContext* aCx, JS::Handle aValue) override; void RejectedCallback(JSContext* aCx, JS::Handle aValue) override; CacheStorage* CacheStorage_() { AssertIsOnMainThread(); MOZ_ASSERT(mCacheStorage); return mCacheStorage; } void ComparisonFinished(nsresult aStatus) { MOZ_ASSERT(NS_FAILED(aStatus)); ComparisonFinished(aStatus, false /* aIsEqual */, false /* aIsMainScript */, EmptyCString(), nsIRequest::LOAD_NORMAL); } void ComparisonFinished(nsresult aStatus, bool aIsEqual, bool aIsMainScript, const nsACString& aMaxScope, nsLoadFlags aLoadFlags) { AssertIsOnMainThread(); if (NS_WARN_IF(NS_FAILED(aStatus))) { NotifyComparisonResult(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) { NotifyComparisonResult(aStatus, true /* aSameScripts */, EmptyString(), mMaxScope, mLoadFlags); return; } // Write to Cache so ScriptLoader reads succeed. mState = WaitingForOpen; WriteNetworkBufferToNewCache(); } private: ~CompareManager() { AssertIsOnMainThread(); MOZ_ASSERT(mCNs.Length() == 0); } void Initialize(nsIPrincipal* aPrincipal, const nsAString& aURL, const nsAString& aCacheName, nsILoadGroup* aLoadGroup); // Only used for notifying falsy results. void NotifyComparisonResult(nsresult aStatus); void NotifyComparisonResult(nsresult aStatus, bool aSameScripts, const nsAString& aNewCacheName, const nsACString& aMaxScope, nsLoadFlags aLoadFlags); void FetchScript(const nsAString& aURL, bool aIsMainScript, Cache* const aCache) { RefPtr cn = new CompareNetwork(this, mPrincipal, aURL, aIsMainScript, mLoadGroup, aCache); mCNs.AppendElement(cn.forget()); mPendingCount += 1; } void ManageOldCache(JSContext* aCx, JS::Handle aValue) { MOZ_ASSERT(mState == WaitingForExistingOpen); if (NS_WARN_IF(!aValue.isObject())) { NotifyComparisonResult(NS_ERROR_FAILURE); 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)))) { NotifyComparisonResult(NS_ERROR_FAILURE); return; } Optional request; CacheQueryOptions options; ErrorResult error; RefPtr promise = mOldCache->Keys(request, options, error); if (NS_WARN_IF(error.Failed())) { NotifyComparisonResult(error.StealNSResult()); return; } mState = WaitingForExistingKeys; promise->AppendNativeHandler(this); return; } void ManageOldKeys(JSContext* aCx, JS::Handle aValue) { MOZ_ASSERT(mState == WaitingForExistingKeys); if (NS_WARN_IF(!aValue.isObject())) { NotifyComparisonResult(NS_ERROR_FAILURE); return; } JS::Rooted obj(aCx, &aValue.toObject()); if (NS_WARN_IF(!obj)) { NotifyComparisonResult(NS_ERROR_FAILURE); return; } uint32_t len = 0; if (!JS_GetArrayLength(aCx, obj, &len)) { NotifyComparisonResult(NS_ERROR_FAILURE); return; } // Fetch the new scripts. MOZ_ASSERT(mPendingCount == 0); mState = WaitingForScriptOrComparisonResult; 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())) { NotifyComparisonResult(NS_ERROR_FAILURE); return; } Request* request; JS::Rooted requestObj(aCx, &val.toObject()); if (NS_WARN_IF(NS_FAILED(UNWRAP_OBJECT(Request, requestObj, request)))) { NotifyComparisonResult(NS_ERROR_FAILURE); continue; }; nsString URL; request->GetUrl(URL); FetchScript(URL, mURL == URL /* aIsMainScript */, mOldCache); } return; } void ManageNewCache(JSContext* aCx, JS::Handle aValue) { MOZ_ASSERT(mState == WaitingForOpen); if (NS_WARN_IF(!aValue.isObject())) { NotifyComparisonResult(NS_ERROR_FAILURE); return; } JS::Rooted obj(aCx, &aValue.toObject()); if (NS_WARN_IF(!obj)) { NotifyComparisonResult(NS_ERROR_FAILURE); return; } Cache* cache = nullptr; nsresult rv = UNWRAP_OBJECT(Cache, obj, cache); if (NS_WARN_IF(NS_FAILED(rv))) { NotifyComparisonResult(rv); return; } // Just to be safe. RefPtr kungfuDeathGrip = cache; mState = WaitingForPut; MOZ_ASSERT(mPendingCount == 0); for (uint32_t i = 0; i < mCNs.Length(); ++i) { WriteToCache(cache, mCNs[i]); } return; } void WriteNetworkBufferToNewCache() { AssertIsOnMainThread(); MOZ_ASSERT(mCNs.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()); NotifyComparisonResult(result.StealNSResult()); return; } RefPtr cacheOpenPromise = mCacheStorage->Open(mNewCacheName, result); if (NS_WARN_IF(result.Failed())) { MOZ_ASSERT(!result.IsErrorWithMessage()); NotifyComparisonResult(result.StealNSResult()); return; } cacheOpenPromise->AppendNativeHandler(this); } void WriteToCache(Cache* aCache, CompareNetwork* aCN) { AssertIsOnMainThread(); MOZ_ASSERT(aCache); MOZ_ASSERT(aCN); MOZ_ASSERT(mState == WaitingForPut); if (!aCN->Succeeded()) { return; } ErrorResult result; nsCOMPtr body; result = NS_NewCStringInputStream(getter_AddRefs(body), NS_ConvertUTF16toUTF8(aCN->Buffer())); if (NS_WARN_IF(result.Failed())) { MOZ_ASSERT(!result.IsErrorWithMessage()); NotifyComparisonResult(result.StealNSResult()); return; } RefPtr ir = new InternalResponse(200, NS_LITERAL_CSTRING("OK")); ir->SetBody(body, aCN->Buffer().Length()); ir->InitChannelInfo(aCN->GetChannelInfo()); UniquePtr principalInfo = aCN->TakePrincipalInfo(); if (principalInfo) { ir->SetPrincipalInfo(Move(principalInfo)); } IgnoredErrorResult ignored; RefPtr internalHeaders = aCN->GetInternalHeaders(); ir->Headers()->Fill(*(internalHeaders.get()), ignored); RefPtr response = new Response(aCache->GetGlobalObject(), ir); RequestOrUSVString request; request.SetAsUSVString().Rebind(aCN->URL().Data(), URL().Length()); // 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. RefPtr cachePromise = aCache->Put(request, *response, result); if (NS_WARN_IF(result.Failed())) { MOZ_ASSERT(!result.IsErrorWithMessage()); NotifyComparisonResult(result.StealNSResult()); return; } mPendingCount += 1; cachePromise->AppendNativeHandler(this); } RefPtr mRegistration; RefPtr mCallback; JS::PersistentRooted mSandbox; RefPtr mCacheStorage; nsTArray> mCNs; nsString mURL; RefPtr mPrincipal; RefPtr mLoadGroup; // Used for the old cache where saves the old source scripts. nsString mOldCacheName; RefPtr mOldCache; // Only used if the network script has changed and needs to be cached. nsString mNewCacheName; enum { WaitingForInitialization, WaitingForExistingOpen, WaitingForExistingKeys, WaitingForScriptOrComparisonResult, WaitingForOpen, WaitingForPut, Redundant } mState; uint32_t mPendingCount; bool mAreScriptsEqual; nsCString mMaxScope; nsLoadFlags mLoadFlags; }; NS_IMPL_ISUPPORTS0(CompareManager) void CompareNetwork::Initialize(nsIPrincipal* aPrincipal, const nsAString& aURL, bool aIsMainScript, nsILoadGroup* aLoadGroup, Cache* const aCache) { AssertIsOnMainThread(); MOZ_ASSERT(aPrincipal); MOZ_ASSERT(mState == WaitingForInitialization); // RAII error notifier nsresult rv = NS_ERROR_FAILURE; auto guard = MakeScopeExit([&] { NetworkFinished(rv); }); mURL = aURL; nsCOMPtr uri; rv = NS_NewURI(getter_AddRefs(uri), aURL, nullptr, nullptr); if (NS_WARN_IF(NS_FAILED(rv))) { return; } mIsMainScript = aIsMainScript; nsCOMPtr loadGroup; rv = NS_NewLoadGroup(getter_AddRefs(loadGroup), aPrincipal); if (NS_WARN_IF(NS_FAILED(rv))) { return; } // Update mLoadFlags, which is passed to script loaders later. RefPtr registration = mManager->GetRegistration(); mLoadFlags |= registration->GetLoadFlags(); if (registration->IsLastUpdateCheckTimeOverOneDay()) { mLoadFlags |= nsIRequest::LOAD_BYPASS_CACHE; } // 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, nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED, nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER, loadGroup, nullptr, // aCallbacks mLoadFlags); if (NS_WARN_IF(NS_FAILED(rv))) { return; } nsCOMPtr httpChannel = do_QueryInterface(mChannel); if (httpChannel) { // Spec says no redirects allowed for SW scripts. rv = httpChannel->SetRedirectionLimit(0); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = httpChannel->SetRequestHeader(NS_LITERAL_CSTRING("Service-Worker"), NS_LITERAL_CSTRING("script"), /* 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 = mChannel->AsyncOpen2(loader); if (NS_WARN_IF(NS_FAILED(rv))) { return; } // If we don't have an existing cache to compare with. if (!aCache) { mState = WaitingForNetworkFinished; guard.release(); return; } mCC = new CompareCache(this, aURL, aCache); mState = WaitingForBothFinished; guard.release(); } void CompareNetwork::Finished() { MOZ_ASSERT(mState != Redundant); mState = Redundant; bool same = true; nsresult rv = NS_OK; // mNetworkResult is prior to mCacheResult, since it's needed for reporting // various error to the web contenet. 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, same, mIsMainScript, mMaxScope, mLoadFlags); mCC = nullptr; } void CompareNetwork::NetworkFinished(nsresult aRv) { mNetworkResult = aRv; if (mState == WaitingForBothFinished) { mState = WaitingForCacheFinished; return; } if (mState == WaitingForInitialization || mState == WaitingForNetworkFinished) { Finished(); return; } MOZ_CRASH("Unacceptable state."); } void CompareNetwork::CacheFinished(nsresult aRv) { mCacheResult = aRv; if (mState == WaitingForBothFinished) { mState = WaitingForNetworkFinished; return; } if (mState == WaitingForCacheFinished) { Finished(); return; } MOZ_CRASH("Unacceptable state."); } void CompareNetwork::Abort() { AssertIsOnMainThread(); if (mState != Redundant) { mState = Redundant; MOZ_ASSERT(mChannel); mChannel->Cancel(NS_BINDING_ABORTED); mChannel = nullptr; if (mCC) { mCC->Abort(); mCC = nullptr; } } } NS_IMETHODIMP CompareNetwork::OnStartRequest(nsIRequest* aRequest, nsISupports* aContext) { AssertIsOnMainThread(); // If no channel, Abort() has been called. if (!mChannel) { return NS_OK; } #ifdef DEBUG nsCOMPtr channel = do_QueryInterface(aRequest); MOZ_ASSERT(channel == mChannel); #endif MOZ_ASSERT(!mChannelInfo.IsInitialized()); mChannelInfo.InitFromChannel(mChannel); nsresult rv = SetPrincipalInfo(mChannel); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } mInternalHeaders->FillResponseHeaders(mChannel); return NS_OK; } nsresult CompareNetwork::SetPrincipalInfo(nsIChannel* aChannel) { nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); NS_ASSERTION(ssm, "Should never be null!"); 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 = Move(principalInfo); return NS_OK; } NS_IMETHODIMP CompareNetwork::OnStopRequest(nsIRequest* aRequest, nsISupports* aContext, 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) { AssertIsOnMainThread(); // If no channel, Abort() has been called. if (!mChannel) { return NS_OK; } if (NS_WARN_IF(NS_FAILED(aStatus))) { if (aStatus == NS_ERROR_REDIRECT_LOOP) { NetworkFinished(NS_ERROR_DOM_SECURITY_ERR); } else { NetworkFinished(aStatus); } return NS_OK; } nsCOMPtr request; nsresult rv = aLoader->GetRequest(getter_AddRefs(request)); if (NS_WARN_IF(NS_FAILED(rv))) { NetworkFinished(rv); return NS_OK; } nsCOMPtr httpChannel = do_QueryInterface(request); MOZ_ASSERT(httpChannel, "How come we don't have an HTTP channel?"); bool requestSucceeded; rv = httpChannel->GetRequestSucceeded(&requestSucceeded); if (NS_WARN_IF(NS_FAILED(rv))) { NetworkFinished(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); RefPtr registration = mManager->GetRegistration(); ServiceWorkerManager::LocalizeAndReportToAllClients( registration->mScope, "ServiceWorkerRegisterNetworkError", nsTArray { NS_ConvertUTF8toUTF16(registration->mScope), statusAsText, mManager->URL() }); NetworkFinished(NS_ERROR_FAILURE); return NS_OK; } if (mIsMainScript) { // 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( NS_LITERAL_CSTRING("Service-Worker-Allowed"), mMaxScope); } bool isFromCache = false; nsCOMPtr cacheChannel(do_QueryInterface(httpChannel)); if (cacheChannel) { cacheChannel->IsFromCache(&isFromCache); } // [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 (!isFromCache) { RefPtr registration = mManager->GetRegistration(); registration->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. NetworkFinished(NS_ERROR_DOM_SECURITY_ERR); return rv; } if (!mimeType.LowerCaseEqualsLiteral("text/javascript") && !mimeType.LowerCaseEqualsLiteral("application/x-javascript") && !mimeType.LowerCaseEqualsLiteral("application/javascript")) { RefPtr registration = mManager->GetRegistration(); ServiceWorkerManager::LocalizeAndReportToAllClients( registration->mScope, "ServiceWorkerRegisterMimeTypeError", nsTArray { NS_ConvertUTF8toUTF16(registration->mScope), NS_ConvertUTF8toUTF16(mimeType), mManager->URL() }); NetworkFinished(NS_ERROR_DOM_SECURITY_ERR); return rv; } char16_t* buffer = nullptr; size_t len = 0; rv = ScriptLoader::ConvertToUTF16(httpChannel, aString, aLen, NS_LITERAL_STRING("UTF-8"), nullptr, buffer, len); if (NS_WARN_IF(NS_FAILED(rv))) { NetworkFinished(rv); return rv; } mBuffer.Adopt(buffer, len); NetworkFinished(NS_OK); return NS_OK; } void CompareCache::Initialize(const nsAString& aURL, Cache* const aCache) { MOZ_ASSERT(aCache); MOZ_ASSERT(mState == WaitingForInitialization); AssertIsOnMainThread(); // RAII error notifier nsresult rv = NS_ERROR_FAILURE; auto guard = MakeScopeExit([&] { Finished(rv); }); mURL = aURL; RequestOrUSVString request; request.SetAsUSVString().Rebind(mURL.Data(), mURL.Length()); ErrorResult error; CacheQueryOptions params; RefPtr promise = aCache->Match(request, params, error); if (NS_WARN_IF(error.Failed())) { rv = error.StealNSResult(); return; } mState = WaitingForValue; promise->AppendNativeHandler(this); guard.release(); } void CompareCache::Finished(nsresult aStatus, bool aInCache) { if (mState != Redundant) { mState = Redundant; mInCache = aInCache; mCN->CacheFinished(aStatus); } } void CompareCache::Abort() { AssertIsOnMainThread(); if (mState != Redundant) { mState = Redundant; if (mPump) { mPump->Cancel(NS_BINDING_ABORTED); mPump = nullptr; } mCN = nullptr; } } NS_IMETHODIMP CompareCache::OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aContext, nsresult aStatus, uint32_t aLen, const uint8_t* aString) { AssertIsOnMainThread(); if (mState == Redundant) { return aStatus; } if (NS_WARN_IF(NS_FAILED(aStatus))) { Finished(aStatus, false); return aStatus; } char16_t* buffer = nullptr; size_t len = 0; nsresult rv = ScriptLoader::ConvertToUTF16(nullptr, aString, aLen, NS_LITERAL_STRING("UTF-8"), nullptr, buffer, len); if (NS_WARN_IF(NS_FAILED(rv))) { Finished(rv, false); return rv; } mBuffer.Adopt(buffer, len); Finished(NS_OK, true); return NS_OK; } // This class manages only 1 promise: For the value from the cache. void CompareCache::ResolvedCallback(JSContext* aCx, JS::Handle aValue) { AssertIsOnMainThread(); if (mState == Redundant) { return; } MOZ_ASSERT(mState == WaitingForValue); ManageValueResult(aCx, aValue); } void CompareCache::RejectedCallback(JSContext* aCx, JS::Handle aValue) { AssertIsOnMainThread(); if (mState == Redundant) { return; } Finished(NS_ERROR_FAILURE, false); } void CompareCache::ManageValueResult(JSContext* aCx, JS::Handle aValue) { AssertIsOnMainThread(); // The cache returns undefined if the object is not stored. if (aValue.isUndefined()) { Finished(NS_OK, false); return; } MOZ_ASSERT(aValue.isObject()); JS::Rooted obj(aCx, &aValue.toObject()); if (NS_WARN_IF(!obj)) { Finished(NS_ERROR_FAILURE, false); return; } Response* response = nullptr; nsresult rv = UNWRAP_OBJECT(Response, obj, response); if (NS_WARN_IF(NS_FAILED(rv))) { Finished(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); if (NS_WARN_IF(NS_FAILED(rv))) { Finished(rv, false); return; } nsCOMPtr loader; rv = NS_NewStreamLoader(getter_AddRefs(loader), this); if (NS_WARN_IF(NS_FAILED(rv))) { Finished(rv, false); return; } rv = mPump->AsyncRead(loader, nullptr); if (NS_WARN_IF(NS_FAILED(rv))) { mPump = nullptr; Finished(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; Finished(rv, false); return; } } } void CompareManager::Initialize(nsIPrincipal* aPrincipal, const nsAString& aURL, const nsAString& aCacheName, nsILoadGroup* aLoadGroup) { AssertIsOnMainThread(); MOZ_ASSERT(aPrincipal); MOZ_ASSERT(mState == WaitingForInitialization); MOZ_ASSERT(mPendingCount == 0); // RAII error notifier nsresult rv = NS_ERROR_FAILURE; auto guard = MakeScopeExit([&] { NotifyComparisonResult(rv); }); mURL = aURL; mPrincipal = aPrincipal; mLoadGroup = aLoadGroup; mOldCacheName = aCacheName; // 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; mSandbox.init(jsapi.cx()); mCacheStorage = CreateCacheStorage(jsapi.cx(), aPrincipal, result, &mSandbox); if (NS_WARN_IF(result.Failed())) { MOZ_ASSERT(!result.IsErrorWithMessage()); rv = result.StealNSResult(); return; } // Open the cache saving the old source scripts. if (!mOldCacheName.IsEmpty()) { RefPtr promise = mCacheStorage->Open(mOldCacheName, result); if (NS_WARN_IF(result.Failed())) { MOZ_ASSERT(!result.IsErrorWithMessage()); rv = result.StealNSResult(); return; } mState = WaitingForExistingOpen; promise->AppendNativeHandler(this); guard.release(); return; } // Go fetch the script directly without comparison. mState = WaitingForScriptOrComparisonResult; FetchScript(mURL, true /* aIsMainScript */, nullptr); guard.release(); } // 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) { AssertIsOnMainThread(); MOZ_ASSERT(mCallback); if (mState == WaitingForExistingOpen) { ManageOldCache(aCx, aValue); return; } if (mState == WaitingForExistingKeys) { ManageOldKeys(aCx, aValue); return; } if (mState == WaitingForOpen) { ManageNewCache(aCx, aValue); return; } MOZ_ASSERT(mState == WaitingForPut); MOZ_DIAGNOSTIC_ASSERT(mPendingCount > 0); if (--mPendingCount) { return; } NotifyComparisonResult(NS_OK, false /* aSameScripts */, mNewCacheName, mMaxScope, mLoadFlags); } void CompareManager::RejectedCallback(JSContext* aCx, JS::Handle aValue) { AssertIsOnMainThread(); if (mState == WaitingForExistingKeys) { NS_WARNING("Could not get the existing URLs."); } else if (mState == WaitingForExistingKeys){ NS_WARNING("Could not get the existing URLs."); } else if (mState == WaitingForOpen) { NS_WARNING("Could not open cache."); } else { NS_WARNING("Could not write to cache."); } NotifyComparisonResult(NS_ERROR_FAILURE); } void CompareManager::NotifyComparisonResult(nsresult aState) { MOZ_ASSERT(NS_FAILED(aState)); NotifyComparisonResult(aState, false /* aSameScripts*/, EmptyString(), EmptyCString(), nsIRequest::LOAD_NORMAL); } void CompareManager::NotifyComparisonResult(nsresult aStatus, bool aSameScripts, const nsAString& aNewCacheName, const nsACString& aMaxScope, nsLoadFlags aLoadFlags) { AssertIsOnMainThread(); // If mState is Redundant, the comparison result is sent out and this // CompareManager cleaned up before. Thus, there is nothing left to do here. if (mState == Redundant) { return; } // Mark this CompareManager is Redundant. mState = Redundant; // Send the result to mCallBack. MOZ_ASSERT(mCallback); mCallback->ComparisonResult(aStatus, aSameScripts, aNewCacheName, aMaxScope, aLoadFlags); mCallback = nullptr; // Abort and release CompareNetworks. MOZ_ASSERT(mCNs.Length()); for (uint32_t i = 0; i < mCNs.Length(); ++i) { mCNs[i]->Abort(); } mCNs.Clear(); } } // namespace nsresult PurgeCache(nsIPrincipal* aPrincipal, const nsAString& aCacheName) { AssertIsOnMainThread(); MOZ_ASSERT(aPrincipal); if (aCacheName.IsEmpty()) { return NS_OK; } AutoJSAPI jsapi; jsapi.Init(); ErrorResult rv; JS::Rooted sandboxObject(jsapi.cx()); RefPtr cacheStorage = CreateCacheStorage(jsapi.cx(), aPrincipal, rv, &sandboxObject); 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(); } // 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; } void Compare(ServiceWorkerRegistrationInfo* aRegistration, nsIPrincipal* aPrincipal, const nsAString& aCacheName, const nsAString& aURL, CompareCallback* aCallback, nsILoadGroup* aLoadGroup) { AssertIsOnMainThread(); MOZ_ASSERT(aRegistration); MOZ_ASSERT(aPrincipal); MOZ_ASSERT(!aURL.IsEmpty()); MOZ_ASSERT(aCallback); RefPtr cm = new CompareManager(aRegistration, aCallback, aPrincipal, aURL, aCacheName, aLoadGroup); } } // namespace serviceWorkerScriptCache END_WORKERS_NAMESPACE