diff options
Diffstat (limited to 'dom/workers/loader')
-rw-r--r-- | dom/workers/loader/CacheLoadHandler.cpp | 646 | ||||
-rw-r--r-- | dom/workers/loader/CacheLoadHandler.h | 219 | ||||
-rw-r--r-- | dom/workers/loader/NetworkLoadHandler.cpp | 393 | ||||
-rw-r--r-- | dom/workers/loader/NetworkLoadHandler.h | 79 | ||||
-rw-r--r-- | dom/workers/loader/ScriptResponseHeaderProcessor.cpp | 64 | ||||
-rw-r--r-- | dom/workers/loader/ScriptResponseHeaderProcessor.h | 78 | ||||
-rw-r--r-- | dom/workers/loader/WorkerLoadContext.cpp | 73 | ||||
-rw-r--r-- | dom/workers/loader/WorkerLoadContext.h | 205 | ||||
-rw-r--r-- | dom/workers/loader/moz.build | 32 |
9 files changed, 1789 insertions, 0 deletions
diff --git a/dom/workers/loader/CacheLoadHandler.cpp b/dom/workers/loader/CacheLoadHandler.cpp new file mode 100644 index 0000000000..a3ad48fc98 --- /dev/null +++ b/dom/workers/loader/CacheLoadHandler.cpp @@ -0,0 +1,646 @@ +/* -*- 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 "CacheLoadHandler.h" +#include "ScriptResponseHeaderProcessor.h" // ScriptResponseHeaderProcessor +#include "WorkerLoadContext.h" // WorkerLoadContext + +#include "nsIPrincipal.h" + +#include "nsIThreadRetargetableRequest.h" +#include "nsIXPConnect.h" + +#include "jsapi.h" +#include "nsNetUtil.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Encoding.h" +#include "mozilla/dom/CacheBinding.h" +#include "mozilla/dom/cache/CacheTypes.h" +#include "mozilla/dom/Response.h" +#include "mozilla/dom/ServiceWorkerBinding.h" // ServiceWorkerState +#include "mozilla/Result.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/WorkerScope.h" + +#include "mozilla/dom/workerinternals/ScriptLoader.h" // WorkerScriptLoader + +namespace mozilla { +namespace dom { + +namespace workerinternals::loader { + +NS_IMPL_ISUPPORTS0(CacheCreator) + +NS_IMPL_ISUPPORTS(CacheLoadHandler, nsIStreamLoaderObserver) + +NS_IMPL_ISUPPORTS0(CachePromiseHandler) + +CachePromiseHandler::CachePromiseHandler( + WorkerScriptLoader* aLoader, ThreadSafeRequestHandle* aRequestHandle) + : mLoader(aLoader), mRequestHandle(aRequestHandle) { + AssertIsOnMainThread(); + MOZ_ASSERT(mLoader); +} + +void CachePromiseHandler::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + AssertIsOnMainThread(); + if (mRequestHandle->IsEmpty()) { + return; + } + WorkerLoadContext* loadContext = mRequestHandle->GetContext(); + + // May already have been canceled by CacheLoadHandler::Fail from + // CancelMainThread. + MOZ_ASSERT(loadContext->mCacheStatus == WorkerLoadContext::WritingToCache || + loadContext->mCacheStatus == WorkerLoadContext::Cancel); + MOZ_ASSERT_IF(loadContext->mCacheStatus == WorkerLoadContext::Cancel, + !loadContext->mCachePromise); + + if (loadContext->mCachePromise) { + loadContext->mCacheStatus = WorkerLoadContext::Cached; + loadContext->mCachePromise = nullptr; + mRequestHandle->MaybeExecuteFinishedScripts(); + } +} + +void CachePromiseHandler::RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + AssertIsOnMainThread(); + if (mRequestHandle->IsEmpty()) { + return; + } + WorkerLoadContext* loadContext = mRequestHandle->GetContext(); + + // May already have been canceled by CacheLoadHandler::Fail from + // CancelMainThread. + MOZ_ASSERT(loadContext->mCacheStatus == WorkerLoadContext::WritingToCache || + loadContext->mCacheStatus == WorkerLoadContext::Cancel); + loadContext->mCacheStatus = WorkerLoadContext::Cancel; + + loadContext->mCachePromise = nullptr; + + // This will delete the cache object and will call LoadingFinished() with an + // error for each ongoing operation. + auto* cacheCreator = mRequestHandle->GetCacheCreator(); + if (cacheCreator) { + cacheCreator->DeleteCache(NS_ERROR_FAILURE); + } +} + +CacheCreator::CacheCreator(WorkerPrivate* aWorkerPrivate) + : mCacheName(aWorkerPrivate->ServiceWorkerCacheName()), + mOriginAttributes(aWorkerPrivate->GetOriginAttributes()) { + MOZ_ASSERT(aWorkerPrivate->IsServiceWorker()); +} + +nsresult CacheCreator::CreateCacheStorage(nsIPrincipal* aPrincipal) { + AssertIsOnMainThread(); + MOZ_ASSERT(!mCacheStorage); + MOZ_ASSERT(aPrincipal); + + nsIXPConnect* xpc = nsContentUtils::XPConnect(); + MOZ_ASSERT(xpc, "This should never be null!"); + + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + JS::Rooted<JSObject*> sandbox(cx); + nsresult rv = xpc->CreateSandbox(cx, aPrincipal, sandbox.address()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // The JSContext is not in a realm, so CreateSandbox returned an unwrapped + // global. + MOZ_ASSERT(JS_IsGlobalObject(sandbox)); + + 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, 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, + ErrorResult& aRv) { + AssertIsOnMainThread(); + FailLoaders(NS_ERROR_FAILURE); +} + +void CacheCreator::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + 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) { + mLoaders[i]->Load(cache); + } +} + +void CacheCreator::DeleteCache(nsresult aReason) { + 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. + RefPtr<Promise> promise = mCacheStorage->Delete(mCacheName, IgnoreErrors()); + + // 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); +} + +CacheLoadHandler::CacheLoadHandler(ThreadSafeWorkerRef* aWorkerRef, + ThreadSafeRequestHandle* aRequestHandle, + bool aIsWorkerScript, + WorkerScriptLoader* aLoader) + : mRequestHandle(aRequestHandle), + mLoader(aLoader), + mWorkerRef(aWorkerRef), + mIsWorkerScript(aIsWorkerScript), + mFailed(false), + mState(aWorkerRef->Private()->GetServiceWorkerDescriptor().State()) { + MOZ_ASSERT(aWorkerRef); + MOZ_ASSERT(aWorkerRef->Private()->IsServiceWorker()); + mMainThreadEventTarget = aWorkerRef->Private()->MainThreadEventTarget(); + MOZ_ASSERT(mMainThreadEventTarget); + mBaseURI = mLoader->GetBaseURI(); + AssertIsOnMainThread(); + + // Worker scripts are always decoded as UTF-8 per spec. + mDecoder = MakeUnique<ScriptDecoder>(UTF_8_ENCODING, + ScriptDecoder::BOMHandling::Remove); +} + +void CacheLoadHandler::Fail(nsresult aRv) { + AssertIsOnMainThread(); + MOZ_ASSERT(NS_FAILED(aRv)); + + if (mFailed) { + return; + } + + mFailed = true; + + if (mPump) { + MOZ_ASSERT_IF(!mRequestHandle->IsEmpty(), + mRequestHandle->GetContext()->mCacheStatus == + WorkerLoadContext::ReadingFromCache); + mPump->Cancel(aRv); + mPump = nullptr; + } + if (mRequestHandle->IsEmpty()) { + return; + } + + WorkerLoadContext* loadContext = mRequestHandle->GetContext(); + + loadContext->mCacheStatus = WorkerLoadContext::Cancel; + + if (loadContext->mCachePromise) { + loadContext->mCachePromise->MaybeReject(aRv); + } + + loadContext->mCachePromise = nullptr; + + mRequestHandle->LoadingFinished(aRv); +} + +void CacheLoadHandler::Load(Cache* aCache) { + AssertIsOnMainThread(); + MOZ_ASSERT(aCache); + MOZ_ASSERT(!mRequestHandle->IsEmpty()); + WorkerLoadContext* loadContext = mRequestHandle->GetContext(); + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), loadContext->mRequest->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(loadContext->mFullURL.IsEmpty()); + CopyUTF8toUTF16(spec, loadContext->mFullURL); + + mozilla::dom::RequestOrUSVString request; + request.SetAsUSVString().ShareOrDependUpon(loadContext->mFullURL); + + mozilla::dom::CacheQueryOptions params; + + // This JSContext will not end up executing JS code because here there are + // no ReadableStreams involved. + AutoJSAPI jsapi; + jsapi.Init(); + + ErrorResult error; + RefPtr<Promise> promise = aCache->Match(jsapi.cx(), request, params, error); + if (NS_WARN_IF(error.Failed())) { + Fail(error.StealNSResult()); + return; + } + + promise->AppendNativeHandler(this); +} + +void CacheLoadHandler::RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + AssertIsOnMainThread(); + MOZ_ASSERT(!mRequestHandle->IsEmpty()); + + MOZ_ASSERT(mRequestHandle->GetContext()->mCacheStatus == + WorkerLoadContext::Uncached); + Fail(NS_ERROR_FAILURE); +} + +void CacheLoadHandler::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + AssertIsOnMainThread(); + MOZ_ASSERT(!mRequestHandle->IsEmpty()); + WorkerLoadContext* loadContext = mRequestHandle->GetContext(); + + // If we have already called 'Fail', we should not proceed. If we cancelled, + // we should similarily not proceed. + if (mFailed) { + return; + } + + MOZ_ASSERT(loadContext->mCacheStatus == WorkerLoadContext::Uncached); + + nsresult rv; + + // The ServiceWorkerScriptCache will store data for any scripts it + // it knows about. This is always at least the top level script. + // Depending on if a previous version of the service worker has + // been installed or not it may also know about importScripts(). We + // must handle loading and offlining new importScripts() here, however. + if (aValue.isUndefined()) { + // If this is the main script or we're not loading a new service worker + // then this is an error. This can happen for internal reasons, like + // storage was probably wiped without removing the service worker + // registration. It can also happen for exposed reasons like the + // service worker script calling importScripts() after install. + if (NS_WARN_IF(mIsWorkerScript || + (mState != ServiceWorkerState::Parsed && + mState != ServiceWorkerState::Installing))) { + Fail(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + loadContext->mCacheStatus = WorkerLoadContext::ToBeCached; + rv = mLoader->LoadScript(mRequestHandle); + 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; + } + + InternalHeaders* headers = response->GetInternalHeaders(); + + headers->Get("content-security-policy"_ns, mCSPHeaderValue, IgnoreErrors()); + headers->Get("content-security-policy-report-only"_ns, + mCSPReportOnlyHeaderValue, IgnoreErrors()); + headers->Get("referrer-policy"_ns, mReferrerPolicyHeaderValue, + IgnoreErrors()); + + nsAutoCString coepHeader; + headers->Get("cross-origin-embedder-policy"_ns, coepHeader, IgnoreErrors()); + + nsILoadInfo::CrossOriginEmbedderPolicy coep = + NS_GetCrossOriginEmbedderPolicyFromHeader( + coepHeader, mWorkerRef->Private()->Trials().IsEnabled( + OriginTrial::CoepCredentialless)); + + rv = ScriptResponseHeaderProcessor::ProcessCrossOriginEmbedderPolicyHeader( + mWorkerRef->Private(), coep, loadContext->IsTopLevel()); + + 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) { + loadContext->mCacheStatus = WorkerLoadContext::Cached; + + if (mRequestHandle->IsCancelled()) { + auto* cacheCreator = mRequestHandle->GetCacheCreator(); + if (cacheCreator) { + cacheCreator->DeleteCache(mRequestHandle->GetCancelResult()); + } + return; + } + + nsresult rv = DataReceivedFromCache( + (uint8_t*)"", 0, mChannelInfo, std::move(mPrincipalInfo), + mCSPHeaderValue, mCSPReportOnlyHeaderValue, mReferrerPolicyHeaderValue); + + mRequestHandle->OnStreamComplete(rv); + return; + } + + MOZ_ASSERT(!mPump); + rv = NS_NewInputStreamPump(getter_AddRefs(mPump), inputStream.forget(), + 0, /* default segsize */ + 0, /* default segcount */ + false, /* default closeWhenDone */ + mMainThreadEventTarget); + 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); + 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."); + } + } + + loadContext->mCacheStatus = WorkerLoadContext::ReadingFromCache; +} + +NS_IMETHODIMP +CacheLoadHandler::OnStreamComplete(nsIStreamLoader* aLoader, + nsISupports* aContext, nsresult aStatus, + uint32_t aStringLen, + const uint8_t* aString) { + AssertIsOnMainThread(); + if (mRequestHandle->IsEmpty()) { + return NS_OK; + } + WorkerLoadContext* loadContext = mRequestHandle->GetContext(); + + mPump = nullptr; + + if (NS_FAILED(aStatus)) { + MOZ_ASSERT(loadContext->mCacheStatus == + WorkerLoadContext::ReadingFromCache || + loadContext->mCacheStatus == WorkerLoadContext::Cancel); + Fail(aStatus); + return NS_OK; + } + + MOZ_ASSERT(loadContext->mCacheStatus == WorkerLoadContext::ReadingFromCache); + loadContext->mCacheStatus = WorkerLoadContext::Cached; + + MOZ_ASSERT(mPrincipalInfo); + + nsresult rv = DataReceivedFromCache( + aString, aStringLen, mChannelInfo, std::move(mPrincipalInfo), + mCSPHeaderValue, mCSPReportOnlyHeaderValue, mReferrerPolicyHeaderValue); + return mRequestHandle->OnStreamComplete(rv); +} + +nsresult CacheLoadHandler::DataReceivedFromCache( + const uint8_t* aString, uint32_t aStringLen, + const mozilla::dom::ChannelInfo& aChannelInfo, + UniquePtr<PrincipalInfo> aPrincipalInfo, const nsACString& aCSPHeaderValue, + const nsACString& aCSPReportOnlyHeaderValue, + const nsACString& aReferrerPolicyHeaderValue) { + AssertIsOnMainThread(); + if (mRequestHandle->IsEmpty()) { + return NS_OK; + } + WorkerLoadContext* loadContext = mRequestHandle->GetContext(); + + MOZ_ASSERT(loadContext->mCacheStatus == WorkerLoadContext::Cached); + MOZ_ASSERT(loadContext->mRequest); + + auto responsePrincipalOrErr = PrincipalInfoToPrincipal(*aPrincipalInfo); + MOZ_DIAGNOSTIC_ASSERT(responsePrincipalOrErr.isOk()); + + nsIPrincipal* principal = mWorkerRef->Private()->GetPrincipal(); + if (!principal) { + WorkerPrivate* parentWorker = mWorkerRef->Private()->GetParent(); + MOZ_ASSERT(parentWorker, "Must have a parent!"); + principal = parentWorker->GetPrincipal(); + } + + nsCOMPtr<nsIPrincipal> responsePrincipal = responsePrincipalOrErr.unwrap(); + + loadContext->mMutedErrorFlag.emplace(!principal->Subsumes(responsePrincipal)); + + // May be null. + Document* parentDoc = mWorkerRef->Private()->GetDocument(); + + // Use the regular ScriptDecoder Decoder for this grunt work! Should be just + // fine because we're running on the main thread. + nsresult rv; + + // Set the Source type to "text" for decoding. + loadContext->mRequest->SetTextSource(); + + rv = mDecoder->DecodeRawData(loadContext->mRequest, aString, aStringLen, + /* aEndOfStream = */ true); + NS_ENSURE_SUCCESS(rv, rv); + + if (!loadContext->mRequest->ScriptTextLength()) { + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns, + parentDoc, nsContentUtils::eDOM_PROPERTIES, + "EmptyWorkerSourceWarning"); + } + + if (loadContext->IsTopLevel()) { + nsCOMPtr<nsIURI> finalURI; + rv = NS_NewURI(getter_AddRefs(finalURI), loadContext->mFullURL); + if (NS_SUCCEEDED(rv)) { + mWorkerRef->Private()->SetBaseURI(finalURI); + } + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + nsIPrincipal* principal = mWorkerRef->Private()->GetPrincipal(); + MOZ_DIAGNOSTIC_ASSERT(principal); + + bool equal = false; + MOZ_ALWAYS_SUCCEEDS(responsePrincipal->Equals(principal, &equal)); + MOZ_DIAGNOSTIC_ASSERT(equal); + + nsCOMPtr<nsIContentSecurityPolicy> csp; + if (parentDoc) { + csp = parentDoc->GetCsp(); + } + MOZ_DIAGNOSTIC_ASSERT(!csp); +#endif + + mWorkerRef->Private()->InitChannelInfo(aChannelInfo); + + nsILoadGroup* loadGroup = mWorkerRef->Private()->GetLoadGroup(); + MOZ_DIAGNOSTIC_ASSERT(loadGroup); + + // Override the principal on the WorkerPrivate. This is only necessary + // in order to get a principal with exactly the correct URL. The fetch + // referrer logic depends on the WorkerPrivate principal having a URL + // that matches the worker script URL. If bug 1340694 is ever fixed + // this can be removed. + // XXX: force the partitionedPrincipal to be equal to the response one. + // This is OK for now because we don't want to expose partitionedPrincipal + // functionality in ServiceWorkers yet. + rv = mWorkerRef->Private()->SetPrincipalsAndCSPOnMainThread( + responsePrincipal, responsePrincipal, loadGroup, nullptr); + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + + rv = mWorkerRef->Private()->SetCSPFromHeaderValues( + aCSPHeaderValue, aCSPReportOnlyHeaderValue); + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); + + mWorkerRef->Private()->UpdateReferrerInfoFromHeader( + aReferrerPolicyHeaderValue); + } + + if (NS_SUCCEEDED(rv)) { + DataReceived(); + } + + return rv; +} + +void CacheLoadHandler::DataReceived() { + MOZ_ASSERT(!mRequestHandle->IsEmpty()); + WorkerLoadContext* loadContext = mRequestHandle->GetContext(); + + if (loadContext->IsTopLevel()) { + WorkerPrivate* parent = mWorkerRef->Private()->GetParent(); + + if (parent) { + // XHR Params Allowed + mWorkerRef->Private()->SetXHRParamsAllowed(parent->XHRParamsAllowed()); + + // Set Eval and ContentSecurityPolicy + mWorkerRef->Private()->SetCsp(parent->GetCsp()); + mWorkerRef->Private()->SetEvalAllowed(parent->IsEvalAllowed()); + mWorkerRef->Private()->SetWasmEvalAllowed(parent->IsWasmEvalAllowed()); + } + } +} + +} // namespace workerinternals::loader + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/loader/CacheLoadHandler.h b/dom/workers/loader/CacheLoadHandler.h new file mode 100644 index 0000000000..88744c4b93 --- /dev/null +++ b/dom/workers/loader/CacheLoadHandler.h @@ -0,0 +1,219 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_workers_CacheLoadHandler_h__ +#define mozilla_dom_workers_CacheLoadHandler_h__ + +#include "nsIContentPolicy.h" +#include "nsIInputStreamPump.h" +#include "nsIStreamLoader.h" +#include "nsStringFwd.h" +#include "nsStreamUtils.h" + +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/dom/CacheBinding.h" +#include "mozilla/dom/ChannelInfo.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/ScriptLoadHandler.h" +#include "mozilla/dom/cache/Cache.h" +#include "mozilla/dom/cache/CacheStorage.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/dom/WorkerRef.h" + +#include "mozilla/dom/workerinternals/ScriptLoader.h" + +using mozilla::dom::cache::Cache; +using mozilla::dom::cache::CacheStorage; +using mozilla::ipc::PrincipalInfo; + +namespace mozilla::dom { + +class WorkerLoadContext; + +namespace workerinternals::loader { + +/* + * [DOMDOC] CacheLoadHandler for Workers + * + * A LoadHandler is a ScriptLoader helper class that reacts to an + * nsIStreamLoader's events for loading JS scripts. It is primarily responsible + * for decoding the stream into UTF8 or UTF16. Additionally, it takes care of + * any work that needs to follow the completion of a stream. Every LoadHandler + * also manages additional tasks for the type of load that it is doing. + * + * CacheLoadHandler is a specialized LoadHandler used by ServiceWorkers to + * implement the installation model used by ServiceWorkers to support running + * offline. When a ServiceWorker is installed, its main script is evaluated and + * all script resources that are loaded are saved. The spec does not specify the + * storage mechanism for this, but we chose to reuse the Cache API[1] mechanism + * that we expose to content to also store the script and its dependencies. We + * store the script resources in a special chrome namespace CacheStorage that is + * not visible to content. Each distinct ServiceWorker installation gets its own + * Cache keyed by a randomly-generated UUID. + * + * In terms of specification, this class implements step 4 of + * https://w3c.github.io/ServiceWorker/#importscripts + * + * Relationship to NetworkLoadHandler + * + * During ServiceWorker installation, the CacheLoadHandler falls back on the + * NetworkLoadHandler by calling `mLoader->LoadScript(...)`. If a script has not + * been seen before, then we will fall back on loading from the network. + * However, if the ServiceWorker is already installed, an error will be + * generated and the ServiceWorker will fail to load, per spec. + * + * CacheLoadHandler does not persist some pieces of information, such as the + * sourceMapUrl. Also, the DOM Cache API storage does not yet support alternate + * data streams for JS Bytecode or WASM caching; this is tracked by Bug 1336199. + * + * [1]: https://developer.mozilla.org/en-US/docs/Web/API/caches + * + */ + +class CacheLoadHandler final : public PromiseNativeHandler, + public nsIStreamLoaderObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLOADEROBSERVER + + CacheLoadHandler(ThreadSafeWorkerRef* aWorkerRef, + ThreadSafeRequestHandle* aRequestHandle, + bool aIsWorkerScript, WorkerScriptLoader* aLoader); + + void Fail(nsresult aRv); + + void Load(Cache* aCache); + + virtual void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + virtual void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + private: + ~CacheLoadHandler() { AssertIsOnMainThread(); } + + nsresult DataReceivedFromCache(const uint8_t* aString, uint32_t aStringLen, + const mozilla::dom::ChannelInfo& aChannelInfo, + UniquePtr<PrincipalInfo> aPrincipalInfo, + const nsACString& aCSPHeaderValue, + const nsACString& aCSPReportOnlyHeaderValue, + const nsACString& aReferrerPolicyHeaderValue); + void DataReceived(); + + RefPtr<ThreadSafeRequestHandle> mRequestHandle; + const RefPtr<WorkerScriptLoader> mLoader; + RefPtr<ThreadSafeWorkerRef> mWorkerRef; + const bool mIsWorkerScript; + bool mFailed; + const ServiceWorkerState mState; + nsCOMPtr<nsIInputStreamPump> mPump; + nsCOMPtr<nsIURI> mBaseURI; + mozilla::dom::ChannelInfo mChannelInfo; + UniquePtr<PrincipalInfo> mPrincipalInfo; + UniquePtr<ScriptDecoder> mDecoder; + nsCString mCSPHeaderValue; + nsCString mCSPReportOnlyHeaderValue; + nsCString mReferrerPolicyHeaderValue; + nsCOMPtr<nsIEventTarget> mMainThreadEventTarget; +}; + +/* + * CacheCreator + * + * The CacheCreator is responsible for maintaining a CacheStorage for the + * purposes of caching ServiceWorkers (see comment on CacheLoadHandler). In + * addition, it tracks all CacheLoadHandlers and is used for cleanup once + * loading has finished. + * + */ + +class CacheCreator final : public PromiseNativeHandler { + public: + NS_DECL_ISUPPORTS + + explicit CacheCreator(WorkerPrivate* aWorkerPrivate); + + void AddLoader(MovingNotNull<RefPtr<CacheLoadHandler>> aLoader) { + AssertIsOnMainThread(); + MOZ_ASSERT(!mCacheStorage); + mLoaders.AppendElement(std::move(aLoader)); + } + + virtual void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + virtual void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) 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(nsresult aReason); + + private: + ~CacheCreator() = default; + + nsresult CreateCacheStorage(nsIPrincipal* aPrincipal); + + void FailLoaders(nsresult aRv); + + RefPtr<Cache> mCache; + RefPtr<CacheStorage> mCacheStorage; + nsCOMPtr<nsIGlobalObject> mSandboxGlobalObject; + nsTArray<NotNull<RefPtr<CacheLoadHandler>>> mLoaders; + + nsString mCacheName; + OriginAttributes mOriginAttributes; +}; + +/* + * CachePromiseHandler + * + * This promise handler is used to track if a ServiceWorker has been written to + * Cache. It is responsible for tracking the state of the ServiceWorker being + * cached. It also handles cancelling caching of a ServiceWorker if loading is + * interrupted. It is initialized by the NetworkLoadHandler as part of the first + * load of a ServiceWorker. + * + */ +class CachePromiseHandler final : public PromiseNativeHandler { + public: + NS_DECL_ISUPPORTS + + CachePromiseHandler(WorkerScriptLoader* aLoader, + ThreadSafeRequestHandle* aRequestHandle); + + virtual void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + virtual void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + private: + ~CachePromiseHandler() { AssertIsOnMainThread(); } + + RefPtr<WorkerScriptLoader> mLoader; + RefPtr<ThreadSafeRequestHandle> mRequestHandle; +}; + +} // namespace workerinternals::loader +} // namespace mozilla::dom + +#endif /* mozilla_dom_workers_CacheLoadHandler_h__ */ diff --git a/dom/workers/loader/NetworkLoadHandler.cpp b/dom/workers/loader/NetworkLoadHandler.cpp new file mode 100644 index 0000000000..bc6a31c4cc --- /dev/null +++ b/dom/workers/loader/NetworkLoadHandler.cpp @@ -0,0 +1,393 @@ +/* -*- 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 "NetworkLoadHandler.h" +#include "CacheLoadHandler.h" // CachePromiseHandler + +#include "nsContentUtils.h" +#include "nsIChannel.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIPrincipal.h" +#include "nsIScriptError.h" +#include "nsNetUtil.h" + +#include "mozilla/Encoding.h" +#include "mozilla/dom/BlobURLProtocolHandler.h" +#include "mozilla/dom/InternalResponse.h" +#include "mozilla/dom/ServiceWorkerBinding.h" +#include "mozilla/dom/ServiceWorkerManager.h" +#include "mozilla/dom/ScriptLoader.h" +#include "mozilla/dom/Response.h" +#include "mozilla/dom/WorkerScope.h" + +#include "mozilla/dom/workerinternals/ScriptLoader.h" // WorkerScriptLoader + +using mozilla::ipc::PrincipalInfo; + +namespace mozilla { +namespace dom { + +namespace workerinternals::loader { + +NS_IMPL_ISUPPORTS(NetworkLoadHandler, nsIStreamLoaderObserver, + nsIRequestObserver) + +NetworkLoadHandler::NetworkLoadHandler(WorkerScriptLoader* aLoader, + ThreadSafeRequestHandle* aRequestHandle) + : mLoader(aLoader), + mWorkerRef(aLoader->mWorkerRef), + mRequestHandle(aRequestHandle) { + MOZ_ASSERT(mLoader); + + // Worker scripts are always decoded as UTF-8 per spec. + mDecoder = MakeUnique<ScriptDecoder>(UTF_8_ENCODING, + ScriptDecoder::BOMHandling::Remove); +} + +NS_IMETHODIMP +NetworkLoadHandler::OnStreamComplete(nsIStreamLoader* aLoader, + nsISupports* aContext, nsresult aStatus, + uint32_t aStringLen, + const uint8_t* aString) { + // If we have cancelled, or we have no mRequest, it means that the loader has + // shut down and we can exit early. If the cancel result is still NS_OK + if (mRequestHandle->IsEmpty()) { + return NS_OK; + } + nsresult rv = DataReceivedFromNetwork(aLoader, aStatus, aStringLen, aString); + return mRequestHandle->OnStreamComplete(rv); +} + +nsresult NetworkLoadHandler::DataReceivedFromNetwork(nsIStreamLoader* aLoader, + nsresult aStatus, + uint32_t aStringLen, + const uint8_t* aString) { + AssertIsOnMainThread(); + MOZ_ASSERT(!mRequestHandle->IsEmpty()); + WorkerLoadContext* loadContext = mRequestHandle->GetContext(); + + if (!loadContext->mChannel) { + return NS_BINDING_ABORTED; + } + + loadContext->mChannel = nullptr; + + if (NS_FAILED(aStatus)) { + return aStatus; + } + + if (mRequestHandle->IsCancelled()) { + return mRequestHandle->GetCancelResult(); + } + + 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 = mWorkerRef->Private()->GetPrincipal(); + if (!principal) { + WorkerPrivate* parentWorker = mWorkerRef->Private()->GetParent(); + MOZ_ASSERT(parentWorker, "Must have a parent!"); + principal = parentWorker->GetPrincipal(); + } + +#ifdef DEBUG + if (loadContext->IsTopLevel()) { + nsCOMPtr<nsIPrincipal> loadingPrincipal = + mWorkerRef->Private()->GetLoadingPrincipal(); + // if we are not in a ServiceWorker, and the principal is not null, then + // the loading principal must subsume the worker principal if it is not a + // nullPrincipal (sandbox). + MOZ_ASSERT(!loadingPrincipal || loadingPrincipal->GetIsNullPrincipal() || + principal->GetIsNullPrincipal() || + loadingPrincipal->Subsumes(principal)); + } +#endif + + // 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. + loadContext->mMutedErrorFlag.emplace(!loadContext->IsTopLevel() && + !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; + } + + Unused << httpChannel->GetResponseHeader("content-security-policy"_ns, + tCspHeaderValue); + + Unused << httpChannel->GetResponseHeader( + "content-security-policy-report-only"_ns, tCspROHeaderValue); + + Unused << httpChannel->GetResponseHeader("referrer-policy"_ns, + tRPHeaderCValue); + + nsAutoCString sourceMapURL; + if (nsContentUtils::GetSourceMapURL(httpChannel, sourceMapURL)) { + loadContext->mRequest->mSourceMapURL = + Some(NS_ConvertUTF8toUTF16(sourceMapURL)); + } + } + + // May be null. + Document* parentDoc = mWorkerRef->Private()->GetDocument(); + + // Set the Source type to "text" for decoding. + loadContext->mRequest->SetTextSource(); + + // Use the regular ScriptDecoder Decoder for this grunt work! Should be just + // fine because we're running on the main thread. + rv = mDecoder->DecodeRawData(loadContext->mRequest, aString, aStringLen, + /* aEndOfStream = */ true); + NS_ENSURE_SUCCESS(rv, rv); + + if (!loadContext->mRequest->ScriptTextLength()) { + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns, + parentDoc, nsContentUtils::eDOM_PROPERTIES, + "EmptyWorkerSourceWarning"); + } + + if (loadContext->mRequest->IsModuleRequest()) { + // For modules, we need to store the base URI on the module request object, + // rather than on the worker private (as we do for classic scripts). This is + // because module loading is shared across multiple components, with + // ScriptLoadRequests being the common structure among them. This specific + // use of the base url is used when resolving the module specifier for child + // modules. + nsCOMPtr<nsIURI> uri; + rv = channel->GetOriginalURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + channel->GetURI(getter_AddRefs(loadContext->mRequest->mBaseURL)); + } + + // Figure out what we actually loaded. + nsCOMPtr<nsIURI> finalURI; + rv = NS_GetFinalChannelURI(channel, getter_AddRefs(finalURI)); + NS_ENSURE_SUCCESS(rv, rv); + + if (principal->IsSameOrigin(finalURI)) { + 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, and is used for debugging. + // The full URL shouldn't be exposed to the debugger if cross origin. + // See Bug 1634872. + loadContext->mRequest->mURL = filename; + } + } + + // Update the principal of the worker and its base URI if we just loaded the + // worker's primary script. + if (loadContext->IsTopLevel()) { + // Take care of the base URI first. + mWorkerRef->Private()->SetBaseURI(finalURI); + + // Store the channel info if needed. + mWorkerRef->Private()->InitChannelInfo(channel); + + // Our final channel principal should match the loading principal + // in terms of the origin. This used to be an assert, but it seems + // there are some rare cases where this check can fail in practice. + // Perhaps some browser script setting nsIChannel.owner, etc. + NS_ENSURE_TRUE(mWorkerRef->Private()->FinalChannelPrincipalIsValid(channel), + NS_ERROR_FAILURE); + + // However, we must still override the principal since the nsIPrincipal + // URL may be different due to same-origin redirects. Unfortunately this + // URL must exactly match the final worker script URL in order to + // properly set the referrer header on fetch/xhr requests. If bug 1340694 + // is ever fixed this can be removed. + rv = mWorkerRef->Private()->SetPrincipalsAndCSPFromChannel(channel); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIContentSecurityPolicy> csp = mWorkerRef->Private()->GetCsp(); + // 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 (!csp) { + rv = mWorkerRef->Private()->SetCSPFromHeaderValues(tCspHeaderValue, + tCspROHeaderValue); + NS_ENSURE_SUCCESS(rv, rv); + } else { + csp->EnsureEventTarget(mWorkerRef->Private()->MainThreadEventTarget()); + } + + mWorkerRef->Private()->UpdateReferrerInfoFromHeader(tRPHeaderCValue); + + WorkerPrivate* parent = mWorkerRef->Private()->GetParent(); + if (parent) { + // XHR Params Allowed + mWorkerRef->Private()->SetXHRParamsAllowed(parent->XHRParamsAllowed()); + } + + nsCOMPtr<nsILoadInfo> chanLoadInfo = channel->LoadInfo(); + if (chanLoadInfo) { + mLoader->SetController(chanLoadInfo->GetController()); + } + + // If we are loading a blob URL we must inherit the controller + // from the parent. This is a bit odd as the blob URL may have + // been created in a different context with a different controller. + // For now, though, this is what the spec says. See: + // + // https://github.com/w3c/ServiceWorker/issues/1261 + // + if (IsBlobURI(mWorkerRef->Private()->GetBaseURI())) { + MOZ_DIAGNOSTIC_ASSERT(mLoader->GetController().isNothing()); + mLoader->SetController(mWorkerRef->Private()->GetParentController()); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +NetworkLoadHandler::OnStartRequest(nsIRequest* aRequest) { + nsresult rv = PrepareForRequest(aRequest); + + if (NS_WARN_IF(NS_FAILED(rv))) { + aRequest->Cancel(rv); + } + + return rv; +} + +nsresult NetworkLoadHandler::PrepareForRequest(nsIRequest* aRequest) { + AssertIsOnMainThread(); + MOZ_ASSERT(!mRequestHandle->IsEmpty()); + WorkerLoadContext* loadContext = mRequestHandle->GetContext(); + + // If one load info cancels or hits an error, it can race with the start + // callback coming from another load info. + if (mRequestHandle->IsCancelled()) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + + // Checking the MIME type is only required for ServiceWorkers' + // importScripts, per step 10 of + // https://w3c.github.io/ServiceWorker/#importscripts + // + // "Extract a MIME type from the response’s header list. If this MIME type + // (ignoring parameters) is not a JavaScript MIME type, return a network + // error." + if (mWorkerRef->Private()->IsServiceWorker()) { + nsAutoCString mimeType; + channel->GetContentType(mimeType); + + if (!nsContentUtils::IsJavascriptMIMEType( + NS_ConvertUTF8toUTF16(mimeType))) { + const nsCString& scope = mWorkerRef->Private() + ->GetServiceWorkerRegistrationDescriptor() + .Scope(); + + ServiceWorkerManager::LocalizeAndReportToAllClients( + scope, "ServiceWorkerRegisterMimeTypeError2", + nsTArray<nsString>{ + NS_ConvertUTF8toUTF16(scope), NS_ConvertUTF8toUTF16(mimeType), + NS_ConvertUTF8toUTF16(loadContext->mRequest->mURL)}); + + return NS_ERROR_DOM_NETWORK_ERR; + } + } + + // We synthesize the result code, but its never exposed to content. + SafeRefPtr<mozilla::dom::InternalResponse> ir = + MakeSafeRefPtr<mozilla::dom::InternalResponse>(200, "OK"_ns); + ir->SetBody(loadContext->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. + loadContext->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; + MOZ_TRY(ssm->GetChannelResultPrincipal(channel, + getter_AddRefs(channelPrincipal))); + + UniquePtr<PrincipalInfo> principalInfo(new PrincipalInfo()); + MOZ_TRY(PrincipalToPrincipalInfo(channelPrincipal, principalInfo.get())); + + ir->SetPrincipalInfo(std::move(principalInfo)); + ir->Headers()->FillResponseHeaders(channel); + + RefPtr<mozilla::dom::Response> response = new mozilla::dom::Response( + mRequestHandle->GetCacheCreator()->Global(), std::move(ir), nullptr); + + mozilla::dom::RequestOrUSVString request; + + MOZ_ASSERT(!loadContext->mFullURL.IsEmpty()); + request.SetAsUSVString().ShareOrDependUpon(loadContext->mFullURL); + + // This JSContext will not end up executing JS code because here there are + // no ReadableStreams involved. + AutoJSAPI jsapi; + jsapi.Init(); + + ErrorResult error; + RefPtr<Promise> cachePromise = + mRequestHandle->GetCacheCreator()->Cache_()->Put(jsapi.cx(), request, + *response, error); + error.WouldReportJSException(); + if (NS_WARN_IF(error.Failed())) { + return error.StealNSResult(); + } + + RefPtr<CachePromiseHandler> promiseHandler = + new CachePromiseHandler(mLoader, mRequestHandle); + cachePromise->AppendNativeHandler(promiseHandler); + + loadContext->mCachePromise.swap(cachePromise); + loadContext->mCacheStatus = WorkerLoadContext::WritingToCache; + + return NS_OK; +} + +} // namespace workerinternals::loader + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/loader/NetworkLoadHandler.h b/dom/workers/loader/NetworkLoadHandler.h new file mode 100644 index 0000000000..b32c9d8d8e --- /dev/null +++ b/dom/workers/loader/NetworkLoadHandler.h @@ -0,0 +1,79 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_workers_NetworkLoadHandler_h__ +#define mozilla_dom_workers_NetworkLoadHandler_h__ + +#include "nsIStreamLoader.h" +#include "mozilla/dom/WorkerLoadContext.h" +#include "mozilla/dom/ScriptLoadHandler.h" +#include "mozilla/dom/WorkerRef.h" + +namespace mozilla::dom::workerinternals::loader { + +class WorkerScriptLoader; + +/* + * [DOMDOC] NetworkLoadHandler for Workers + * + * A LoadHandler is a ScriptLoader helper class that reacts to an + * nsIStreamLoader's events for + * loading JS scripts. It is primarily responsible for decoding the stream into + * UTF8 or UTF16. Additionally, it takes care of any work that needs to follow + * the completion of a stream. Every LoadHandler also manages additional tasks + * for the type of load that it is doing. + * + * As part of worker loading we have an number of tasks that we need to take + * care of after a successfully completed stream, including setting a final URI + * on the WorkerPrivate if we have loaded a main script, or handling CSP issues. + * These are handled in DataReceivedFromNetwork, and implement roughly the same + * set of tasks as you will find in the CacheLoadhandler, which has a companion + * method DataReceivedFromcache. + * + * In the worker context, the LoadHandler is run on the main thread, and all + * work in this file ultimately is done by the main thread, including decoding. + * + */ + +class NetworkLoadHandler final : public nsIStreamLoaderObserver, + public nsIRequestObserver { + public: + NS_DECL_ISUPPORTS + + NetworkLoadHandler(WorkerScriptLoader* aLoader, + ThreadSafeRequestHandle* aRequestHandle); + + NS_IMETHOD + OnStreamComplete(nsIStreamLoader* aLoader, nsISupports* aContext, + nsresult aStatus, uint32_t aStringLen, + const uint8_t* aString) override; + + nsresult DataReceivedFromNetwork(nsIStreamLoader* aLoader, nsresult aStatus, + uint32_t aStringLen, const uint8_t* aString); + + NS_IMETHOD + OnStartRequest(nsIRequest* aRequest) override; + + nsresult PrepareForRequest(nsIRequest* aRequest); + + NS_IMETHOD + OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) override { + // Nothing to do here! + return NS_OK; + } + + private: + ~NetworkLoadHandler() = default; + + RefPtr<WorkerScriptLoader> mLoader; + UniquePtr<ScriptDecoder> mDecoder; + RefPtr<ThreadSafeWorkerRef> mWorkerRef; + RefPtr<ThreadSafeRequestHandle> mRequestHandle; +}; + +} // namespace mozilla::dom::workerinternals::loader + +#endif /* mozilla_dom_workers_NetworkLoadHandler_h__ */ diff --git a/dom/workers/loader/ScriptResponseHeaderProcessor.cpp b/dom/workers/loader/ScriptResponseHeaderProcessor.cpp new file mode 100644 index 0000000000..7b844d6601 --- /dev/null +++ b/dom/workers/loader/ScriptResponseHeaderProcessor.cpp @@ -0,0 +1,64 @@ +/* -*- 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 "ScriptResponseHeaderProcessor.h" +#include "mozilla/dom/WorkerScope.h" + +namespace mozilla { +namespace dom { + +namespace workerinternals { + +namespace loader { + +NS_IMPL_ISUPPORTS(ScriptResponseHeaderProcessor, nsIRequestObserver); + +nsresult ScriptResponseHeaderProcessor::ProcessCrossOriginEmbedderPolicyHeader( + WorkerPrivate* aWorkerPrivate, + nsILoadInfo::CrossOriginEmbedderPolicy aPolicy, bool aIsMainScript) { + MOZ_ASSERT(aWorkerPrivate); + + if (aIsMainScript) { + MOZ_TRY(aWorkerPrivate->SetEmbedderPolicy(aPolicy)); + } else { + // NOTE: Spec doesn't mention non-main scripts must match COEP header with + // the main script, but it must pass CORP checking. + // see: wpt window-simple-success.https.html, the worker import script + // test-incrementer.js without coep header. + Unused << NS_WARN_IF(!aWorkerPrivate->MatchEmbedderPolicy(aPolicy)); + } + + return NS_OK; +} + +nsresult ScriptResponseHeaderProcessor::ProcessCrossOriginEmbedderPolicyHeader( + nsIRequest* aRequest) { + nsCOMPtr<nsIHttpChannelInternal> httpChannel = do_QueryInterface(aRequest); + + // NOTE: the spec doesn't say what to do with non-HTTP workers. + // See: https://github.com/whatwg/html/issues/4916 + if (!httpChannel) { + if (mIsMainScript) { + mWorkerPrivate->InheritOwnerEmbedderPolicyOrNull(aRequest); + } + + return NS_OK; + } + + nsILoadInfo::CrossOriginEmbedderPolicy coep; + MOZ_TRY(httpChannel->GetResponseEmbedderPolicy( + mWorkerPrivate->Trials().IsEnabled(OriginTrial::CoepCredentialless), + &coep)); + + return ProcessCrossOriginEmbedderPolicyHeader(mWorkerPrivate, coep, + mIsMainScript); +} + +} // namespace loader +} // namespace workerinternals + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/loader/ScriptResponseHeaderProcessor.h b/dom/workers/loader/ScriptResponseHeaderProcessor.h new file mode 100644 index 0000000000..755712c66f --- /dev/null +++ b/dom/workers/loader/ScriptResponseHeaderProcessor.h @@ -0,0 +1,78 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_workers_ScriptResponseHeaderProcessor_h__ +#define mozilla_dom_workers_ScriptResponseHeaderProcessor_h__ + +#include "mozilla/dom/WorkerCommon.h" + +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsIStreamLoader.h" +#include "nsStreamUtils.h" +#include "mozilla/StaticPrefs_browser.h" + +namespace mozilla::dom { + +class WorkerPrivate; + +namespace workerinternals::loader { + +/* ScriptResponseHeaderProcessor + * + * This class handles Policy headers. It can be used as a RequestObserver in a + * Tee, as it is for NetworkLoadHandler in WorkerScriptLoader, or the static + * method can be called directly, as it is in CacheLoadHandler. + * + */ + +class ScriptResponseHeaderProcessor final : public nsIRequestObserver { + public: + NS_DECL_ISUPPORTS + + ScriptResponseHeaderProcessor(WorkerPrivate* aWorkerPrivate, + bool aIsMainScript) + : mWorkerPrivate(aWorkerPrivate), mIsMainScript(aIsMainScript) { + AssertIsOnMainThread(); + } + + NS_IMETHOD OnStartRequest(nsIRequest* aRequest) override { + if (!StaticPrefs::browser_tabs_remote_useCrossOriginEmbedderPolicy()) { + return NS_OK; + } + + nsresult rv = ProcessCrossOriginEmbedderPolicyHeader(aRequest); + + if (NS_WARN_IF(NS_FAILED(rv))) { + aRequest->Cancel(rv); + } + + return rv; + } + + NS_IMETHOD OnStopRequest(nsIRequest* aRequest, + nsresult aStatusCode) override { + return NS_OK; + } + + static nsresult ProcessCrossOriginEmbedderPolicyHeader( + WorkerPrivate* aWorkerPrivate, + nsILoadInfo::CrossOriginEmbedderPolicy aPolicy, bool aIsMainScript); + + private: + ~ScriptResponseHeaderProcessor() = default; + + nsresult ProcessCrossOriginEmbedderPolicyHeader(nsIRequest* aRequest); + + WorkerPrivate* const mWorkerPrivate; + const bool mIsMainScript; +}; + +} // namespace workerinternals::loader + +} // namespace mozilla::dom + +#endif /* mozilla_dom_workers_ScriptResponseHeaderProcessor_h__ */ diff --git a/dom/workers/loader/WorkerLoadContext.cpp b/dom/workers/loader/WorkerLoadContext.cpp new file mode 100644 index 0000000000..c4f509fb94 --- /dev/null +++ b/dom/workers/loader/WorkerLoadContext.cpp @@ -0,0 +1,73 @@ +/* -*- 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 "WorkerLoadContext.h" +#include "mozilla/dom/workerinternals/ScriptLoader.h" +#include "CacheLoadHandler.h" // CacheCreator + +namespace mozilla { +namespace dom { + +WorkerLoadContext::WorkerLoadContext(Kind aKind, + const Maybe<ClientInfo>& aClientInfo) + : JS::loader::LoadContextBase(JS::loader::ContextKind::Worker), + mKind(aKind), + mClientInfo(aClientInfo){}; + +ThreadSafeRequestHandle::ThreadSafeRequestHandle( + JS::loader::ScriptLoadRequest* aRequest, nsISerialEventTarget* aSyncTarget) + : mRequest(aRequest), mOwningEventTarget(aSyncTarget) {} + +already_AddRefed<JS::loader::ScriptLoadRequest> +ThreadSafeRequestHandle::ReleaseRequest() { + RefPtr<JS::loader::ScriptLoadRequest> request; + mRequest.swap(request); + mRunnable = nullptr; + return request.forget(); +} + +nsresult ThreadSafeRequestHandle::OnStreamComplete(nsresult aStatus) { + return mRunnable->OnStreamComplete(this, aStatus); +} + +void ThreadSafeRequestHandle::LoadingFinished(nsresult aRv) { + mRunnable->LoadingFinished(this, aRv); +} + +void ThreadSafeRequestHandle::MaybeExecuteFinishedScripts() { + mRunnable->MaybeExecuteFinishedScripts(this); +} + +bool ThreadSafeRequestHandle::IsCancelled() { return mRunnable->IsCancelled(); } + +nsresult ThreadSafeRequestHandle::GetCancelResult() { + return mRunnable->GetCancelResult(); +} + +workerinternals::loader::CacheCreator* +ThreadSafeRequestHandle::GetCacheCreator() { + AssertIsOnMainThread(); + return mRunnable->GetCacheCreator(); +} + +ThreadSafeRequestHandle::~ThreadSafeRequestHandle() { + // Normally we only touch mStrongRef on the owning thread. This is safe, + // however, because when we do use mStrongRef on the owning thread we are + // always holding a strong ref to the ThreadsafeHandle via the owning + // runnable. So we cannot run the ThreadsafeHandle destructor simultaneously. + if (!mRequest || mOwningEventTarget->IsOnCurrentThread()) { + return; + } + + // Dispatch in NS_ProxyRelease is guaranteed to succeed here because we block + // shutdown until all Contexts have been destroyed. Therefore it is ok to have + // MOZ_ALWAYS_SUCCEED here. + MOZ_ALWAYS_SUCCEEDS(NS_ProxyRelease("ThreadSafeRequestHandle::mRequest", + mOwningEventTarget, mRequest.forget())); +} + +} // namespace dom +} // namespace mozilla diff --git a/dom/workers/loader/WorkerLoadContext.h b/dom/workers/loader/WorkerLoadContext.h new file mode 100644 index 0000000000..958d1f3f95 --- /dev/null +++ b/dom/workers/loader/WorkerLoadContext.h @@ -0,0 +1,205 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_workers_WorkerLoadContext_h__ +#define mozilla_dom_workers_WorkerLoadContext_h__ + +#include "nsIChannel.h" +#include "nsIInputStream.h" +#include "nsIRequest.h" +#include "mozilla/CORSMode.h" +#include "mozilla/dom/Promise.h" +#include "js/loader/ScriptKind.h" +#include "js/loader/ScriptLoadRequest.h" +#include "js/loader/LoadContextBase.h" + +class nsIReferrerInfo; +class nsIURI; + +namespace mozilla::dom { + +class ClientInfo; +class WorkerPrivate; + +namespace workerinternals::loader { +class CacheCreator; +class ScriptLoaderRunnable; +} // namespace workerinternals::loader + +/* + * WorkerLoadContext (for all workers) + * + * LoadContexts augment the loading of a ScriptLoadRequest. They + * describe how a ScriptLoadRequests loading and evaluation needs to be + * augmented, based on the information provided by the loading context. The + * WorkerLoadContext has the following generic fields applied to all worker + * ScriptLoadRequests (and primarily used for error handling): + * + * * mMutedErrorFlag + * Set when we finish loading a script, and used to determine whether a + * given error is thrown or muted. + * * mLoadResult + * In order to report errors correctly in the worker thread, we need to + * move them from the main thread to the worker. This field records the + * load error, for throwing when we return to the worker thread. + * * mKind + * See documentation of WorkerLoadContext::Kind. + * * mClientInfo + * A snapshot of a global living in the system (see documentation for + * ClientInfo). In worker loading, this field is important for CSP + * information and knowing what to intercept for Service Worker + * interception. + * * mChannel + * The channel used by this request for it's load. Used for cancellation, + * in order to cancel the stream. + * + * The rest of the fields on this class focus on enabling the ServiceWorker + * usecase, in particular -- using the Cache API to store the worker so that + * in the case of (for example) a page refresh, the service worker itself is + * persisted so that it can do other work. For more details see the + * CacheLoadHandler.h file. + * + */ + +class WorkerLoadContext : public JS::loader::LoadContextBase { + public: + /* Worker Load Context Kinds + * + * A script that is loaded and run as a worker can be one of several species. + * Each may have slightly different behavior, but they fall into roughly two + * categories: the Main Worker Script (the script that triggers the first + * load) and scripts that are attached to this main worker script. + * + * In the specification, the Main Worker Script is referred to as the "top + * level script" and is defined here: + * https://html.spec.whatwg.org/multipage/webappapis.html#fetching-scripts-is-top-level + */ + + enum Kind { + // Indicates that the is-top-level bit is true. This may be a Classic script + // or a Module script. + MainScript, + // We are importing a script from the worker via ImportScript. This may only + // be a Classic script. + ImportScript, + // We have an attached debugger, and these should be treated specially and + // not like a main script (regardless of their type). This is not part of + // the specification. + DebuggerScript + }; + + explicit WorkerLoadContext(Kind aKind, const Maybe<ClientInfo>& aClientInfo); + + // Used to detect if the `is top-level` bit is set on a given module. + bool IsTopLevel() { + return mRequest->IsTopLevel() && (mKind == Kind::MainScript); + }; + + static Kind GetKind(bool isMainScript, bool isDebuggerScript) { + if (isDebuggerScript) { + return Kind::DebuggerScript; + } + if (isMainScript) { + return Kind::MainScript; + } + return Kind::ImportScript; + }; + + /* These fields are used by all workers */ + Maybe<bool> mMutedErrorFlag; + nsresult mLoadResult = NS_ERROR_NOT_INITIALIZED; + bool mLoadingFinished = false; + bool mIsTopLevel = true; + Kind mKind; + Maybe<ClientInfo> mClientInfo; + nsCOMPtr<nsIChannel> mChannel; + + /* These fields are only used by service workers */ + /* TODO: Split out a ServiceWorkerLoadContext */ + // 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; + + 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 = Uncached; + + bool IsAwaitingPromise() const { return bool(mCachePromise); } +}; + +class ThreadSafeRequestHandle final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ThreadSafeRequestHandle) + + ThreadSafeRequestHandle(JS::loader::ScriptLoadRequest* aRequest, + nsISerialEventTarget* aSyncTarget); + + JS::loader::ScriptLoadRequest* GetRequest() const { return mRequest; } + + WorkerLoadContext* GetContext() { return mRequest->GetWorkerLoadContext(); } + + bool IsEmpty() { return !mRequest; } + + // Runnable controls + nsresult OnStreamComplete(nsresult aStatus); + + void LoadingFinished(nsresult aRv); + + void MaybeExecuteFinishedScripts(); + + bool IsCancelled(); + + bool Finished() { + return GetContext()->mLoadingFinished && !GetContext()->IsAwaitingPromise(); + } + + nsresult GetCancelResult(); + + already_AddRefed<JS::loader::ScriptLoadRequest> ReleaseRequest(); + + workerinternals::loader::CacheCreator* GetCacheCreator(); + + RefPtr<workerinternals::loader::ScriptLoaderRunnable> mRunnable; + + bool mExecutionScheduled = false; + + private: + ~ThreadSafeRequestHandle(); + + RefPtr<JS::loader::ScriptLoadRequest> mRequest; + nsCOMPtr<nsISerialEventTarget> mOwningEventTarget; +}; + +} // namespace mozilla::dom +#endif /* mozilla_dom_workers_WorkerLoadContext_h__ */ diff --git a/dom/workers/loader/moz.build b/dom/workers/loader/moz.build new file mode 100644 index 0000000000..3f36fdfc59 --- /dev/null +++ b/dom/workers/loader/moz.build @@ -0,0 +1,32 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Workers") + +# Public stuff. +EXPORTS.mozilla.dom += [ + "WorkerLoadContext.h", +] + +# Private stuff. +EXPORTS.mozilla.dom.workerinternals += [ + "CacheLoadHandler.h", + "NetworkLoadHandler.h", + "ScriptResponseHeaderProcessor.h", +] + +UNIFIED_SOURCES += [ + "CacheLoadHandler.cpp", + "NetworkLoadHandler.cpp", + "ScriptResponseHeaderProcessor.cpp", + "WorkerLoadContext.cpp", +] + + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" |