fune/dom/serviceworkers/ServiceWorkerScriptCache.cpp
Dorel Luca ac34e1d973 Backed out 16 changesets (bug 1525245) for Android failures. CLOSED TREE
Backed out changeset 9f8a1b410320 (bug 1525245)
Backed out changeset 0ef284a9a1d5 (bug 1525245)
Backed out changeset 835e5f642a03 (bug 1525245)
Backed out changeset 362f5a8d033c (bug 1525245)
Backed out changeset 9da3ab33cf67 (bug 1525245)
Backed out changeset 6aacd2d6e835 (bug 1525245)
Backed out changeset 8ff9e8f45e02 (bug 1525245)
Backed out changeset 2020227181cc (bug 1525245)
Backed out changeset fc3c64c330b9 (bug 1525245)
Backed out changeset 2762bf88e050 (bug 1525245)
Backed out changeset ffc10fdc50a6 (bug 1525245)
Backed out changeset bb6ade1207d7 (bug 1525245)
Backed out changeset 1875eb5085e4 (bug 1525245)
Backed out changeset 7e4f67a6d6f1 (bug 1525245)
Backed out changeset e671fc9581eb (bug 1525245)
Backed out changeset b89f5def8d0d (bug 1525245)
2019-03-06 21:07:49 +02:00

1353 lines
38 KiB
C++

