/* -*- 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 "ScriptLoader.h" #include "nsIChannel.h" #include "nsIContentPolicy.h" #include "nsIContentSecurityPolicy.h" #include "nsIDocShell.h" #include "nsIDOMDocument.h" #include "nsIHttpChannel.h" #include "nsIHttpChannelInternal.h" #include "nsIInputStreamPump.h" #include "nsIIOService.h" #include "nsIProtocolHandler.h" #include "nsIScriptError.h" #include "nsIScriptSecurityManager.h" #include "nsIStreamLoader.h" #include "nsIStreamListenerTee.h" #include "nsIThreadRetargetableRequest.h" #include "nsIURI.h" #include "jsapi.h" #include "jsfriendapi.h" #include "nsError.h" #include "nsContentPolicyUtils.h" #include "nsContentUtils.h" #include "nsDocShellCID.h" #include "nsISupportsPrimitives.h" #include "nsNetUtil.h" #include "nsIPipe.h" #include "nsIOutputStream.h" #include "nsPrintfCString.h" #include "nsScriptLoader.h" #include "nsString.h" #include "nsStreamUtils.h" #include "nsTArray.h" #include "nsThreadUtils.h" #include "nsXPCOM.h" #include "xpcpublic.h" #include "mozilla/Assertions.h" #include "mozilla/LoadContext.h" #include "mozilla/Maybe.h" #include "mozilla/ipc/BackgroundUtils.h" #include "mozilla/dom/CacheBinding.h" #include "mozilla/dom/cache/CacheTypes.h" #include "mozilla/dom/cache/Cache.h" #include "mozilla/dom/cache/CacheStorage.h" #include "mozilla/dom/ChannelInfo.h" #include "mozilla/dom/Exceptions.h" #include "mozilla/dom/InternalResponse.h" #include "mozilla/dom/nsCSPService.h" #include "mozilla/dom/nsCSPUtils.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/PromiseNativeHandler.h" #include "mozilla/dom/Response.h" #include "mozilla/dom/ScriptSettings.h" #include "mozilla/dom/SRILogHelper.h" #include "mozilla/UniquePtr.h" #include "Principal.h" #include "WorkerHolder.h" #include "WorkerPrivate.h" #include "WorkerRunnable.h" #include "WorkerScope.h" #define MAX_CONCURRENT_SCRIPTS 1000 USING_WORKERS_NAMESPACE using namespace mozilla; using namespace mozilla::dom; using mozilla::dom::cache::Cache; using mozilla::dom::cache::CacheStorage; using mozilla::ipc::PrincipalInfo; namespace { nsIURI* GetBaseURI(bool aIsMainScript, WorkerPrivate* aWorkerPrivate) { MOZ_ASSERT(aWorkerPrivate); nsIURI* baseURI; WorkerPrivate* parentWorker = aWorkerPrivate->GetParent(); if (aIsMainScript) { if (parentWorker) { baseURI = parentWorker->GetBaseURI(); NS_ASSERTION(baseURI, "Should have been set already!"); } else { // May be null. baseURI = aWorkerPrivate->GetBaseURI(); } } else { baseURI = aWorkerPrivate->GetBaseURI(); NS_ASSERTION(baseURI, "Should have been set already!"); } return baseURI; } nsresult ChannelFromScriptURL(nsIPrincipal* principal, nsIURI* baseURI, nsIDocument* parentDoc, nsILoadGroup* loadGroup, nsIIOService* ios, nsIScriptSecurityManager* secMan, const nsAString& aScriptURL, bool aIsMainScript, WorkerScriptType aWorkerScriptType, nsContentPolicyType aContentPolicyType, nsLoadFlags aLoadFlags, bool aDefaultURIEncoding, nsIChannel** aChannel) { AssertIsOnMainThread(); nsresult rv; nsCOMPtr<nsIURI> uri; if (aDefaultURIEncoding) { rv = NS_NewURI(getter_AddRefs(uri), aScriptURL, nullptr, baseURI); } else { rv = nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(uri), aScriptURL, parentDoc, baseURI); } if (NS_FAILED(rv)) { return NS_ERROR_DOM_SYNTAX_ERR; } // If we have the document, use it. Unfortunately, for dedicated workers // 'parentDoc' ends up being the parent document, which is not the document // that we want to use. So make sure to avoid using 'parentDoc' in that // situation. if (parentDoc && parentDoc->NodePrincipal() != principal) { parentDoc = nullptr; } aLoadFlags |= nsIChannel::LOAD_CLASSIFY_URI; uint32_t secFlags = aIsMainScript ? nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED : nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS; if (aWorkerScriptType == DebuggerScript) { // A DebuggerScript needs to be a local resource like chrome: or resource: bool isUIResource = false; rv = NS_URIChainHasFlags(uri, nsIProtocolHandler::URI_IS_UI_RESOURCE, &isUIResource); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!isUIResource) { return NS_ERROR_DOM_SECURITY_ERR; } secFlags |= nsILoadInfo::SEC_ALLOW_CHROME; } // Note: this is for backwards compatibility and goes against spec. // We should find a better solution. bool isData = false; if (aIsMainScript && NS_SUCCEEDED(uri->SchemeIs("data", &isData)) && isData) { secFlags = nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL; } nsCOMPtr<nsIChannel> channel; // If we have the document, use it. Unfortunately, for dedicated workers // 'parentDoc' ends up being the parent document, which is not the document // that we want to use. So make sure to avoid using 'parentDoc' in that // situation. if (parentDoc && parentDoc->NodePrincipal() == principal) { rv = NS_NewChannel(getter_AddRefs(channel), uri, parentDoc, secFlags, aContentPolicyType, loadGroup, nullptr, // aCallbacks aLoadFlags, ios); } else { // We must have a loadGroup with a load context for the principal to // traverse the channel correctly. MOZ_ASSERT(loadGroup); MOZ_ASSERT(NS_LoadGroupMatchesPrincipal(loadGroup, principal)); rv = NS_NewChannel(getter_AddRefs(channel), uri, principal, secFlags, aContentPolicyType, loadGroup, nullptr, // aCallbacks aLoadFlags, ios); } NS_ENSURE_SUCCESS(rv, rv); if (nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(channel)) { rv = nsContentUtils::SetFetchReferrerURIWithPolicy(principal, parentDoc, httpChannel, mozilla::net::RP_Default); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } channel.forget(aChannel); return rv; } struct ScriptLoadInfo { ScriptLoadInfo() : mScriptTextBuf(nullptr) , mScriptTextLength(0) , mLoadResult(NS_ERROR_NOT_INITIALIZED) , mLoadingFinished(false) , mExecutionScheduled(false) , mExecutionResult(false) , mCacheStatus(Uncached) { } ~ScriptLoadInfo() { if (mScriptTextBuf) { js_free(mScriptTextBuf); } } bool ReadyToExecute() { return !mChannel && NS_SUCCEEDED(mLoadResult) && !mExecutionScheduled; } nsString mURL; // This full URL string is populated only if this object is used in a // ServiceWorker. nsString mFullURL; // This promise is set only when the script is for a ServiceWorker but // it's not in the cache yet. The promise is resolved when the full body is // stored into the cache. mCachePromise will be set to nullptr after // resolution. RefPtr<Promise> mCachePromise; // The reader stream the cache entry should be filled from, for those cases // when we're going to have an mCachePromise. nsCOMPtr<nsIInputStream> mCacheReadStream; nsCOMPtr<nsIChannel> mChannel; char16_t* mScriptTextBuf; size_t mScriptTextLength; nsresult mLoadResult; bool mLoadingFinished; bool mExecutionScheduled; bool mExecutionResult; enum CacheStatus { // By default a normal script is just loaded from the network. But for // ServiceWorkers, we have to check if the cache contains the script and // load it from the cache. Uncached, WritingToCache, ReadingFromCache, // This script has been loaded from the ServiceWorker cache. Cached, // This script must be stored in the ServiceWorker cache. ToBeCached, // Something went wrong or the worker went away. Cancel }; CacheStatus mCacheStatus; Maybe<bool> mMutedErrorFlag; bool Finished() const { return mLoadingFinished && !mCachePromise && !mChannel; } }; class ScriptLoaderRunnable; class ScriptExecutorRunnable final : public MainThreadWorkerSyncRunnable { ScriptLoaderRunnable& mScriptLoader; bool mIsWorkerScript; uint32_t mFirstIndex; uint32_t mLastIndex; public: ScriptExecutorRunnable(ScriptLoaderRunnable& aScriptLoader, nsIEventTarget* aSyncLoopTarget, bool aIsWorkerScript, uint32_t aFirstIndex, uint32_t aLastIndex); private: ~ScriptExecutorRunnable() { } virtual bool IsDebuggerRunnable() const override; virtual bool PreRun(WorkerPrivate* aWorkerPrivate) override; virtual bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override; virtual void PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate, bool aRunResult) override; nsresult Cancel() override; void ShutdownScriptLoader(JSContext* aCx, WorkerPrivate* aWorkerPrivate, bool aResult, bool aMutedError); void LogExceptionToConsole(JSContext* aCx, WorkerPrivate* WorkerPrivate); }; class CacheScriptLoader; class CacheCreator final : public PromiseNativeHandler { public: NS_DECL_ISUPPORTS explicit CacheCreator(WorkerPrivate* aWorkerPrivate) : mCacheName(aWorkerPrivate->ServiceWorkerCacheName()) , mOriginAttributes(aWorkerPrivate->GetOriginAttributes()) { MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); MOZ_ASSERT(aWorkerPrivate->LoadScriptAsPartOfLoadingServiceWorkerScript()); AssertIsOnMainThread(); } void AddLoader(CacheScriptLoader* aLoader) { AssertIsOnMainThread(); MOZ_ASSERT(!mCacheStorage); mLoaders.AppendElement(aLoader); } virtual void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override; virtual void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override; // Try to load from cache with aPrincipal used for cache access. nsresult Load(nsIPrincipal* aPrincipal); Cache* Cache_() const { AssertIsOnMainThread(); MOZ_ASSERT(mCache); return mCache; } nsIGlobalObject* Global() const { AssertIsOnMainThread(); MOZ_ASSERT(mSandboxGlobalObject); return mSandboxGlobalObject; } void DeleteCache(); private: ~CacheCreator() { } nsresult CreateCacheStorage(nsIPrincipal* aPrincipal); void FailLoaders(nsresult aRv); RefPtr<Cache> mCache; RefPtr<CacheStorage> mCacheStorage; nsCOMPtr<nsIGlobalObject> mSandboxGlobalObject; nsTArray<RefPtr<CacheScriptLoader>> mLoaders; nsString mCacheName; PrincipalOriginAttributes mOriginAttributes; }; NS_IMPL_ISUPPORTS0(CacheCreator) class CacheScriptLoader final : public PromiseNativeHandler , public nsIStreamLoaderObserver { public: NS_DECL_ISUPPORTS NS_DECL_NSISTREAMLOADEROBSERVER CacheScriptLoader(WorkerPrivate* aWorkerPrivate, ScriptLoadInfo& aLoadInfo, uint32_t aIndex, bool aIsWorkerScript, ScriptLoaderRunnable* aRunnable) : mLoadInfo(aLoadInfo) , mIndex(aIndex) , mRunnable(aRunnable) , mIsWorkerScript(aIsWorkerScript) , mFailed(false) { MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); mBaseURI = GetBaseURI(mIsWorkerScript, aWorkerPrivate); AssertIsOnMainThread(); } void Fail(nsresult aRv); void Load(Cache* aCache); virtual void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override; virtual void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override; private: ~CacheScriptLoader() { AssertIsOnMainThread(); } ScriptLoadInfo& mLoadInfo; uint32_t mIndex; RefPtr<ScriptLoaderRunnable> mRunnable; bool mIsWorkerScript; bool mFailed; nsCOMPtr<nsIInputStreamPump> mPump; nsCOMPtr<nsIURI> mBaseURI; mozilla::dom::ChannelInfo mChannelInfo; UniquePtr<PrincipalInfo> mPrincipalInfo; }; NS_IMPL_ISUPPORTS(CacheScriptLoader, nsIStreamLoaderObserver) class CachePromiseHandler final : public PromiseNativeHandler { public: NS_DECL_ISUPPORTS CachePromiseHandler(ScriptLoaderRunnable* aRunnable, ScriptLoadInfo& aLoadInfo, uint32_t aIndex) : mRunnable(aRunnable) , mLoadInfo(aLoadInfo) , mIndex(aIndex) { AssertIsOnMainThread(); MOZ_ASSERT(mRunnable); } virtual void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override; virtual void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override; private: ~CachePromiseHandler() { AssertIsOnMainThread(); } RefPtr<ScriptLoaderRunnable> mRunnable; ScriptLoadInfo& mLoadInfo; uint32_t mIndex; }; NS_IMPL_ISUPPORTS0(CachePromiseHandler) class LoaderListener final : public nsIStreamLoaderObserver , public nsIRequestObserver { public: NS_DECL_ISUPPORTS LoaderListener(ScriptLoaderRunnable* aRunnable, uint32_t aIndex) : mRunnable(aRunnable) , mIndex(aIndex) { MOZ_ASSERT(mRunnable); } NS_IMETHOD OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aContext, nsresult aStatus, uint32_t aStringLen, const uint8_t* aString) override; NS_IMETHOD OnStartRequest(nsIRequest* aRequest, nsISupports* aContext) override; NS_IMETHOD OnStopRequest(nsIRequest* aRequest, nsISupports* aContext, nsresult aStatusCode) override { // Nothing to do here! return NS_OK; } private: ~LoaderListener() {} RefPtr<ScriptLoaderRunnable> mRunnable; uint32_t mIndex; }; NS_IMPL_ISUPPORTS(LoaderListener, nsIStreamLoaderObserver, nsIRequestObserver) class ScriptLoaderHolder; class ScriptLoaderRunnable final : public nsIRunnable { friend class ScriptExecutorRunnable; friend class ScriptLoaderHolder; friend class CachePromiseHandler; friend class CacheScriptLoader; friend class LoaderListener; WorkerPrivate* mWorkerPrivate; nsCOMPtr<nsIEventTarget> mSyncLoopTarget; nsTArray<ScriptLoadInfo> mLoadInfos; RefPtr<CacheCreator> mCacheCreator; bool mIsMainScript; WorkerScriptType mWorkerScriptType; bool mCanceled; bool mCanceledMainThread; ErrorResult& mRv; public: NS_DECL_THREADSAFE_ISUPPORTS ScriptLoaderRunnable(WorkerPrivate* aWorkerPrivate, nsIEventTarget* aSyncLoopTarget, nsTArray<ScriptLoadInfo>& aLoadInfos, bool aIsMainScript, WorkerScriptType aWorkerScriptType, ErrorResult& aRv) : mWorkerPrivate(aWorkerPrivate), mSyncLoopTarget(aSyncLoopTarget), mIsMainScript(aIsMainScript), mWorkerScriptType(aWorkerScriptType), mCanceled(false), mCanceledMainThread(false), mRv(aRv) { aWorkerPrivate->AssertIsOnWorkerThread(); MOZ_ASSERT(aSyncLoopTarget); MOZ_ASSERT_IF(aIsMainScript, aLoadInfos.Length() == 1); mLoadInfos.SwapElements(aLoadInfos); } private: ~ScriptLoaderRunnable() { } NS_IMETHOD Run() override { AssertIsOnMainThread(); nsresult rv = RunInternal(); if (NS_WARN_IF(NS_FAILED(rv))) { CancelMainThread(rv); } return NS_OK; } void LoadingFinished(uint32_t aIndex, nsresult aRv) { AssertIsOnMainThread(); MOZ_ASSERT(aIndex < mLoadInfos.Length()); ScriptLoadInfo& loadInfo = mLoadInfos[aIndex]; loadInfo.mLoadResult = aRv; MOZ_ASSERT(!loadInfo.mLoadingFinished); loadInfo.mLoadingFinished = true; MaybeExecuteFinishedScripts(aIndex); } void MaybeExecuteFinishedScripts(uint32_t aIndex) { AssertIsOnMainThread(); MOZ_ASSERT(aIndex < mLoadInfos.Length()); ScriptLoadInfo& loadInfo = mLoadInfos[aIndex]; // We execute the last step if we don't have a pending operation with the // cache and the loading is completed. if (loadInfo.Finished()) { ExecuteFinishedScripts(); } } nsresult OnStreamComplete(nsIStreamLoader* aLoader, uint32_t aIndex, nsresult aStatus, uint32_t aStringLen, const uint8_t* aString) { AssertIsOnMainThread(); MOZ_ASSERT(aIndex < mLoadInfos.Length()); nsresult rv = OnStreamCompleteInternal(aLoader, aStatus, aStringLen, aString, mLoadInfos[aIndex]); LoadingFinished(aIndex, rv); return NS_OK; } nsresult OnStartRequest(nsIRequest* aRequest, uint32_t aIndex) { AssertIsOnMainThread(); MOZ_ASSERT(aIndex < mLoadInfos.Length()); // If one load info cancels or hits an error, it can race with the start // callback coming from another load info. if (mCanceledMainThread || !mCacheCreator) { aRequest->Cancel(NS_ERROR_FAILURE); return NS_ERROR_FAILURE; } ScriptLoadInfo& loadInfo = mLoadInfos[aIndex]; nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); MOZ_ASSERT(channel == loadInfo.mChannel); // We synthesize the result code, but its never exposed to content. RefPtr<mozilla::dom::InternalResponse> ir = new mozilla::dom::InternalResponse(200, NS_LITERAL_CSTRING("OK")); ir->SetBody(loadInfo.mCacheReadStream, InternalResponse::UNKNOWN_BODY_SIZE); // Drop our reference to the stream now that we've passed it along, so it // doesn't hang around once the cache is done with it and keep data alive. loadInfo.mCacheReadStream = nullptr; // Set the channel info of the channel on the response so that it's // saved in the cache. ir->InitChannelInfo(channel); // Save the principal of the channel since its URI encodes the script URI // rather than the ServiceWorkerRegistrationInfo URI. nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); NS_ASSERTION(ssm, "Should never be null!"); nsCOMPtr<nsIPrincipal> channelPrincipal; nsresult rv = ssm->GetChannelResultPrincipal(channel, getter_AddRefs(channelPrincipal)); if (NS_WARN_IF(NS_FAILED(rv))) { channel->Cancel(rv); return rv; } UniquePtr<PrincipalInfo> principalInfo(new PrincipalInfo()); rv = PrincipalToPrincipalInfo(channelPrincipal, principalInfo.get()); if (NS_WARN_IF(NS_FAILED(rv))) { channel->Cancel(rv); return rv; } ir->SetPrincipalInfo(Move(principalInfo)); RefPtr<mozilla::dom::Response> response = new mozilla::dom::Response(mCacheCreator->Global(), ir); mozilla::dom::RequestOrUSVString request; MOZ_ASSERT(!loadInfo.mFullURL.IsEmpty()); request.SetAsUSVString().Rebind(loadInfo.mFullURL.Data(), loadInfo.mFullURL.Length()); ErrorResult error; RefPtr<Promise> cachePromise = mCacheCreator->Cache_()->Put(request, *response, error); if (NS_WARN_IF(error.Failed())) { nsresult rv = error.StealNSResult(); channel->Cancel(rv); return rv; } RefPtr<CachePromiseHandler> promiseHandler = new CachePromiseHandler(this, loadInfo, aIndex); cachePromise->AppendNativeHandler(promiseHandler); loadInfo.mCachePromise.swap(cachePromise); loadInfo.mCacheStatus = ScriptLoadInfo::WritingToCache; return NS_OK; } bool Notify(Status aStatus) { mWorkerPrivate->AssertIsOnWorkerThread(); if (aStatus >= Terminating && !mCanceled) { mCanceled = true; MOZ_ALWAYS_SUCCEEDS( NS_DispatchToMainThread(NewRunnableMethod(this, &ScriptLoaderRunnable::CancelMainThreadWithBindingAborted))); } return true; } bool IsMainWorkerScript() const { return mIsMainScript && mWorkerScriptType == WorkerScript; } void CancelMainThreadWithBindingAborted() { CancelMainThread(NS_BINDING_ABORTED); } void CancelMainThread(nsresult aCancelResult) { AssertIsOnMainThread(); if (mCanceledMainThread) { return; } mCanceledMainThread = true; if (mCacheCreator) { MOZ_ASSERT(mWorkerPrivate->IsServiceWorker()); DeleteCache(); } // Cancel all the channels that were already opened. for (uint32_t index = 0; index < mLoadInfos.Length(); index++) { ScriptLoadInfo& loadInfo = mLoadInfos[index]; // If promise or channel is non-null, their failures will lead to // LoadingFinished being called. bool callLoadingFinished = true; if (loadInfo.mCachePromise) { MOZ_ASSERT(mWorkerPrivate->IsServiceWorker()); loadInfo.mCachePromise->MaybeReject(aCancelResult); loadInfo.mCachePromise = nullptr; callLoadingFinished = false; } if (loadInfo.mChannel) { if (NS_SUCCEEDED(loadInfo.mChannel->Cancel(aCancelResult))) { callLoadingFinished = false; } else { NS_WARNING("Failed to cancel channel!"); } } if (callLoadingFinished && !loadInfo.Finished()) { LoadingFinished(index, aCancelResult); } } ExecuteFinishedScripts(); } void DeleteCache() { AssertIsOnMainThread(); if (!mCacheCreator) { return; } mCacheCreator->DeleteCache(); mCacheCreator = nullptr; } nsresult RunInternal() { AssertIsOnMainThread(); if (IsMainWorkerScript() && mWorkerPrivate->IsServiceWorker()) { mWorkerPrivate->SetLoadingWorkerScript(true); } if (!mWorkerPrivate->IsServiceWorker() || !mWorkerPrivate->LoadScriptAsPartOfLoadingServiceWorkerScript()) { for (uint32_t index = 0, len = mLoadInfos.Length(); index < len; ++index) { nsresult rv = LoadScript(index); if (NS_WARN_IF(NS_FAILED(rv))) { LoadingFinished(index, rv); return rv; } } return NS_OK; } MOZ_ASSERT(!mCacheCreator); mCacheCreator = new CacheCreator(mWorkerPrivate); for (uint32_t index = 0, len = mLoadInfos.Length(); index < len; ++index) { RefPtr<CacheScriptLoader> loader = new CacheScriptLoader(mWorkerPrivate, mLoadInfos[index], index, IsMainWorkerScript(), this); mCacheCreator->AddLoader(loader); } // The worker may have a null principal on first load, but in that case its // parent definitely will have one. nsIPrincipal* principal = mWorkerPrivate->GetPrincipal(); if (!principal) { WorkerPrivate* parentWorker = mWorkerPrivate->GetParent(); MOZ_ASSERT(parentWorker, "Must have a parent!"); principal = parentWorker->GetPrincipal(); } nsresult rv = mCacheCreator->Load(principal); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } nsresult LoadScript(uint32_t aIndex) { AssertIsOnMainThread(); MOZ_ASSERT(aIndex < mLoadInfos.Length()); WorkerPrivate* parentWorker = mWorkerPrivate->GetParent(); // Figure out which principal to use. nsIPrincipal* principal = mWorkerPrivate->GetPrincipal(); nsCOMPtr<nsILoadGroup> loadGroup = mWorkerPrivate->GetLoadGroup(); if (!principal) { NS_ASSERTION(parentWorker, "Must have a principal!"); NS_ASSERTION(mIsMainScript, "Must have a principal for importScripts!"); principal = parentWorker->GetPrincipal(); loadGroup = parentWorker->GetLoadGroup(); } NS_ASSERTION(principal, "This should never be null here!"); MOZ_ASSERT(NS_LoadGroupMatchesPrincipal(loadGroup, principal)); // Figure out our base URI. nsCOMPtr<nsIURI> baseURI = GetBaseURI(mIsMainScript, mWorkerPrivate); // May be null. nsCOMPtr<nsIDocument> parentDoc = mWorkerPrivate->GetDocument(); nsCOMPtr<nsIChannel> channel; if (IsMainWorkerScript()) { // May be null. channel = mWorkerPrivate->ForgetWorkerChannel(); } nsCOMPtr<nsIIOService> ios(do_GetIOService()); nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager(); NS_ASSERTION(secMan, "This should never be null!"); ScriptLoadInfo& loadInfo = mLoadInfos[aIndex]; nsresult& rv = loadInfo.mLoadResult; nsLoadFlags loadFlags = nsIRequest::LOAD_NORMAL; // Get the top-level worker. WorkerPrivate* topWorkerPrivate = mWorkerPrivate; WorkerPrivate* parent = topWorkerPrivate->GetParent(); while (parent) { topWorkerPrivate = parent; parent = topWorkerPrivate->GetParent(); } // If the top-level worker is a dedicated worker and has a window, and the // window has a docshell, the caching behavior of this worker should match // that of that docshell. if (topWorkerPrivate->IsDedicatedWorker()) { nsCOMPtr<nsPIDOMWindowInner> window = topWorkerPrivate->GetWindow(); if (window) { nsCOMPtr<nsIDocShell> docShell = window->GetDocShell(); if (docShell) { nsresult rv = docShell->GetDefaultLoadFlags(&loadFlags); NS_ENSURE_SUCCESS(rv, rv); } } } // If we are loading a script for a ServiceWorker then we must not // try to intercept it. If the interception matches the current // ServiceWorker's scope then we could deadlock the load. if (mWorkerPrivate->IsServiceWorker()) { loadFlags |= nsIChannel::LOAD_BYPASS_SERVICE_WORKER; } if (!channel) { // Only top level workers' main script use the document charset for the // script uri encoding. Otherwise, default encoding (UTF-8) is applied. bool useDefaultEncoding = !(!parentWorker && IsMainWorkerScript()); rv = ChannelFromScriptURL(principal, baseURI, parentDoc, loadGroup, ios, secMan, loadInfo.mURL, IsMainWorkerScript(), mWorkerScriptType, mWorkerPrivate->ContentPolicyType(), loadFlags, useDefaultEncoding, getter_AddRefs(channel)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } // We need to know which index we're on in OnStreamComplete so we know // where to put the result. RefPtr<LoaderListener> listener = new LoaderListener(this, aIndex); // We don't care about progress so just use the simple stream loader for // OnStreamComplete notification only. nsCOMPtr<nsIStreamLoader> loader; rv = NS_NewStreamLoader(getter_AddRefs(loader), listener); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (loadInfo.mCacheStatus != ScriptLoadInfo::ToBeCached) { rv = channel->AsyncOpen2(loader); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { nsCOMPtr<nsIOutputStream> writer; // In case we return early. loadInfo.mCacheStatus = ScriptLoadInfo::Cancel; rv = NS_NewPipe(getter_AddRefs(loadInfo.mCacheReadStream), getter_AddRefs(writer), 0, UINT32_MAX, // unlimited size to avoid writer WOULD_BLOCK case true, false); // non-blocking reader, blocking writer if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr<nsIStreamListenerTee> tee = do_CreateInstance(NS_STREAMLISTENERTEE_CONTRACTID); rv = tee->Init(loader, writer, listener); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsresult rv = channel->AsyncOpen2(tee); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } loadInfo.mChannel.swap(channel); return NS_OK; } nsresult OnStreamCompleteInternal(nsIStreamLoader* aLoader, nsresult aStatus, uint32_t aStringLen, const uint8_t* aString, ScriptLoadInfo& aLoadInfo) { AssertIsOnMainThread(); if (!aLoadInfo.mChannel) { return NS_BINDING_ABORTED; } aLoadInfo.mChannel = nullptr; if (NS_FAILED(aStatus)) { return aStatus; } NS_ASSERTION(aString, "This should never be null!"); nsCOMPtr<nsIRequest> request; nsresult rv = aLoader->GetRequest(getter_AddRefs(request)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr<nsIChannel> channel = do_QueryInterface(request); MOZ_ASSERT(channel); nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); NS_ASSERTION(ssm, "Should never be null!"); nsCOMPtr<nsIPrincipal> channelPrincipal; rv = ssm->GetChannelResultPrincipal(channel, getter_AddRefs(channelPrincipal)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsIPrincipal* principal = mWorkerPrivate->GetPrincipal(); if (!principal) { WorkerPrivate* parentWorker = mWorkerPrivate->GetParent(); MOZ_ASSERT(parentWorker, "Must have a parent!"); principal = parentWorker->GetPrincipal(); } // We don't mute the main worker script becase we've already done // same-origin checks on them so we should be able to see their errors. // Note that for data: url, where we allow it through the same-origin check // but then give it a different origin. aLoadInfo.mMutedErrorFlag.emplace(IsMainWorkerScript() ? false : !principal->Subsumes(channelPrincipal)); // Make sure we're not seeing the result of a 404 or something by checking // the 'requestSucceeded' attribute on the http channel. nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(request); nsAutoCString tCspHeaderValue, tCspROHeaderValue, tRPHeaderCValue; if (httpChannel) { bool requestSucceeded; rv = httpChannel->GetRequestSucceeded(&requestSucceeded); NS_ENSURE_SUCCESS(rv, rv); if (!requestSucceeded) { return NS_ERROR_NOT_AVAILABLE; } httpChannel->GetResponseHeader( NS_LITERAL_CSTRING("content-security-policy"), tCspHeaderValue); httpChannel->GetResponseHeader( NS_LITERAL_CSTRING("content-security-policy-report-only"), tCspROHeaderValue); httpChannel->GetResponseHeader( NS_LITERAL_CSTRING("referrer-policy"), tRPHeaderCValue); } // May be null. nsIDocument* parentDoc = mWorkerPrivate->GetDocument(); // Use the regular nsScriptLoader for this grunt work! Should be just fine // because we're running on the main thread. // Unlike <script> tags, Worker scripts are always decoded as UTF-8, // per spec. So we explicitly pass in the charset hint. rv = nsScriptLoader::ConvertToUTF16(aLoadInfo.mChannel, aString, aStringLen, NS_LITERAL_STRING("UTF-8"), parentDoc, aLoadInfo.mScriptTextBuf, aLoadInfo.mScriptTextLength); if (NS_FAILED(rv)) { return rv; } if (!aLoadInfo.mScriptTextLength && !aLoadInfo.mScriptTextBuf) { nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, NS_LITERAL_CSTRING("DOM"), parentDoc, nsContentUtils::eDOM_PROPERTIES, "EmptyWorkerSourceWarning"); } else if (!aLoadInfo.mScriptTextBuf) { return NS_ERROR_FAILURE; } // Figure out what we actually loaded. nsCOMPtr<nsIURI> finalURI; rv = NS_GetFinalChannelURI(channel, getter_AddRefs(finalURI)); NS_ENSURE_SUCCESS(rv, rv); nsCString filename; rv = finalURI->GetSpec(filename); NS_ENSURE_SUCCESS(rv, rv); if (!filename.IsEmpty()) { // This will help callers figure out what their script url resolved to in // case of errors. aLoadInfo.mURL.Assign(NS_ConvertUTF8toUTF16(filename)); } nsCOMPtr<nsILoadInfo> chanLoadInfo = channel->GetLoadInfo(); if (chanLoadInfo && chanLoadInfo->GetEnforceSRI()) { // importScripts() and the Worker constructor do not support integrity metadata // (or any fetch options). Until then, we can just block. // If we ever have those data in the future, we'll have to the check to // by using the SRICheck module MOZ_LOG(SRILogHelper::GetSriLog(), mozilla::LogLevel::Debug, ("Scriptloader::Load, SRI required but not supported in workers")); nsCOMPtr<nsIContentSecurityPolicy> wcsp; chanLoadInfo->LoadingPrincipal()->GetCsp(getter_AddRefs(wcsp)); MOZ_ASSERT(wcsp, "We sould have a CSP for the worker here"); if (wcsp) { wcsp->LogViolationDetails( nsIContentSecurityPolicy::VIOLATION_TYPE_REQUIRE_SRI_FOR_SCRIPT, aLoadInfo.mURL, EmptyString(), 0, EmptyString(), EmptyString()); } return NS_ERROR_SRI_CORRUPT; } // Update the principal of the worker and its base URI if we just loaded the // worker's primary script. if (IsMainWorkerScript()) { // Take care of the base URI first. mWorkerPrivate->SetBaseURI(finalURI); // Store the channel info if needed. mWorkerPrivate->InitChannelInfo(channel); // Now to figure out which principal to give this worker. WorkerPrivate* parent = mWorkerPrivate->GetParent(); NS_ASSERTION(mWorkerPrivate->GetPrincipal() || parent, "Must have one of these!"); nsCOMPtr<nsIPrincipal> loadPrincipal = mWorkerPrivate->GetPrincipal() ? mWorkerPrivate->GetPrincipal() : parent->GetPrincipal(); nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); NS_ASSERTION(ssm, "Should never be null!"); nsCOMPtr<nsIPrincipal> channelPrincipal; rv = ssm->GetChannelResultPrincipal(channel, getter_AddRefs(channelPrincipal)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr<nsILoadGroup> channelLoadGroup; rv = channel->GetLoadGroup(getter_AddRefs(channelLoadGroup)); NS_ENSURE_SUCCESS(rv, rv); MOZ_ASSERT(channelLoadGroup); // If the load principal is the system principal then the channel // principal must also be the system principal (we do not allow chrome // code to create workers with non-chrome scripts, and if we ever decide // to change this we need to make sure we don't always set // mPrincipalIsSystem to true in WorkerPrivate::GetLoadInfo()). Otherwise // this channel principal must be same origin with the load principal (we // check again here in case redirects changed the location of the script). if (nsContentUtils::IsSystemPrincipal(loadPrincipal)) { if (!nsContentUtils::IsSystemPrincipal(channelPrincipal)) { // See if this is a resource URI. Since JSMs usually come from // resource:// URIs we're currently considering all URIs with the // URI_IS_UI_RESOURCE flag as valid for creating privileged workers. bool isResource; rv = NS_URIChainHasFlags(finalURI, nsIProtocolHandler::URI_IS_UI_RESOURCE, &isResource); NS_ENSURE_SUCCESS(rv, rv); if (isResource) { // Assign the system principal to the resource:// worker only if it // was loaded from code using the system principal. channelPrincipal = loadPrincipal; } else { return NS_ERROR_DOM_BAD_URI; } } } // The principal can change, but it should still match the original // load group's appId and browser element flag. MOZ_ASSERT(NS_LoadGroupMatchesPrincipal(channelLoadGroup, channelPrincipal)); mWorkerPrivate->SetPrincipal(channelPrincipal, channelLoadGroup); // We did inherit CSP in bug 1223647. If we do not already have a CSP, we // should get it from the HTTP headers on the worker script. if (!mWorkerPrivate->GetCSP() && CSPService::sCSPEnabled) { NS_ConvertASCIItoUTF16 cspHeaderValue(tCspHeaderValue); NS_ConvertASCIItoUTF16 cspROHeaderValue(tCspROHeaderValue); nsIPrincipal* principal = mWorkerPrivate->GetPrincipal(); MOZ_ASSERT(principal, "Should not be null"); nsCOMPtr<nsIContentSecurityPolicy> csp; rv = principal->EnsureCSP(nullptr, getter_AddRefs(csp)); if (csp) { // If there's a CSP header, apply it. if (!cspHeaderValue.IsEmpty()) { rv = CSP_AppendCSPFromHeader(csp, cspHeaderValue, false); NS_ENSURE_SUCCESS(rv, rv); } // If there's a report-only CSP header, apply it. if (!cspROHeaderValue.IsEmpty()) { rv = CSP_AppendCSPFromHeader(csp, cspROHeaderValue, true); NS_ENSURE_SUCCESS(rv, rv); } // Set evalAllowed, default value is set in GetAllowsEval bool evalAllowed = false; bool reportEvalViolations = false; rv = csp->GetAllowsEval(&reportEvalViolations, &evalAllowed); NS_ENSURE_SUCCESS(rv, rv); mWorkerPrivate->SetCSP(csp); mWorkerPrivate->SetEvalAllowed(evalAllowed); mWorkerPrivate->SetReportCSPViolations(reportEvalViolations); // Set ReferrerPolicy, default value is set in GetReferrerPolicy bool hasReferrerPolicy = false; uint32_t rp = mozilla::net::RP_Default; rv = csp->GetReferrerPolicy(&rp, &hasReferrerPolicy); NS_ENSURE_SUCCESS(rv, rv); if (hasReferrerPolicy) { //FIXME bug 1307366: move RP out of CSP code mWorkerPrivate->SetReferrerPolicy(static_cast<net::ReferrerPolicy>(rp)); } } } if (parent) { // XHR Params Allowed mWorkerPrivate->SetXHRParamsAllowed(parent->XHRParamsAllowed()); } } NS_ConvertUTF8toUTF16 tRPHeaderValue(tRPHeaderCValue); // If there's a Referrer-Policy header, apply it. if (!tRPHeaderValue.IsEmpty()) { net::ReferrerPolicy policy = nsContentUtils::GetReferrerPolicyFromHeader(tRPHeaderValue); if (policy != net::RP_Unset) { mWorkerPrivate->SetReferrerPolicy(policy); } } return NS_OK; } void DataReceivedFromCache(uint32_t aIndex, const uint8_t* aString, uint32_t aStringLen, const mozilla::dom::ChannelInfo& aChannelInfo, UniquePtr<PrincipalInfo> aPrincipalInfo) { AssertIsOnMainThread(); MOZ_ASSERT(aIndex < mLoadInfos.Length()); ScriptLoadInfo& loadInfo = mLoadInfos[aIndex]; MOZ_ASSERT(loadInfo.mCacheStatus == ScriptLoadInfo::Cached); nsCOMPtr<nsIPrincipal> responsePrincipal = PrincipalInfoToPrincipal(*aPrincipalInfo); nsIPrincipal* principal = mWorkerPrivate->GetPrincipal(); if (!principal) { WorkerPrivate* parentWorker = mWorkerPrivate->GetParent(); MOZ_ASSERT(parentWorker, "Must have a parent!"); principal = parentWorker->GetPrincipal(); } loadInfo.mMutedErrorFlag.emplace(!principal->Subsumes(responsePrincipal)); // May be null. nsIDocument* parentDoc = mWorkerPrivate->GetDocument(); MOZ_ASSERT(!loadInfo.mScriptTextBuf); nsresult rv = nsScriptLoader::ConvertToUTF16(nullptr, aString, aStringLen, NS_LITERAL_STRING("UTF-8"), parentDoc, loadInfo.mScriptTextBuf, loadInfo.mScriptTextLength); if (NS_SUCCEEDED(rv) && IsMainWorkerScript()) { nsCOMPtr<nsIURI> finalURI; rv = NS_NewURI(getter_AddRefs(finalURI), loadInfo.mFullURL, nullptr, nullptr); if (NS_SUCCEEDED(rv)) { mWorkerPrivate->SetBaseURI(finalURI); } mozilla::DebugOnly<nsIPrincipal*> principal = mWorkerPrivate->GetPrincipal(); MOZ_ASSERT(principal); nsILoadGroup* loadGroup = mWorkerPrivate->GetLoadGroup(); MOZ_ASSERT(loadGroup); mozilla::DebugOnly<bool> equal = false; MOZ_ASSERT(responsePrincipal && NS_SUCCEEDED(responsePrincipal->Equals(principal, &equal))); MOZ_ASSERT(equal); mWorkerPrivate->InitChannelInfo(aChannelInfo); mWorkerPrivate->SetPrincipal(responsePrincipal, loadGroup); } if (NS_SUCCEEDED(rv)) { DataReceived(); } LoadingFinished(aIndex, rv); } void DataReceived() { if (IsMainWorkerScript()) { WorkerPrivate* parent = mWorkerPrivate->GetParent(); if (parent) { // XHR Params Allowed mWorkerPrivate->SetXHRParamsAllowed(parent->XHRParamsAllowed()); // Set Eval and ContentSecurityPolicy mWorkerPrivate->SetCSP(parent->GetCSP()); mWorkerPrivate->SetEvalAllowed(parent->IsEvalAllowed()); } } } void ExecuteFinishedScripts() { AssertIsOnMainThread(); if (IsMainWorkerScript()) { mWorkerPrivate->WorkerScriptLoaded(); } uint32_t firstIndex = UINT32_MAX; uint32_t lastIndex = UINT32_MAX; // Find firstIndex based on whether mExecutionScheduled is unset. for (uint32_t index = 0; index < mLoadInfos.Length(); index++) { if (!mLoadInfos[index].mExecutionScheduled) { firstIndex = index; break; } } // Find lastIndex based on whether mChannel is set, and update // mExecutionScheduled on the ones we're about to schedule. if (firstIndex != UINT32_MAX) { for (uint32_t index = firstIndex; index < mLoadInfos.Length(); index++) { ScriptLoadInfo& loadInfo = mLoadInfos[index]; if (!loadInfo.Finished()) { break; } // We can execute this one. loadInfo.mExecutionScheduled = true; lastIndex = index; } } // This is the last index, we can unused things before the exection of the // script and the stopping of the sync loop. if (lastIndex == mLoadInfos.Length() - 1) { mCacheCreator = nullptr; } if (firstIndex != UINT32_MAX && lastIndex != UINT32_MAX) { RefPtr<ScriptExecutorRunnable> runnable = new ScriptExecutorRunnable(*this, mSyncLoopTarget, IsMainWorkerScript(), firstIndex, lastIndex); if (!runnable->Dispatch()) { MOZ_ASSERT(false, "This should never fail!"); } } } }; NS_IMPL_ISUPPORTS(ScriptLoaderRunnable, nsIRunnable) class MOZ_STACK_CLASS ScriptLoaderHolder final : public WorkerHolder { // Raw pointer because this holder object follows the mRunnable life-time. ScriptLoaderRunnable* mRunnable; public: explicit ScriptLoaderHolder(ScriptLoaderRunnable* aRunnable) : mRunnable(aRunnable) { MOZ_ASSERT(aRunnable); } virtual bool Notify(Status aStatus) override { mRunnable->Notify(aStatus); return true; } }; NS_IMETHODIMP LoaderListener::OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aContext, nsresult aStatus, uint32_t aStringLen, const uint8_t* aString) { return mRunnable->OnStreamComplete(aLoader, mIndex, aStatus, aStringLen, aString); } NS_IMETHODIMP LoaderListener::OnStartRequest(nsIRequest* aRequest, nsISupports* aContext) { return mRunnable->OnStartRequest(aRequest, mIndex); } void CachePromiseHandler::ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) { AssertIsOnMainThread(); // May already have been canceled by CacheScriptLoader::Fail from // CancelMainThread. MOZ_ASSERT(mLoadInfo.mCacheStatus == ScriptLoadInfo::WritingToCache || mLoadInfo.mCacheStatus == ScriptLoadInfo::Cancel); MOZ_ASSERT_IF(mLoadInfo.mCacheStatus == ScriptLoadInfo::Cancel, !mLoadInfo.mCachePromise); if (mLoadInfo.mCachePromise) { mLoadInfo.mCacheStatus = ScriptLoadInfo::Cached; mLoadInfo.mCachePromise = nullptr; mRunnable->MaybeExecuteFinishedScripts(mIndex); } } void CachePromiseHandler::RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) { AssertIsOnMainThread(); // May already have been canceled by CacheScriptLoader::Fail from // CancelMainThread. MOZ_ASSERT(mLoadInfo.mCacheStatus == ScriptLoadInfo::WritingToCache || mLoadInfo.mCacheStatus == ScriptLoadInfo::Cancel); mLoadInfo.mCacheStatus = ScriptLoadInfo::Cancel; mLoadInfo.mCachePromise = nullptr; // This will delete the cache object and will call LoadingFinished() with an // error for each ongoing operation. mRunnable->DeleteCache(); } nsresult CacheCreator::CreateCacheStorage(nsIPrincipal* aPrincipal) { AssertIsOnMainThread(); MOZ_ASSERT(!mCacheStorage); MOZ_ASSERT(aPrincipal); nsIXPConnect* xpc = nsContentUtils::XPConnect(); MOZ_ASSERT(xpc, "This should never be null!"); mozilla::AutoSafeJSContext cx; JS::Rooted<JSObject*> sandbox(cx); nsresult rv = xpc->CreateSandbox(cx, aPrincipal, sandbox.address()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } mSandboxGlobalObject = xpc::NativeGlobal(sandbox); if (NS_WARN_IF(!mSandboxGlobalObject)) { return NS_ERROR_FAILURE; } // If we're in private browsing mode, don't even try to create the // CacheStorage. Instead, just fail immediately to terminate the // ServiceWorker load. if (NS_WARN_IF(mOriginAttributes.mPrivateBrowsingId > 0)) { return NS_ERROR_DOM_SECURITY_ERR; } // Create a CacheStorage bypassing its trusted origin checks. The // ServiceWorker has already performed its own checks before getting // to this point. ErrorResult error; mCacheStorage = CacheStorage::CreateOnMainThread(mozilla::dom::cache::CHROME_ONLY_NAMESPACE, mSandboxGlobalObject, aPrincipal, false, /* privateBrowsing can't be true here */ true /* force trusted origin */, error); if (NS_WARN_IF(error.Failed())) { return error.StealNSResult(); } return NS_OK; } nsresult CacheCreator::Load(nsIPrincipal* aPrincipal) { AssertIsOnMainThread(); MOZ_ASSERT(!mLoaders.IsEmpty()); nsresult rv = CreateCacheStorage(aPrincipal); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } ErrorResult error; MOZ_ASSERT(!mCacheName.IsEmpty()); RefPtr<Promise> promise = mCacheStorage->Open(mCacheName, error); if (NS_WARN_IF(error.Failed())) { return error.StealNSResult(); } promise->AppendNativeHandler(this); return NS_OK; } void CacheCreator::FailLoaders(nsresult aRv) { AssertIsOnMainThread(); // Fail() can call LoadingFinished() which may call ExecuteFinishedScripts() // which sets mCacheCreator to null, so hold a ref. RefPtr<CacheCreator> kungfuDeathGrip = this; for (uint32_t i = 0, len = mLoaders.Length(); i < len; ++i) { mLoaders[i]->Fail(aRv); } mLoaders.Clear(); } void CacheCreator::RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) { AssertIsOnMainThread(); FailLoaders(NS_ERROR_FAILURE); } void CacheCreator::ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) { AssertIsOnMainThread(); if (!aValue.isObject()) { FailLoaders(NS_ERROR_FAILURE); return; } JS::Rooted<JSObject*> obj(aCx, &aValue.toObject()); Cache* cache = nullptr; nsresult rv = UNWRAP_OBJECT(Cache, &obj, cache); if (NS_WARN_IF(NS_FAILED(rv))) { FailLoaders(NS_ERROR_FAILURE); return; } mCache = cache; MOZ_DIAGNOSTIC_ASSERT(mCache); // If the worker is canceled, CancelMainThread() will have cleared the // loaders via DeleteCache(). for (uint32_t i = 0, len = mLoaders.Length(); i < len; ++i) { MOZ_DIAGNOSTIC_ASSERT(mLoaders[i]); mLoaders[i]->Load(cache); } } void CacheCreator::DeleteCache() { AssertIsOnMainThread(); // This is called when the load is canceled which can occur before // mCacheStorage is initialized. if (mCacheStorage) { // It's safe to do this while Cache::Match() and Cache::Put() calls are // running. IgnoredErrorResult rv; RefPtr<Promise> promise = mCacheStorage->Delete(mCacheName, rv); // We don't care to know the result of the promise object. } // Always call this here to ensure the loaders array is cleared. FailLoaders(NS_ERROR_FAILURE); } void CacheScriptLoader::Fail(nsresult aRv) { AssertIsOnMainThread(); MOZ_ASSERT(NS_FAILED(aRv)); if (mFailed) { return; } mFailed = true; if (mPump) { MOZ_ASSERT(mLoadInfo.mCacheStatus == ScriptLoadInfo::ReadingFromCache); mPump->Cancel(aRv); mPump = nullptr; } mLoadInfo.mCacheStatus = ScriptLoadInfo::Cancel; // Stop if the load was aborted on the main thread. // Can't use Finished() because mCachePromise may still be true. if (mLoadInfo.mLoadingFinished) { MOZ_ASSERT(!mLoadInfo.mChannel); MOZ_ASSERT_IF(mLoadInfo.mCachePromise, mLoadInfo.mCacheStatus == ScriptLoadInfo::WritingToCache || mLoadInfo.mCacheStatus == ScriptLoadInfo::Cancel); return; } mRunnable->LoadingFinished(mIndex, aRv); } void CacheScriptLoader::Load(Cache* aCache) { AssertIsOnMainThread(); MOZ_ASSERT(aCache); nsCOMPtr<nsIURI> uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), mLoadInfo.mURL, nullptr, mBaseURI); if (NS_WARN_IF(NS_FAILED(rv))) { Fail(rv); return; } nsAutoCString spec; rv = uri->GetSpec(spec); if (NS_WARN_IF(NS_FAILED(rv))) { Fail(rv); return; } MOZ_ASSERT(mLoadInfo.mFullURL.IsEmpty()); CopyUTF8toUTF16(spec, mLoadInfo.mFullURL); mozilla::dom::RequestOrUSVString request; request.SetAsUSVString().Rebind(mLoadInfo.mFullURL.Data(), mLoadInfo.mFullURL.Length()); mozilla::dom::CacheQueryOptions params; ErrorResult error; RefPtr<Promise> promise = aCache->Match(request, params, error); if (NS_WARN_IF(error.Failed())) { Fail(error.StealNSResult()); return; } promise->AppendNativeHandler(this); } void CacheScriptLoader::RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) { AssertIsOnMainThread(); MOZ_ASSERT(mLoadInfo.mCacheStatus == ScriptLoadInfo::Uncached); Fail(NS_ERROR_FAILURE); } void CacheScriptLoader::ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) { AssertIsOnMainThread(); // If we have already called 'Fail', we should not proceed. if (mFailed) { return; } MOZ_ASSERT(mLoadInfo.mCacheStatus == ScriptLoadInfo::Uncached); nsresult rv; if (aValue.isUndefined()) { mLoadInfo.mCacheStatus = ScriptLoadInfo::ToBeCached; rv = mRunnable->LoadScript(mIndex); if (NS_WARN_IF(NS_FAILED(rv))) { Fail(rv); } return; } MOZ_ASSERT(aValue.isObject()); JS::Rooted<JSObject*> obj(aCx, &aValue.toObject()); mozilla::dom::Response* response = nullptr; rv = UNWRAP_OBJECT(Response, &obj, response); if (NS_WARN_IF(NS_FAILED(rv))) { Fail(rv); return; } nsCOMPtr<nsIInputStream> inputStream; response->GetBody(getter_AddRefs(inputStream)); mChannelInfo = response->GetChannelInfo(); const UniquePtr<PrincipalInfo>& pInfo = response->GetPrincipalInfo(); if (pInfo) { mPrincipalInfo = mozilla::MakeUnique<PrincipalInfo>(*pInfo); } if (!inputStream) { mLoadInfo.mCacheStatus = ScriptLoadInfo::Cached; mRunnable->DataReceivedFromCache(mIndex, (uint8_t*)"", 0, mChannelInfo, Move(mPrincipalInfo)); return; } MOZ_ASSERT(!mPump); rv = NS_NewInputStreamPump(getter_AddRefs(mPump), inputStream); if (NS_WARN_IF(NS_FAILED(rv))) { Fail(rv); return; } nsCOMPtr<nsIStreamLoader> loader; rv = NS_NewStreamLoader(getter_AddRefs(loader), this); if (NS_WARN_IF(NS_FAILED(rv))) { Fail(rv); return; } rv = mPump->AsyncRead(loader, nullptr); if (NS_WARN_IF(NS_FAILED(rv))) { mPump = nullptr; Fail(rv); return; } nsCOMPtr<nsIThreadRetargetableRequest> rr = do_QueryInterface(mPump); if (rr) { nsCOMPtr<nsIEventTarget> sts = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); rv = rr->RetargetDeliveryTo(sts); if (NS_FAILED(rv)) { NS_WARNING("Failed to dispatch the nsIInputStreamPump to a IO thread."); } } mLoadInfo.mCacheStatus = ScriptLoadInfo::ReadingFromCache; } NS_IMETHODIMP CacheScriptLoader::OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aContext, nsresult aStatus, uint32_t aStringLen, const uint8_t* aString) { AssertIsOnMainThread(); mPump = nullptr; if (NS_FAILED(aStatus)) { MOZ_ASSERT(mLoadInfo.mCacheStatus == ScriptLoadInfo::ReadingFromCache || mLoadInfo.mCacheStatus == ScriptLoadInfo::Cancel); Fail(aStatus); return NS_OK; } MOZ_ASSERT(mLoadInfo.mCacheStatus == ScriptLoadInfo::ReadingFromCache); mLoadInfo.mCacheStatus = ScriptLoadInfo::Cached; MOZ_ASSERT(mPrincipalInfo); mRunnable->DataReceivedFromCache(mIndex, aString, aStringLen, mChannelInfo, Move(mPrincipalInfo)); return NS_OK; } class ChannelGetterRunnable final : public WorkerMainThreadRunnable { const nsAString& mScriptURL; nsIChannel** mChannel; nsresult mResult; public: ChannelGetterRunnable(WorkerPrivate* aParentWorker, const nsAString& aScriptURL, nsIChannel** aChannel) : WorkerMainThreadRunnable(aParentWorker, NS_LITERAL_CSTRING("ScriptLoader :: ChannelGetter")) , mScriptURL(aScriptURL) , mChannel(aChannel) , mResult(NS_ERROR_FAILURE) { MOZ_ASSERT(aParentWorker); aParentWorker->AssertIsOnWorkerThread(); } virtual bool MainThreadRun() override { AssertIsOnMainThread(); nsIPrincipal* principal = mWorkerPrivate->GetPrincipal(); MOZ_ASSERT(principal); // Figure out our base URI. nsCOMPtr<nsIURI> baseURI = mWorkerPrivate->GetBaseURI(); MOZ_ASSERT(baseURI); // May be null. nsCOMPtr<nsIDocument> parentDoc = mWorkerPrivate->GetDocument(); nsCOMPtr<nsILoadGroup> loadGroup = mWorkerPrivate->GetLoadGroup(); nsCOMPtr<nsIChannel> channel; mResult = scriptloader::ChannelFromScriptURLMainThread(principal, baseURI, parentDoc, loadGroup, mScriptURL, // Nested workers are always dedicated. nsIContentPolicy::TYPE_INTERNAL_WORKER, // Nested workers use default uri encoding. true, getter_AddRefs(channel)); if (NS_SUCCEEDED(mResult)) { channel.forget(mChannel); } return true; } nsresult GetResult() const { return mResult; } private: virtual ~ChannelGetterRunnable() { } }; ScriptExecutorRunnable::ScriptExecutorRunnable( ScriptLoaderRunnable& aScriptLoader, nsIEventTarget* aSyncLoopTarget, bool aIsWorkerScript, uint32_t aFirstIndex, uint32_t aLastIndex) : MainThreadWorkerSyncRunnable(aScriptLoader.mWorkerPrivate, aSyncLoopTarget), mScriptLoader(aScriptLoader), mIsWorkerScript(aIsWorkerScript), mFirstIndex(aFirstIndex), mLastIndex(aLastIndex) { MOZ_ASSERT(aFirstIndex <= aLastIndex); MOZ_ASSERT(aLastIndex < aScriptLoader.mLoadInfos.Length()); } bool ScriptExecutorRunnable::IsDebuggerRunnable() const { // ScriptExecutorRunnable is used to execute both worker and debugger scripts. // In the latter case, the runnable needs to be dispatched to the debugger // queue. return mScriptLoader.mWorkerScriptType == DebuggerScript; } bool ScriptExecutorRunnable::PreRun(WorkerPrivate* aWorkerPrivate) { aWorkerPrivate->AssertIsOnWorkerThread(); if (!mIsWorkerScript) { return true; } if (!aWorkerPrivate->GetJSContext()) { return false; } MOZ_ASSERT(mFirstIndex == 0); MOZ_ASSERT(!mScriptLoader.mRv.Failed()); AutoJSAPI jsapi; jsapi.Init(); WorkerGlobalScope* globalScope = aWorkerPrivate->GetOrCreateGlobalScope(jsapi.cx()); if (NS_WARN_IF(!globalScope)) { NS_WARNING("Failed to make global!"); // There's no way to report the exception on jsapi right now, because there // is no way to even enter a compartment on this thread anymore. Just clear // the exception. We'll report some sort of error to our caller in // ShutdownScriptLoader, but it will get squelched for the same reason we're // squelching here: all the error reporting machinery relies on being able // to enter a compartment to report the error. jsapi.ClearException(); return false; } return true; } bool ScriptExecutorRunnable::WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) { aWorkerPrivate->AssertIsOnWorkerThread(); nsTArray<ScriptLoadInfo>& loadInfos = mScriptLoader.mLoadInfos; // Don't run if something else has already failed. for (uint32_t index = 0; index < mFirstIndex; index++) { ScriptLoadInfo& loadInfo = loadInfos.ElementAt(index); NS_ASSERTION(!loadInfo.mChannel, "Should no longer have a channel!"); NS_ASSERTION(loadInfo.mExecutionScheduled, "Should be scheduled!"); if (!loadInfo.mExecutionResult) { return true; } } // If nothing else has failed, our ErrorResult better not be a failure either. MOZ_ASSERT(!mScriptLoader.mRv.Failed(), "Who failed it and why?"); // Slightly icky action at a distance, but there's no better place to stash // this value, really. JS::Rooted<JSObject*> global(aCx, JS::CurrentGlobalOrNull(aCx)); MOZ_ASSERT(global); for (uint32_t index = mFirstIndex; index <= mLastIndex; index++) { ScriptLoadInfo& loadInfo = loadInfos.ElementAt(index); NS_ASSERTION(!loadInfo.mChannel, "Should no longer have a channel!"); NS_ASSERTION(loadInfo.mExecutionScheduled, "Should be scheduled!"); NS_ASSERTION(!loadInfo.mExecutionResult, "Should not have executed yet!"); MOZ_ASSERT(!mScriptLoader.mRv.Failed(), "Who failed it and why?"); mScriptLoader.mRv.MightThrowJSException(); if (NS_FAILED(loadInfo.mLoadResult)) { scriptloader::ReportLoadError(mScriptLoader.mRv, loadInfo.mLoadResult, loadInfo.mURL); // Top level scripts only! if (mIsWorkerScript) { aWorkerPrivate->MaybeDispatchLoadFailedRunnable(); } return true; } NS_ConvertUTF16toUTF8 filename(loadInfo.mURL); JS::CompileOptions options(aCx); options.setFileAndLine(filename.get(), 1) .setNoScriptRval(true); if (mScriptLoader.mWorkerScriptType == DebuggerScript) { options.setVersion(JSVERSION_LATEST); } MOZ_ASSERT(loadInfo.mMutedErrorFlag.isSome()); options.setMutedErrors(loadInfo.mMutedErrorFlag.valueOr(true)); JS::SourceBufferHolder srcBuf(loadInfo.mScriptTextBuf, loadInfo.mScriptTextLength, JS::SourceBufferHolder::GiveOwnership); loadInfo.mScriptTextBuf = nullptr; loadInfo.mScriptTextLength = 0; // Our ErrorResult still shouldn't be a failure. MOZ_ASSERT(!mScriptLoader.mRv.Failed(), "Who failed it and why?"); JS::Rooted<JS::Value> unused(aCx); if (!JS::Evaluate(aCx, options, srcBuf, &unused)) { mScriptLoader.mRv.StealExceptionFromJSContext(aCx); return true; } loadInfo.mExecutionResult = true; } return true; } void ScriptExecutorRunnable::PostRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate, bool aRunResult) { aWorkerPrivate->AssertIsOnWorkerThread(); MOZ_ASSERT(!JS_IsExceptionPending(aCx), "Who left an exception on there?"); nsTArray<ScriptLoadInfo>& loadInfos = mScriptLoader.mLoadInfos; if (mLastIndex == loadInfos.Length() - 1) { // All done. If anything failed then return false. bool result = true; bool mutedError = false; for (uint32_t index = 0; index < loadInfos.Length(); index++) { if (!loadInfos[index].mExecutionResult) { mutedError = loadInfos[index].mMutedErrorFlag.valueOr(true); result = false; break; } } // The only way we can get here with "result" false but without // mScriptLoader.mRv being a failure is if we're loading the main worker // script and GetOrCreateGlobalScope() fails. In that case we would have // returned false from WorkerRun, so assert that. MOZ_ASSERT_IF(!result && !mScriptLoader.mRv.Failed(), !aRunResult); ShutdownScriptLoader(aCx, aWorkerPrivate, result, mutedError); } } nsresult ScriptExecutorRunnable::Cancel() { if (mLastIndex == mScriptLoader.mLoadInfos.Length() - 1) { ShutdownScriptLoader(mWorkerPrivate->GetJSContext(), mWorkerPrivate, false, false); } return MainThreadWorkerSyncRunnable::Cancel(); } void ScriptExecutorRunnable::ShutdownScriptLoader(JSContext* aCx, WorkerPrivate* aWorkerPrivate, bool aResult, bool aMutedError) { aWorkerPrivate->AssertIsOnWorkerThread(); MOZ_ASSERT(mLastIndex == mScriptLoader.mLoadInfos.Length() - 1); if (mIsWorkerScript && aWorkerPrivate->IsServiceWorker()) { aWorkerPrivate->SetLoadingWorkerScript(false); } if (!aResult) { // At this point there are two possibilities: // // 1) mScriptLoader.mRv.Failed(). In that case we just want to leave it // as-is, except if it has a JS exception and we need to mute JS // exceptions. In that case, we log the exception without firing any // events and then replace it on the ErrorResult with a NetworkError, // per spec. // // 2) mScriptLoader.mRv succeeded. As far as I can tell, this can only // happen when loading the main worker script and // GetOrCreateGlobalScope() fails or if ScriptExecutorRunnable::Cancel // got called. Does it matter what we throw in this case? I'm not // sure... if (mScriptLoader.mRv.Failed()) { if (aMutedError && mScriptLoader.mRv.IsJSException()) { LogExceptionToConsole(aCx, aWorkerPrivate); mScriptLoader.mRv.Throw(NS_ERROR_DOM_NETWORK_ERR); } } else { mScriptLoader.mRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); } } aWorkerPrivate->StopSyncLoop(mSyncLoopTarget, aResult); } void ScriptExecutorRunnable::LogExceptionToConsole(JSContext* aCx, WorkerPrivate* aWorkerPrivate) { aWorkerPrivate->AssertIsOnWorkerThread(); MOZ_ASSERT(mScriptLoader.mRv.IsJSException()); JS::Rooted<JS::Value> exn(aCx); if (!ToJSValue(aCx, mScriptLoader.mRv, &exn)) { return; } // Now the exception state should all be in exn. MOZ_ASSERT(!JS_IsExceptionPending(aCx)); MOZ_ASSERT(!mScriptLoader.mRv.Failed()); js::ErrorReport report(aCx); if (!report.init(aCx, exn, js::ErrorReport::WithSideEffects)) { JS_ClearPendingException(aCx); return; } RefPtr<xpc::ErrorReport> xpcReport = new xpc::ErrorReport(); xpcReport->Init(report.report(), report.toStringResult().c_str(), aWorkerPrivate->IsChromeWorker(), aWorkerPrivate->WindowID()); RefPtr<AsyncErrorReporter> r = new AsyncErrorReporter(xpcReport); NS_DispatchToMainThread(r); } void LoadAllScripts(WorkerPrivate* aWorkerPrivate, nsTArray<ScriptLoadInfo>& aLoadInfos, bool aIsMainScript, WorkerScriptType aWorkerScriptType, ErrorResult& aRv) { aWorkerPrivate->AssertIsOnWorkerThread(); NS_ASSERTION(!aLoadInfos.IsEmpty(), "Bad arguments!"); AutoSyncLoopHolder syncLoop(aWorkerPrivate); RefPtr<ScriptLoaderRunnable> loader = new ScriptLoaderRunnable(aWorkerPrivate, syncLoop.EventTarget(), aLoadInfos, aIsMainScript, aWorkerScriptType, aRv); NS_ASSERTION(aLoadInfos.IsEmpty(), "Should have swapped!"); ScriptLoaderHolder workerHolder(loader); if (NS_WARN_IF(!workerHolder.HoldWorker(aWorkerPrivate, Terminating))) { aRv.Throw(NS_ERROR_FAILURE); return; } if (NS_FAILED(NS_DispatchToMainThread(loader))) { NS_ERROR("Failed to dispatch!"); aRv.Throw(NS_ERROR_FAILURE); return; } syncLoop.Run(); } } /* anonymous namespace */ BEGIN_WORKERS_NAMESPACE namespace scriptloader { nsresult ChannelFromScriptURLMainThread(nsIPrincipal* aPrincipal, nsIURI* aBaseURI, nsIDocument* aParentDoc, nsILoadGroup* aLoadGroup, const nsAString& aScriptURL, nsContentPolicyType aContentPolicyType, bool aDefaultURIEncoding, nsIChannel** aChannel) { AssertIsOnMainThread(); nsCOMPtr<nsIIOService> ios(do_GetIOService()); nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager(); NS_ASSERTION(secMan, "This should never be null!"); return ChannelFromScriptURL(aPrincipal, aBaseURI, aParentDoc, aLoadGroup, ios, secMan, aScriptURL, true, WorkerScript, aContentPolicyType, nsIRequest::LOAD_NORMAL, aDefaultURIEncoding, aChannel); } nsresult ChannelFromScriptURLWorkerThread(JSContext* aCx, WorkerPrivate* aParent, const nsAString& aScriptURL, nsIChannel** aChannel) { aParent->AssertIsOnWorkerThread(); RefPtr<ChannelGetterRunnable> getter = new ChannelGetterRunnable(aParent, aScriptURL, aChannel); ErrorResult rv; getter->Dispatch(rv); if (rv.Failed()) { NS_ERROR("Failed to dispatch!"); return rv.StealNSResult(); } return getter->GetResult(); } void ReportLoadError(ErrorResult& aRv, nsresult aLoadResult, const nsAString& aScriptURL) { MOZ_ASSERT(!aRv.Failed()); switch (aLoadResult) { case NS_ERROR_FILE_NOT_FOUND: case NS_ERROR_NOT_AVAILABLE: aLoadResult = NS_ERROR_DOM_NETWORK_ERR; break; case NS_ERROR_MALFORMED_URI: aLoadResult = NS_ERROR_DOM_SYNTAX_ERR; break; case NS_BINDING_ABORTED: // Note: we used to pretend like we didn't set an exception for // NS_BINDING_ABORTED, but then ShutdownScriptLoader did it anyway. The // other callsite, in WorkerPrivate::Constructor, never passed in // NS_BINDING_ABORTED. So just throw it directly here. Consumers will // deal as needed. But note that we do NOT want to ThrowDOMException() // for this case, because that will make it impossible for consumers to // realize that our error was NS_BINDING_ABORTED. aRv.Throw(aLoadResult); return; case NS_ERROR_DOM_SECURITY_ERR: case NS_ERROR_DOM_SYNTAX_ERR: break; case NS_ERROR_DOM_BAD_URI: // This is actually a security error. aLoadResult = NS_ERROR_DOM_SECURITY_ERR; break; default: // For lack of anything better, go ahead and throw a NetworkError here. // We don't want to throw a JS exception, because for toplevel script // loads that would get squelched. aRv.ThrowDOMException(NS_ERROR_DOM_NETWORK_ERR, nsPrintfCString("Failed to load worker script at %s (nsresult = 0x%x)", NS_ConvertUTF16toUTF8(aScriptURL).get(), aLoadResult)); return; } aRv.ThrowDOMException(aLoadResult, NS_LITERAL_CSTRING("Failed to load worker script at \"") + NS_ConvertUTF16toUTF8(aScriptURL) + NS_LITERAL_CSTRING("\"")); } void LoadMainScript(WorkerPrivate* aWorkerPrivate, const nsAString& aScriptURL, WorkerScriptType aWorkerScriptType, ErrorResult& aRv) { nsTArray<ScriptLoadInfo> loadInfos; ScriptLoadInfo* info = loadInfos.AppendElement(); info->mURL = aScriptURL; LoadAllScripts(aWorkerPrivate, loadInfos, true, aWorkerScriptType, aRv); } void Load(WorkerPrivate* aWorkerPrivate, const nsTArray<nsString>& aScriptURLs, WorkerScriptType aWorkerScriptType, ErrorResult& aRv) { const uint32_t urlCount = aScriptURLs.Length(); if (!urlCount) { return; } if (urlCount > MAX_CONCURRENT_SCRIPTS) { aRv.Throw(NS_ERROR_OUT_OF_MEMORY); return; } nsTArray<ScriptLoadInfo> loadInfos; loadInfos.SetLength(urlCount); for (uint32_t index = 0; index < urlCount; index++) { loadInfos[index].mURL = aScriptURLs[index]; } LoadAllScripts(aWorkerPrivate, loadInfos, false, aWorkerScriptType, aRv); } } // namespace scriptloader END_WORKERS_NAMESPACE