/* -*- 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 #include #include "nsIChannel.h" #include "nsIContentPolicy.h" #include "nsIContentSecurityPolicy.h" #include "nsICookieJarSettings.h" #include "nsIDocShell.h" #include "nsIHttpChannel.h" #include "nsIHttpChannelInternal.h" #include "nsIIOService.h" #include "nsIOService.h" #include "nsIPrincipal.h" #include "nsIProtocolHandler.h" #include "nsIScriptError.h" #include "nsIScriptSecurityManager.h" #include "nsIStreamListenerTee.h" #include "nsIThreadRetargetableRequest.h" #include "nsIURI.h" #include "nsIXPConnect.h" #include "jsapi.h" #include "jsfriendapi.h" #include "js/CompilationAndEvaluation.h" #include "js/Exception.h" #include "js/SourceText.h" #include "nsError.h" #include "nsComponentManagerUtils.h" #include "nsContentPolicyUtils.h" #include "nsContentUtils.h" #include "nsDocShellCID.h" #include "nsJSEnvironment.h" #include "nsNetUtil.h" #include "nsIPipe.h" #include "nsIOutputStream.h" #include "nsPrintfCString.h" #include "nsString.h" #include "nsTArray.h" #include "nsThreadUtils.h" #include "nsXPCOM.h" #include "xpcpublic.h" #include "mozilla/AntiTrackingUtils.h" #include "mozilla/ArrayAlgorithm.h" #include "mozilla/Assertions.h" #include "mozilla/Encoding.h" #include "mozilla/LoadContext.h" #include "mozilla/Maybe.h" #include "mozilla/ipc/BackgroundUtils.h" #include "mozilla/dom/ClientChannelHelper.h" #include "mozilla/dom/ClientInfo.h" #include "mozilla/dom/Exceptions.h" #include "mozilla/dom/nsCSPService.h" #include "mozilla/dom/nsCSPUtils.h" #include "mozilla/dom/PerformanceStorage.h" #include "mozilla/dom/Response.h" #include "mozilla/dom/ScriptSettings.h" #include "mozilla/dom/SerializedStackHolder.h" #include "mozilla/dom/workerinternals/CacheLoadHandler.h" #include "mozilla/dom/workerinternals/NetworkLoadHandler.h" #include "mozilla/dom/workerinternals/ScriptResponseHeaderProcessor.h" #include "mozilla/Result.h" #include "mozilla/ResultExtensions.h" #include "mozilla/StaticPrefs_browser.h" #include "mozilla/UniquePtr.h" #include "WorkerRunnable.h" #include "WorkerScope.h" #define MAX_CONCURRENT_SCRIPTS 1000 using JS::loader::ScriptKind; using JS::loader::ScriptLoadRequest; using mozilla::ipc::PrincipalInfo; namespace mozilla::dom::workerinternals { namespace { nsresult ConstructURI(const nsAString& aScriptURL, nsIURI* baseURI, const mozilla::Encoding* aDocumentEncoding, nsIURI** aResult) { nsresult rv; // Only top level workers' main script use the document charset for the // script uri encoding. Otherwise, default encoding (UTF-8) is applied. if (aDocumentEncoding) { nsAutoCString charset; aDocumentEncoding->Name(charset); rv = NS_NewURI(aResult, aScriptURL, charset.get(), baseURI); } else { rv = NS_NewURI(aResult, aScriptURL, nullptr, baseURI); } if (NS_FAILED(rv)) { return NS_ERROR_DOM_SYNTAX_ERR; } return NS_OK; } nsresult ChannelFromScriptURL( nsIPrincipal* principal, Document* parentDoc, WorkerPrivate* aWorkerPrivate, nsILoadGroup* loadGroup, nsIIOService* ios, nsIScriptSecurityManager* secMan, nsIURI* aScriptURL, const Maybe& aClientInfo, const Maybe& aController, bool aIsMainScript, WorkerScriptType aWorkerScriptType, nsContentPolicyType aMainScriptContentPolicyType, nsLoadFlags aLoadFlags, nsICookieJarSettings* aCookieJarSettings, nsIReferrerInfo* aReferrerInfo, nsIChannel** aChannel) { AssertIsOnMainThread(); nsresult rv; nsCOMPtr uri = aScriptURL; // 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; } uint32_t secFlags = aIsMainScript ? nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED : nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT; bool inheritAttrs = nsContentUtils::ChannelShouldInheritPrincipal( principal, uri, true /* aInheritForAboutBlank */, false /* aForceInherit */); bool isData = uri->SchemeIs("data"); if (inheritAttrs && !isData) { secFlags |= nsILoadInfo::SEC_FORCE_INHERIT_PRINCIPAL; } 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. if (aIsMainScript && isData) { secFlags = nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL; } nsContentPolicyType contentPolicyType = aIsMainScript ? aMainScriptContentPolicyType : nsIContentPolicy::TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS; // The main service worker script should never be loaded over the network // in this path. It should always be offlined by ServiceWorkerScriptCache. // We assert here since this error should also be caught by the runtime // check in CacheLoadHandler. // // Note, if we ever allow service worker scripts to be loaded from network // here we need to configure the channel properly. For example, it must // not allow redirects. MOZ_DIAGNOSTIC_ASSERT(contentPolicyType != nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER); nsCOMPtr channel; if (parentDoc) { rv = NS_NewChannel(getter_AddRefs(channel), uri, parentDoc, secFlags, contentPolicyType, nullptr, // aPerformanceStorage loadGroup, nullptr, // aCallbacks aLoadFlags, ios); NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SECURITY_ERR); } 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)); RefPtr performanceStorage; nsCOMPtr cspEventListener; if (aWorkerPrivate && !aIsMainScript) { performanceStorage = aWorkerPrivate->GetPerformanceStorage(); cspEventListener = aWorkerPrivate->CSPEventListener(); } if (aClientInfo.isSome()) { rv = NS_NewChannel(getter_AddRefs(channel), uri, principal, aClientInfo.ref(), aController, secFlags, contentPolicyType, aCookieJarSettings, performanceStorage, loadGroup, nullptr, // aCallbacks aLoadFlags, ios); } else { rv = NS_NewChannel(getter_AddRefs(channel), uri, principal, secFlags, contentPolicyType, aCookieJarSettings, performanceStorage, loadGroup, nullptr, // aCallbacks aLoadFlags, ios); } NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_SECURITY_ERR); if (cspEventListener) { nsCOMPtr loadInfo = channel->LoadInfo(); rv = loadInfo->SetCspEventListener(cspEventListener); NS_ENSURE_SUCCESS(rv, rv); } } if (aReferrerInfo) { nsCOMPtr httpChannel = do_QueryInterface(channel); if (httpChannel) { rv = httpChannel->SetReferrerInfo(aReferrerInfo); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } } channel.forget(aChannel); return rv; } void LoadAllScripts(WorkerPrivate* aWorkerPrivate, UniquePtr aOriginStack, const nsTArray& aScriptURLs, bool aIsMainScript, WorkerScriptType aWorkerScriptType, ErrorResult& aRv, const mozilla::Encoding* aDocumentEncoding = nullptr) { aWorkerPrivate->AssertIsOnWorkerThread(); NS_ASSERTION(!aScriptURLs.IsEmpty(), "Bad arguments!"); AutoSyncLoopHolder syncLoop(aWorkerPrivate, Canceling); nsCOMPtr syncLoopTarget = syncLoop.GetSerialEventTarget(); if (!syncLoopTarget) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return; } RefPtr loader = new loader::WorkerScriptLoader(aWorkerPrivate, std::move(aOriginStack), syncLoopTarget, aWorkerScriptType, aRv); if (NS_WARN_IF(aRv.Failed())) { return; } loader->CreateScriptRequests(aScriptURLs, aDocumentEncoding, aIsMainScript); if (loader->DispatchLoadScripts()) { syncLoop.Run(); } } class ChannelGetterRunnable final : public WorkerMainThreadRunnable { const nsAString& mScriptURL; const ClientInfo mClientInfo; WorkerLoadInfo& mLoadInfo; nsresult mResult; public: ChannelGetterRunnable(WorkerPrivate* aParentWorker, const nsAString& aScriptURL, WorkerLoadInfo& aLoadInfo) : WorkerMainThreadRunnable(aParentWorker, "ScriptLoader :: ChannelGetter"_ns), mScriptURL(aScriptURL) // ClientInfo should always be present since this should not be called // if parent's status is greater than Running. , mClientInfo(aParentWorker->GlobalScope()->GetClientInfo().ref()), mLoadInfo(aLoadInfo), mResult(NS_ERROR_FAILURE) { MOZ_ASSERT(aParentWorker); aParentWorker->AssertIsOnWorkerThread(); } virtual bool MainThreadRun() override { AssertIsOnMainThread(); // Initialize the WorkerLoadInfo principal to our triggering principal // before doing anything else. Normally we do this in the WorkerPrivate // Constructor, but we can't do so off the main thread when creating // a nested worker. So do it here instead. mLoadInfo.mLoadingPrincipal = mWorkerPrivate->GetPrincipal(); MOZ_DIAGNOSTIC_ASSERT(mLoadInfo.mLoadingPrincipal); mLoadInfo.mPrincipal = mLoadInfo.mLoadingPrincipal; // Figure out our base URI. nsCOMPtr baseURI = mWorkerPrivate->GetBaseURI(); MOZ_ASSERT(baseURI); // May be null. nsCOMPtr parentDoc = mWorkerPrivate->GetDocument(); mLoadInfo.mLoadGroup = mWorkerPrivate->GetLoadGroup(); mLoadInfo.mCookieJarSettings = mWorkerPrivate->CookieJarSettings(); // Nested workers use default uri encoding. nsCOMPtr url; mResult = ConstructURI(mScriptURL, baseURI, nullptr, getter_AddRefs(url)); NS_ENSURE_SUCCESS(mResult, true); Maybe clientInfo; clientInfo.emplace(mClientInfo); nsCOMPtr channel; nsCOMPtr referrerInfo = ReferrerInfo::CreateForFetch(mLoadInfo.mLoadingPrincipal, nullptr); mLoadInfo.mReferrerInfo = static_cast(referrerInfo.get()) ->CloneWithNewPolicy(mWorkerPrivate->GetReferrerPolicy()); mResult = workerinternals::ChannelFromScriptURLMainThread( mLoadInfo.mLoadingPrincipal, parentDoc, mLoadInfo.mLoadGroup, url, clientInfo, // Nested workers are always dedicated. nsIContentPolicy::TYPE_INTERNAL_WORKER, mLoadInfo.mCookieJarSettings, mLoadInfo.mReferrerInfo, getter_AddRefs(channel)); NS_ENSURE_SUCCESS(mResult, true); mResult = mLoadInfo.SetPrincipalsAndCSPFromChannel(channel); NS_ENSURE_SUCCESS(mResult, true); mLoadInfo.mChannel = std::move(channel); return true; } nsresult GetResult() const { return mResult; } private: virtual ~ChannelGetterRunnable() = default; }; } // anonymous namespace namespace loader { class ScriptExecutorRunnable final : public MainThreadWorkerSyncRunnable { RefPtr mScriptLoader; const Span> mLoadedRequests; public: ScriptExecutorRunnable(WorkerScriptLoader* aScriptLoader, WorkerPrivate* aWorkerPrivate, nsISerialEventTarget* aSyncLoopTarget, Span> aLoadedRequests); private: ~ScriptExecutorRunnable() = default; virtual bool IsDebuggerRunnable() const override; virtual bool PreRun(WorkerPrivate* aWorkerPrivate) override; virtual bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override; nsresult Cancel() override; }; template static bool EvaluateSourceBuffer(JSContext* aCx, const JS::CompileOptions& aOptions, JS::SourceText& aSourceBuffer) { static_assert(std::is_same::value || std::is_same::value, "inferred units must be UTF-8 or UTF-16"); JS::Rooted unused(aCx); return Evaluate(aCx, aOptions, aSourceBuffer, &unused); } WorkerScriptLoader::WorkerScriptLoader( WorkerPrivate* aWorkerPrivate, UniquePtr aOriginStack, nsISerialEventTarget* aSyncLoopTarget, WorkerScriptType aWorkerScriptType, ErrorResult& aRv) : mOriginStack(std::move(aOriginStack)), mSyncLoopTarget(aSyncLoopTarget), mWorkerScriptType(aWorkerScriptType), mRv(aRv), mCleanedUp(false), mCleanUpLock("cleanUpLock") { aWorkerPrivate->AssertIsOnWorkerThread(); MOZ_ASSERT(aSyncLoopTarget); RefPtr self = this; RefPtr workerRef = StrongWorkerRef::Create(aWorkerPrivate, "ScriptLoader", [self]() {}); if (workerRef) { mWorkerRef = new ThreadSafeWorkerRef(workerRef); } else { mRv.Throw(NS_ERROR_FAILURE); return; } nsIGlobalObject* global = GetGlobal(); mController = global->GetController(); } void WorkerScriptLoader::CreateScriptRequests( const nsTArray& aScriptURLs, const mozilla::Encoding* aDocumentEncoding, bool aIsMainScript) { for (const nsString& scriptURL : aScriptURLs) { RefPtr request = CreateScriptLoadRequest(scriptURL, aDocumentEncoding, aIsMainScript); mLoadingRequests.AppendElement(request); } } nsTArray> WorkerScriptLoader::GetLoadingList() { mWorkerRef->Private()->AssertIsOnWorkerThread(); nsTArray> list; for (ScriptLoadRequest* req = mLoadingRequests.getFirst(); req; req = req->getNext()) { RefPtr handle = new ThreadSafeRequestHandle(req, mSyncLoopTarget.get()); list.AppendElement(handle.forget()); } return list; } already_AddRefed WorkerScriptLoader::CreateScriptLoadRequest( const nsString& aScriptURL, const mozilla::Encoding* aDocumentEncoding, bool aIsMainScript) { WorkerLoadContext::Kind kind = WorkerLoadContext::GetKind(aIsMainScript, IsDebuggerScript()); Maybe clientInfo = GetGlobal()->GetClientInfo(); RefPtr loadContext = new WorkerLoadContext(kind, clientInfo); // Create ScriptLoadRequests for this WorkerScriptLoader ReferrerPolicy referrerPolicy = mWorkerRef->Private()->GetReferrerPolicy(); // Only top level workers' main script use the document charset for the // script uri encoding. Otherwise, default encoding (UTF-8) is applied. MOZ_ASSERT_IF(bool(aDocumentEncoding), aIsMainScript && !mWorkerRef->Private()->GetParent()); nsCOMPtr baseURI = aIsMainScript ? GetInitialBaseURI() : GetBaseURI(); nsCOMPtr uri; nsresult rv = ConstructURI(aScriptURL, baseURI, aDocumentEncoding, getter_AddRefs(uri)); // If we failed to construct the URI, handle it in the LoadContext so it is // thrown in the right order. if (NS_WARN_IF(NS_FAILED(rv))) { loadContext->mLoadResult = rv; } RefPtr fetchOptions = new ScriptFetchOptions(CORSMode::CORS_NONE, referrerPolicy, nullptr); RefPtr request = new ScriptLoadRequest(ScriptKind::eClassic, uri, fetchOptions, SRIMetadata(), nullptr, /* = aReferrer */ loadContext); // Set the mURL, it will be used for error handling and debugging. request->mURL = NS_ConvertUTF16toUTF8(aScriptURL); return request.forget(); } bool WorkerScriptLoader::DispatchLoadScript(ScriptLoadRequest* aRequest) { mWorkerRef->Private()->AssertIsOnWorkerThread(); nsTArray> scriptLoadList; RefPtr handle = new ThreadSafeRequestHandle(aRequest, mSyncLoopTarget.get()); scriptLoadList.AppendElement(handle.forget()); RefPtr runnable = new ScriptLoaderRunnable(this, std::move(scriptLoadList)); RefPtr workerRef = StrongWorkerRef::Create( mWorkerRef->Private(), "ScriptLoader", [runnable]() { NS_DispatchToMainThread(NewRunnableMethod( "ScriptLoaderRunnable::CancelMainThreadWithBindingAborted", runnable, &ScriptLoaderRunnable::CancelMainThreadWithBindingAborted)); }); if (NS_FAILED(NS_DispatchToMainThread(runnable))) { NS_ERROR("Failed to dispatch!"); mRv.Throw(NS_ERROR_FAILURE); return false; } return true; } bool WorkerScriptLoader::DispatchLoadScripts() { mWorkerRef->Private()->AssertIsOnWorkerThread(); nsTArray> scriptLoadList = GetLoadingList(); RefPtr runnable = new ScriptLoaderRunnable(this, std::move(scriptLoadList)); RefPtr workerRef = StrongWorkerRef::Create( mWorkerRef->Private(), "ScriptLoader", [runnable]() { NS_DispatchToMainThread(NewRunnableMethod( "ScriptLoaderRunnable::CancelMainThreadWithBindingAborted", runnable, &ScriptLoaderRunnable::CancelMainThreadWithBindingAborted)); }); if (NS_FAILED(NS_DispatchToMainThread(runnable))) { NS_ERROR("Failed to dispatch!"); mRv.Throw(NS_ERROR_FAILURE); return false; } return true; } nsIURI* WorkerScriptLoader::GetInitialBaseURI() { MOZ_ASSERT(mWorkerRef->Private()); nsIURI* baseURI; WorkerPrivate* parentWorker = mWorkerRef->Private()->GetParent(); if (parentWorker) { baseURI = parentWorker->GetBaseURI(); } else { // May be null. baseURI = mWorkerRef->Private()->GetBaseURI(); } return baseURI; } nsIURI* WorkerScriptLoader::GetBaseURI() const { MOZ_ASSERT(mWorkerRef); nsIURI* baseURI; baseURI = mWorkerRef->Private()->GetBaseURI(); NS_ASSERTION(baseURI, "Should have been set already!"); return baseURI; } nsIGlobalObject* WorkerScriptLoader::GetGlobal() { mWorkerRef->Private()->AssertIsOnWorkerThread(); return mWorkerScriptType == WorkerScript ? static_cast( mWorkerRef->Private()->GlobalScope()) : mWorkerRef->Private()->DebuggerGlobalScope(); } void WorkerScriptLoader::MaybeMoveToLoadedList(ScriptLoadRequest* aRequest) { mWorkerRef->Private()->AssertIsOnWorkerThread(); aRequest->SetReady(); // If the request is not in a list, we are in an illegal state. MOZ_RELEASE_ASSERT(aRequest->isInList()); while (!mLoadingRequests.isEmpty()) { ScriptLoadRequest* request = mLoadingRequests.getFirst(); // We need to move requests in post order. If prior requests have not // completed, delay execution. if (!request->IsReadyToRun()) { break; } RefPtr req = mLoadingRequests.Steal(request); mLoadedRequests.AppendElement(req); } } bool WorkerScriptLoader::StoreCSP() { // We must be on the same worker as we started on. mWorkerRef->Private()->AssertIsOnWorkerThread(); if (!mWorkerRef->Private()->GetJSContext()) { return false; } MOZ_ASSERT(!mRv.Failed()); // Move the CSP from the workerLoadInfo in the corresponding Client // where the CSP code expects it! mWorkerRef->Private()->StoreCSPOnClient(); return true; } bool WorkerScriptLoader::ProcessPendingRequests(JSContext* aCx) { mWorkerRef->Private()->AssertIsOnWorkerThread(); // Don't run if something else has already failed. if (mExecutionAborted) { mLoadedRequests.CancelRequestsAndClear(); TryShutdown(); return true; } // If nothing else has failed, our ErrorResult better not be a failure // either. MOZ_ASSERT(!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 global(aCx, JS::CurrentGlobalOrNull(aCx)); MOZ_ASSERT(global); while (!mLoadedRequests.isEmpty()) { RefPtr req = mLoadedRequests.StealFirst(); // We don't have a ProcessRequest method (like we do on the DOM), as there // isn't much processing that we need to do per request that isn't related // to evaluation (the processsing done for the DOM is handled in // DataRecievedFrom{Cache,Network} for workers. // So, this inner loop calls EvaluateScript directly. This will change // once modules are introduced as we will have some extra work to do. if (!EvaluateScript(aCx, req)) { mExecutionAborted = true; WorkerLoadContext* loadContext = req->GetWorkerLoadContext(); mMutedErrorFlag = loadContext->mMutedErrorFlag.valueOr(true); mLoadedRequests.CancelRequestsAndClear(); break; } } TryShutdown(); return true; } nsresult WorkerScriptLoader::LoadScript( ThreadSafeRequestHandle* aRequestHandle) { AssertIsOnMainThread(); WorkerLoadContext* loadContext = aRequestHandle->GetContext(); MOZ_ASSERT_IF(loadContext->IsTopLevel(), !IsDebuggerScript()); // The URL passed to us for loading was invalid, stop loading at this point. if (loadContext->mLoadResult != NS_ERROR_NOT_INITIALIZED) { return loadContext->mLoadResult; } WorkerPrivate* parentWorker = mWorkerRef->Private()->GetParent(); // For JavaScript debugging, the devtools server must run on the same // thread as the debuggee, indicating the worker uses content principal. // However, in Bug 863246, web content will no longer be able to load // resource:// URIs by default, so we need system principal to load // debugger scripts. nsIPrincipal* principal = (IsDebuggerScript()) ? nsContentUtils::GetSystemPrincipal() : mWorkerRef->Private()->GetPrincipal(); nsCOMPtr loadGroup = mWorkerRef->Private()->GetLoadGroup(); MOZ_DIAGNOSTIC_ASSERT(principal); NS_ENSURE_TRUE(NS_LoadGroupMatchesPrincipal(loadGroup, principal), NS_ERROR_FAILURE); // May be null. nsCOMPtr parentDoc = mWorkerRef->Private()->GetDocument(); nsCOMPtr channel; if (loadContext->IsTopLevel()) { // May be null. channel = mWorkerRef->Private()->ForgetWorkerChannel(); } nsCOMPtr ios(do_GetIOService()); nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager(); NS_ASSERTION(secMan, "This should never be null!"); nsresult& rv = loadContext->mLoadResult; nsLoadFlags loadFlags = mWorkerRef->Private()->GetLoadFlags(); // Get the top-level worker. WorkerPrivate* topWorkerPrivate = mWorkerRef->Private(); 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 window = topWorkerPrivate->GetWindow(); if (window) { nsCOMPtr docShell = window->GetDocShell(); if (docShell) { nsresult rv = docShell->GetDefaultLoadFlags(&loadFlags); NS_ENSURE_SUCCESS(rv, rv); } } } if (!channel) { nsCOMPtr referrerInfo = ReferrerInfo::CreateForFetch(principal, nullptr); if (parentWorker && !loadContext->IsTopLevel()) { referrerInfo = static_cast(referrerInfo.get()) ->CloneWithNewPolicy(parentWorker->GetReferrerPolicy()); } rv = ChannelFromScriptURL( principal, parentDoc, mWorkerRef->Private(), loadGroup, ios, secMan, aRequestHandle->GetRequest()->mURI, loadContext->mClientInfo, mController, loadContext->IsTopLevel(), mWorkerScriptType, mWorkerRef->Private()->ContentPolicyType(), loadFlags, mWorkerRef->Private()->CookieJarSettings(), referrerInfo, getter_AddRefs(channel)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } // Associate any originating stack with the channel. if (!mOriginStackJSON.IsEmpty()) { NotifyNetworkMonitorAlternateStack(channel, mOriginStackJSON); } // We need to know which index we're on in OnStreamComplete so we know // where to put the result. RefPtr listener = new NetworkLoadHandler(this, aRequestHandle); RefPtr headerProcessor = nullptr; // For each debugger script, a non-debugger script load of the same script // should have occured prior that processed the headers. if (!IsDebuggerScript()) { headerProcessor = MakeRefPtr( mWorkerRef->Private(), loadContext->IsTopLevel()); } nsCOMPtr loader; rv = NS_NewStreamLoader(getter_AddRefs(loader), listener, headerProcessor); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (loadContext->IsTopLevel()) { MOZ_DIAGNOSTIC_ASSERT(loadContext->mClientInfo.isSome()); // In order to get the correct foreign partitioned prinicpal, we need to // set the `IsThirdPartyContextToTopWindow` to the channel's loadInfo. // This flag reflects the fact that if the worker is created under a // third-party context. nsCOMPtr loadInfo = channel->LoadInfo(); loadInfo->SetIsThirdPartyContextToTopWindow( mWorkerRef->Private()->IsThirdPartyContextToTopWindow()); Maybe clientInfo; clientInfo.emplace(loadContext->mClientInfo.ref()); rv = AddClientChannelHelper(channel, std::move(clientInfo), Maybe(), mWorkerRef->Private()->HybridEventTarget()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } if (StaticPrefs::browser_tabs_remote_useCrossOriginEmbedderPolicy()) { nsILoadInfo::CrossOriginEmbedderPolicy respectedCOEP = mWorkerRef->Private()->GetEmbedderPolicy(); if (mWorkerRef->Private()->IsDedicatedWorker() && respectedCOEP == nsILoadInfo::EMBEDDER_POLICY_NULL) { respectedCOEP = mWorkerRef->Private()->GetOwnerEmbedderPolicy(); } nsCOMPtr channelLoadInfo = channel->LoadInfo(); channelLoadInfo->SetLoadingEmbedderPolicy(respectedCOEP); } if (loadContext->mCacheStatus != WorkerLoadContext::ToBeCached) { rv = channel->AsyncOpen(loader); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } else { nsCOMPtr writer; // In case we return early. loadContext->mCacheStatus = WorkerLoadContext::Cancel; NS_NewPipe(getter_AddRefs(loadContext->mCacheReadStream), getter_AddRefs(writer), 0, UINT32_MAX, // unlimited size to avoid writer WOULD_BLOCK case true, false); // non-blocking reader, blocking writer nsCOMPtr tee = do_CreateInstance(NS_STREAMLISTENERTEE_CONTRACTID); rv = tee->Init(loader, writer, listener); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsresult rv = channel->AsyncOpen(tee); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } loadContext->mChannel.swap(channel); return NS_OK; } nsresult WorkerScriptLoader::FillCompileOptionsForRequest( JSContext* cx, ScriptLoadRequest* aRequest, JS::CompileOptions* aOptions, JS::MutableHandle aIntroductionScript) { // The full URL shouldn't be exposed to the debugger. See Bug 1634872 aOptions->setFileAndLine(aRequest->mURL.get(), 1); aOptions->setNoScriptRval(true); aOptions->setMutedErrors( aRequest->GetWorkerLoadContext()->mMutedErrorFlag.value()); if (aRequest->mSourceMapURL) { aOptions->setSourceMapURL(aRequest->mSourceMapURL->get()); } return NS_OK; } bool WorkerScriptLoader::EvaluateScript(JSContext* aCx, ScriptLoadRequest* aRequest) { mWorkerRef->Private()->AssertIsOnWorkerThread(); WorkerLoadContext* loadContext = aRequest->GetWorkerLoadContext(); NS_ASSERTION(!loadContext->mChannel, "Should no longer have a channel!"); NS_ASSERTION(aRequest->IsReadyToRun(), "Should be scheduled!"); MOZ_ASSERT(!mRv.Failed(), "Who failed it and why?"); mRv.MightThrowJSException(); if (NS_FAILED(loadContext->mLoadResult)) { ReportErrorToConsole(aRequest, loadContext->mLoadResult); return false; } // If this is a top level script that succeeded, then mark the // Client execution ready and possible controlled by a service worker. if (loadContext->IsTopLevel()) { if (mController.isSome()) { MOZ_ASSERT(mWorkerScriptType == WorkerScript, "Debugger clients can't be controlled."); mWorkerRef->Private()->GlobalScope()->Control(mController.ref()); } mWorkerRef->Private()->ExecutionReady(); } JS::CompileOptions options(aCx); // The introduction script is used by the DOM script loader as a way // to fill the Debugger Metadata for the JS Execution context. We don't use // the JS Execution context as we are not making use of async compilation // (delegation to another worker to produce bytecode or compile a string to a // JSScript), so it is not used in this context. JS::Rooted unusedIntroductionScript(aCx); nsresult rv = FillCompileOptionsForRequest(aCx, aRequest, &options, &unusedIntroductionScript); MOZ_ASSERT(NS_SUCCEEDED(rv), "Filling compile options should not fail"); // Our ErrorResult still shouldn't be a failure. MOZ_ASSERT(!mRv.Failed(), "Who failed it and why?"); // Get the source text. ScriptLoadRequest::MaybeSourceText maybeSource; rv = aRequest->GetScriptSource(aCx, &maybeSource); if (NS_FAILED(rv)) { mRv.StealExceptionFromJSContext(aCx); return false; } bool successfullyEvaluated = aRequest->IsUTF8Text() ? EvaluateSourceBuffer(aCx, options, maybeSource.ref>()) : EvaluateSourceBuffer(aCx, options, maybeSource.ref>()); if (!successfullyEvaluated) { mRv.StealExceptionFromJSContext(aCx); return false; } // steal the loadContext so that the cycle is broken and cycle collector can // collect the scriptLoadRequest. return true; } void WorkerScriptLoader::TryShutdown() { if (AllScriptsExecuted()) { ShutdownScriptLoader(!mExecutionAborted, mMutedErrorFlag); } } void WorkerScriptLoader::ShutdownScriptLoader(bool aResult, bool aMutedError) { MOZ_ASSERT(AllScriptsExecuted()); mWorkerRef->Private()->AssertIsOnWorkerThread(); if (!aResult) { // At this point there are two possibilities: // // 1) 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) 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 (mRv.Failed()) { if (aMutedError && mRv.IsJSException()) { LogExceptionToConsole(mWorkerRef->Private()->GetJSContext(), mWorkerRef->Private()); mRv.Throw(NS_ERROR_DOM_NETWORK_ERR); } } else { mRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); } } // Lock, shutdown, and cleanup state. After this the Loader is closed. { MutexAutoLock lock(CleanUpLock()); if (CleanedUp()) { return; } mWorkerRef->Private()->AssertIsOnWorkerThread(); mWorkerRef->Private()->StopSyncLoop(mSyncLoopTarget, aResult ? NS_OK : NS_ERROR_FAILURE); // Signal cleanup mCleanedUp = true; // Allow worker shutdown. mWorkerRef = nullptr; } } void WorkerScriptLoader::ReportErrorToConsole(ScriptLoadRequest* aRequest, nsresult aResult) const { nsAutoString url = NS_ConvertUTF8toUTF16(aRequest->mURL); workerinternals::ReportLoadError(mRv, aResult, url); } void WorkerScriptLoader::LogExceptionToConsole(JSContext* aCx, WorkerPrivate* aWorkerPrivate) { aWorkerPrivate->AssertIsOnWorkerThread(); MOZ_ASSERT(mRv.IsJSException()); JS::Rooted exn(aCx); if (!ToJSValue(aCx, std::move(mRv), &exn)) { return; } // Now the exception state should all be in exn. MOZ_ASSERT(!JS_IsExceptionPending(aCx)); MOZ_ASSERT(!mRv.Failed()); JS::ExceptionStack exnStack(aCx, exn, nullptr); JS::ErrorReportBuilder report(aCx); if (!report.init(aCx, exnStack, JS::ErrorReportBuilder::WithSideEffects)) { JS_ClearPendingException(aCx); return; } RefPtr xpcReport = new xpc::ErrorReport(); xpcReport->Init(report.report(), report.toStringResult().c_str(), aWorkerPrivate->IsChromeWorker(), aWorkerPrivate->WindowID()); RefPtr r = new AsyncErrorReporter(xpcReport); NS_DispatchToMainThread(r); } NS_IMPL_ISUPPORTS(ScriptLoaderRunnable, nsIRunnable, nsINamed) NS_IMPL_ISUPPORTS(WorkerScriptLoader, nsINamed) ScriptLoaderRunnable::ScriptLoaderRunnable( WorkerScriptLoader* aScriptLoader, nsTArray> aLoadingRequests) : mScriptLoader(aScriptLoader), mWorkerRef(aScriptLoader->mWorkerRef), mLoadingRequests(std::move(aLoadingRequests)), mCancelMainThread(Nothing()) { MOZ_ASSERT(aScriptLoader); } nsresult ScriptLoaderRunnable::Run() { AssertIsOnMainThread(); // Convert the origin stack to JSON (which must be done on the main // thread) explicitly, so that we can use the stack to notify the net // monitor about every script we load. We do this, rather than pass // the stack directly to the netmonitor, in order to be able to use this // for all subsequent scripts. if (mScriptLoader->mOriginStack && mScriptLoader->mOriginStackJSON.IsEmpty()) { ConvertSerializedStackToJSON(std::move(mScriptLoader->mOriginStack), mScriptLoader->mOriginStackJSON); } if (!mWorkerRef->Private()->IsServiceWorker() || mScriptLoader->IsDebuggerScript()) { for (ThreadSafeRequestHandle* handle : mLoadingRequests) { handle->mRunnable = this; nsresult rv = mScriptLoader->LoadScript(handle); if (NS_WARN_IF(NS_FAILED(rv))) { LoadingFinished(handle, rv); CancelMainThread(rv); return rv; } } return NS_OK; } MOZ_ASSERT(!mCacheCreator); mCacheCreator = new CacheCreator(mWorkerRef->Private()); for (ThreadSafeRequestHandle* handle : mLoadingRequests) { handle->mRunnable = this; WorkerLoadContext* loadContext = handle->GetContext(); mCacheCreator->AddLoader(MakeNotNull>( mWorkerRef, handle, loadContext->IsTopLevel(), mScriptLoader)); } // The worker may have a null principal on first load, but in that case its // parent definitely will have one. nsIPrincipal* principal = mWorkerRef->Private()->GetPrincipal(); if (!principal) { WorkerPrivate* parentWorker = mWorkerRef->Private()->GetParent(); MOZ_ASSERT(parentWorker, "Must have a parent!"); principal = parentWorker->GetPrincipal(); } nsresult rv = mCacheCreator->Load(principal); if (NS_WARN_IF(NS_FAILED(rv))) { CancelMainThread(rv); return rv; } return NS_OK; } nsresult ScriptLoaderRunnable::OnStreamComplete( ThreadSafeRequestHandle* aRequestHandle, nsresult aStatus) { AssertIsOnMainThread(); LoadingFinished(aRequestHandle, aStatus); return NS_OK; } void ScriptLoaderRunnable::LoadingFinished( ThreadSafeRequestHandle* aRequestHandle, nsresult aRv) { AssertIsOnMainThread(); WorkerLoadContext* loadContext = aRequestHandle->GetContext(); loadContext->mLoadResult = aRv; MOZ_ASSERT(!loadContext->mLoadingFinished); loadContext->mLoadingFinished = true; if (loadContext->IsTopLevel() && NS_SUCCEEDED(aRv)) { MOZ_DIAGNOSTIC_ASSERT( mWorkerRef->Private()->PrincipalURIMatchesScriptURL()); } MaybeExecuteFinishedScripts(aRequestHandle); } void ScriptLoaderRunnable::MaybeExecuteFinishedScripts( ThreadSafeRequestHandle* aRequestHandle) { AssertIsOnMainThread(); // We execute the last step if we don't have a pending operation with the // cache and the loading is completed. WorkerLoadContext* loadContext = aRequestHandle->GetContext(); if (!loadContext->IsAwaitingPromise()) { if (aRequestHandle->GetContext()->IsTopLevel()) { mWorkerRef->Private()->WorkerScriptLoaded(); } DispatchProcessPendingRequests(); } } void ScriptLoaderRunnable::CancelMainThreadWithBindingAborted() { AssertIsOnMainThread(); CancelMainThread(NS_BINDING_ABORTED); } void ScriptLoaderRunnable::CancelMainThread(nsresult aCancelResult) { AssertIsOnMainThread(); if (IsCancelled()) { return; } { MutexAutoLock lock(mScriptLoader->CleanUpLock()); // Check if we have already cancelled, or if the worker has been killed // before we cancel. if (mScriptLoader->CleanedUp()) { return; } mCancelMainThread = Some(aCancelResult); for (ThreadSafeRequestHandle* handle : mLoadingRequests) { if (handle->IsEmpty()) { continue; } bool callLoadingFinished = true; WorkerLoadContext* loadContext = handle->GetContext(); if (!loadContext) { continue; } if (loadContext->IsAwaitingPromise()) { MOZ_ASSERT(mWorkerRef->Private()->IsServiceWorker()); loadContext->mCachePromise->MaybeReject(NS_BINDING_ABORTED); loadContext->mCachePromise = nullptr; callLoadingFinished = false; } if (loadContext->mChannel) { if (NS_SUCCEEDED(loadContext->mChannel->Cancel(aCancelResult))) { callLoadingFinished = false; } else { NS_WARNING("Failed to cancel channel!"); } } if (callLoadingFinished && !loadContext->mLoadingFinished) { LoadingFinished(handle, aCancelResult); } } DispatchProcessPendingRequests(); } } void ScriptLoaderRunnable::DispatchProcessPendingRequests() { AssertIsOnMainThread(); const auto begin = mLoadingRequests.begin(); const auto end = mLoadingRequests.end(); using Iterator = decltype(begin); const auto maybeRangeToExecute = [begin, end]() -> Maybe> { // firstItToExecute is the first loadInfo where mExecutionScheduled is // unset. auto firstItToExecute = std::find_if( begin, end, [](const RefPtr& requestHandle) { return !requestHandle->mExecutionScheduled; }); if (firstItToExecute == end) { return Nothing(); } // firstItUnexecutable is the first loadInfo that is not yet finished. // Update mExecutionScheduled on the ones we're about to schedule for // execution. const auto firstItUnexecutable = std::find_if(firstItToExecute, end, [](RefPtr& requestHandle) { MOZ_ASSERT(!requestHandle->IsEmpty()); if (!requestHandle->Finished()) { return true; } // We can execute this one. requestHandle->mExecutionScheduled = true; return false; }); return firstItUnexecutable == firstItToExecute ? Nothing() : Some(std::pair(firstItToExecute, firstItUnexecutable)); }(); // If there are no unexecutable load infos, we can unuse things before the // execution of the scripts and the stopping of the sync loop. if (maybeRangeToExecute) { if (maybeRangeToExecute->second == end) { mCacheCreator = nullptr; } RefPtr runnable = new ScriptExecutorRunnable( mScriptLoader, mWorkerRef->Private(), mScriptLoader->mSyncLoopTarget, Span>{maybeRangeToExecute->first, maybeRangeToExecute->second}); if (!runnable->Dispatch()) { MOZ_ASSERT(false, "This should never fail!"); } } } ScriptExecutorRunnable::ScriptExecutorRunnable( WorkerScriptLoader* aScriptLoader, WorkerPrivate* aWorkerPrivate, nsISerialEventTarget* aSyncLoopTarget, Span> aLoadedRequests) : MainThreadWorkerSyncRunnable(aWorkerPrivate, aSyncLoopTarget), mScriptLoader(aScriptLoader), mLoadedRequests(aLoadedRequests) {} 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->IsDebuggerScript(); } bool ScriptExecutorRunnable::PreRun(WorkerPrivate* aWorkerPrivate) { aWorkerPrivate->AssertIsOnWorkerThread(); // We must be on the same worker as we started on. MOZ_ASSERT( mScriptLoader->mSyncLoopTarget == mSyncLoopTarget, "Unexpected SyncLoopTarget. Check if the sync loop was closed early"); if (!mLoadedRequests.begin()->get()->GetContext()->IsTopLevel()) { return true; } return mScriptLoader->StoreCSP(); } bool ScriptExecutorRunnable::WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) { aWorkerPrivate->AssertIsOnWorkerThread(); // There is a possibility that we cleaned up while this task was waiting to // run. If this has happened, return and exit. { MutexAutoLock lock(mScriptLoader->CleanUpLock()); if (mScriptLoader->CleanedUp()) { return true; } // We must be on the same worker as we started on. MOZ_ASSERT( mScriptLoader->mSyncLoopTarget == mSyncLoopTarget, "Unexpected SyncLoopTarget. Check if the sync loop was closed early"); for (const auto& requestHandle : mLoadedRequests) { // The request must be valid. MOZ_ASSERT(!requestHandle->IsEmpty()); // Release the request to the worker. From this point on, the Request // Handle is empty. RefPtr request = requestHandle->ReleaseRequest(); mScriptLoader->MaybeMoveToLoadedList(request); } } return mScriptLoader->ProcessPendingRequests(aCx); } nsresult ScriptExecutorRunnable::Cancel() { // We need to check first if cancel is called twice nsresult rv = MainThreadWorkerSyncRunnable::Cancel(); NS_ENSURE_SUCCESS(rv, rv); if (mScriptLoader->AllScriptsExecuted()) { mScriptLoader->ShutdownScriptLoader(false, false); } return NS_OK; } } /* namespace loader */ nsresult ChannelFromScriptURLMainThread( nsIPrincipal* aPrincipal, Document* aParentDoc, nsILoadGroup* aLoadGroup, nsIURI* aScriptURL, const Maybe& aClientInfo, nsContentPolicyType aMainScriptContentPolicyType, nsICookieJarSettings* aCookieJarSettings, nsIReferrerInfo* aReferrerInfo, nsIChannel** aChannel) { AssertIsOnMainThread(); nsCOMPtr ios(do_GetIOService()); nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager(); NS_ASSERTION(secMan, "This should never be null!"); return ChannelFromScriptURL( aPrincipal, aParentDoc, nullptr, aLoadGroup, ios, secMan, aScriptURL, aClientInfo, Maybe(), true, WorkerScript, aMainScriptContentPolicyType, nsIRequest::LOAD_NORMAL, aCookieJarSettings, aReferrerInfo, aChannel); } nsresult ChannelFromScriptURLWorkerThread(JSContext* aCx, WorkerPrivate* aParent, const nsAString& aScriptURL, WorkerLoadInfo& aLoadInfo) { aParent->AssertIsOnWorkerThread(); RefPtr getter = new ChannelGetterRunnable(aParent, aScriptURL, aLoadInfo); ErrorResult rv; getter->Dispatch(Canceling, 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()); nsPrintfCString err("Failed to load worker script at \"%s\"", NS_ConvertUTF16toUTF8(aScriptURL).get()); switch (aLoadResult) { case NS_ERROR_FILE_NOT_FOUND: case NS_ERROR_NOT_AVAILABLE: case NS_ERROR_CORRUPTED_CONTENT: aRv.Throw(NS_ERROR_DOM_NETWORK_ERR); break; case NS_ERROR_MALFORMED_URI: case NS_ERROR_DOM_SYNTAX_ERR: aRv.ThrowSyntaxError(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 use one of the // Throw*Error() methods on ErrorResult 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_BAD_URI: // This is actually a security error. case NS_ERROR_DOM_SECURITY_ERR: aRv.ThrowSecurityError(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.ThrowNetworkError(nsPrintfCString( "Failed to load worker script at %s (nsresult = 0x%" PRIx32 ")", NS_ConvertUTF16toUTF8(aScriptURL).get(), static_cast(aLoadResult))); return; } } void LoadMainScript(WorkerPrivate* aWorkerPrivate, UniquePtr aOriginStack, const nsAString& aScriptURL, WorkerScriptType aWorkerScriptType, ErrorResult& aRv, const mozilla::Encoding* aDocumentEncoding) { nsTArray scriptURLs; scriptURLs.AppendElement(aScriptURL); LoadAllScripts(aWorkerPrivate, std::move(aOriginStack), scriptURLs, true, aWorkerScriptType, aRv, aDocumentEncoding); } void Load(WorkerPrivate* aWorkerPrivate, UniquePtr aOriginStack, const nsTArray& 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; } LoadAllScripts(aWorkerPrivate, std::move(aOriginStack), aScriptURLs, false, aWorkerScriptType, aRv); } } // namespace mozilla::dom::workerinternals