/* -*- 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/SystemGroup.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/dom/WorkerCommon.h"
#include "mozilla/ipc/BackgroundUtils.h"
#include "mozilla/ipc/PBackgroundSharedTypes.h"
#include "nsICacheInfoChannel.h"
#include "nsIHttpChannelInternal.h"
#include "nsIStreamLoader.h"
#include "nsIThreadRetargetableRequest.h"
#include "nsIInputStreamPump.h"
#include "nsIPrincipal.h"
#include "nsIScriptError.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<CacheStorage> 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<JSObject*> 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<nsIGlobalObject> 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 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,
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<InternalHeaders> GetInternalHeaders() const {
RefPtr<InternalHeaders> internalHeaders = mInternalHeaders;
return internalHeaders.forget();
}
UniquePtr<PrincipalInfo> TakePrincipalInfo() {
return std::move(mPrincipalInfo);
}
bool Succeeded() const { return NS_SUCCEEDED(mNetworkResult); }
const nsTArray<nsCString>& URLList() const { return mURLList; }
private:
~CompareNetwork() {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(!mCC);
}
void Finish();
nsresult SetPrincipalInfo(nsIChannel* aChannel);
RefPtr<CompareManager> mManager;
RefPtr<CompareCache> mCC;
RefPtr<ServiceWorkerRegistrationInfo> mRegistration;
nsCOMPtr<nsIChannel> mChannel;
nsString mBuffer;
nsString mURL;
ChannelInfo mChannelInfo;
RefPtr<InternalHeaders> mInternalHeaders;
UniquePtr<PrincipalInfo> mPrincipalInfo;
nsTArray<nsCString> 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<JS::Value> aValue) override;
virtual void RejectedCallback(JSContext* aCx,
JS::Handle<JS::Value> 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<JS::Value> aValue);
RefPtr<CompareNetwork> mCN;
nsCOMPtr<nsIInputStreamPump> 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<JS::Value> aValue) override;
void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> 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,
EmptyString(), 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<CompareNetwork> 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<JS::Value> 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<JSObject*> obj(aCx, &aValue.toObject());
if (NS_WARN_IF(!obj) ||
NS_WARN_IF(NS_FAILED(UNWRAP_OBJECT(Cache, obj, mOldCache)))) {
return;
}
Optional<RequestOrUSVString> request;
CacheQueryOptions options;
ErrorResult error;
RefPtr<Promise> 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<JS::Value> 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<JSObject*> 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<nsString, 8> urlList;
// Extract the list of URLs in the old cache.
for (uint32_t i = 0; i < len; ++i) {
JS::Rooted<JS::Value> val(aCx);
if (NS_WARN_IF(!JS_GetElement(aCx, obj, i, &val)) ||
NS_WARN_IF(!val.isObject())) {
return;
}
Request* request;
JS::Rooted<JSObject*> 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<JS::Value> 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<JSObject*> 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<Cache> 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<Promise> 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<nsIInputStream> body;
nsresult rv = NS_NewCStringInputStream(
getter_AddRefs(body), NS_ConvertUTF16toUTF8(aCN->Buffer()));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
RefPtr<InternalResponse> ir =
new InternalResponse(200, NS_LITERAL_CSTRING("OK"));
ir->SetBody(body, aCN->Buffer().Length());
ir->SetURLList(aCN->URLList());
ir->InitChannelInfo(aCN->GetChannelInfo());
UniquePtr<PrincipalInfo> principalInfo = aCN->TakePrincipalInfo();
if (principalInfo) {
ir->SetPrincipalInfo(std::move(principalInfo));
}
RefPtr<InternalHeaders> internalHeaders = aCN->GetInternalHeaders();
ir->Headers()->Fill(*(internalHeaders.get()), IgnoreErrors());
RefPtr<Response> response =
new Response(aCache->GetGlobalObject(), ir, nullptr);
RequestOrUSVString request;
request.SetAsUSVString().Rebind(aCN->URL().Data(), aCN->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.
ErrorResult result;
RefPtr<Promise> 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<ServiceWorkerRegistrationInfo> mRegistration;
RefPtr<CompareCallback> mCallback;
RefPtr<CacheStorage> mCacheStorage;
nsTArray<RefPtr<CompareNetwork>> mCNList;
nsString mURL;
RefPtr<nsIPrincipal> mPrincipal;
// Used for the old cache where saves the old source scripts.
RefPtr<Cache> 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<nsIURI> uri;
nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL, nullptr, nullptr);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
mURL = aURL;
mURLList.AppendElement(NS_ConvertUTF16toUTF8(mURL));
nsCOMPtr<nsILoadGroup> 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_DATA_INHERITS;
nsContentPolicyType contentPolicyType =
mIsMainScript ? nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER
: nsIContentPolicy::TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS;
// 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, nullptr, /* aPerformanceStorage */
loadGroup, nullptr /* aCallbacks */, mLoadFlags);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
nsCOMPtr<nsIHttpChannel> 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(NS_LITERAL_CSTRING("Service-Worker"),
NS_LITERAL_CSTRING("script"),
/* merge */ false);
MOZ_ASSERT(NS_SUCCEEDED(rv));
}
nsCOMPtr<nsIStreamLoader> 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<nsIChannel> 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<nsICacheInfoChannel> 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<nsIPrincipal> channelPrincipal;
nsresult rv = ssm->GetChannelResultPrincipal(
aChannel, getter_AddRefs(channelPrincipal));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
UniquePtr<PrincipalInfo> principalInfo = MakeUnique<PrincipalInfo>();
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<nsIRequest> request;
rv = aLoader->GetRequest(getter_AddRefs(request));
if (NS_WARN_IF(NS_FAILED(rv))) {
return NS_OK;
}
nsCOMPtr<nsIHttpChannel> 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))) {
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<nsString>{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(
NS_LITERAL_CSTRING("Service-Worker-Allowed"), 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<nsString>{NS_ConvertUTF8toUTF16(mRegistration->Scope()),
NS_ConvertUTF8toUTF16(mimeType), mURL});
rv = NS_ERROR_DOM_SECURITY_ERR;
return rv;
}
nsCOMPtr<nsIURI> 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,
NS_LITERAL_STRING("UTF-8"), 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().Rebind(aURL.Data(), aURL.Length());
ErrorResult error;
CacheQueryOptions params;
RefPtr<Promise> 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, NS_LITERAL_STRING("UTF-8"), 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<JS::Value> 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<JS::Value> aValue) {
MOZ_ASSERT(NS_IsMainThread());
if (mState != Finished) {
Finish(NS_ERROR_FAILURE, false);
return;
}
}
void CompareCache::ManageValueResult(JSContext* aCx,
JS::Handle<JS::Value> 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<JSObject*> 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<nsIInputStream> 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 */
SystemGroup::EventTargetFor(TaskCategory::Other));
if (NS_WARN_IF(NS_FAILED(rv))) {
Finish(rv, false);
return;
}
nsCOMPtr<nsIStreamLoader> loader;
rv = NS_NewStreamLoader(getter_AddRefs(loader), this);
if (NS_WARN_IF(NS_FAILED(rv))) {
Finish(rv, false);
return;
}
rv = mPump->AsyncRead(loader, nullptr);
if (NS_WARN_IF(NS_FAILED(rv))) {
mPump = nullptr;
Finish(rv, false);
return;
}
nsCOMPtr<nsIThreadRetargetableRequest> rr = do_QueryInterface(mPump);
if (rr) {
nsCOMPtr<nsIEventTarget> 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> 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<JS::Value> 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<JS::Value> 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,
EmptyString(), EmptyCString(), 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> 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> 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<nsIUUIDGenerator> 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<CompareManager> 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