/* -*- 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 "nsIThreadRetargetableRequest.h" #include "nsIPrincipal.h" #include "Workers.h" using mozilla::dom::cache::Cache; using mozilla::dom::cache::CacheStorage; BEGIN_WORKERS_NAMESPACE namespace serviceWorkerScriptCache { namespace { already_AddRefed CreateCacheStorage(nsIPrincipal* aPrincipal, ErrorResult& aRv, nsIXPConnectJSObjectHolder** aHolder = nullptr) { AssertIsOnMainThread(); MOZ_ASSERT(aPrincipal); nsIXPConnect* xpc = nsContentUtils::XPConnect(); MOZ_ASSERT(xpc, "This should never be null!"); AutoJSAPI jsapi; jsapi.Init(); nsCOMPtr sandbox; aRv = xpc->CreateSandbox(jsapi.cx(), aPrincipal, getter_AddRefs(sandbox)); if (NS_WARN_IF(aRv.Failed())) { return nullptr; } nsCOMPtr sandboxGlobalObject = xpc::NativeGlobal(sandbox->GetJSObject()); if (!sandboxGlobalObject) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } if (aHolder) { sandbox.forget(aHolder); } return CacheStorage::CreateOnMainThread(cache::CHROME_ONLY_NAMESPACE, sandboxGlobalObject, aPrincipal, aRv); } class CompareManager; // This class downloads a URL from the network and then it calls // NetworkFinished() in the CompareManager. class CompareNetwork final : public nsIStreamLoaderObserver, public nsIRequestObserver { public: NS_DECL_ISUPPORTS NS_DECL_NSISTREAMLOADEROBSERVER NS_DECL_NSIREQUESTOBSERVER explicit CompareNetwork(CompareManager* aManager) : mManager(aManager) { MOZ_ASSERT(aManager); AssertIsOnMainThread(); } nsresult Initialize(nsIPrincipal* aPrincipal, const nsAString& aURL) { MOZ_ASSERT(aPrincipal); AssertIsOnMainThread(); nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL, nullptr, nullptr); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = NS_NewChannel(getter_AddRefs(mChannel), uri, aPrincipal, nsILoadInfo::SEC_NORMAL, nsIContentPolicy::TYPE_SCRIPT); // FIXME(nsm): TYPE_SERVICEWORKER if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsLoadFlags flags; rv = mChannel->GetLoadFlags(&flags); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } flags |= nsIRequest::LOAD_BYPASS_CACHE; rv = mChannel->SetLoadFlags(flags); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr httpChannel = do_QueryInterface(mChannel); if (httpChannel) { // Spec says no redirects allowed for SW scripts. httpChannel->SetRedirectionLimit(0); } // Don't let serviceworker intercept. nsCOMPtr internalChannel = do_QueryInterface(mChannel); if (internalChannel) { internalChannel->ForceNoIntercept(); } nsCOMPtr loader; rv = NS_NewStreamLoader(getter_AddRefs(loader), this, this); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = mChannel->AsyncOpen(loader, nullptr); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } void Abort() { AssertIsOnMainThread(); MOZ_ASSERT(mChannel); mChannel->Cancel(NS_BINDING_ABORTED); mChannel = nullptr; } const nsString& Buffer() const { AssertIsOnMainThread(); return mBuffer; } private: ~CompareNetwork() { AssertIsOnMainThread(); } nsRefPtr mManager; nsCOMPtr mChannel; nsString mBuffer; }; 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 explicit CompareCache(CompareManager* aManager) : mManager(aManager) , mState(WaitingForCache) , mAborted(false) { MOZ_ASSERT(aManager); AssertIsOnMainThread(); } nsresult Initialize(nsIPrincipal* aPrincipal, const nsAString& aURL, const nsAString& aCacheName); void Abort() { AssertIsOnMainThread(); MOZ_ASSERT(!mAborted); mAborted = true; if (mPump) { mPump->Cancel(NS_BINDING_ABORTED); mPump = nullptr; } } // This class manages 2 promises: 1 is to retrieve cache object, and 2 is for // the value from the cache. For this reason we have mState to know what // reject/resolve callback we are handling. virtual void ResolvedCallback(JSContext* aCx, JS::Handle aValue) override { AssertIsOnMainThread(); if (mAborted) { return; } if (mState == WaitingForCache) { ManageCacheResult(aCx, aValue); return; } MOZ_ASSERT(mState == WaitingForValue); ManageValueResult(aCx, aValue); } virtual void RejectedCallback(JSContext* aCx, JS::Handle aValue) override; const nsString& Buffer() const { AssertIsOnMainThread(); return mBuffer; } const nsString& URL() const { AssertIsOnMainThread(); return mURL; } private: ~CompareCache() { AssertIsOnMainThread(); } void ManageCacheResult(JSContext* aCx, JS::Handle aValue); void ManageValueResult(JSContext* aCx, JS::Handle aValue); nsRefPtr mManager; nsCOMPtr mPump; nsString mURL; nsString mBuffer; enum { WaitingForCache, WaitingForValue } mState; bool mAborted; }; NS_IMPL_ISUPPORTS(CompareCache, nsIStreamLoaderObserver) class CompareManager final : public PromiseNativeHandler { public: explicit CompareManager(CompareCallback* aCallback) : mCallback(aCallback) , mState(WaitingForOpen) , mNetworkFinished(false) , mCacheFinished(false) , mInCache(false) { AssertIsOnMainThread(); } nsresult Initialize(nsIPrincipal* aPrincipal, const nsAString& aURL, const nsAString& aCacheName) { AssertIsOnMainThread(); MOZ_ASSERT(aPrincipal); mURL = aURL; // Always create a CacheStorage since we want to write the network entry to // the cache even if there isn't an existing one. ErrorResult result; mCacheStorage = CreateCacheStorage(aPrincipal, result, getter_AddRefs(mSandbox)); if (NS_WARN_IF(result.Failed())) { MOZ_ASSERT(!result.IsErrorWithMessage()); return result.StealNSResult(); } mCN = new CompareNetwork(this); nsresult rv = mCN->Initialize(aPrincipal, aURL); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!aCacheName.IsEmpty()) { mCC = new CompareCache(this); rv = mCC->Initialize(aPrincipal, aURL, aCacheName); if (NS_WARN_IF(NS_FAILED(rv))) { mCN->Abort(); return rv; } } return NS_OK; } const nsAString& URL() const { AssertIsOnMainThread(); return mURL; } void SetMaxScope(const nsACString& aMaxScope) { MOZ_ASSERT(!mNetworkFinished); mMaxScope = aMaxScope; } void NetworkFinished(nsresult aStatus) { AssertIsOnMainThread(); mNetworkFinished = true; if (NS_FAILED(aStatus)) { if (mCC) { mCC->Abort(); } ComparisonFinished(aStatus, false); return; } MaybeCompare(); } void CacheFinished(nsresult aStatus, bool aInCache) { AssertIsOnMainThread(); mCacheFinished = true; mInCache = aInCache; if (NS_FAILED(aStatus)) { if (mCN) { mCN->Abort(); } ComparisonFinished(aStatus, false); return; } MaybeCompare(); } void MaybeCompare() { AssertIsOnMainThread(); if (!mNetworkFinished || (mCC && !mCacheFinished)) { return; } if (!mCC || !mInCache) { ComparisonFinished(NS_OK, false); return; } ComparisonFinished(NS_OK, mCC->Buffer().Equals(mCN->Buffer())); } // This class manages 2 promises: 1 is to retrieve Cache object, and 2 is to // Put the value in the cache. For this reason we have mState to know what // callback we are handling. void ResolvedCallback(JSContext* aCx, JS::Handle aValue) override { AssertIsOnMainThread(); MOZ_ASSERT(mCallback); if (mState == WaitingForOpen) { if (NS_WARN_IF(!aValue.isObject())) { Fail(NS_ERROR_FAILURE); return; } JS::Rooted obj(aCx, &aValue.toObject()); if (NS_WARN_IF(!obj)) { Fail(NS_ERROR_FAILURE); return; } Cache* cache = nullptr; nsresult rv = UNWRAP_OBJECT(Cache, obj, cache); if (NS_WARN_IF(NS_FAILED(rv))) { Fail(rv); return; } // Just to be safe. nsRefPtr kungfuDeathGrip = cache; WriteToCache(cache); return; } MOZ_ASSERT(mState == WaitingForPut); mCallback->ComparisonResult(NS_OK, false /* aIsEqual */, mNewCacheName, mMaxScope); Cleanup(); } void RejectedCallback(JSContext* aCx, JS::Handle aValue) override { AssertIsOnMainThread(); if (mState == WaitingForOpen) { NS_WARNING("Could not open cache."); } else { NS_WARNING("Could not write to cache."); } Fail(NS_ERROR_FAILURE); } CacheStorage* CacheStorage_() { AssertIsOnMainThread(); MOZ_ASSERT(mCacheStorage); return mCacheStorage; } void InitChannelInfo(nsIChannel* aChannel) { mChannelInfo.InitFromChannel(aChannel); } private: ~CompareManager() { AssertIsOnMainThread(); MOZ_ASSERT(!mCC); MOZ_ASSERT(!mCN); } void Fail(nsresult aStatus) { AssertIsOnMainThread(); mCallback->ComparisonResult(aStatus, false /* aIsEqual */, EmptyString(), EmptyCString()); Cleanup(); } void Cleanup() { AssertIsOnMainThread(); MOZ_ASSERT(mCallback); mCallback = nullptr; mCN = nullptr; mCC = nullptr; } void ComparisonFinished(nsresult aStatus, bool aIsEqual) { AssertIsOnMainThread(); MOZ_ASSERT(mCallback); if (NS_FAILED(aStatus)) { Fail(aStatus); return; } if (aIsEqual) { mCallback->ComparisonResult(aStatus, aIsEqual, EmptyString(), mMaxScope); Cleanup(); return; } // Write to Cache so ScriptLoader reads succeed. WriteNetworkBufferToNewCache(); } void WriteNetworkBufferToNewCache() { AssertIsOnMainThread(); MOZ_ASSERT(mCN); 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; } nsRefPtr cacheOpenPromise = mCacheStorage->Open(mNewCacheName, result); if (NS_WARN_IF(result.Failed())) { MOZ_ASSERT(!result.IsErrorWithMessage()); Fail(result.StealNSResult()); return; } cacheOpenPromise->AppendNativeHandler(this); } void WriteToCache(Cache* aCache) { AssertIsOnMainThread(); MOZ_ASSERT(aCache); MOZ_ASSERT(mState == WaitingForOpen); ErrorResult result; nsCOMPtr body; result = NS_NewStringInputStream(getter_AddRefs(body), mCN->Buffer()); if (NS_WARN_IF(result.Failed())) { MOZ_ASSERT(!result.IsErrorWithMessage()); Fail(result.StealNSResult()); return; } nsRefPtr ir = new InternalResponse(200, NS_LITERAL_CSTRING("OK")); ir->SetBody(body); ir->InitChannelInfo(mChannelInfo); nsRefPtr response = new Response(aCache->GetGlobalObject(), ir); RequestOrUSVString request; request.SetAsUSVString().Rebind(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. nsRefPtr cachePromise = aCache->Put(request, *response, result); if (NS_WARN_IF(result.Failed())) { MOZ_ASSERT(!result.IsErrorWithMessage()); Fail(result.StealNSResult()); return; } mState = WaitingForPut; cachePromise->AppendNativeHandler(this); } nsRefPtr mCallback; nsCOMPtr mSandbox; nsRefPtr mCacheStorage; nsRefPtr mCN; nsRefPtr mCC; nsString mURL; // Only used if the network script has changed and needs to be cached. nsString mNewCacheName; ChannelInfo mChannelInfo; nsCString mMaxScope; enum { WaitingForOpen, WaitingForPut } mState; bool mNetworkFinished; bool mCacheFinished; bool mInCache; }; 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 mManager->InitChannelInfo(mChannel); 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))) { mManager->NetworkFinished(aStatus); return NS_OK; } nsCOMPtr request; nsresult rv = aLoader->GetRequest(getter_AddRefs(request)); if (NS_WARN_IF(NS_FAILED(rv))) { mManager->NetworkFinished(rv); return NS_OK; } nsCOMPtr httpChannel = do_QueryInterface(request); if (httpChannel) { bool requestSucceeded; rv = httpChannel->GetRequestSucceeded(&requestSucceeded); if (NS_WARN_IF(NS_FAILED(rv))) { mManager->NetworkFinished(rv); return NS_OK; } if (!requestSucceeded) { mManager->NetworkFinished(NS_ERROR_FAILURE); return NS_OK; } nsAutoCString maxScope; // Note: we explicitly don't check for the return value here, because the // absense of the header is not an error condition. unused << httpChannel->GetResponseHeader(NS_LITERAL_CSTRING("Service-Worker-Allowed"), maxScope); mManager->SetMaxScope(maxScope); } else { // The only supported request schemes are http, https, and app. // Above, we check to ensure that the request is http or https // based on the channel qi. Here we test the scheme to ensure // that it is app. Otherwise, bail. nsCOMPtr channel = do_QueryInterface(request); if (NS_WARN_IF(!channel)) { mManager->NetworkFinished(NS_ERROR_FAILURE); return NS_OK; } nsCOMPtr uri; rv = channel->GetURI(getter_AddRefs(uri)); if (NS_WARN_IF(NS_FAILED(rv))) { mManager->NetworkFinished(rv); return NS_OK; } nsAutoCString scheme; rv = uri->GetScheme(scheme); if (NS_WARN_IF(NS_FAILED(rv))) { mManager->NetworkFinished(rv); return NS_OK; } if (!scheme.LowerCaseEqualsLiteral("app")) { mManager->NetworkFinished(NS_ERROR_FAILURE); return NS_OK; } } // FIXME(nsm): "Extract mime type..." char16_t* buffer = nullptr; size_t len = 0; rv = nsScriptLoader::ConvertToUTF16(httpChannel, aString, aLen, NS_LITERAL_STRING("UTF-8"), nullptr, buffer, len); if (NS_WARN_IF(NS_FAILED(rv))) { mManager->NetworkFinished(rv); return rv; } mBuffer.Adopt(buffer, len); mManager->NetworkFinished(NS_OK); return NS_OK; } nsresult CompareCache::Initialize(nsIPrincipal* aPrincipal, const nsAString& aURL, const nsAString& aCacheName) { MOZ_ASSERT(aPrincipal); AssertIsOnMainThread(); mURL = aURL; ErrorResult rv; nsRefPtr promise = mManager->CacheStorage_()->Open(aCacheName, rv); if (NS_WARN_IF(rv.Failed())) { MOZ_ASSERT(!rv.IsErrorWithMessage()); return rv.StealNSResult(); } promise->AppendNativeHandler(this); return NS_OK; } NS_IMETHODIMP CompareCache::OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aContext, nsresult aStatus, uint32_t aLen, const uint8_t* aString) { AssertIsOnMainThread(); if (mAborted) { return aStatus; } if (NS_WARN_IF(NS_FAILED(aStatus))) { mManager->CacheFinished(aStatus, false); return aStatus; } char16_t* buffer = nullptr; size_t len = 0; nsresult rv = nsScriptLoader::ConvertToUTF16(nullptr, aString, aLen, NS_LITERAL_STRING("UTF-8"), nullptr, buffer, len); if (NS_WARN_IF(NS_FAILED(rv))) { mManager->CacheFinished(rv, false); return rv; } mBuffer.Adopt(buffer, len); mManager->CacheFinished(NS_OK, true); return NS_OK; } void CompareCache::RejectedCallback(JSContext* aCx, JS::Handle aValue) { AssertIsOnMainThread(); if (mAborted) { return; } mManager->CacheFinished(NS_ERROR_FAILURE, false); } void CompareCache::ManageCacheResult(JSContext* aCx, JS::Handle aValue) { AssertIsOnMainThread(); if (NS_WARN_IF(!aValue.isObject())) { mManager->CacheFinished(NS_ERROR_FAILURE, false); return; } JS::Rooted obj(aCx, &aValue.toObject()); if (NS_WARN_IF(!obj)) { mManager->CacheFinished(NS_ERROR_FAILURE, false); return; } Cache* cache = nullptr; nsresult rv = UNWRAP_OBJECT(Cache, obj, cache); if (NS_WARN_IF(NS_FAILED(rv))) { mManager->CacheFinished(rv, false); return; } RequestOrUSVString request; request.SetAsUSVString().Rebind(mURL.Data(), mURL.Length()); ErrorResult error; CacheQueryOptions params; nsRefPtr promise = cache->Match(request, params, error); if (NS_WARN_IF(error.Failed())) { mManager->CacheFinished(error.StealNSResult(), false); return; } promise->AppendNativeHandler(this); mState = WaitingForValue; } void CompareCache::ManageValueResult(JSContext* aCx, JS::Handle aValue) { AssertIsOnMainThread(); // The cache returns undefined if the object is not stored. if (aValue.isUndefined()) { mManager->CacheFinished(NS_OK, false); return; } MOZ_ASSERT(aValue.isObject()); JS::Rooted obj(aCx, &aValue.toObject()); if (NS_WARN_IF(!obj)) { mManager->CacheFinished(NS_ERROR_FAILURE, false); return; } Response* response = nullptr; nsresult rv = UNWRAP_OBJECT(Response, obj, response); if (NS_WARN_IF(NS_FAILED(rv))) { mManager->CacheFinished(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))) { mManager->CacheFinished(rv, false); return; } nsCOMPtr loader; rv = NS_NewStreamLoader(getter_AddRefs(loader), this); if (NS_WARN_IF(NS_FAILED(rv))) { mManager->CacheFinished(rv, false); return; } rv = mPump->AsyncRead(loader, nullptr); if (NS_WARN_IF(NS_FAILED(rv))) { mPump = nullptr; mManager->CacheFinished(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; mManager->CacheFinished(rv, false); return; } } } } // anonymous namespace nsresult PurgeCache(nsIPrincipal* aPrincipal, const nsAString& aCacheName) { AssertIsOnMainThread(); MOZ_ASSERT(aPrincipal); if (aCacheName.IsEmpty()) { return NS_OK; } ErrorResult rv; nsRefPtr cacheStorage = CreateCacheStorage(aPrincipal, rv); if (NS_WARN_IF(rv.Failed())) { return rv.StealNSResult(); } // We use the ServiceWorker scope as key for the cacheStorage. nsRefPtr 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); aName.AssignASCII(chars, NSID_LENGTH); return NS_OK; } nsresult Compare(nsIPrincipal* aPrincipal, const nsAString& aCacheName, const nsAString& aURL, CompareCallback* aCallback) { AssertIsOnMainThread(); MOZ_ASSERT(aPrincipal); MOZ_ASSERT(!aURL.IsEmpty()); MOZ_ASSERT(aCallback); nsRefPtr cm = new CompareManager(aCallback); nsresult rv = cm->Initialize(aPrincipal, aURL, aCacheName); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } } // serviceWorkerScriptCache namespace END_WORKERS_NAMESPACE