1414 lines
48 KiB
C++
1414 lines
48 KiB
C++
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "ScriptLoader.h"
|
|
|
|
#include <algorithm>
|
|
#include <type_traits>
|
|
|
|
#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 "Principal.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<ClientInfo>& aClientInfo,
|
|
const Maybe<ServiceWorkerDescriptor>& aController, bool aIsMainScript,
|
|
WorkerScriptType aWorkerScriptType,
|
|
nsContentPolicyType aMainScriptContentPolicyType, nsLoadFlags aLoadFlags,
|
|
nsICookieJarSettings* aCookieJarSettings, nsIReferrerInfo* aReferrerInfo,
|
|
nsIChannel** aChannel) {
|
|
AssertIsOnMainThread();
|
|
|
|
nsresult rv;
|
|
nsCOMPtr<nsIURI> 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<nsIChannel> 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> performanceStorage;
|
|
nsCOMPtr<nsICSPEventListener> 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<nsILoadInfo> loadInfo = channel->LoadInfo();
|
|
rv = loadInfo->SetCspEventListener(cspEventListener);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
}
|
|
}
|
|
|
|
if (aReferrerInfo) {
|
|
nsCOMPtr<nsIHttpChannel> 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<SerializedStackHolder> aOriginStack,
|
|
const nsTArray<nsString>& 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<nsISerialEventTarget> syncLoopTarget =
|
|
syncLoop.GetSerialEventTarget();
|
|
if (!syncLoopTarget) {
|
|
aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
|
|
return;
|
|
}
|
|
|
|
RefPtr<loader::WorkerScriptLoader> 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<nsIURI> baseURI = mWorkerPrivate->GetBaseURI();
|
|
MOZ_ASSERT(baseURI);
|
|
|
|
// May be null.
|
|
nsCOMPtr<Document> parentDoc = mWorkerPrivate->GetDocument();
|
|
|
|
mLoadInfo.mLoadGroup = mWorkerPrivate->GetLoadGroup();
|
|
mLoadInfo.mCookieJarSettings = mWorkerPrivate->CookieJarSettings();
|
|
|
|
// Nested workers use default uri encoding.
|
|
nsCOMPtr<nsIURI> url;
|
|
mResult = ConstructURI(mScriptURL, baseURI, nullptr, getter_AddRefs(url));
|
|
NS_ENSURE_SUCCESS(mResult, true);
|
|
|
|
Maybe<ClientInfo> clientInfo;
|
|
clientInfo.emplace(mClientInfo);
|
|
|
|
nsCOMPtr<nsIChannel> channel;
|
|
nsCOMPtr<nsIReferrerInfo> referrerInfo =
|
|
ReferrerInfo::CreateForFetch(mLoadInfo.mLoadingPrincipal, nullptr);
|
|
mLoadInfo.mReferrerInfo =
|
|
static_cast<ReferrerInfo*>(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<WorkerScriptLoader> mScriptLoader;
|
|
const Span<RefPtr<ThreadSafeRequestHandle>> mLoadedRequests;
|
|
|
|
public:
|
|
ScriptExecutorRunnable(WorkerScriptLoader* aScriptLoader,
|
|
WorkerPrivate* aWorkerPrivate,
|
|
nsISerialEventTarget* aSyncLoopTarget,
|
|
Span<RefPtr<ThreadSafeRequestHandle>> 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 <typename Unit>
|
|
static bool EvaluateSourceBuffer(JSContext* aCx,
|
|
const JS::CompileOptions& aOptions,
|
|
JS::SourceText<Unit>& aSourceBuffer) {
|
|
static_assert(std::is_same<Unit, char16_t>::value ||
|
|
std::is_same<Unit, Utf8Unit>::value,
|
|
"inferred units must be UTF-8 or UTF-16");
|
|
|
|
JS::Rooted<JS::Value> unused(aCx);
|
|
return Evaluate(aCx, aOptions, aSourceBuffer, &unused);
|
|
}
|
|
|
|
WorkerScriptLoader::WorkerScriptLoader(
|
|
WorkerPrivate* aWorkerPrivate,
|
|
UniquePtr<SerializedStackHolder> 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<WorkerScriptLoader> self = this;
|
|
|
|
RefPtr<StrongWorkerRef> 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<nsString>& aScriptURLs,
|
|
const mozilla::Encoding* aDocumentEncoding, bool aIsMainScript) {
|
|
for (const nsString& scriptURL : aScriptURLs) {
|
|
RefPtr<ScriptLoadRequest> request =
|
|
CreateScriptLoadRequest(scriptURL, aDocumentEncoding, aIsMainScript);
|
|
mLoadingRequests.AppendElement(request);
|
|
}
|
|
}
|
|
|
|
nsTArray<RefPtr<ThreadSafeRequestHandle>> WorkerScriptLoader::GetLoadingList() {
|
|
mWorkerRef->Private()->AssertIsOnWorkerThread();
|
|
nsTArray<RefPtr<ThreadSafeRequestHandle>> list;
|
|
for (ScriptLoadRequest* req = mLoadingRequests.getFirst(); req;
|
|
req = req->getNext()) {
|
|
RefPtr<ThreadSafeRequestHandle> handle =
|
|
new ThreadSafeRequestHandle(req, mSyncLoopTarget.get());
|
|
list.AppendElement(handle.forget());
|
|
}
|
|
return list;
|
|
}
|
|
|
|
already_AddRefed<ScriptLoadRequest> WorkerScriptLoader::CreateScriptLoadRequest(
|
|
const nsString& aScriptURL, const mozilla::Encoding* aDocumentEncoding,
|
|
bool aIsMainScript) {
|
|
WorkerLoadContext::Kind kind =
|
|
WorkerLoadContext::GetKind(aIsMainScript, IsDebuggerScript());
|
|
|
|
Maybe<ClientInfo> clientInfo = GetGlobal()->GetClientInfo();
|
|
|
|
RefPtr<WorkerLoadContext> 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<nsIURI> baseURI = aIsMainScript ? GetInitialBaseURI() : GetBaseURI();
|
|
nsCOMPtr<nsIURI> 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<ScriptFetchOptions> fetchOptions =
|
|
new ScriptFetchOptions(CORSMode::CORS_NONE, referrerPolicy, nullptr);
|
|
|
|
RefPtr<ScriptLoadRequest> 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<RefPtr<ThreadSafeRequestHandle>> scriptLoadList;
|
|
RefPtr<ThreadSafeRequestHandle> handle =
|
|
new ThreadSafeRequestHandle(aRequest, mSyncLoopTarget.get());
|
|
scriptLoadList.AppendElement(handle.forget());
|
|
|
|
RefPtr<ScriptLoaderRunnable> runnable =
|
|
new ScriptLoaderRunnable(this, std::move(scriptLoadList));
|
|
|
|
RefPtr<StrongWorkerRef> 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<RefPtr<ThreadSafeRequestHandle>> scriptLoadList = GetLoadingList();
|
|
|
|
RefPtr<ScriptLoaderRunnable> runnable =
|
|
new ScriptLoaderRunnable(this, std::move(scriptLoadList));
|
|
|
|
RefPtr<StrongWorkerRef> 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<nsIGlobalObject*>(
|
|
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<ScriptLoadRequest> 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<JSObject*> global(aCx, JS::CurrentGlobalOrNull(aCx));
|
|
MOZ_ASSERT(global);
|
|
|
|
while (!mLoadedRequests.isEmpty()) {
|
|
RefPtr<ScriptLoadRequest> 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<nsILoadGroup> loadGroup = mWorkerRef->Private()->GetLoadGroup();
|
|
MOZ_DIAGNOSTIC_ASSERT(principal);
|
|
|
|
NS_ENSURE_TRUE(NS_LoadGroupMatchesPrincipal(loadGroup, principal),
|
|
NS_ERROR_FAILURE);
|
|
|
|
// May be null.
|
|
nsCOMPtr<Document> parentDoc = mWorkerRef->Private()->GetDocument();
|
|
|
|
nsCOMPtr<nsIChannel> channel;
|
|
if (loadContext->IsTopLevel()) {
|
|
// May be null.
|
|
channel = mWorkerRef->Private()->ForgetWorkerChannel();
|
|
}
|
|
|
|
nsCOMPtr<nsIIOService> 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<nsPIDOMWindowInner> window = topWorkerPrivate->GetWindow();
|
|
if (window) {
|
|
nsCOMPtr<nsIDocShell> docShell = window->GetDocShell();
|
|
if (docShell) {
|
|
nsresult rv = docShell->GetDefaultLoadFlags(&loadFlags);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!channel) {
|
|
nsCOMPtr<nsIReferrerInfo> referrerInfo =
|
|
ReferrerInfo::CreateForFetch(principal, nullptr);
|
|
if (parentWorker && !loadContext->IsTopLevel()) {
|
|
referrerInfo =
|
|
static_cast<ReferrerInfo*>(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<NetworkLoadHandler> listener =
|
|
new NetworkLoadHandler(this, aRequestHandle);
|
|
|
|
RefPtr<ScriptResponseHeaderProcessor> 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<ScriptResponseHeaderProcessor>(
|
|
mWorkerRef->Private(), loadContext->IsTopLevel());
|
|
}
|
|
|
|
nsCOMPtr<nsIStreamLoader> 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<nsILoadInfo> loadInfo = channel->LoadInfo();
|
|
loadInfo->SetIsThirdPartyContextToTopWindow(
|
|
mWorkerRef->Private()->IsThirdPartyContextToTopWindow());
|
|
|
|
Maybe<ClientInfo> clientInfo;
|
|
clientInfo.emplace(loadContext->mClientInfo.ref());
|
|
rv = AddClientChannelHelper(channel, std::move(clientInfo),
|
|
Maybe<ClientInfo>(),
|
|
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<nsILoadInfo> 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<nsIOutputStream> 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<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->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<JSScript*> 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<JSScript*> 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<JS::SourceText<Utf8Unit>>())
|
|
: EvaluateSourceBuffer(aCx, options,
|
|
maybeSource.ref<JS::SourceText<char16_t>>());
|
|
|
|
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);
|
|
|
|
// 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<JS::Value> 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<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);
|
|
}
|
|
|
|
NS_IMPL_ISUPPORTS(ScriptLoaderRunnable, nsIRunnable, nsINamed)
|
|
|
|
NS_IMPL_ISUPPORTS(WorkerScriptLoader, nsINamed)
|
|
|
|
ScriptLoaderRunnable::ScriptLoaderRunnable(
|
|
WorkerScriptLoader* aScriptLoader,
|
|
nsTArray<RefPtr<ThreadSafeRequestHandle>> 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<RefPtr<CacheLoadHandler>>(
|
|
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<std::pair<Iterator, Iterator>> {
|
|
// firstItToExecute is the first loadInfo where mExecutionScheduled is
|
|
// unset.
|
|
auto firstItToExecute = std::find_if(
|
|
begin, end, [](const RefPtr<ThreadSafeRequestHandle>& 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<ThreadSafeRequestHandle>& 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<ScriptExecutorRunnable> runnable = new ScriptExecutorRunnable(
|
|
mScriptLoader, mWorkerRef->Private(), mScriptLoader->mSyncLoopTarget,
|
|
Span<RefPtr<ThreadSafeRequestHandle>>{maybeRangeToExecute->first,
|
|
maybeRangeToExecute->second});
|
|
if (!runnable->Dispatch()) {
|
|
MOZ_ASSERT(false, "This should never fail!");
|
|
}
|
|
}
|
|
}
|
|
|
|
ScriptExecutorRunnable::ScriptExecutorRunnable(
|
|
WorkerScriptLoader* aScriptLoader, WorkerPrivate* aWorkerPrivate,
|
|
nsISerialEventTarget* aSyncLoopTarget,
|
|
Span<RefPtr<ThreadSafeRequestHandle>> 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<ScriptLoadRequest> 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<ClientInfo>& aClientInfo,
|
|
nsContentPolicyType aMainScriptContentPolicyType,
|
|
nsICookieJarSettings* aCookieJarSettings, nsIReferrerInfo* aReferrerInfo,
|
|
nsIChannel** aChannel) {
|
|
AssertIsOnMainThread();
|
|
|
|
nsCOMPtr<nsIIOService> 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<ServiceWorkerDescriptor>(), true, WorkerScript,
|
|
aMainScriptContentPolicyType, nsIRequest::LOAD_NORMAL, aCookieJarSettings,
|
|
aReferrerInfo, aChannel);
|
|
}
|
|
|
|
nsresult ChannelFromScriptURLWorkerThread(JSContext* aCx,
|
|
WorkerPrivate* aParent,
|
|
const nsAString& aScriptURL,
|
|
WorkerLoadInfo& aLoadInfo) {
|
|
aParent->AssertIsOnWorkerThread();
|
|
|
|
RefPtr<ChannelGetterRunnable> 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<uint32_t>(aLoadResult)));
|
|
return;
|
|
}
|
|
}
|
|
|
|
void LoadMainScript(WorkerPrivate* aWorkerPrivate,
|
|
UniquePtr<SerializedStackHolder> aOriginStack,
|
|
const nsAString& aScriptURL,
|
|
WorkerScriptType aWorkerScriptType, ErrorResult& aRv,
|
|
const mozilla::Encoding* aDocumentEncoding) {
|
|
nsTArray<nsString> scriptURLs;
|
|
|
|
scriptURLs.AppendElement(aScriptURL);
|
|
|
|
LoadAllScripts(aWorkerPrivate, std::move(aOriginStack), scriptURLs, true,
|
|
aWorkerScriptType, aRv, aDocumentEncoding);
|
|
}
|
|
|
|
void Load(WorkerPrivate* aWorkerPrivate,
|
|
UniquePtr<SerializedStackHolder> aOriginStack,
|
|
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;
|
|
}
|
|
|
|
LoadAllScripts(aWorkerPrivate, std::move(aOriginStack), aScriptURLs, false,
|
|
aWorkerScriptType, aRv);
|
|
}
|
|
|
|
} // namespace mozilla::dom::workerinternals
|