diff options
Diffstat (limited to 'dom/fetch')
57 files changed, 13558 insertions, 0 deletions
diff --git a/dom/fetch/BodyExtractor.cpp b/dom/fetch/BodyExtractor.cpp new file mode 100644 index 0000000000..c4a6e3a399 --- /dev/null +++ b/dom/fetch/BodyExtractor.cpp @@ -0,0 +1,182 @@ +/* -*- 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 "BodyExtractor.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/FormData.h" +#include "mozilla/dom/TypedArray.h" +#include "mozilla/dom/URLSearchParams.h" +#include "mozilla/dom/XMLHttpRequest.h" +#include "mozilla/UniquePtr.h" +#include "nsContentUtils.h" +#include "nsDOMSerializer.h" +#include "nsIGlobalObject.h" +#include "nsIInputStream.h" +#include "nsIOutputStream.h" +#include "nsIStorageStream.h" +#include "nsStringStream.h" + +namespace mozilla::dom { + +static nsresult GetBufferDataAsStream( + const uint8_t* aData, uint32_t aDataLength, nsIInputStream** aResult, + uint64_t* aContentLength, nsACString& aContentType, nsACString& aCharset) { + aContentType.SetIsVoid(true); + aCharset.Truncate(); + + *aContentLength = aDataLength; + const char* data = reinterpret_cast<const char*>(aData); + + nsCOMPtr<nsIInputStream> stream; + nsresult rv = NS_NewByteInputStream( + getter_AddRefs(stream), Span(data, aDataLength), NS_ASSIGNMENT_COPY); + NS_ENSURE_SUCCESS(rv, rv); + + stream.forget(aResult); + + return NS_OK; +} + +template <> +nsresult BodyExtractor<const ArrayBuffer>::GetAsStream( + nsIInputStream** aResult, uint64_t* aContentLength, + nsACString& aContentTypeWithCharset, nsACString& aCharset) const { + mBody->ComputeState(); + return GetBufferDataAsStream(mBody->Data(), mBody->Length(), aResult, + aContentLength, aContentTypeWithCharset, + aCharset); +} + +template <> +nsresult BodyExtractor<const ArrayBufferView>::GetAsStream( + nsIInputStream** aResult, uint64_t* aContentLength, + nsACString& aContentTypeWithCharset, nsACString& aCharset) const { + mBody->ComputeState(); + return GetBufferDataAsStream(mBody->Data(), mBody->Length(), aResult, + aContentLength, aContentTypeWithCharset, + aCharset); +} + +template <> +nsresult BodyExtractor<Document>::GetAsStream( + nsIInputStream** aResult, uint64_t* aContentLength, + nsACString& aContentTypeWithCharset, nsACString& aCharset) const { + NS_ENSURE_STATE(mBody); + aCharset.AssignLiteral("UTF-8"); + + nsresult rv; + nsCOMPtr<nsIStorageStream> storStream; + rv = NS_NewStorageStream(4096, UINT32_MAX, getter_AddRefs(storStream)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIOutputStream> output; + rv = storStream->GetOutputStream(0, getter_AddRefs(output)); + NS_ENSURE_SUCCESS(rv, rv); + + if (mBody->IsHTMLDocument()) { + aContentTypeWithCharset.AssignLiteral("text/html;charset=UTF-8"); + + nsString serialized; + if (!nsContentUtils::SerializeNodeToMarkup(mBody, true, serialized)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + nsAutoCString utf8Serialized; + if (!AppendUTF16toUTF8(serialized, utf8Serialized, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + uint32_t written; + rv = output->Write(utf8Serialized.get(), utf8Serialized.Length(), &written); + NS_ENSURE_SUCCESS(rv, rv); + + MOZ_ASSERT(written == utf8Serialized.Length()); + } else { + aContentTypeWithCharset.AssignLiteral("application/xml;charset=UTF-8"); + + auto serializer = MakeUnique<nsDOMSerializer>(); + + // Make sure to use the encoding we'll send + ErrorResult res; + serializer->SerializeToStream(*mBody, output, u"UTF-8"_ns, res); + if (NS_WARN_IF(res.Failed())) { + return res.StealNSResult(); + } + } + + output->Close(); + + uint32_t length; + rv = storStream->GetLength(&length); + NS_ENSURE_SUCCESS(rv, rv); + *aContentLength = length; + + rv = storStream->NewInputStream(0, aResult); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +template <> +nsresult BodyExtractor<const nsAString>::GetAsStream( + nsIInputStream** aResult, uint64_t* aContentLength, + nsACString& aContentTypeWithCharset, nsACString& aCharset) const { + nsCString encoded; + if (!CopyUTF16toUTF8(*mBody, encoded, fallible)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + uint32_t encodedLength = encoded.Length(); + nsresult rv = NS_NewCStringInputStream(aResult, std::move(encoded)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + *aContentLength = encodedLength; + aContentTypeWithCharset.AssignLiteral("text/plain;charset=UTF-8"); + aCharset.AssignLiteral("UTF-8"); + return NS_OK; +} + +template <> +nsresult BodyExtractor<nsIInputStream>::GetAsStream( + nsIInputStream** aResult, uint64_t* aContentLength, + nsACString& aContentTypeWithCharset, nsACString& aCharset) const { + aContentTypeWithCharset.AssignLiteral("text/plain"); + aCharset.Truncate(); + + nsresult rv = mBody->Available(aContentLength); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIInputStream> stream(mBody); + stream.forget(aResult); + return NS_OK; +} + +template <> +nsresult BodyExtractor<const Blob>::GetAsStream( + nsIInputStream** aResult, uint64_t* aContentLength, + nsACString& aContentTypeWithCharset, nsACString& aCharset) const { + return mBody->GetSendInfo(aResult, aContentLength, aContentTypeWithCharset, + aCharset); +} + +template <> +nsresult BodyExtractor<const FormData>::GetAsStream( + nsIInputStream** aResult, uint64_t* aContentLength, + nsACString& aContentTypeWithCharset, nsACString& aCharset) const { + return mBody->GetSendInfo(aResult, aContentLength, aContentTypeWithCharset, + aCharset); +} + +template <> +nsresult BodyExtractor<const URLSearchParams>::GetAsStream( + nsIInputStream** aResult, uint64_t* aContentLength, + nsACString& aContentTypeWithCharset, nsACString& aCharset) const { + return mBody->GetSendInfo(aResult, aContentLength, aContentTypeWithCharset, + aCharset); +} + +} // namespace mozilla::dom diff --git a/dom/fetch/BodyExtractor.h b/dom/fetch/BodyExtractor.h new file mode 100644 index 0000000000..08e5210afc --- /dev/null +++ b/dom/fetch/BodyExtractor.h @@ -0,0 +1,42 @@ +/* -*- 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_BodyExtractor_h +#define mozilla_dom_BodyExtractor_h + +#include "nsString.h" + +class nsIInputStream; +class nsIGlobalObject; + +namespace mozilla::dom { + +class BodyExtractorBase { + public: + virtual nsresult GetAsStream(nsIInputStream** aResult, + uint64_t* aContentLength, + nsACString& aContentTypeWithCharset, + nsACString& aCharset) const = 0; +}; + +// The implementation versions of this template are: +// ArrayBuffer, ArrayBufferView, Blob, FormData, +// URLSearchParams, nsAString, Document, nsIInputStream. +template <typename Type> +class BodyExtractor final : public BodyExtractorBase { + Type* mBody; + + public: + explicit BodyExtractor(Type* aBody) : mBody(aBody) {} + + nsresult GetAsStream(nsIInputStream** aResult, uint64_t* aContentLength, + nsACString& aContentTypeWithCharset, + nsACString& aCharset) const override; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_BodyExtractor_h diff --git a/dom/fetch/ChannelInfo.cpp b/dom/fetch/ChannelInfo.cpp new file mode 100644 index 0000000000..71b9e3277b --- /dev/null +++ b/dom/fetch/ChannelInfo.cpp @@ -0,0 +1,95 @@ +/* -*- 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 "mozilla/dom/ChannelInfo.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsIChannel.h" +#include "mozilla/dom/Document.h" +#include "nsIGlobalObject.h" +#include "nsIHttpChannel.h" +#include "nsSerializationHelper.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/net/HttpBaseChannel.h" +#include "nsNetUtil.h" + +using namespace mozilla; +using namespace mozilla::dom; + +void ChannelInfo::InitFromDocument(Document* aDoc) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mInited, "Cannot initialize the object twice"); + + nsCOMPtr<nsITransportSecurityInfo> securityInfo(aDoc->GetSecurityInfo()); + if (securityInfo) { + SetSecurityInfo(securityInfo); + } + + mInited = true; +} + +void ChannelInfo::InitFromChannel(nsIChannel* aChannel) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mInited, "Cannot initialize the object twice"); + + nsCOMPtr<nsITransportSecurityInfo> securityInfo; + aChannel->GetSecurityInfo(getter_AddRefs(securityInfo)); + if (securityInfo) { + SetSecurityInfo(securityInfo); + } + + mInited = true; +} + +void ChannelInfo::InitFromChromeGlobal(nsIGlobalObject* aGlobal) { + MOZ_ASSERT(!mInited, "Cannot initialize the object twice"); + MOZ_ASSERT(aGlobal); + + MOZ_RELEASE_ASSERT(aGlobal->PrincipalOrNull()->IsSystemPrincipal()); + + mSecurityInfo = nullptr; + mInited = true; +} + +void ChannelInfo::InitFromTransportSecurityInfo( + nsITransportSecurityInfo* aSecurityInfo) { + MOZ_ASSERT(!mInited, "Cannot initialize the object twice"); + + mSecurityInfo = aSecurityInfo; + mInited = true; +} + +void ChannelInfo::SetSecurityInfo(nsITransportSecurityInfo* aSecurityInfo) { + MOZ_ASSERT(!mSecurityInfo, "security info should only be set once"); + mSecurityInfo = aSecurityInfo; +} + +nsresult ChannelInfo::ResurrectInfoOnChannel(nsIChannel* aChannel) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mInited); + + if (mSecurityInfo) { + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel); + MOZ_ASSERT(httpChannel); + net::HttpBaseChannel* httpBaseChannel = + static_cast<net::HttpBaseChannel*>(httpChannel.get()); + nsresult rv = httpBaseChannel->OverrideSecurityInfo(mSecurityInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return NS_OK; +} + +already_AddRefed<nsITransportSecurityInfo> ChannelInfo::SecurityInfo() const { + // This may be called when mInited is false, for example if we try to store + // a synthesized Response object into the Cache. Uninitialized and empty + // ChannelInfo objects are indistinguishable at the IPC level, so this is + // fine. + nsCOMPtr<nsITransportSecurityInfo> securityInfo(mSecurityInfo); + return securityInfo.forget(); +} diff --git a/dom/fetch/ChannelInfo.h b/dom/fetch/ChannelInfo.h new file mode 100644 index 0000000000..7df6c53ec4 --- /dev/null +++ b/dom/fetch/ChannelInfo.h @@ -0,0 +1,70 @@ +/* -*- 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_ChannelInfo_h +#define mozilla_dom_ChannelInfo_h + +#include "nsCOMPtr.h" +#include "nsITransportSecurityInfo.h" +#include "nsString.h" + +class nsIChannel; +class nsIGlobalObject; +class nsIURI; + +namespace mozilla { +namespace dom { + +class Document; + +// This class represents the information related to a Response that we +// retrieve from the corresponding channel that is used to perform the fetch. +// +// When adding new members to this object, the following code needs to be +// updated: +// * InitFromChannel and InitFromTransportSecurityInfo members +// * ResurrectInfoOnChannel member +// * SecurityInfo member +// * constructors and assignment operators for this class. +// * DOM Cache schema code (in dom/cache/DBSchema.cpp) to ensure that the newly +// added member is saved into the DB and loaded from it properly. +// +// Care must be taken when initializing this object, or when calling +// ResurrectInfoOnChannel(). This object cannot be initialized twice, and +// ResurrectInfoOnChannel() cannot be called on it before it has been +// initialized. There are assertions ensuring these invariants. +class ChannelInfo final { + public: + ChannelInfo() : mInited(false) {} + + ChannelInfo(const ChannelInfo& aRHS) = default; + + ChannelInfo& operator=(const ChannelInfo& aRHS) = default; + + void InitFromDocument(Document* aDoc); + void InitFromChannel(nsIChannel* aChannel); + void InitFromChromeGlobal(nsIGlobalObject* aGlobal); + void InitFromTransportSecurityInfo(nsITransportSecurityInfo* aSecurityInfo); + + // This restores every possible information stored from a previous channel + // object on a new one. + nsresult ResurrectInfoOnChannel(nsIChannel* aChannel); + + bool IsInitialized() const { return mInited; } + + already_AddRefed<nsITransportSecurityInfo> SecurityInfo() const; + + private: + void SetSecurityInfo(nsITransportSecurityInfo* aSecurityInfo); + + nsCOMPtr<nsITransportSecurityInfo> mSecurityInfo; + bool mInited; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ChannelInfo_h diff --git a/dom/fetch/Fetch.cpp b/dom/fetch/Fetch.cpp new file mode 100644 index 0000000000..c4589bd513 --- /dev/null +++ b/dom/fetch/Fetch.cpp @@ -0,0 +1,1683 @@ +/* -*- 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 "Fetch.h" + +#include "js/RootingAPI.h" +#include "js/Value.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/dom/Document.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "nsIGlobalObject.h" + +#include "nsDOMString.h" +#include "nsJSUtils.h" +#include "nsNetUtil.h" +#include "nsReadableUtils.h" +#include "nsStreamUtils.h" +#include "nsStringStream.h" +#include "nsProxyRelease.h" + +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/BodyConsumer.h" +#include "mozilla/dom/Exceptions.h" +#include "mozilla/dom/DOMException.h" +#include "mozilla/dom/FetchDriver.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/FormData.h" +#include "mozilla/dom/Headers.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseWorkerProxy.h" +#include "mozilla/dom/ReadableStreamDefaultReader.h" +#include "mozilla/dom/RemoteWorkerChild.h" +#include "mozilla/dom/Request.h" +#include "mozilla/dom/Response.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/URLSearchParams.h" +#include "mozilla/net/CookieJarSettings.h" + +#include "BodyExtractor.h" +#include "FetchChild.h" +#include "FetchObserver.h" +#include "InternalRequest.h" +#include "InternalResponse.h" + +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/dom/WorkerRef.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "mozilla/dom/WorkerScope.h" + +namespace mozilla::dom { + +namespace { + +// Step 17.2.1.2 and 17.2.2 of +// https://fetch.spec.whatwg.org/#concept-http-network-fetch +// If stream is readable, then error stream with ... +void AbortStream(JSContext* aCx, ReadableStream* aReadableStream, + ErrorResult& aRv, JS::Handle<JS::Value> aReasonDetails) { + if (aReadableStream->State() != ReadableStream::ReaderState::Readable) { + return; + } + + JS::Rooted<JS::Value> value(aCx, aReasonDetails); + + if (aReasonDetails.isUndefined()) { + RefPtr<DOMException> e = DOMException::Create(NS_ERROR_DOM_ABORT_ERR); + if (!GetOrCreateDOMReflector(aCx, e, &value)) { + return; + } + } + + aReadableStream->ErrorNative(aCx, value, aRv); +} + +} // namespace + +class AbortSignalMainThread final : public AbortSignalImpl { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(AbortSignalMainThread) + + explicit AbortSignalMainThread(bool aAborted) + : AbortSignalImpl(aAborted, JS::UndefinedHandleValue) { + mozilla::HoldJSObjects(this); + } + + private: + ~AbortSignalMainThread() { mozilla::DropJSObjects(this); }; +}; + +NS_IMPL_CYCLE_COLLECTION_CLASS(AbortSignalMainThread) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AbortSignalMainThread) + AbortSignalImpl::Unlink(static_cast<AbortSignalImpl*>(tmp)); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AbortSignalMainThread) + AbortSignalImpl::Traverse(static_cast<AbortSignalImpl*>(tmp), cb); +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(AbortSignalMainThread) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mReason) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AbortSignalMainThread) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(AbortSignalMainThread) +NS_IMPL_CYCLE_COLLECTING_RELEASE(AbortSignalMainThread) + +class AbortSignalProxy; + +// This runnable propagates changes from the AbortSignalImpl on workers to the +// AbortSignalImpl on main-thread. +class AbortSignalProxyRunnable final : public Runnable { + RefPtr<AbortSignalProxy> mProxy; + + public: + explicit AbortSignalProxyRunnable(AbortSignalProxy* aProxy) + : Runnable("dom::AbortSignalProxyRunnable"), mProxy(aProxy) {} + + NS_IMETHOD Run() override; +}; + +// This class orchestrates the proxying of AbortSignal operations between the +// main thread and a worker thread. +class AbortSignalProxy final : public AbortFollower { + // This is created and released on the main-thread. + RefPtr<AbortSignalImpl> mSignalImplMainThread; + + // The main-thread event target for runnable dispatching. + nsCOMPtr<nsIEventTarget> mMainThreadEventTarget; + + // This value is used only when creating mSignalImplMainThread on the main + // thread, to create it in already-aborted state if necessary. It does *not* + // reflect the instantaneous is-aborted status of the worker thread's + // AbortSignal. + const bool mAborted; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + + AbortSignalProxy(AbortSignalImpl* aSignalImpl, + nsIEventTarget* aMainThreadEventTarget) + : mMainThreadEventTarget(aMainThreadEventTarget), + mAborted(aSignalImpl->Aborted()) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(mMainThreadEventTarget); + Follow(aSignalImpl); + } + + // AbortFollower + void RunAbortAlgorithm() override; + + AbortSignalImpl* GetOrCreateSignalImplForMainThread() { + MOZ_ASSERT(NS_IsMainThread()); + if (!mSignalImplMainThread) { + mSignalImplMainThread = new AbortSignalMainThread(mAborted); + } + return mSignalImplMainThread; + } + + AbortSignalImpl* GetSignalImplForTargetThread() { + MOZ_ASSERT(!NS_IsMainThread()); + return Signal(); + } + + nsIEventTarget* MainThreadEventTarget() { return mMainThreadEventTarget; } + + void Shutdown() { + MOZ_ASSERT(!NS_IsMainThread()); + Unfollow(); + } + + private: + ~AbortSignalProxy() { + NS_ProxyRelease("AbortSignalProxy::mSignalImplMainThread", + mMainThreadEventTarget, mSignalImplMainThread.forget()); + } +}; + +NS_IMPL_ISUPPORTS0(AbortSignalProxy) + +NS_IMETHODIMP AbortSignalProxyRunnable::Run() { + MOZ_ASSERT(NS_IsMainThread()); + AbortSignalImpl* signalImpl = mProxy->GetOrCreateSignalImplForMainThread(); + signalImpl->SignalAbort(JS::UndefinedHandleValue); + return NS_OK; +} + +void AbortSignalProxy::RunAbortAlgorithm() { + MOZ_ASSERT(!NS_IsMainThread()); + RefPtr<AbortSignalProxyRunnable> runnable = + new AbortSignalProxyRunnable(this); + MainThreadEventTarget()->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL); +} + +class WorkerFetchResolver final : public FetchDriverObserver { + // Thread-safe: + RefPtr<PromiseWorkerProxy> mPromiseProxy; + RefPtr<AbortSignalProxy> mSignalProxy; + + // Touched only on the worker thread. + RefPtr<FetchObserver> mFetchObserver; + RefPtr<WeakWorkerRef> mWorkerRef; + bool mIsShutdown; + + Atomic<bool> mNeedOnDataAvailable; + + public: + // Returns null if worker is shutting down. + static already_AddRefed<WorkerFetchResolver> Create( + WorkerPrivate* aWorkerPrivate, Promise* aPromise, + AbortSignalImpl* aSignalImpl, FetchObserver* aObserver) { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + RefPtr<PromiseWorkerProxy> proxy = + PromiseWorkerProxy::Create(aWorkerPrivate, aPromise); + if (!proxy) { + return nullptr; + } + + RefPtr<AbortSignalProxy> signalProxy; + if (aSignalImpl) { + signalProxy = new AbortSignalProxy( + aSignalImpl, aWorkerPrivate->MainThreadEventTarget()); + } + + RefPtr<WorkerFetchResolver> r = + new WorkerFetchResolver(proxy, signalProxy, aObserver); + + RefPtr<WeakWorkerRef> workerRef = WeakWorkerRef::Create( + aWorkerPrivate, [r]() { r->Shutdown(r->mWorkerRef->GetPrivate()); }); + if (NS_WARN_IF(!workerRef)) { + return nullptr; + } + + r->mWorkerRef = std::move(workerRef); + + return r.forget(); + } + + AbortSignalImpl* GetAbortSignalForMainThread() { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mSignalProxy) { + return nullptr; + } + + return mSignalProxy->GetOrCreateSignalImplForMainThread(); + } + + AbortSignalImpl* GetAbortSignalForTargetThread() { + mPromiseProxy->GetWorkerPrivate()->AssertIsOnWorkerThread(); + + if (!mSignalProxy) { + return nullptr; + } + + return mSignalProxy->GetSignalImplForTargetThread(); + } + + PromiseWorkerProxy* PromiseProxy() const { + MOZ_ASSERT(NS_IsMainThread()); + return mPromiseProxy; + } + + Promise* WorkerPromise(WorkerPrivate* aWorkerPrivate) const { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + MOZ_ASSERT(!mIsShutdown); + + return mPromiseProxy->WorkerPromise(); + } + + FetchObserver* GetFetchObserver(WorkerPrivate* aWorkerPrivate) const { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + return mFetchObserver; + } + + void OnResponseAvailableInternal( + SafeRefPtr<InternalResponse> aResponse) override; + + void OnResponseEnd(FetchDriverObserver::EndReason aReason, + JS::Handle<JS::Value> aReasonDetails) override; + + bool NeedOnDataAvailable() override; + + void OnDataAvailable() override; + + void Shutdown(WorkerPrivate* aWorkerPrivate) { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + mIsShutdown = true; + mPromiseProxy->CleanUp(); + + mNeedOnDataAvailable = false; + mFetchObserver = nullptr; + + if (mSignalProxy) { + mSignalProxy->Shutdown(); + } + + mWorkerRef = nullptr; + } + + bool IsShutdown(WorkerPrivate* aWorkerPrivate) const { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + return mIsShutdown; + } + + private: + WorkerFetchResolver(PromiseWorkerProxy* aProxy, + AbortSignalProxy* aSignalProxy, FetchObserver* aObserver) + : mPromiseProxy(aProxy), + mSignalProxy(aSignalProxy), + mFetchObserver(aObserver), + mIsShutdown(false), + mNeedOnDataAvailable(!!aObserver) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(mPromiseProxy); + } + + ~WorkerFetchResolver() = default; + + virtual void FlushConsoleReport() override; +}; + +void FetchDriverObserver::OnResponseAvailable( + SafeRefPtr<InternalResponse> aResponse) { + MOZ_ASSERT(!mGotResponseAvailable); + mGotResponseAvailable = true; + OnResponseAvailableInternal(std::move(aResponse)); +} + +class MainThreadFetchResolver final : public FetchDriverObserver { + RefPtr<Promise> mPromise; + RefPtr<Response> mResponse; + RefPtr<FetchObserver> mFetchObserver; + RefPtr<AbortSignalImpl> mSignalImpl; + const bool mMozErrors; + + nsCOMPtr<nsILoadGroup> mLoadGroup; + + NS_DECL_OWNINGTHREAD + public: + MainThreadFetchResolver(Promise* aPromise, FetchObserver* aObserver, + AbortSignalImpl* aSignalImpl, bool aMozErrors) + : mPromise(aPromise), + mFetchObserver(aObserver), + mSignalImpl(aSignalImpl), + mMozErrors(aMozErrors) {} + + void OnResponseAvailableInternal( + SafeRefPtr<InternalResponse> aResponse) override; + + void SetLoadGroup(nsILoadGroup* aLoadGroup) { mLoadGroup = aLoadGroup; } + + void OnResponseEnd(FetchDriverObserver::EndReason aReason, + JS::Handle<JS::Value> aReasonDetails) override { + if (aReason == eAborted) { + if (!aReasonDetails.isUndefined()) { + mPromise->MaybeReject(aReasonDetails); + } else { + mPromise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + } + } + + mFetchObserver = nullptr; + + FlushConsoleReport(); + } + + bool NeedOnDataAvailable() override; + + void OnDataAvailable() override; + + private: + ~MainThreadFetchResolver(); + + void FlushConsoleReport() override { + mReporter->FlushConsoleReports(mLoadGroup); + } +}; + +class MainThreadFetchRunnable : public Runnable { + RefPtr<WorkerFetchResolver> mResolver; + const ClientInfo mClientInfo; + const Maybe<ServiceWorkerDescriptor> mController; + nsCOMPtr<nsICSPEventListener> mCSPEventListener; + SafeRefPtr<InternalRequest> mRequest; + UniquePtr<SerializedStackHolder> mOriginStack; + + public: + MainThreadFetchRunnable(WorkerFetchResolver* aResolver, + const ClientInfo& aClientInfo, + const Maybe<ServiceWorkerDescriptor>& aController, + nsICSPEventListener* aCSPEventListener, + SafeRefPtr<InternalRequest> aRequest, + UniquePtr<SerializedStackHolder>&& aOriginStack) + : Runnable("dom::MainThreadFetchRunnable"), + mResolver(aResolver), + mClientInfo(aClientInfo), + mController(aController), + mCSPEventListener(aCSPEventListener), + mRequest(std::move(aRequest)), + mOriginStack(std::move(aOriginStack)) { + MOZ_ASSERT(mResolver); + } + + NS_IMETHOD + Run() override { + AssertIsOnMainThread(); + RefPtr<FetchDriver> fetch; + RefPtr<PromiseWorkerProxy> proxy = mResolver->PromiseProxy(); + + { + // Acquire the proxy mutex while getting data from the WorkerPrivate... + MutexAutoLock lock(proxy->Lock()); + if (proxy->CleanedUp()) { + NS_WARNING("Aborting Fetch because worker already shut down"); + return NS_OK; + } + + WorkerPrivate* workerPrivate = proxy->GetWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + nsCOMPtr<nsIPrincipal> principal = workerPrivate->GetPrincipal(); + MOZ_ASSERT(principal); + nsCOMPtr<nsILoadGroup> loadGroup = workerPrivate->GetLoadGroup(); + MOZ_ASSERT(loadGroup); + // We don't track if a worker is spawned from a tracking script for now, + // so pass false as the last argument to FetchDriver(). + fetch = new FetchDriver(mRequest.clonePtr(), principal, loadGroup, + workerPrivate->MainThreadEventTarget(), + workerPrivate->CookieJarSettings(), + workerPrivate->GetPerformanceStorage(), false); + nsAutoCString spec; + if (proxy->GetWorkerPrivate()->GetBaseURI()) { + proxy->GetWorkerPrivate()->GetBaseURI()->GetAsciiSpec(spec); + } + fetch->SetWorkerScript(spec); + + fetch->SetClientInfo(mClientInfo); + fetch->SetController(mController); + fetch->SetCSPEventListener(mCSPEventListener); + } + + fetch->SetOriginStack(std::move(mOriginStack)); + + RefPtr<AbortSignalImpl> signalImpl = + mResolver->GetAbortSignalForMainThread(); + + // ...but release it before calling Fetch, because mResolver's callback can + // be called synchronously and they want the mutex, too. + return fetch->Fetch(signalImpl, mResolver); + } +}; + +already_AddRefed<Promise> FetchRequest(nsIGlobalObject* aGlobal, + const RequestOrUSVString& aInput, + const RequestInit& aInit, + CallerType aCallerType, + ErrorResult& aRv) { + RefPtr<Promise> p = Promise::Create(aGlobal, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + MOZ_ASSERT(aGlobal); + + // Double check that we have chrome privileges if the Request's content + // policy type has been overridden. + MOZ_ASSERT_IF(aInput.IsRequest() && + aInput.GetAsRequest().IsContentPolicyTypeOverridden(), + aCallerType == CallerType::System); + + AutoJSAPI jsapi; + if (!jsapi.Init(aGlobal)) { + aRv.Throw(NS_ERROR_NOT_AVAILABLE); + return nullptr; + } + + JSContext* cx = jsapi.cx(); + JS::Rooted<JSObject*> jsGlobal(cx, aGlobal->GetGlobalJSObject()); + GlobalObject global(cx, jsGlobal); + + SafeRefPtr<Request> request = + Request::Constructor(global, aInput, aInit, aRv); + if (aRv.Failed()) { + return nullptr; + } + + SafeRefPtr<InternalRequest> r = request->GetInternalRequest(); + + // Restore information of InterceptedHttpChannel if they are passed with the + // Request. Since Request::Constructor would not copy these members. + if (aInput.IsRequest()) { + RefPtr<Request> inputReq = &aInput.GetAsRequest(); + SafeRefPtr<InternalRequest> inputInReq = inputReq->GetInternalRequest(); + if (inputInReq->GetInterceptionTriggeringPrincipalInfo()) { + r->SetInterceptionContentPolicyType( + inputInReq->InterceptionContentPolicyType()); + r->SetInterceptionTriggeringPrincipalInfo( + MakeUnique<mozilla::ipc::PrincipalInfo>( + *(inputInReq->GetInterceptionTriggeringPrincipalInfo().get()))); + if (!inputInReq->InterceptionRedirectChain().IsEmpty()) { + r->SetInterceptionRedirectChain( + inputInReq->InterceptionRedirectChain()); + } + r->SetInterceptionFromThirdParty( + inputInReq->InterceptionFromThirdParty()); + } + } + + RefPtr<AbortSignalImpl> signalImpl = request->GetSignalImpl(); + + if (signalImpl && signalImpl->Aborted()) { + // Already aborted signal rejects immediately. + JS::Rooted<JS::Value> reason(cx, signalImpl->RawReason()); + if (reason.get().isUndefined()) { + aRv.Throw(NS_ERROR_DOM_ABORT_ERR); + return nullptr; + } + + p->MaybeReject(reason); + return p.forget(); + } + + JS::Realm* realm = JS::GetCurrentRealmOrNull(cx); + if (realm && JS::GetDebuggerObservesWasm(realm)) { + r->SetSkipWasmCaching(); + } + + RefPtr<FetchObserver> observer; + if (aInit.mObserve.WasPassed()) { + observer = new FetchObserver(aGlobal, signalImpl); + aInit.mObserve.Value().HandleEvent(*observer); + } + + if (NS_IsMainThread()) { + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal); + nsCOMPtr<Document> doc; + nsCOMPtr<nsILoadGroup> loadGroup; + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + nsIPrincipal* principal; + bool isTrackingFetch = false; + if (window) { + doc = window->GetExtantDoc(); + if (!doc) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + principal = doc->NodePrincipal(); + loadGroup = doc->GetDocumentLoadGroup(); + cookieJarSettings = doc->CookieJarSettings(); + + isTrackingFetch = doc->IsScriptTracking(cx); + } else { + principal = aGlobal->PrincipalOrNull(); + if (NS_WARN_IF(!principal)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + cookieJarSettings = mozilla::net::CookieJarSettings::Create(principal); + } + + if (!loadGroup) { + nsresult rv = NS_NewLoadGroup(getter_AddRefs(loadGroup), principal); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.Throw(rv); + return nullptr; + } + } + + RefPtr<MainThreadFetchResolver> resolver = new MainThreadFetchResolver( + p, observer, signalImpl, request->MozErrors()); + RefPtr<FetchDriver> fetch = + new FetchDriver(std::move(r), principal, loadGroup, + aGlobal->EventTargetFor(TaskCategory::Other), + cookieJarSettings, nullptr, // PerformanceStorage + isTrackingFetch); + fetch->SetDocument(doc); + resolver->SetLoadGroup(loadGroup); + aRv = fetch->Fetch(signalImpl, resolver); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } else { + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(worker); + + if (worker->IsServiceWorker()) { + r->SetSkipServiceWorker(); + } + + // PFetch gives no benefit for the fetch in the parent process. + // Dispatch fetch to the parent process main thread directly for that case. + // For child process, dispatch fetch op to the parent. + if (StaticPrefs::dom_workers_pFetch_enabled() && !XRE_IsParentProcess()) { + RefPtr<FetchChild> actor = + FetchChild::Create(worker, p, signalImpl, observer); + if (!actor) { + NS_WARNING("Could not keep the worker alive."); + aRv.Throw(NS_ERROR_DOM_ABORT_ERR); + return nullptr; + } + + Maybe<ClientInfo> clientInfo(worker->GlobalScope()->GetClientInfo()); + if (clientInfo.isNothing()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + auto* backgroundChild = + mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); + Unused << NS_WARN_IF(!backgroundChild->SendPFetchConstructor(actor)); + + FetchOpArgs ipcArgs; + + ipcArgs.request() = IPCInternalRequest(); + r->ToIPCInternalRequest(&(ipcArgs.request()), backgroundChild); + + ipcArgs.principalInfo() = worker->GetPrincipalInfo(); + ipcArgs.clientInfo() = clientInfo.ref().ToIPC(); + if (worker->GetBaseURI()) { + worker->GetBaseURI()->GetAsciiSpec(ipcArgs.workerScript()); + } + if (worker->GlobalScope()->GetController().isSome()) { + ipcArgs.controller() = + Some(worker->GlobalScope()->GetController().ref().ToIPC()); + } + if (worker->CookieJarSettings()) { + ipcArgs.cookieJarSettings() = Some(worker->CookieJarSettingsArgs()); + } + if (worker->CSPEventListener()) { + ipcArgs.hasCSPEventListener() = true; + actor->SetCSPEventListener(worker->CSPEventListener()); + } else { + ipcArgs.hasCSPEventListener() = false; + } + + ipcArgs.associatedBrowsingContextID() = + worker->AssociatedBrowsingContextID(); + + if (worker->IsWatchedByDevTools()) { + UniquePtr<SerializedStackHolder> stack; + stack = GetCurrentStackForNetMonitor(cx); + actor->SetOriginStack(std::move(stack)); + } + + actor->DoFetchOp(ipcArgs); + + return p.forget(); + } + + RefPtr<WorkerFetchResolver> resolver = + WorkerFetchResolver::Create(worker, p, signalImpl, observer); + if (!resolver) { + NS_WARNING("Could not keep the worker alive."); + aRv.Throw(NS_ERROR_DOM_ABORT_ERR); + return nullptr; + } + + Maybe<ClientInfo> clientInfo(worker->GlobalScope()->GetClientInfo()); + if (clientInfo.isNothing()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + UniquePtr<SerializedStackHolder> stack; + if (worker->IsWatchedByDevTools()) { + stack = GetCurrentStackForNetMonitor(cx); + } + + RefPtr<MainThreadFetchRunnable> run = new MainThreadFetchRunnable( + resolver, clientInfo.ref(), worker->GlobalScope()->GetController(), + worker->CSPEventListener(), std::move(r), std::move(stack)); + worker->DispatchToMainThread(run.forget()); + } + + return p.forget(); +} + +class ResolveFetchPromise : public Runnable { + public: + ResolveFetchPromise(Promise* aPromise, Response* aResponse) + : Runnable("ResolveFetchPromise"), + mPromise(aPromise), + mResponse(aResponse) {} + + NS_IMETHOD Run() override { + mPromise->MaybeResolve(mResponse); + return NS_OK; + } + RefPtr<Promise> mPromise; + RefPtr<Response> mResponse; +}; + +void MainThreadFetchResolver::OnResponseAvailableInternal( + SafeRefPtr<InternalResponse> aResponse) { + NS_ASSERT_OWNINGTHREAD(MainThreadFetchResolver); + AssertIsOnMainThread(); + + if (aResponse->Type() != ResponseType::Error) { + nsCOMPtr<nsIGlobalObject> go = mPromise->GetParentObject(); + nsCOMPtr<nsPIDOMWindowInner> inner = do_QueryInterface(go); + + // Notify the document when a fetch completes successfully. This is + // used by the password manager as a hint to observe DOM mutations. + // Call this prior to setting state to Complete so we can set up the + // observer before mutations occurs. + Document* doc = inner ? inner->GetExtantDoc() : nullptr; + if (doc) { + doc->NotifyFetchOrXHRSuccess(); + } + + if (mFetchObserver) { + mFetchObserver->SetState(FetchState::Complete); + } + + mResponse = new Response(go, std::move(aResponse), mSignalImpl); + // response headers received from the network should be immutable + // all response header settings must be done before this point + // see Bug 1574174 + ErrorResult result; + mResponse->Headers_()->SetGuard(HeadersGuardEnum::Immutable, result); + MOZ_ASSERT(!result.Failed()); + + BrowsingContext* bc = inner ? inner->GetBrowsingContext() : nullptr; + bc = bc ? bc->Top() : nullptr; + if (bc && bc->IsLoading()) { + bc->AddDeprioritizedLoadRunner( + new ResolveFetchPromise(mPromise, mResponse)); + } else { + mPromise->MaybeResolve(mResponse); + } + } else { + if (mFetchObserver) { + mFetchObserver->SetState(FetchState::Errored); + } + + if (mMozErrors) { + mPromise->MaybeReject(aResponse->GetErrorCode()); + return; + } + + mPromise->MaybeRejectWithTypeError<MSG_FETCH_FAILED>(); + } +} + +bool MainThreadFetchResolver::NeedOnDataAvailable() { + NS_ASSERT_OWNINGTHREAD(MainThreadFetchResolver); + return !!mFetchObserver; +} + +void MainThreadFetchResolver::OnDataAvailable() { + NS_ASSERT_OWNINGTHREAD(MainThreadFetchResolver); + AssertIsOnMainThread(); + + if (!mFetchObserver) { + return; + } + + if (mFetchObserver->State() == FetchState::Requesting) { + mFetchObserver->SetState(FetchState::Responding); + } +} + +MainThreadFetchResolver::~MainThreadFetchResolver() { + NS_ASSERT_OWNINGTHREAD(MainThreadFetchResolver); +} + +class WorkerFetchResponseRunnable final : public MainThreadWorkerRunnable { + RefPtr<WorkerFetchResolver> mResolver; + // Passed from main thread to worker thread after being initialized. + SafeRefPtr<InternalResponse> mInternalResponse; + + public: + WorkerFetchResponseRunnable(WorkerPrivate* aWorkerPrivate, + WorkerFetchResolver* aResolver, + SafeRefPtr<InternalResponse> aResponse) + : MainThreadWorkerRunnable(aWorkerPrivate), + mResolver(aResolver), + mInternalResponse(std::move(aResponse)) { + MOZ_ASSERT(mResolver); + } + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + RefPtr<Promise> promise = mResolver->WorkerPromise(aWorkerPrivate); + RefPtr<FetchObserver> fetchObserver = + mResolver->GetFetchObserver(aWorkerPrivate); + + if (mInternalResponse->Type() != ResponseType::Error) { + if (fetchObserver) { + fetchObserver->SetState(FetchState::Complete); + } + + RefPtr<nsIGlobalObject> global = aWorkerPrivate->GlobalScope(); + RefPtr<Response> response = + new Response(global, mInternalResponse.clonePtr(), + mResolver->GetAbortSignalForTargetThread()); + + // response headers received from the network should be immutable, + // all response header settings must be done before this point + // see Bug 1574174 + ErrorResult result; + response->Headers_()->SetGuard(HeadersGuardEnum::Immutable, result); + MOZ_ASSERT(!result.Failed()); + + promise->MaybeResolve(response); + } else { + if (fetchObserver) { + fetchObserver->SetState(FetchState::Errored); + } + + promise->MaybeRejectWithTypeError<MSG_FETCH_FAILED>(); + } + return true; + } +}; + +class WorkerDataAvailableRunnable final : public MainThreadWorkerRunnable { + RefPtr<WorkerFetchResolver> mResolver; + + public: + WorkerDataAvailableRunnable(WorkerPrivate* aWorkerPrivate, + WorkerFetchResolver* aResolver) + : MainThreadWorkerRunnable(aWorkerPrivate), mResolver(aResolver) {} + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + RefPtr<FetchObserver> fetchObserver = + mResolver->GetFetchObserver(aWorkerPrivate); + + if (fetchObserver && fetchObserver->State() == FetchState::Requesting) { + fetchObserver->SetState(FetchState::Responding); + } + + return true; + } +}; + +class WorkerFetchResponseEndBase { + protected: + RefPtr<WorkerFetchResolver> mResolver; + + public: + explicit WorkerFetchResponseEndBase(WorkerFetchResolver* aResolver) + : mResolver(aResolver) { + MOZ_ASSERT(aResolver); + } + + void WorkerRunInternal(WorkerPrivate* aWorkerPrivate) { + mResolver->Shutdown(aWorkerPrivate); + } +}; + +class WorkerFetchResponseEndRunnable final : public MainThreadWorkerRunnable, + public WorkerFetchResponseEndBase { + FetchDriverObserver::EndReason mReason; + + public: + WorkerFetchResponseEndRunnable(WorkerPrivate* aWorkerPrivate, + WorkerFetchResolver* aResolver, + FetchDriverObserver::EndReason aReason) + : MainThreadWorkerRunnable(aWorkerPrivate), + WorkerFetchResponseEndBase(aResolver), + mReason(aReason) {} + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + if (mResolver->IsShutdown(aWorkerPrivate)) { + return true; + } + + if (mReason == FetchDriverObserver::eAborted) { + mResolver->WorkerPromise(aWorkerPrivate) + ->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + } + + WorkerRunInternal(aWorkerPrivate); + return true; + } + + nsresult Cancel() override { + // We need to check first if cancel is called twice + nsresult rv = WorkerRunnable::Cancel(); + NS_ENSURE_SUCCESS(rv, rv); + + // Execute Run anyway to make sure we cleanup our promise proxy to avoid + // leaking the worker thread + Run(); + return NS_OK; + } +}; + +class WorkerFetchResponseEndControlRunnable final + : public MainThreadWorkerControlRunnable, + public WorkerFetchResponseEndBase { + public: + WorkerFetchResponseEndControlRunnable(WorkerPrivate* aWorkerPrivate, + WorkerFetchResolver* aResolver) + : MainThreadWorkerControlRunnable(aWorkerPrivate), + WorkerFetchResponseEndBase(aResolver) {} + + bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override { + WorkerRunInternal(aWorkerPrivate); + return true; + } + + // Control runnable cancel already calls Run(). +}; + +void WorkerFetchResolver::OnResponseAvailableInternal( + SafeRefPtr<InternalResponse> aResponse) { + AssertIsOnMainThread(); + + MutexAutoLock lock(mPromiseProxy->Lock()); + if (mPromiseProxy->CleanedUp()) { + return; + } + + RefPtr<WorkerFetchResponseRunnable> r = new WorkerFetchResponseRunnable( + mPromiseProxy->GetWorkerPrivate(), this, std::move(aResponse)); + + if (!r->Dispatch()) { + NS_WARNING("Could not dispatch fetch response"); + } +} + +bool WorkerFetchResolver::NeedOnDataAvailable() { + AssertIsOnMainThread(); + return mNeedOnDataAvailable; +} + +void WorkerFetchResolver::OnDataAvailable() { + AssertIsOnMainThread(); + + MutexAutoLock lock(mPromiseProxy->Lock()); + if (mPromiseProxy->CleanedUp()) { + return; + } + + RefPtr<WorkerDataAvailableRunnable> r = + new WorkerDataAvailableRunnable(mPromiseProxy->GetWorkerPrivate(), this); + Unused << r->Dispatch(); +} + +void WorkerFetchResolver::OnResponseEnd(FetchDriverObserver::EndReason aReason, + JS::Handle<JS::Value> aReasonDetails) { + AssertIsOnMainThread(); + MutexAutoLock lock(mPromiseProxy->Lock()); + if (mPromiseProxy->CleanedUp()) { + return; + } + + FlushConsoleReport(); + + Unused << aReasonDetails; + + RefPtr<WorkerFetchResponseEndRunnable> r = new WorkerFetchResponseEndRunnable( + mPromiseProxy->GetWorkerPrivate(), this, aReason); + + if (!r->Dispatch()) { + RefPtr<WorkerFetchResponseEndControlRunnable> cr = + new WorkerFetchResponseEndControlRunnable( + mPromiseProxy->GetWorkerPrivate(), this); + // This can fail if the worker thread is canceled or killed causing + // the PromiseWorkerProxy to give up its WorkerRef immediately, + // allowing the worker thread to become Dead. + if (!cr->Dispatch()) { + NS_WARNING("Failed to dispatch WorkerFetchResponseEndControlRunnable"); + } + } +} + +void WorkerFetchResolver::FlushConsoleReport() { + AssertIsOnMainThread(); + MOZ_ASSERT(mPromiseProxy); + + if (!mReporter) { + return; + } + + WorkerPrivate* worker = mPromiseProxy->GetWorkerPrivate(); + if (!worker) { + mReporter->FlushReportsToConsole(0); + return; + } + + if (worker->IsServiceWorker()) { + // Flush to service worker + mReporter->FlushReportsToConsoleForServiceWorkerScope( + worker->ServiceWorkerScope()); + return; + } + + if (worker->IsSharedWorker()) { + // Flush to shared worker + worker->GetRemoteWorkerController()->FlushReportsOnMainThread(mReporter); + return; + } + + // Flush to dedicated worker + mReporter->FlushConsoleReports(worker->GetLoadGroup()); +} + +nsresult ExtractByteStreamFromBody(const fetch::OwningBodyInit& aBodyInit, + nsIInputStream** aStream, + nsCString& aContentTypeWithCharset, + uint64_t& aContentLength) { + MOZ_ASSERT(aStream); + nsAutoCString charset; + aContentTypeWithCharset.SetIsVoid(true); + + if (aBodyInit.IsArrayBuffer()) { + BodyExtractor<const ArrayBuffer> body(&aBodyInit.GetAsArrayBuffer()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + if (aBodyInit.IsArrayBufferView()) { + BodyExtractor<const ArrayBufferView> body( + &aBodyInit.GetAsArrayBufferView()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + if (aBodyInit.IsBlob()) { + Blob& blob = aBodyInit.GetAsBlob(); + BodyExtractor<const Blob> body(&blob); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + if (aBodyInit.IsFormData()) { + FormData& formData = aBodyInit.GetAsFormData(); + BodyExtractor<const FormData> body(&formData); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + if (aBodyInit.IsUSVString()) { + BodyExtractor<const nsAString> body(&aBodyInit.GetAsUSVString()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + if (aBodyInit.IsURLSearchParams()) { + URLSearchParams& usp = aBodyInit.GetAsURLSearchParams(); + BodyExtractor<const URLSearchParams> body(&usp); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + MOZ_ASSERT_UNREACHABLE("Should never reach here"); + return NS_ERROR_FAILURE; +} + +nsresult ExtractByteStreamFromBody(const fetch::BodyInit& aBodyInit, + nsIInputStream** aStream, + nsCString& aContentTypeWithCharset, + uint64_t& aContentLength) { + MOZ_ASSERT(aStream); + MOZ_ASSERT(!*aStream); + + nsAutoCString charset; + aContentTypeWithCharset.SetIsVoid(true); + + if (aBodyInit.IsArrayBuffer()) { + BodyExtractor<const ArrayBuffer> body(&aBodyInit.GetAsArrayBuffer()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + if (aBodyInit.IsArrayBufferView()) { + BodyExtractor<const ArrayBufferView> body( + &aBodyInit.GetAsArrayBufferView()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + if (aBodyInit.IsBlob()) { + BodyExtractor<const Blob> body(&aBodyInit.GetAsBlob()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + if (aBodyInit.IsFormData()) { + BodyExtractor<const FormData> body(&aBodyInit.GetAsFormData()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + if (aBodyInit.IsUSVString()) { + BodyExtractor<const nsAString> body(&aBodyInit.GetAsUSVString()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + if (aBodyInit.IsURLSearchParams()) { + BodyExtractor<const URLSearchParams> body( + &aBodyInit.GetAsURLSearchParams()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + MOZ_ASSERT_UNREACHABLE("Should never reach here"); + return NS_ERROR_FAILURE; +} + +nsresult ExtractByteStreamFromBody(const fetch::ResponseBodyInit& aBodyInit, + nsIInputStream** aStream, + nsCString& aContentTypeWithCharset, + uint64_t& aContentLength) { + MOZ_ASSERT(aStream); + MOZ_ASSERT(!*aStream); + + // ReadableStreams should be handled by + // BodyExtractorReadableStream::GetAsStream. + MOZ_ASSERT(!aBodyInit.IsReadableStream()); + + nsAutoCString charset; + aContentTypeWithCharset.SetIsVoid(true); + + if (aBodyInit.IsArrayBuffer()) { + BodyExtractor<const ArrayBuffer> body(&aBodyInit.GetAsArrayBuffer()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + if (aBodyInit.IsArrayBufferView()) { + BodyExtractor<const ArrayBufferView> body( + &aBodyInit.GetAsArrayBufferView()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + if (aBodyInit.IsBlob()) { + BodyExtractor<const Blob> body(&aBodyInit.GetAsBlob()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + if (aBodyInit.IsFormData()) { + BodyExtractor<const FormData> body(&aBodyInit.GetAsFormData()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + if (aBodyInit.IsUSVString()) { + BodyExtractor<const nsAString> body(&aBodyInit.GetAsUSVString()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + if (aBodyInit.IsURLSearchParams()) { + BodyExtractor<const URLSearchParams> body( + &aBodyInit.GetAsURLSearchParams()); + return body.GetAsStream(aStream, &aContentLength, aContentTypeWithCharset, + charset); + } + + MOZ_ASSERT_UNREACHABLE("Should never reach here"); + return NS_ERROR_FAILURE; +} + +NS_IMPL_CYCLE_COLLECTION(FetchBodyBase, mReadableStreamBody) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(FetchBodyBase) +NS_IMPL_CYCLE_COLLECTING_RELEASE(FetchBodyBase) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FetchBodyBase) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +template <class Derived> +FetchBody<Derived>::FetchBody(nsIGlobalObject* aOwner) + : mOwner(aOwner), mBodyUsed(false) { + MOZ_ASSERT(aOwner); + + if (!NS_IsMainThread()) { + WorkerPrivate* wp = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(wp); + mMainThreadEventTarget = wp->MainThreadEventTarget(); + } else { + mMainThreadEventTarget = aOwner->EventTargetFor(TaskCategory::Other); + } + + MOZ_ASSERT(mMainThreadEventTarget); +} + +template FetchBody<Request>::FetchBody(nsIGlobalObject* aOwner); + +template FetchBody<Response>::FetchBody(nsIGlobalObject* aOwner); + +template <class Derived> +FetchBody<Derived>::~FetchBody() { + Unfollow(); +} + +template FetchBody<Request>::~FetchBody(); + +template FetchBody<Response>::~FetchBody(); + +template <class Derived> +bool FetchBody<Derived>::BodyUsed() const { + if (mBodyUsed) { + return true; + } + + // If this stream is disturbed, return true. + if (mReadableStreamBody) { + return mReadableStreamBody->Disturbed(); + } + + return false; +} + +template bool FetchBody<Request>::BodyUsed() const; + +template bool FetchBody<Response>::BodyUsed() const; + +template <class Derived> +void FetchBody<Derived>::SetBodyUsed(JSContext* aCx, ErrorResult& aRv) { + MOZ_ASSERT(aCx); + MOZ_ASSERT(mOwner->EventTargetFor(TaskCategory::Other)->IsOnCurrentThread()); + + MOZ_DIAGNOSTIC_ASSERT(!BodyUsed(), "Consuming already used body?"); + if (BodyUsed()) { + return; + } + + mBodyUsed = true; + + // If we already have a ReadableStreamBody and it has been created by DOM, we + // have to lock it now because it can have been shared with other objects. + if (mReadableStreamBody) { + if (mReadableStreamBody->MaybeGetInputStreamIfUnread()) { + LockStream(aCx, mReadableStreamBody, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } else { + MOZ_ASSERT(mFetchStreamReader); + // Let's activate the FetchStreamReader. + mFetchStreamReader->StartConsuming(aCx, mReadableStreamBody, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + } +} + +template void FetchBody<Request>::SetBodyUsed(JSContext* aCx, ErrorResult& aRv); + +template void FetchBody<Response>::SetBodyUsed(JSContext* aCx, + ErrorResult& aRv); + +template <class Derived> +already_AddRefed<Promise> FetchBody<Derived>::ConsumeBody( + JSContext* aCx, BodyConsumer::ConsumeType aType, ErrorResult& aRv) { + aRv.MightThrowJSException(); + + RefPtr<AbortSignalImpl> signalImpl = + DerivedClass()->GetSignalImplToConsumeBody(); + + if (signalImpl && signalImpl->Aborted()) { + JS::Rooted<JS::Value> abortReason(aCx, signalImpl->RawReason()); + + if (abortReason.get().isUndefined()) { + aRv.Throw(NS_ERROR_DOM_ABORT_ERR); + return nullptr; + } + + nsCOMPtr<nsIGlobalObject> go = DerivedClass()->GetParentObject(); + + RefPtr<Promise> promise = Promise::Create(go, aRv); + promise->MaybeReject(abortReason); + return promise.forget(); + } + + if (BodyUsed()) { + aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>(); + return nullptr; + } + + nsAutoCString mimeType; + nsAutoCString mixedCaseMimeType; + DerivedClass()->GetMimeType(mimeType, mixedCaseMimeType); + + // Null bodies are a special-case in the fetch spec. The Body mix-in can only + // be "disturbed" or "locked" if its associated "body" is non-null. + // Additionally, the Body min-in's "consume body" algorithm explicitly creates + // a fresh empty ReadableStream object in step 2. This means that `bodyUsed` + // will never return true for a null body. + // + // To this end, we create a fresh (empty) body every time a request is made + // and consume its body here, without marking this FetchBody consumed via + // SetBodyUsed. + nsCOMPtr<nsIInputStream> bodyStream; + DerivedClass()->GetBody(getter_AddRefs(bodyStream)); + if (!bodyStream) { + RefPtr<EmptyBody> emptyBody = + EmptyBody::Create(DerivedClass()->GetParentObject(), + DerivedClass()->GetPrincipalInfo().get(), signalImpl, + mimeType, mixedCaseMimeType, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return emptyBody->ConsumeBody(aCx, aType, aRv); + } + + SetBodyUsed(aCx, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + nsCOMPtr<nsIGlobalObject> global = DerivedClass()->GetParentObject(); + + MutableBlobStorage::MutableBlobStorageType blobStorageType = + MutableBlobStorage::eOnlyInMemory; + const mozilla::UniquePtr<mozilla::ipc::PrincipalInfo>& principalInfo = + DerivedClass()->GetPrincipalInfo(); + // We support temporary file for blobs only if the principal is known and + // it's system or content not in private Browsing. + if (principalInfo && + (principalInfo->type() == + mozilla::ipc::PrincipalInfo::TSystemPrincipalInfo || + (principalInfo->type() == + mozilla::ipc::PrincipalInfo::TContentPrincipalInfo && + principalInfo->get_ContentPrincipalInfo().attrs().mPrivateBrowsingId == + 0))) { + blobStorageType = MutableBlobStorage::eCouldBeInTemporaryFile; + } + + RefPtr<Promise> promise = BodyConsumer::Create( + global, mMainThreadEventTarget, bodyStream, signalImpl, aType, + BodyBlobURISpec(), BodyLocalPath(), mimeType, mixedCaseMimeType, + blobStorageType, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return promise.forget(); +} + +template already_AddRefed<Promise> FetchBody<Request>::ConsumeBody( + JSContext* aCx, BodyConsumer::ConsumeType aType, ErrorResult& aRv); + +template already_AddRefed<Promise> FetchBody<Response>::ConsumeBody( + JSContext* aCx, BodyConsumer::ConsumeType aType, ErrorResult& aRv); + +template already_AddRefed<Promise> FetchBody<EmptyBody>::ConsumeBody( + JSContext* aCx, BodyConsumer::ConsumeType aType, ErrorResult& aRv); + +template <class Derived> +void FetchBody<Derived>::GetMimeType(nsACString& aMimeType, + nsACString& aMixedCaseMimeType) { + // Extract mime type. + ErrorResult result; + nsCString contentTypeValues; + MOZ_ASSERT(DerivedClass()->GetInternalHeaders()); + DerivedClass()->GetInternalHeaders()->Get("Content-Type"_ns, + contentTypeValues, result); + MOZ_ALWAYS_TRUE(!result.Failed()); + + // HTTP ABNF states Content-Type may have only one value. + // This is from the "parse a header value" of the fetch spec. + if (!contentTypeValues.IsVoid() && contentTypeValues.Find(",") == -1) { + // Convert from a bytestring to a UTF8 CString. + CopyLatin1toUTF8(contentTypeValues, aMimeType); + aMixedCaseMimeType = aMimeType; + ToLowerCase(aMimeType); + } +} + +template void FetchBody<Request>::GetMimeType(nsACString& aMimeType, + nsACString& aMixedCaseMimeType); +template void FetchBody<Response>::GetMimeType(nsACString& aMimeType, + nsACString& aMixedCaseMimeType); + +template <class Derived> +const nsACString& FetchBody<Derived>::BodyBlobURISpec() const { + return DerivedClass()->BodyBlobURISpec(); +} + +template const nsACString& FetchBody<Request>::BodyBlobURISpec() const; + +template const nsACString& FetchBody<Response>::BodyBlobURISpec() const; + +template const nsACString& FetchBody<EmptyBody>::BodyBlobURISpec() const; + +template <class Derived> +const nsAString& FetchBody<Derived>::BodyLocalPath() const { + return DerivedClass()->BodyLocalPath(); +} + +template const nsAString& FetchBody<Request>::BodyLocalPath() const; + +template const nsAString& FetchBody<Response>::BodyLocalPath() const; + +template const nsAString& FetchBody<EmptyBody>::BodyLocalPath() const; + +template <class Derived> +void FetchBody<Derived>::SetReadableStreamBody(JSContext* aCx, + ReadableStream* aBody) { + MOZ_ASSERT(!mReadableStreamBody); + MOZ_ASSERT(aBody); + mReadableStreamBody = aBody; + + RefPtr<AbortSignalImpl> signalImpl = DerivedClass()->GetSignalImpl(); + if (!signalImpl) { + return; + } + + bool aborted = signalImpl->Aborted(); + if (aborted) { + IgnoredErrorResult result; + JS::Rooted<JS::Value> abortReason(aCx, signalImpl->RawReason()); + AbortStream(aCx, mReadableStreamBody, result, abortReason); + if (NS_WARN_IF(result.Failed())) { + return; + } + } else if (!IsFollowing()) { + Follow(signalImpl); + } +} + +template void FetchBody<Request>::SetReadableStreamBody(JSContext* aCx, + ReadableStream* aBody); + +template void FetchBody<Response>::SetReadableStreamBody(JSContext* aCx, + ReadableStream* aBody); + +template <class Derived> +already_AddRefed<ReadableStream> FetchBody<Derived>::GetBody(JSContext* aCx, + ErrorResult& aRv) { + if (mReadableStreamBody) { + return do_AddRef(mReadableStreamBody); + } + + nsCOMPtr<nsIInputStream> inputStream; + DerivedClass()->GetBody(getter_AddRefs(inputStream)); + + if (!inputStream) { + return nullptr; + } + + // The spec immediately creates ReadableStream on Response/Request constructor + // via https://fetch.spec.whatwg.org/#concept-bodyinit-extract, but Gecko + // creates nsIInputStream there instead and creates ReadableStream only when + // .body is accessed. Thus we only follow step 4 of it here. + // + // Step 4: Otherwise, set stream to a new ReadableStream object, and set up + // stream with byte reading support. + auto algorithms = + MakeRefPtr<NonAsyncInputToReadableStreamAlgorithms>(*inputStream); + RefPtr<ReadableStream> body = ReadableStream::CreateByteNative( + aCx, DerivedClass()->GetParentObject(), *algorithms, Nothing(), aRv); + if (aRv.Failed()) { + return nullptr; + } + mReadableStreamBody = body; + + // If the body has been already consumed, we lock the stream. + if (BodyUsed()) { + LockStream(aCx, body, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } + + RefPtr<AbortSignalImpl> signalImpl = DerivedClass()->GetSignalImpl(); + if (signalImpl) { + if (signalImpl->Aborted()) { + JS::Rooted<JS::Value> abortReason(aCx, signalImpl->RawReason()); + AbortStream(aCx, body, aRv, abortReason); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } else if (!IsFollowing()) { + Follow(signalImpl); + } + } + + return body.forget(); +} + +template already_AddRefed<ReadableStream> FetchBody<Request>::GetBody( + JSContext* aCx, ErrorResult& aRv); + +template already_AddRefed<ReadableStream> FetchBody<Response>::GetBody( + JSContext* aCx, ErrorResult& aRv); + +template <class Derived> +void FetchBody<Derived>::LockStream(JSContext* aCx, ReadableStream* aStream, + ErrorResult& aRv) { + // This is native stream, creating a reader will not execute any JS code. + RefPtr<ReadableStreamDefaultReader> reader = aStream->GetReader(aRv); + if (aRv.Failed()) { + return; + } +} + +template void FetchBody<Request>::LockStream(JSContext* aCx, + ReadableStream* aStream, + ErrorResult& aRv); + +template void FetchBody<Response>::LockStream(JSContext* aCx, + ReadableStream* aStream, + ErrorResult& aRv); + +template <class Derived> +void FetchBody<Derived>::MaybeTeeReadableStreamBody( + JSContext* aCx, ReadableStream** aBodyOut, + FetchStreamReader** aStreamReader, nsIInputStream** aInputStream, + ErrorResult& aRv) { + MOZ_DIAGNOSTIC_ASSERT(aStreamReader); + MOZ_DIAGNOSTIC_ASSERT(aInputStream); + MOZ_DIAGNOSTIC_ASSERT(!BodyUsed()); + + *aBodyOut = nullptr; + *aStreamReader = nullptr; + *aInputStream = nullptr; + + if (!mReadableStreamBody) { + return; + } + + // If this is a ReadableStream with an native source, this has been + // generated by a Fetch. In this case, Fetch will be able to recreate it + // again when GetBody() is called. + if (mReadableStreamBody->MaybeGetInputStreamIfUnread()) { + *aBodyOut = nullptr; + return; + } + + nsTArray<RefPtr<ReadableStream> > branches; + MOZ_KnownLive(mReadableStreamBody)->Tee(aCx, branches, aRv); + if (aRv.Failed()) { + return; + } + + mReadableStreamBody = branches[0]; + branches[1].forget(aBodyOut); + + aRv = FetchStreamReader::Create(aCx, mOwner, aStreamReader, aInputStream); + if (NS_WARN_IF(aRv.Failed())) { + return; + } +} + +template void FetchBody<Request>::MaybeTeeReadableStreamBody( + JSContext* aCx, ReadableStream** aBodyOut, + FetchStreamReader** aStreamReader, nsIInputStream** aInputStream, + ErrorResult& aRv); + +template void FetchBody<Response>::MaybeTeeReadableStreamBody( + JSContext* aCx, ReadableStream** aBodyOut, + FetchStreamReader** aStreamReader, nsIInputStream** aInputStream, + ErrorResult& aRv); + +template <class Derived> +void FetchBody<Derived>::RunAbortAlgorithm() { + if (!mReadableStreamBody) { + return; + } + + AutoJSAPI jsapi; + if (!jsapi.Init(mOwner)) { + return; + } + + JSContext* cx = jsapi.cx(); + + RefPtr<ReadableStream> body(mReadableStreamBody); + IgnoredErrorResult result; + + JS::Rooted<JS::Value> abortReason(cx); + + AbortSignalImpl* signalImpl = Signal(); + if (signalImpl) { + abortReason.set(signalImpl->RawReason()); + } + + AbortStream(cx, body, result, abortReason); +} + +template void FetchBody<Request>::RunAbortAlgorithm(); + +template void FetchBody<Response>::RunAbortAlgorithm(); + +NS_IMPL_ADDREF_INHERITED(EmptyBody, FetchBody<EmptyBody>) +NS_IMPL_RELEASE_INHERITED(EmptyBody, FetchBody<EmptyBody>) + +NS_IMPL_CYCLE_COLLECTION_CLASS(EmptyBody) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(EmptyBody, FetchBody<EmptyBody>) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAbortSignalImpl) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mFetchStreamReader) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(EmptyBody, + FetchBody<EmptyBody>) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAbortSignalImpl) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFetchStreamReader) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(EmptyBody, FetchBody<EmptyBody>) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(EmptyBody) +NS_INTERFACE_MAP_END_INHERITING(FetchBody<EmptyBody>) + +EmptyBody::EmptyBody(nsIGlobalObject* aGlobal, + mozilla::ipc::PrincipalInfo* aPrincipalInfo, + AbortSignalImpl* aAbortSignalImpl, + const nsACString& aMimeType, + const nsACString& aMixedCaseMimeType, + already_AddRefed<nsIInputStream> aBodyStream) + : FetchBody<EmptyBody>(aGlobal), + mAbortSignalImpl(aAbortSignalImpl), + mMimeType(aMimeType), + mMixedCaseMimeType(aMixedCaseMimeType), + mBodyStream(std::move(aBodyStream)) { + if (aPrincipalInfo) { + mPrincipalInfo = MakeUnique<mozilla::ipc::PrincipalInfo>(*aPrincipalInfo); + } +} + +EmptyBody::~EmptyBody() = default; + +/* static */ +already_AddRefed<EmptyBody> EmptyBody::Create( + nsIGlobalObject* aGlobal, mozilla::ipc::PrincipalInfo* aPrincipalInfo, + AbortSignalImpl* aAbortSignalImpl, const nsACString& aMimeType, + const nsACString& aMixedCaseMimeType, ErrorResult& aRv) { + nsCOMPtr<nsIInputStream> bodyStream; + aRv = NS_NewCStringInputStream(getter_AddRefs(bodyStream), ""_ns); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + RefPtr<EmptyBody> emptyBody = + new EmptyBody(aGlobal, aPrincipalInfo, aAbortSignalImpl, aMimeType, + aMixedCaseMimeType, bodyStream.forget()); + return emptyBody.forget(); +} + +void EmptyBody::GetBody(nsIInputStream** aStream, int64_t* aBodyLength) { + MOZ_ASSERT(aStream); + + if (aBodyLength) { + *aBodyLength = 0; + } + + nsCOMPtr<nsIInputStream> bodyStream = mBodyStream; + bodyStream.forget(aStream); +} + +} // namespace mozilla::dom diff --git a/dom/fetch/Fetch.h b/dom/fetch/Fetch.h new file mode 100644 index 0000000000..9b9dccd1ee --- /dev/null +++ b/dom/fetch/Fetch.h @@ -0,0 +1,310 @@ +/* -*- 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_Fetch_h +#define mozilla_dom_Fetch_h + +#include "mozilla/Attributes.h" +#include "nsCOMPtr.h" +#include "nsError.h" +#include "nsProxyRelease.h" +#include "nsString.h" + +#include "mozilla/DebugOnly.h" +#include "mozilla/dom/AbortSignal.h" +#include "mozilla/dom/BodyConsumer.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/FetchStreamReader.h" +#include "mozilla/dom/ReadableStream.h" +#include "mozilla/dom/ReadableStreamDefaultReaderBinding.h" +#include "mozilla/dom/RequestBinding.h" +#include "mozilla/dom/workerinternals/RuntimeService.h" + +class nsIGlobalObject; +class nsIEventTarget; + +namespace mozilla { +class ErrorResult; + +namespace ipc { +class PrincipalInfo; +} // namespace ipc + +namespace dom { + +class BlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrUSVString; +class + BlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrReadableStreamOrUSVString; +class BlobImpl; +class InternalRequest; +class + OwningBlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrUSVString; + +class ReadableStreamDefaultReader; +class RequestOrUSVString; +class WorkerPrivate; + +enum class CallerType : uint32_t; + +already_AddRefed<Promise> FetchRequest(nsIGlobalObject* aGlobal, + const RequestOrUSVString& aInput, + const RequestInit& aInit, + CallerType aCallerType, + ErrorResult& aRv); + +nsresult UpdateRequestReferrer(nsIGlobalObject* aGlobal, + InternalRequest* aRequest); + +namespace fetch { +using BodyInit = + BlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrUSVString; +using ResponseBodyInit = + BlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrReadableStreamOrUSVString; +using OwningBodyInit = + OwningBlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrUSVString; +}; // namespace fetch + +/* + * Creates an nsIInputStream based on the fetch specifications 'extract a byte + * stream algorithm' - http://fetch.spec.whatwg.org/#concept-bodyinit-extract. + * Stores content type in out param aContentType. + */ +nsresult ExtractByteStreamFromBody(const fetch::OwningBodyInit& aBodyInit, + nsIInputStream** aStream, + nsCString& aContentType, + uint64_t& aContentLength); + +/* + * Non-owning version. + */ +nsresult ExtractByteStreamFromBody(const fetch::BodyInit& aBodyInit, + nsIInputStream** aStream, + nsCString& aContentType, + uint64_t& aContentLength); + +/* + * Non-owning version. This method should go away when BodyInit will contain + * ReadableStream. + */ +nsresult ExtractByteStreamFromBody(const fetch::ResponseBodyInit& aBodyInit, + nsIInputStream** aStream, + nsCString& aContentType, + uint64_t& aContentLength); + +/* + * FetchBody's body consumption uses nsIInputStreamPump to read from the + * underlying stream to a block of memory, which is then adopted by + * ContinueConsumeBody() and converted to the right type based on the JS + * function called. + * + * Use of the nsIInputStreamPump complicates things on the worker thread. + * The solution used here is similar to WebSockets. + * The difference is that we are only interested in completion and not data + * events, and nsIInputStreamPump can only deliver completion on the main + * thread. + * + * Before starting the pump on the main thread, we addref the FetchBody to keep + * it alive. Then we add a feature, to track the status of the worker. + * + * ContinueConsumeBody() is the function that cleans things up in both success + * and error conditions and so all callers call it with the appropriate status. + * + * Once the read is initiated on the main thread there are two possibilities. + * + * 1) Pump finishes before worker has finished Running. + * In this case we adopt the data and dispatch a runnable to the worker, + * which derefs FetchBody and removes the feature and resolves the Promise. + * + * 2) Pump still working while worker has stopped Running. + * The feature is Notify()ed and ContinueConsumeBody() is called with + * NS_BINDING_ABORTED. We first Cancel() the pump using a sync runnable to + * ensure that mFetchBody remains alive (since mConsumeBodyPump is strongly + * held by it) until pump->Cancel() is called. OnStreamComplete() will not + * do anything if the error code is NS_BINDING_ABORTED, so we don't have to + * worry about keeping anything alive. + * + * The pump is always released on the main thread. + */ + +class FetchBodyBase : public nsISupports { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(FetchBodyBase) + + protected: + virtual ~FetchBodyBase() = default; + + RefPtr<ReadableStream> mReadableStreamBody; +}; + +template <class Derived> +class FetchBody : public FetchBodyBase, public AbortFollower { + public: + using FetchBodyBase::QueryInterface; + + NS_INLINE_DECL_REFCOUNTING_INHERITED(FetchBody, FetchBodyBase) + + bool BodyUsed() const; + + already_AddRefed<Promise> ArrayBuffer(JSContext* aCx, ErrorResult& aRv) { + return ConsumeBody(aCx, BodyConsumer::CONSUME_ARRAYBUFFER, aRv); + } + + already_AddRefed<Promise> Blob(JSContext* aCx, ErrorResult& aRv) { + return ConsumeBody(aCx, BodyConsumer::CONSUME_BLOB, aRv); + } + + already_AddRefed<Promise> FormData(JSContext* aCx, ErrorResult& aRv) { + return ConsumeBody(aCx, BodyConsumer::CONSUME_FORMDATA, aRv); + } + + already_AddRefed<Promise> Json(JSContext* aCx, ErrorResult& aRv) { + return ConsumeBody(aCx, BodyConsumer::CONSUME_JSON, aRv); + } + + already_AddRefed<Promise> Text(JSContext* aCx, ErrorResult& aRv) { + return ConsumeBody(aCx, BodyConsumer::CONSUME_TEXT, aRv); + } + + already_AddRefed<ReadableStream> GetBody(JSContext* aCx, ErrorResult& aRv); + void GetMimeType(nsACString& aMimeType, nsACString& aMixedCaseMimeType); + + const nsACString& BodyBlobURISpec() const; + + const nsAString& BodyLocalPath() const; + + // If the body contains a ReadableStream body object, this method produces a + // tee() of it. + // + // This is marked as a script boundary minimize changes required for + // annotation while we work out how to correctly annotate this code. + // Tracked in Bug 1750650. + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void MaybeTeeReadableStreamBody(JSContext* aCx, ReadableStream** aBodyOut, + FetchStreamReader** aStreamReader, + nsIInputStream** aInputStream, + ErrorResult& aRv); + + // Utility public methods accessed by various runnables. + + // This method _must_ be called in order to set the body as used. If the body + // is a ReadableStream, this method will start reading the stream. + // More in details, this method does: + // 1) It uses an internal flag to track if the body is used. This is tracked + // separately from the ReadableStream disturbed state due to purely native + // streams. + // 2) If there is a ReadableStream reflector for the native stream it is + // Locked. + // 3) If there is a JS ReadableStream then we begin pumping it into the native + // body stream. This effectively locks and disturbs the stream. + // + // Note that JSContext is used only if there is a ReadableStream (this can + // happen because the body is a ReadableStream or because attribute body has + // already been used by content). If something goes wrong using + // ReadableStream, errors will be reported via ErrorResult and not as JS + // exceptions in JSContext. This is done in order to have a centralized error + // reporting way. + // + // Exceptions generated when reading from the ReadableStream are directly sent + // to the Console. + void SetBodyUsed(JSContext* aCx, ErrorResult& aRv); + + virtual AbortSignalImpl* GetSignalImpl() const = 0; + + virtual AbortSignalImpl* GetSignalImplToConsumeBody() const = 0; + + // AbortFollower + void RunAbortAlgorithm() override; + + already_AddRefed<Promise> ConsumeBody(JSContext* aCx, + BodyConsumer::ConsumeType aType, + ErrorResult& aRv); + + protected: + nsCOMPtr<nsIGlobalObject> mOwner; + + // This is the Reader used to retrieve data from the body. This needs to be + // traversed by subclasses. + RefPtr<FetchStreamReader> mFetchStreamReader; + + explicit FetchBody(nsIGlobalObject* aOwner); + + virtual ~FetchBody(); + + void SetReadableStreamBody(JSContext* aCx, ReadableStream* aBody); + + private: + Derived* DerivedClass() const { + return static_cast<Derived*>(const_cast<FetchBody*>(this)); + } + + void LockStream(JSContext* aCx, ReadableStream* aStream, ErrorResult& aRv); + + void AssertIsOnTargetThread() { + MOZ_ASSERT(NS_IsMainThread() == !GetCurrentThreadWorkerPrivate()); + } + + // Only ever set once, always on target thread. + bool mBodyUsed; + + // The main-thread event target for runnable dispatching. + nsCOMPtr<nsISerialEventTarget> mMainThreadEventTarget; +}; + +class EmptyBody final : public FetchBody<EmptyBody> { + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(EmptyBody, + FetchBody<EmptyBody>) + + public: + static already_AddRefed<EmptyBody> Create( + nsIGlobalObject* aGlobal, mozilla::ipc::PrincipalInfo* aPrincipalInfo, + AbortSignalImpl* aAbortSignalImpl, const nsACString& aMimeType, + const nsACString& aMixedCaseMimeType, ErrorResult& aRv); + + nsIGlobalObject* GetParentObject() const { return mOwner; } + + AbortSignalImpl* GetSignalImpl() const override { return mAbortSignalImpl; } + AbortSignalImpl* GetSignalImplToConsumeBody() const final { return nullptr; } + + const UniquePtr<mozilla::ipc::PrincipalInfo>& GetPrincipalInfo() const { + return mPrincipalInfo; + } + + void GetMimeType(nsACString& aMimeType, nsACString& aMixedCaseMimeType) { + aMimeType = mMimeType; + aMixedCaseMimeType = mMixedCaseMimeType; + } + + void GetBody(nsIInputStream** aStream, int64_t* aBodyLength = nullptr); + + using FetchBody::BodyBlobURISpec; + + const nsACString& BodyBlobURISpec() const { return EmptyCString(); } + + using FetchBody::BodyLocalPath; + + const nsAString& BodyLocalPath() const { return EmptyString(); } + + private: + EmptyBody(nsIGlobalObject* aGlobal, + mozilla::ipc::PrincipalInfo* aPrincipalInfo, + AbortSignalImpl* aAbortSignalImpl, const nsACString& aMimeType, + const nsACString& aMixedCaseMimeType, + already_AddRefed<nsIInputStream> aBodyStream); + + ~EmptyBody(); + + UniquePtr<mozilla::ipc::PrincipalInfo> mPrincipalInfo; + RefPtr<AbortSignalImpl> mAbortSignalImpl; + nsCString mMimeType; + nsCString mMixedCaseMimeType; + nsCOMPtr<nsIInputStream> mBodyStream; +}; +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_Fetch_h diff --git a/dom/fetch/FetchChild.cpp b/dom/fetch/FetchChild.cpp new file mode 100644 index 0000000000..774eb516e7 --- /dev/null +++ b/dom/fetch/FetchChild.cpp @@ -0,0 +1,380 @@ +/* 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 "FetchChild.h" +#include "FetchLog.h" +#include "FetchObserver.h" +#include "InternalResponse.h" +#include "Request.h" +#include "Response.h" +#include "mozilla/ConsoleReportCollector.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/dom/PerformanceTiming.h" +#include "mozilla/dom/PerformanceStorage.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/RemoteWorkerChild.h" +#include "mozilla/dom/SecurityPolicyViolationEventBinding.h" +#include "mozilla/dom/WorkerChannelInfo.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRef.h" +#include "mozilla/dom/WorkerScope.h" +#include "nsIAsyncInputStream.h" +#include "nsIGlobalObject.h" +#include "nsIObserverService.h" +#include "nsIRunnable.h" +#include "nsIURI.h" +#include "nsNetUtil.h" +#include "nsThreadUtils.h" + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS0(FetchChild) + +mozilla::ipc::IPCResult FetchChild::Recv__delete__(const nsresult&& aResult) { + FETCH_LOG(("FetchChild::Recv__delete__ [%p]", this)); + if (mIsShutdown) { + return IPC_OK(); + } + // Shutdown has not been called, so mWorkerRef->Private() should be still + // alive. + MOZ_ASSERT(mWorkerRef->Private()); + mWorkerRef->Private()->AssertIsOnWorkerThread(); + + if (mPromise->State() == Promise::PromiseState::Pending) { + if (NS_FAILED(aResult)) { + mPromise->MaybeReject(aResult); + if (mFetchObserver) { + mFetchObserver->SetState(FetchState::Errored); + } + } else { + mPromise->MaybeResolve(aResult); + if (mFetchObserver) { + mFetchObserver->SetState(FetchState::Complete); + } + } + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult FetchChild::RecvOnResponseAvailableInternal( + ParentToChildInternalResponse&& aResponse) { + FETCH_LOG(("FetchChild::RecvOnResponseAvailableInternal [%p]", this)); + if (mIsShutdown) { + return IPC_OK(); + } + // Shutdown has not been called, so mWorkerRef->Private() should be still + // alive. + MOZ_ASSERT(mWorkerRef->Private()); + mWorkerRef->Private()->AssertIsOnWorkerThread(); + SafeRefPtr<InternalResponse> internalResponse = + InternalResponse::FromIPC(aResponse); + IgnoredErrorResult result; + internalResponse->Headers()->SetGuard(HeadersGuardEnum::Immutable, result); + MOZ_ASSERT(internalResponse); + + if (internalResponse->Type() != ResponseType::Error) { + if (internalResponse->Type() == ResponseType::Opaque) { + internalResponse->GeneratePaddingInfo(); + } + + if (mFetchObserver) { + mFetchObserver->SetState(FetchState::Complete); + } + nsCOMPtr<nsIGlobalObject> global; + global = mWorkerRef->Private()->GlobalScope(); + RefPtr<Response> response = + new Response(global, internalResponse.clonePtr(), mSignalImpl); + mPromise->MaybeResolve(response); + + return IPC_OK(); + } + + FETCH_LOG( + ("FetchChild::RecvOnResponseAvailableInternal [%p] response type is " + "Error(0x%x)", + this, static_cast<int32_t>(internalResponse->GetErrorCode()))); + if (mFetchObserver) { + mFetchObserver->SetState(FetchState::Errored); + } + mPromise->MaybeRejectWithTypeError<MSG_FETCH_FAILED>(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult FetchChild::RecvOnResponseEnd(ResponseEndArgs&& aArgs) { + FETCH_LOG(("FetchChild::RecvOnResponseEnd [%p]", this)); + if (mIsShutdown) { + return IPC_OK(); + } + // Shutdown has not been called, so mWorkerRef->Private() should be still + // alive. + MOZ_ASSERT(mWorkerRef->Private()); + mWorkerRef->Private()->AssertIsOnWorkerThread(); + + if (aArgs.endReason() == FetchDriverObserver::eAborted) { + FETCH_LOG( + ("FetchChild::RecvOnResponseEnd [%p] endReason is eAborted", this)); + if (mFetchObserver) { + mFetchObserver->SetState(FetchState::Errored); + } + mPromise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + } + + Unfollow(); + return IPC_OK(); +} + +mozilla::ipc::IPCResult FetchChild::RecvOnDataAvailable() { + FETCH_LOG(("FetchChild::RecvOnDataAvailable [%p]", this)); + if (mIsShutdown) { + return IPC_OK(); + } + // Shutdown has not been called, so mWorkerRef->Private() should be still + // alive. + MOZ_ASSERT(mWorkerRef->Private()); + mWorkerRef->Private()->AssertIsOnWorkerThread(); + + if (mFetchObserver && mFetchObserver->State() == FetchState::Requesting) { + mFetchObserver->SetState(FetchState::Responding); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult FetchChild::RecvOnFlushConsoleReport( + nsTArray<net::ConsoleReportCollected>&& aReports) { + FETCH_LOG(("FetchChild::RecvOnFlushConsoleReport [%p]", this)); + if (mIsShutdown) { + return IPC_OK(); + } + // Shutdown has not been called, so mWorkerRef->Private() should be still + // alive. + MOZ_ASSERT(mWorkerRef->Private()); + mWorkerRef->Private()->AssertIsOnWorkerThread(); + MOZ_ASSERT(mReporter); + + RefPtr<ThreadSafeWorkerRef> workerRef = mWorkerRef; + nsCOMPtr<nsIConsoleReportCollector> reporter = mReporter; + + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [reports = std::move(aReports), reporter = std::move(reporter), + workerRef = std::move(workerRef)]() mutable { + for (const auto& report : reports) { + reporter->AddConsoleReport( + report.errorFlags(), report.category(), + static_cast<nsContentUtils::PropertiesFile>( + report.propertiesFile()), + report.sourceFileURI(), report.lineNumber(), + report.columnNumber(), report.messageName(), + report.stringParams()); + } + + if (workerRef->Private()->IsServiceWorker()) { + reporter->FlushReportsToConsoleForServiceWorkerScope( + workerRef->Private()->ServiceWorkerScope()); + } + + if (workerRef->Private()->IsSharedWorker()) { + workerRef->Private() + ->GetRemoteWorkerController() + ->FlushReportsOnMainThread(reporter); + } + + reporter->FlushConsoleReports(workerRef->Private()->GetLoadGroup()); + }); + MOZ_ALWAYS_SUCCEEDS( + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); + + return IPC_OK(); +} + +RefPtr<FetchChild> FetchChild::Create(WorkerPrivate* aWorkerPrivate, + RefPtr<Promise> aPromise, + RefPtr<AbortSignalImpl> aSignalImpl, + RefPtr<FetchObserver> aObserver) { + MOZ_DIAGNOSTIC_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + RefPtr<FetchChild> actor = MakeRefPtr<FetchChild>( + std::move(aPromise), std::move(aSignalImpl), std::move(aObserver)); + + RefPtr<StrongWorkerRef> workerRef = + StrongWorkerRef::Create(aWorkerPrivate, "FetchChild", [actor]() { + FETCH_LOG(("StrongWorkerRef callback")); + actor->Shutdown(); + }); + if (NS_WARN_IF(!workerRef)) { + return nullptr; + } + + actor->mWorkerRef = new ThreadSafeWorkerRef(workerRef); + if (NS_WARN_IF(!actor->mWorkerRef)) { + return nullptr; + } + return actor; +} + +mozilla::ipc::IPCResult FetchChild::RecvOnCSPViolationEvent( + const nsAString& aJSON) { + FETCH_LOG(("FetchChild::RecvOnCSPViolationEvent [%p] aJSON: %s\n", this, + NS_ConvertUTF16toUTF8(aJSON).BeginReading())); + + nsString JSON(aJSON); + + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(__func__, [JSON]() mutable { + SecurityPolicyViolationEventInit violationEventInit; + if (NS_WARN_IF(!violationEventInit.Init(JSON))) { + return; + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = + NS_NewURI(getter_AddRefs(uri), violationEventInit.mBlockedURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (!observerService) { + return; + } + + rv = observerService->NotifyObservers( + uri, CSP_VIOLATION_TOPIC, violationEventInit.mViolatedDirective.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + }); + MOZ_ALWAYS_SUCCEEDS( + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); + + if (mCSPEventListener) { + Unused << NS_WARN_IF( + NS_FAILED(mCSPEventListener->OnCSPViolationEvent(aJSON))); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult FetchChild::RecvOnReportPerformanceTiming( + ResponseTiming&& aTiming) { + FETCH_LOG(("FetchChild::RecvOnReportPerformanceTiming [%p]", this)); + if (mIsShutdown) { + return IPC_OK(); + } + // Shutdown has not been called, so mWorkerRef->Private() should be still + // alive. + MOZ_ASSERT(mWorkerRef->Private()); + mWorkerRef->Private()->AssertIsOnWorkerThread(); + + RefPtr<PerformanceStorage> performanceStorage = + mWorkerRef->Private()->GetPerformanceStorage(); + if (performanceStorage) { + performanceStorage->AddEntry( + aTiming.entryName(), aTiming.initiatorType(), + MakeUnique<PerformanceTimingData>(aTiming.timingData())); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult FetchChild::RecvOnNotifyNetworkMonitorAlternateStack( + uint64_t aChannelID) { + FETCH_LOG( + ("FetchChild::RecvOnNotifyNetworkMonitorAlternateStack [%p]", this)); + if (mIsShutdown) { + return IPC_OK(); + } + // Shutdown has not been called, so mWorkerRef->Private() should be still + // alive. + MOZ_ASSERT(mWorkerRef->Private()); + mWorkerRef->Private()->AssertIsOnWorkerThread(); + + if (!mOriginStack) { + return IPC_OK(); + } + + if (!mWorkerChannelInfo) { + mWorkerChannelInfo = MakeRefPtr<WorkerChannelInfo>( + aChannelID, mWorkerRef->Private()->AssociatedBrowsingContextID()); + } + + // Unfortunately, SerializedStackHolder can only be read on the main thread. + // However, it doesn't block the fetch execution. + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [channel = mWorkerChannelInfo, + stack = std::move(mOriginStack)]() mutable { + NotifyNetworkMonitorAlternateStack(channel, std::move(stack)); + }); + + MOZ_ALWAYS_SUCCEEDS( + SchedulerGroup::Dispatch(TaskCategory::Other, r.forget())); + + return IPC_OK(); +} + +void FetchChild::SetCSPEventListener(nsICSPEventListener* aListener) { + MOZ_ASSERT(aListener && !mCSPEventListener); + mCSPEventListener = aListener; +} + +FetchChild::FetchChild(RefPtr<Promise>&& aPromise, + RefPtr<AbortSignalImpl>&& aSignalImpl, + RefPtr<FetchObserver>&& aObserver) + : mPromise(std::move(aPromise)), + mSignalImpl(std::move(aSignalImpl)), + mFetchObserver(std::move(aObserver)), + mReporter(new ConsoleReportCollector()) { + FETCH_LOG(("FetchChild::FetchChild [%p]", this)); +} + +void FetchChild::RunAbortAlgorithm() { + FETCH_LOG(("FetchChild::RunAbortAlgorithm [%p]", this)); + if (mIsShutdown) { + return; + } + if (mWorkerRef) { + Unused << SendAbortFetchOp(); + } +} + +void FetchChild::DoFetchOp(const FetchOpArgs& aArgs) { + FETCH_LOG(("FetchChild::DoFetchOp [%p]", this)); + if (mSignalImpl) { + if (mSignalImpl->Aborted()) { + Unused << SendAbortFetchOp(); + return; + } + Follow(mSignalImpl); + } + Unused << SendFetchOp(aArgs); +} + +void FetchChild::Shutdown() { + FETCH_LOG(("FetchChild::Shutdown [%p]", this)); + if (mIsShutdown) { + return; + } + mIsShutdown.Flip(); + + // If mWorkerRef is nullptr here, that means Recv__delete__() must be called + if (!mWorkerRef) { + return; + } + mPromise = nullptr; + mFetchObserver = nullptr; + Unfollow(); + mSignalImpl = nullptr; + mCSPEventListener = nullptr; + Unused << SendAbortFetchOp(); + mWorkerRef = nullptr; +} + +void FetchChild::ActorDestroy(ActorDestroyReason aReason) { + FETCH_LOG(("FetchChild::ActorDestroy [%p]", this)); + mPromise = nullptr; + mFetchObserver = nullptr; + mSignalImpl = nullptr; + mCSPEventListener = nullptr; + mWorkerRef = nullptr; +} + +} // namespace mozilla::dom diff --git a/dom/fetch/FetchChild.h b/dom/fetch/FetchChild.h new file mode 100644 index 0000000000..986fc728cb --- /dev/null +++ b/dom/fetch/FetchChild.h @@ -0,0 +1,91 @@ +/* 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_fetchChild_h__ +#define mozilla_dom_fetchChild_h__ + +#include "mozilla/dom/AbortSignal.h" +#include "mozilla/dom/AbortFollower.h" +#include "mozilla/dom/FlippedOnce.h" +#include "mozilla/dom/PFetchChild.h" +#include "mozilla/dom/SerializedStackHolder.h" +#include "nsIConsoleReportCollector.h" +#include "nsIContentSecurityPolicy.h" +#include "nsISupports.h" +#include "nsIWorkerChannelInfo.h" + +namespace mozilla::dom { + +class FetchObserver; +class ThreadSafeWorkerRef; +class Promise; +class WorkerPrivate; + +class FetchChild final : public PFetchChild, public AbortFollower { + friend class PFetchChild; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + + mozilla::ipc::IPCResult Recv__delete__(const nsresult&& aResult); + + mozilla::ipc::IPCResult RecvOnResponseAvailableInternal( + ParentToChildInternalResponse&& aResponse); + + mozilla::ipc::IPCResult RecvOnResponseEnd(ResponseEndArgs&& aArgs); + + mozilla::ipc::IPCResult RecvOnDataAvailable(); + + mozilla::ipc::IPCResult RecvOnFlushConsoleReport( + nsTArray<net::ConsoleReportCollected>&& aReports); + + mozilla::ipc::IPCResult RecvOnCSPViolationEvent(const nsAString& aJSon); + + mozilla::ipc::IPCResult RecvOnReportPerformanceTiming( + ResponseTiming&& aTiming); + + mozilla::ipc::IPCResult RecvOnNotifyNetworkMonitorAlternateStack( + uint64_t aChannelID); + + void SetCSPEventListener(nsICSPEventListener* aListener); + + static RefPtr<FetchChild> Create(WorkerPrivate* aWorkerPrivate, + RefPtr<Promise> aPromise, + RefPtr<AbortSignalImpl> aSignalImpl, + RefPtr<FetchObserver> aObserver); + + FetchChild(RefPtr<Promise>&& aPromise, RefPtr<AbortSignalImpl>&& aSignalImpl, + RefPtr<FetchObserver>&& aObserver); + + // AbortFollower + void RunAbortAlgorithm() override; + + void DoFetchOp(const FetchOpArgs& aArgs); + + void SetOriginStack(UniquePtr<SerializedStackHolder>&& aStack) { + MOZ_ASSERT(!mOriginStack); + mOriginStack = std::move(aStack); + } + + private: + ~FetchChild() = default; + + // WorkerPrivate shutdown callback. + void Shutdown(); + void ActorDestroy(ActorDestroyReason aReason) override; + + RefPtr<ThreadSafeWorkerRef> mWorkerRef; + RefPtr<Promise> mPromise; + RefPtr<AbortSignalImpl> mSignalImpl; + RefPtr<FetchObserver> mFetchObserver; + UniquePtr<SerializedStackHolder> mOriginStack; + nsCOMPtr<nsICSPEventListener> mCSPEventListener; + nsCOMPtr<nsIConsoleReportCollector> mReporter; + FlippedOnce<false> mIsShutdown; + nsCOMPtr<nsIWorkerChannelInfo> mWorkerChannelInfo; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/fetch/FetchDriver.cpp b/dom/fetch/FetchDriver.cpp new file mode 100644 index 0000000000..9fca1de608 --- /dev/null +++ b/dom/fetch/FetchDriver.cpp @@ -0,0 +1,1756 @@ +/* -*- 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 "js/Value.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/TaskQueue.h" +#include "mozilla/dom/FetchDriver.h" + +#include "mozilla/dom/ReferrerInfo.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "mozilla/dom/Document.h" +#include "nsICookieJarSettings.h" +#include "nsIFile.h" +#include "nsIInputStream.h" +#include "nsIInterceptionInfo.h" +#include "nsIOutputStream.h" +#include "nsIFileChannel.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsISupportsPriority.h" +#include "nsIThreadRetargetableRequest.h" +#include "nsIUploadChannel2.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIPipe.h" +#include "nsIRedirectHistoryEntry.h" + +#include "nsContentPolicyUtils.h" +#include "nsDataHandler.h" +#include "nsNetUtil.h" +#include "nsPrintfCString.h" +#include "nsProxyRelease.h" +#include "nsStreamUtils.h" +#include "nsStringStream.h" +#include "nsHttpChannel.h" + +#include "mozilla/dom/BlobURLProtocolHandler.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/PerformanceStorage.h" +#include "mozilla/dom/PerformanceTiming.h" +#include "mozilla/dom/ServiceWorkerInterceptController.h" +#include "mozilla/dom/UserActivation.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/PreloaderBase.h" +#include "mozilla/net/InterceptionInfo.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/StaticPrefs_javascript.h" +#include "mozilla/Unused.h" + +#include "Fetch.h" +#include "FetchUtil.h" +#include "InternalRequest.h" +#include "InternalResponse.h" + +namespace mozilla::dom { + +namespace { + +void GetBlobURISpecFromChannel(nsIRequest* aRequest, nsCString& aBlobURISpec) { + MOZ_ASSERT(aRequest); + + aBlobURISpec.SetIsVoid(true); + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + if (!channel) { + return; + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_GetFinalChannelURI(channel, getter_AddRefs(uri)); + if (NS_FAILED(rv)) { + return; + } + + if (!dom::IsBlobURI(uri)) { + return; + } + + uri->GetSpec(aBlobURISpec); +} + +bool ShouldCheckSRI(const InternalRequest& aRequest, + const InternalResponse& aResponse) { + return !aRequest.GetIntegrity().IsEmpty() && + aResponse.Type() != ResponseType::Error; +} + +} // anonymous namespace + +//----------------------------------------------------------------------------- +// AlternativeDataStreamListener +//----------------------------------------------------------------------------- +class AlternativeDataStreamListener final + : public nsIStreamListener, + public nsIThreadRetargetableStreamListener { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + + // The status of AlternativeDataStreamListener + // LOADING: is the initial status, loading the alternative data + // COMPLETED: Alternative data loading is completed + // CANCELED: Alternative data loading is canceled, this would make + // AlternativeDataStreamListener ignore all channel callbacks + // FALLBACK: fallback the channel callbacks to FetchDriver + // Depends on different situaions, the status transition could be followings + // 1. LOADING->COMPLETED + // This is the normal status transition for alternative data loading + // + // 2. LOADING->CANCELED + // LOADING->COMPLETED->CANCELED + // Alternative data loading could be canceled when cacheId from alternative + // data channel does not match with from main data channel(The cacheID + // checking is in FetchDriver::OnStartRequest). + // Notice the alternative data loading could finish before the cacheID + // checking, so the statust transition could be + // LOADING->COMPLETED->CANCELED + // + // 3. LOADING->FALLBACK + // For the case that alternative data loading could not be initialized, + // i.e. alternative data does not exist or no preferred alternative data + // type is requested. Once the status becomes FALLBACK, + // AlternativeDataStreamListener transits the channel callback request to + // FetchDriver, and the status should not go back to LOADING, COMPLETED, or + // CANCELED anymore. + enum eStatus { LOADING = 0, COMPLETED, CANCELED, FALLBACK }; + + AlternativeDataStreamListener(FetchDriver* aFetchDriver, nsIChannel* aChannel, + const nsACString& aAlternativeDataType); + eStatus Status(); + void Cancel(); + uint64_t GetAlternativeDataCacheEntryId(); + const nsACString& GetAlternativeDataType() const; + already_AddRefed<nsICacheInfoChannel> GetCacheInfoChannel(); + already_AddRefed<nsIInputStream> GetAlternativeInputStream(); + + private: + ~AlternativeDataStreamListener() = default; + + // This creates a strong reference cycle with FetchDriver and its + // mAltDataListener. We need to clear at least one reference of them once the + // data loading finishes. + RefPtr<FetchDriver> mFetchDriver; + nsCString mAlternativeDataType; + nsCOMPtr<nsIInputStream> mPipeAlternativeInputStream; + nsCOMPtr<nsIOutputStream> mPipeAlternativeOutputStream; + uint64_t mAlternativeDataCacheEntryId; + nsCOMPtr<nsICacheInfoChannel> mCacheInfoChannel; + nsCOMPtr<nsIChannel> mChannel; + Atomic<eStatus> mStatus; +}; + +NS_IMPL_ISUPPORTS(AlternativeDataStreamListener, nsIStreamListener, + nsIThreadRetargetableStreamListener) + +AlternativeDataStreamListener::AlternativeDataStreamListener( + FetchDriver* aFetchDriver, nsIChannel* aChannel, + const nsACString& aAlternativeDataType) + : mFetchDriver(aFetchDriver), + mAlternativeDataType(aAlternativeDataType), + mAlternativeDataCacheEntryId(0), + mChannel(aChannel), + mStatus(AlternativeDataStreamListener::LOADING) { + MOZ_DIAGNOSTIC_ASSERT(mFetchDriver); + MOZ_DIAGNOSTIC_ASSERT(mChannel); +} + +AlternativeDataStreamListener::eStatus AlternativeDataStreamListener::Status() { + return mStatus; +} + +void AlternativeDataStreamListener::Cancel() { + mAlternativeDataCacheEntryId = 0; + mCacheInfoChannel = nullptr; + mPipeAlternativeOutputStream = nullptr; + mPipeAlternativeInputStream = nullptr; + if (mChannel && mStatus != AlternativeDataStreamListener::FALLBACK) { + // if mStatus is fallback, we need to keep channel to forward request back + // to FetchDriver + mChannel->CancelWithReason(NS_BINDING_ABORTED, + "AlternativeDataStreamListener::Cancel"_ns); + mChannel = nullptr; + } + mStatus = AlternativeDataStreamListener::CANCELED; +} + +uint64_t AlternativeDataStreamListener::GetAlternativeDataCacheEntryId() { + return mAlternativeDataCacheEntryId; +} + +const nsACString& AlternativeDataStreamListener::GetAlternativeDataType() + const { + return mAlternativeDataType; +} + +already_AddRefed<nsIInputStream> +AlternativeDataStreamListener::GetAlternativeInputStream() { + nsCOMPtr<nsIInputStream> inputStream = mPipeAlternativeInputStream; + return inputStream.forget(); +} + +already_AddRefed<nsICacheInfoChannel> +AlternativeDataStreamListener::GetCacheInfoChannel() { + nsCOMPtr<nsICacheInfoChannel> channel = mCacheInfoChannel; + return channel.forget(); +} + +NS_IMETHODIMP +AlternativeDataStreamListener::OnStartRequest(nsIRequest* aRequest) { + AssertIsOnMainThread(); + MOZ_ASSERT(!mAlternativeDataType.IsEmpty()); + // Checking the alternative data type is the same between we asked and the + // saved in the channel. + nsAutoCString alternativeDataType; + nsCOMPtr<nsICacheInfoChannel> cic = do_QueryInterface(aRequest); + mStatus = AlternativeDataStreamListener::LOADING; + if (cic && NS_SUCCEEDED(cic->GetAlternativeDataType(alternativeDataType)) && + mAlternativeDataType.Equals(alternativeDataType) && + NS_SUCCEEDED(cic->GetCacheEntryId(&mAlternativeDataCacheEntryId))) { + MOZ_DIAGNOSTIC_ASSERT(!mPipeAlternativeInputStream); + MOZ_DIAGNOSTIC_ASSERT(!mPipeAlternativeOutputStream); + NS_NewPipe(getter_AddRefs(mPipeAlternativeInputStream), + getter_AddRefs(mPipeAlternativeOutputStream), + 0 /* default segment size */, UINT32_MAX /* infinite pipe */, + true /* non-blocking input, otherwise you deadlock */, + false /* blocking output, since the pipe is 'in'finite */); + + MOZ_DIAGNOSTIC_ASSERT(!mCacheInfoChannel); + mCacheInfoChannel = cic; + + // call FetchDriver::HttpFetch to load main body + MOZ_ASSERT(mFetchDriver); + return mFetchDriver->HttpFetch(); + } + // Needn't load alternative data, since alternative data does not exist. + // Set status to FALLBACK to reuse the opened channel to load main body, + // then call FetchDriver::OnStartRequest to continue the work. Unfortunately + // can't change the stream listener to mFetchDriver, need to keep + // AlternativeDataStreamListener alive to redirect OnDataAvailable and + // OnStopRequest to mFetchDriver. + MOZ_ASSERT(alternativeDataType.IsEmpty()); + mStatus = AlternativeDataStreamListener::FALLBACK; + mAlternativeDataCacheEntryId = 0; + MOZ_ASSERT(mFetchDriver); + return mFetchDriver->OnStartRequest(aRequest); +} + +NS_IMETHODIMP +AlternativeDataStreamListener::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, + uint32_t aCount) { + if (mStatus == AlternativeDataStreamListener::LOADING) { + MOZ_ASSERT(mPipeAlternativeOutputStream); + uint32_t read = 0; + return aInputStream->ReadSegments( + NS_CopySegmentToStream, mPipeAlternativeOutputStream, aCount, &read); + } + if (mStatus == AlternativeDataStreamListener::FALLBACK) { + MOZ_ASSERT(mFetchDriver); + return mFetchDriver->OnDataAvailable(aRequest, aInputStream, aOffset, + aCount); + } + return NS_OK; +} + +NS_IMETHODIMP +AlternativeDataStreamListener::OnStopRequest(nsIRequest* aRequest, + nsresult aStatusCode) { + AssertIsOnMainThread(); + + // Alternative data loading is going to finish, breaking the reference cycle + // here by taking the ownership to a loacl variable. + RefPtr<FetchDriver> fetchDriver = std::move(mFetchDriver); + + if (mStatus == AlternativeDataStreamListener::CANCELED) { + // do nothing + return NS_OK; + } + + if (mStatus == AlternativeDataStreamListener::FALLBACK) { + MOZ_ASSERT(fetchDriver); + return fetchDriver->OnStopRequest(aRequest, aStatusCode); + } + + MOZ_DIAGNOSTIC_ASSERT(mStatus == AlternativeDataStreamListener::LOADING); + + MOZ_ASSERT(!mAlternativeDataType.IsEmpty() && mPipeAlternativeOutputStream && + mPipeAlternativeInputStream); + + mPipeAlternativeOutputStream->Close(); + mPipeAlternativeOutputStream = nullptr; + + // Cleanup the states for alternative data if needed. + if (NS_FAILED(aStatusCode)) { + mAlternativeDataCacheEntryId = 0; + mCacheInfoChannel = nullptr; + mPipeAlternativeInputStream = nullptr; + } + mStatus = AlternativeDataStreamListener::COMPLETED; + // alternative data loading finish, call FetchDriver::FinishOnStopRequest to + // continue the final step for the case FetchDriver::OnStopRequest is called + // earlier than AlternativeDataStreamListener::OnStopRequest + MOZ_ASSERT(fetchDriver); + fetchDriver->FinishOnStopRequest(this); + return NS_OK; +} + +NS_IMETHODIMP +AlternativeDataStreamListener::CheckListenerChain() { return NS_OK; } +//----------------------------------------------------------------------------- +// FetchDriver +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(FetchDriver, nsIStreamListener, nsIChannelEventSink, + nsIInterfaceRequestor, nsIThreadRetargetableStreamListener, + nsINetworkInterceptController) + +FetchDriver::FetchDriver(SafeRefPtr<InternalRequest> aRequest, + nsIPrincipal* aPrincipal, nsILoadGroup* aLoadGroup, + nsIEventTarget* aMainThreadEventTarget, + nsICookieJarSettings* aCookieJarSettings, + PerformanceStorage* aPerformanceStorage, + bool aIsTrackingFetch) + : mPrincipal(aPrincipal), + mLoadGroup(aLoadGroup), + mRequest(std::move(aRequest)), + mMainThreadEventTarget(aMainThreadEventTarget), + mCookieJarSettings(aCookieJarSettings), + mPerformanceStorage(aPerformanceStorage), + mNeedToObserveOnDataAvailable(false), + mIsTrackingFetch(aIsTrackingFetch), + mOnStopRequestCalled(false) +#ifdef DEBUG + , + mResponseAvailableCalled(false), + mFetchCalled(false) +#endif +{ + AssertIsOnMainThread(); + + MOZ_ASSERT(mRequest); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aMainThreadEventTarget); +} + +FetchDriver::~FetchDriver() { + AssertIsOnMainThread(); + + // We assert this since even on failures, we should call + // FailWithNetworkError(). + MOZ_ASSERT(mResponseAvailableCalled); + if (mObserver) { + mObserver = nullptr; + } +} + +already_AddRefed<PreloaderBase> FetchDriver::FindPreload(nsIURI* aURI) { + // Decide if we allow reuse of an existing <link rel=preload as=fetch> + // response for this request. First examine this fetch requets itself if it + // is 'pure' enough to use the response and then try to find a preload. + + if (!mDocument) { + // Preloads are mapped on the document, no document, no preload. + return nullptr; + } + CORSMode cors; + switch (mRequest->Mode()) { + case RequestMode::No_cors: + cors = CORSMode::CORS_NONE; + break; + case RequestMode::Cors: + cors = mRequest->GetCredentialsMode() == RequestCredentials::Include + ? CORSMode::CORS_USE_CREDENTIALS + : CORSMode::CORS_ANONYMOUS; + break; + default: + // Can't be satisfied by a preload because preload cannot define any of + // remaining modes. + return nullptr; + } + if (!mRequest->Headers()->HasOnlySimpleHeaders()) { + // Preload can't set any headers. + return nullptr; + } + if (!mRequest->GetIntegrity().IsEmpty()) { + // There is currently no support for SRI checking in the fetch preloader. + return nullptr; + } + if (mRequest->GetCacheMode() != RequestCache::Default) { + // Preload can only go with the default caching mode. + return nullptr; + } + if (mRequest->SkipServiceWorker()) { + // Preload can't be forbidden interception. + return nullptr; + } + if (mRequest->GetRedirectMode() != RequestRedirect::Follow) { + // Preload always follows redirects. + return nullptr; + } + nsAutoCString method; + mRequest->GetMethod(method); + if (!method.EqualsLiteral("GET")) { + // Preload can only do GET, this also eliminates the case we do upload, so + // no need to check if the request has any body to send out. + return nullptr; + } + + // OK, this request can be satisfied by a preloaded response, try to find one. + + auto preloadKey = PreloadHashKey::CreateAsFetch(aURI, cors); + return mDocument->Preloads().LookupPreload(preloadKey); +} + +void FetchDriver::UpdateReferrerInfoFromNewChannel(nsIChannel* aChannel) { + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel); + if (!httpChannel) { + return; + } + + nsCOMPtr<nsIReferrerInfo> referrerInfo = httpChannel->GetReferrerInfo(); + if (!referrerInfo) { + return; + } + + nsAutoString computedReferrerSpec; + mRequest->SetReferrerPolicy(referrerInfo->ReferrerPolicy()); + Unused << referrerInfo->GetComputedReferrerSpec(computedReferrerSpec); + mRequest->SetReferrer(computedReferrerSpec); +} + +nsresult FetchDriver::Fetch(AbortSignalImpl* aSignalImpl, + FetchDriverObserver* aObserver) { + AssertIsOnMainThread(); +#ifdef DEBUG + MOZ_ASSERT(!mFetchCalled); + mFetchCalled = true; +#endif + + mObserver = aObserver; + + // FIXME(nsm): Deal with HSTS. + + MOZ_RELEASE_ASSERT(!mRequest->IsSynchronous(), + "Synchronous fetch not supported"); + + UniquePtr<mozilla::ipc::PrincipalInfo> principalInfo( + new mozilla::ipc::PrincipalInfo()); + nsresult rv = PrincipalToPrincipalInfo(mPrincipal, principalInfo.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mRequest->SetPrincipalInfo(std::move(principalInfo)); + + // If the signal is aborted, it's time to inform the observer and terminate + // the operation. + if (aSignalImpl) { + if (aSignalImpl->Aborted()) { + FetchDriverAbortActions(aSignalImpl); + return NS_OK; + } + + Follow(aSignalImpl); + } + + rv = HttpFetch(mRequest->GetPreferredAlternativeDataType()); + if (NS_FAILED(rv)) { + FailWithNetworkError(rv); + } + + // Any failure is handled by FailWithNetworkError notifying the aObserver. + return NS_OK; +} + +// This function implements the "HTTP Fetch" algorithm from the Fetch spec. +// Functionality is often split between here, the CORS listener proxy and the +// Necko HTTP implementation. +nsresult FetchDriver::HttpFetch( + const nsACString& aPreferredAlternativeDataType) { + MOZ_ASSERT(NS_IsMainThread()); + + // Step 1. "Let response be null." + mResponse = nullptr; + mOnStopRequestCalled = false; + nsresult rv; + + nsCOMPtr<nsIIOService> ios = do_GetIOService(&rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString url; + mRequest->GetURL(url); + nsCOMPtr<nsIURI> uri; + rv = NS_NewURI(getter_AddRefs(uri), url); + NS_ENSURE_SUCCESS(rv, rv); + + // Unsafe requests aren't allowed with when using no-core mode. + if (mRequest->Mode() == RequestMode::No_cors && mRequest->UnsafeRequest() && + (!mRequest->HasSimpleMethod() || + !mRequest->Headers()->HasOnlySimpleHeaders())) { + MOZ_ASSERT(false, "The API should have caught this"); + return NS_ERROR_DOM_BAD_URI; + } + + // non-GET requests aren't allowed for blob. + if (IsBlobURI(uri)) { + nsAutoCString method; + mRequest->GetMethod(method); + if (!method.EqualsLiteral("GET")) { + return NS_ERROR_DOM_NETWORK_ERR; + } + } + + RefPtr<PreloaderBase> fetchPreload = FindPreload(uri); + if (fetchPreload) { + fetchPreload->RemoveSelf(mDocument); + fetchPreload->NotifyUsage(mDocument, PreloaderBase::LoadBackground::Keep); + + rv = fetchPreload->AsyncConsume(this); + if (NS_SUCCEEDED(rv)) { + mFromPreload = true; + + mChannel = fetchPreload->Channel(); + MOZ_ASSERT(mChannel); + mChannel->SetNotificationCallbacks(this); + + // Copied from AsyncOnChannelRedirect. + for (const auto& redirect : fetchPreload->Redirects()) { + if (redirect.Flags() & nsIChannelEventSink::REDIRECT_INTERNAL) { + mRequest->SetURLForInternalRedirect(redirect.Flags(), redirect.Spec(), + redirect.Fragment()); + } else { + mRequest->AddURL(redirect.Spec(), redirect.Fragment()); + } + } + + return NS_OK; + } + + // The preload failed to be consumed. Behave like there were no preload. + fetchPreload = nullptr; + } + + // Step 2 deals with letting ServiceWorkers intercept requests. This is + // handled by Necko after the channel is opened. + // FIXME(nsm): Bug 1119026: The channel's skip service worker flag should be + // set based on the Request's flag. + + // Step 3.1 "If the CORS preflight flag is set and one of these conditions is + // true..." is handled by the CORS proxy. + // + // Step 3.2 "Set request's skip service worker flag." This isn't required + // since Necko will fall back to the network if the ServiceWorker does not + // respond with a valid Response. + // + // NS_StartCORSPreflight() will automatically kick off the original request + // if it succeeds, so we need to have everything setup for the original + // request too. + + // Step 3.3 "Let credentials flag be set if one of + // - request's credentials mode is "include" + // - request's credentials mode is "same-origin" and either the CORS flag + // is unset or response tainting is "opaque" + // is true, and unset otherwise." + + // Set skip serviceworker flag. + // While the spec also gates on the client being a ServiceWorker, we can't + // infer that here. Instead we rely on callers to set the flag correctly. + const nsLoadFlags bypassFlag = mRequest->SkipServiceWorker() + ? nsIChannel::LOAD_BYPASS_SERVICE_WORKER + : 0; + + nsSecurityFlags secFlags = 0; + if (mRequest->Mode() == RequestMode::Cors) { + secFlags |= nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT; + } else if (mRequest->Mode() == RequestMode::Same_origin || + mRequest->Mode() == RequestMode::Navigate) { + secFlags |= nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT; + } else if (mRequest->Mode() == RequestMode::No_cors) { + secFlags |= nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT; + } else { + MOZ_ASSERT_UNREACHABLE("Unexpected request mode!"); + return NS_ERROR_UNEXPECTED; + } + + if (mRequest->GetRedirectMode() != RequestRedirect::Follow) { + secFlags |= nsILoadInfo::SEC_DONT_FOLLOW_REDIRECTS; + } + + // This handles the use credentials flag in "HTTP + // network or cache fetch" in the spec and decides whether to transmit + // cookies and other identifying information. + if (mRequest->GetCredentialsMode() == RequestCredentials::Include) { + secFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE; + } else if (mRequest->GetCredentialsMode() == RequestCredentials::Omit) { + secFlags |= nsILoadInfo::SEC_COOKIES_OMIT; + } else if (mRequest->GetCredentialsMode() == + RequestCredentials::Same_origin) { + secFlags |= nsILoadInfo::SEC_COOKIES_SAME_ORIGIN; + } else { + MOZ_ASSERT_UNREACHABLE("Unexpected credentials mode!"); + return NS_ERROR_UNEXPECTED; + } + + // From here on we create a channel and set its properties with the + // information from the InternalRequest. This is an implementation detail. + MOZ_ASSERT(mLoadGroup); + nsCOMPtr<nsIChannel> chan; + + nsLoadFlags loadFlags = nsIRequest::LOAD_BACKGROUND | bypassFlag; + if (mDocument) { + MOZ_ASSERT(mDocument->NodePrincipal() == mPrincipal); + MOZ_ASSERT(mDocument->CookieJarSettings() == mCookieJarSettings); + rv = NS_NewChannel(getter_AddRefs(chan), uri, mDocument, secFlags, + mRequest->ContentPolicyType(), + nullptr, /* aPerformanceStorage */ + mLoadGroup, nullptr, /* aCallbacks */ + loadFlags, ios); + } else if (mClientInfo.isSome()) { + rv = NS_NewChannel(getter_AddRefs(chan), uri, mPrincipal, mClientInfo.ref(), + mController, secFlags, mRequest->ContentPolicyType(), + mCookieJarSettings, mPerformanceStorage, mLoadGroup, + nullptr, /* aCallbacks */ + loadFlags, ios); + } else { + rv = + NS_NewChannel(getter_AddRefs(chan), uri, mPrincipal, secFlags, + mRequest->ContentPolicyType(), mCookieJarSettings, + mPerformanceStorage, mLoadGroup, nullptr, /* aCallbacks */ + loadFlags, ios); + } + NS_ENSURE_SUCCESS(rv, rv); + + if (mCSPEventListener) { + nsCOMPtr<nsILoadInfo> loadInfo = chan->LoadInfo(); + rv = loadInfo->SetCspEventListener(mCSPEventListener); + NS_ENSURE_SUCCESS(rv, rv); + } + + { + nsCOMPtr<nsILoadInfo> loadInfo = chan->LoadInfo(); + rv = loadInfo->SetLoadingEmbedderPolicy(mRequest->GetEmbedderPolicy()); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (mAssociatedBrowsingContextID) { + nsCOMPtr<nsILoadInfo> loadInfo = chan->LoadInfo(); + rv = loadInfo->SetWorkerAssociatedBrowsingContextID( + mAssociatedBrowsingContextID); + } + + // If the fetch is created by FetchEvent.request or NavigationPreload request, + // corresponding InterceptedHttpChannel information need to propagte to the + // channel of the fetch. + if (mRequest->GetInterceptionTriggeringPrincipalInfo()) { + auto principalOrErr = mozilla::ipc::PrincipalInfoToPrincipal( + *(mRequest->GetInterceptionTriggeringPrincipalInfo().get())); + if (!principalOrErr.isErr()) { + nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); + + nsTArray<nsCOMPtr<nsIRedirectHistoryEntry>> redirectChain; + if (!mRequest->InterceptionRedirectChain().IsEmpty()) { + for (const RedirectHistoryEntryInfo& entryInfo : + mRequest->InterceptionRedirectChain()) { + nsCOMPtr<nsIRedirectHistoryEntry> entry = + mozilla::ipc::RHEntryInfoToRHEntry(entryInfo); + redirectChain.AppendElement(entry); + } + } + + nsCOMPtr<nsILoadInfo> loadInfo = chan->LoadInfo(); + MOZ_ASSERT(loadInfo); + loadInfo->SetInterceptionInfo(new mozilla::net::InterceptionInfo( + principal, mRequest->InterceptionContentPolicyType(), redirectChain, + mRequest->InterceptionFromThirdParty())); + } + } + + if (mDocument && mDocument->GetEmbedderElement() && + mDocument->GetEmbedderElement()->IsAnyOfHTMLElements(nsGkAtoms::object, + nsGkAtoms::embed)) { + nsCOMPtr<nsILoadInfo> loadInfo = chan->LoadInfo(); + rv = loadInfo->SetIsFromObjectOrEmbed(true); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Insert ourselves into the notification callbacks chain so we can set + // headers on redirects. +#ifdef DEBUG + { + nsCOMPtr<nsIInterfaceRequestor> notificationCallbacks; + chan->GetNotificationCallbacks(getter_AddRefs(notificationCallbacks)); + MOZ_ASSERT(!notificationCallbacks); + } +#endif + chan->SetNotificationCallbacks(this); + + nsCOMPtr<nsIClassOfService> cos(do_QueryInterface(chan)); + // Mark channel as urgent-start if the Fetch is triggered by user input + // events. + if (cos && UserActivation::IsHandlingUserInput()) { + cos->AddClassFlags(nsIClassOfService::UrgentStart); + } + + // Step 3.5 begins "HTTP network or cache fetch". + // HTTP network or cache fetch + // --------------------------- + // Step 1 "Let HTTPRequest..." The channel is the HTTPRequest. + nsCOMPtr<nsIHttpChannel> httpChan = do_QueryInterface(chan); + if (httpChan) { + // Copy the method. + nsAutoCString method; + mRequest->GetMethod(method); + rv = httpChan->SetRequestMethod(method); + NS_ENSURE_SUCCESS(rv, rv); + + // Set the same headers. + SetRequestHeaders(httpChan, false, false); + + // Step 5 of https://fetch.spec.whatwg.org/#main-fetch + // If request's referrer policy is the empty string and request's client is + // non-null, then set request's referrer policy to request's client's + // associated referrer policy. + // Basically, "client" is not in our implementation, we use + // EnvironmentReferrerPolicy of the worker or document context + ReferrerPolicy referrerPolicy = mRequest->GetEnvironmentReferrerPolicy(); + if (mRequest->ReferrerPolicy_() == ReferrerPolicy::_empty) { + mRequest->SetReferrerPolicy(referrerPolicy); + } + // Step 6 of https://fetch.spec.whatwg.org/#main-fetch + // If request’s referrer policy is the empty string, + // then set request’s referrer policy to the user-set default policy. + if (mRequest->ReferrerPolicy_() == ReferrerPolicy::_empty) { + nsCOMPtr<nsILoadInfo> loadInfo = httpChan->LoadInfo(); + bool isPrivate = loadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + referrerPolicy = + ReferrerInfo::GetDefaultReferrerPolicy(httpChan, uri, isPrivate); + mRequest->SetReferrerPolicy(referrerPolicy); + } + + rv = FetchUtil::SetRequestReferrer(mPrincipal, mDocument, httpChan, + *mRequest); + NS_ENSURE_SUCCESS(rv, rv); + + // Bug 1120722 - Authorization will be handled later. + // Auth may require prompting, we don't support it yet. + // The next patch in this same bug prevents this from aborting the request. + // Credentials checks for CORS are handled by nsCORSListenerProxy, + + nsCOMPtr<nsIHttpChannelInternal> internalChan = do_QueryInterface(httpChan); + + rv = internalChan->SetRequestMode(mRequest->Mode()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + // Conversion between enumerations is safe due to static asserts in + // dom/workers/ServiceWorkerManager.cpp + rv = internalChan->SetRedirectMode( + static_cast<uint32_t>(mRequest->GetRedirectMode())); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + mRequest->MaybeSkipCacheIfPerformingRevalidation(); + rv = internalChan->SetFetchCacheMode( + static_cast<uint32_t>(mRequest->GetCacheMode())); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = internalChan->SetIntegrityMetadata(mRequest->GetIntegrity()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Set the initiator type + nsCOMPtr<nsITimedChannel> timedChannel(do_QueryInterface(httpChan)); + if (timedChannel) { + timedChannel->SetInitiatorType(u"fetch"_ns); + } + } + + // Step 5. Proxy authentication will be handled by Necko. + + // Continue setting up 'HTTPRequest'. Content-Type and body data. + nsCOMPtr<nsIUploadChannel2> uploadChan = do_QueryInterface(chan); + if (uploadChan) { + nsAutoCString contentType; + ErrorResult result; + mRequest->Headers()->GetFirst("content-type"_ns, contentType, result); + // We don't actually expect "result" to have failed here: that only happens + // for invalid header names. But if for some reason it did, just propagate + // it out. + if (result.Failed()) { + return result.StealNSResult(); + } + + // Now contentType is the header that was set in mRequest->Headers(), or a + // void string if no header was set. +#ifdef DEBUG + bool hasContentTypeHeader = + mRequest->Headers()->Has("content-type"_ns, result); + MOZ_ASSERT(!result.Failed()); + MOZ_ASSERT_IF(!hasContentTypeHeader, contentType.IsVoid()); +#endif // DEBUG + + int64_t bodyLength; + nsCOMPtr<nsIInputStream> bodyStream; + mRequest->GetBody(getter_AddRefs(bodyStream), &bodyLength); + if (bodyStream) { + nsAutoCString method; + mRequest->GetMethod(method); + rv = uploadChan->ExplicitSetUploadStream(bodyStream, contentType, + bodyLength, method, + false /* aStreamHasHeaders */); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // If preflight is required, start a "CORS preflight fetch" + // https://fetch.spec.whatwg.org/#cors-preflight-fetch-0. All the + // implementation is handled by the http channel calling into + // nsCORSListenerProxy. We just inform it which unsafe headers are included + // in the request. + if (mRequest->Mode() == RequestMode::Cors) { + AutoTArray<nsCString, 5> unsafeHeaders; + mRequest->Headers()->GetUnsafeHeaders(unsafeHeaders); + nsCOMPtr<nsILoadInfo> loadInfo = chan->LoadInfo(); + loadInfo->SetCorsPreflightInfo(unsafeHeaders, false); + } + + if (mIsTrackingFetch && StaticPrefs::network_http_tailing_enabled() && cos) { + cos->AddClassFlags(nsIClassOfService::Throttleable | + nsIClassOfService::Tail); + } + + if (mIsTrackingFetch && + StaticPrefs::privacy_trackingprotection_lower_network_priority()) { + nsCOMPtr<nsISupportsPriority> p = do_QueryInterface(chan); + if (p) { + p->SetPriority(nsISupportsPriority::PRIORITY_LOWEST); + } + } + + NotifyNetworkMonitorAlternateStack(chan, std::move(mOriginStack)); + if (mObserver && httpChan) { + mObserver->OnNotifyNetworkMonitorAlternateStack(httpChan->ChannelId()); + } + + // if the preferred alternative data type in InternalRequest is not empty, set + // the data type on the created channel and also create a + // AlternativeDataStreamListener to be the stream listener of the channel. + if (!aPreferredAlternativeDataType.IsEmpty()) { + nsCOMPtr<nsICacheInfoChannel> cic = do_QueryInterface(chan); + if (cic) { + cic->PreferAlternativeDataType( + aPreferredAlternativeDataType, ""_ns, + nsICacheInfoChannel::PreferredAlternativeDataDeliveryType::ASYNC); + MOZ_ASSERT(!mAltDataListener); + mAltDataListener = new AlternativeDataStreamListener( + this, chan, aPreferredAlternativeDataType); + rv = chan->AsyncOpen(mAltDataListener); + } else { + rv = chan->AsyncOpen(this); + } + } else { + // Integrity check cannot be done on alt-data yet. + if (mRequest->GetIntegrity().IsEmpty()) { + MOZ_ASSERT(!FetchUtil::WasmAltDataType.IsEmpty()); + nsCOMPtr<nsICacheInfoChannel> cic = do_QueryInterface(chan); + if (cic && StaticPrefs::javascript_options_wasm_caching() && + !mRequest->SkipWasmCaching()) { + cic->PreferAlternativeDataType( + FetchUtil::WasmAltDataType, nsLiteralCString(WASM_CONTENT_TYPE), + nsICacheInfoChannel::PreferredAlternativeDataDeliveryType:: + SERIALIZE); + } + } + + rv = chan->AsyncOpen(this); + } + + if (NS_FAILED(rv)) { + return rv; + } + + // Step 4 onwards of "HTTP Fetch" is handled internally by Necko. + + mChannel = chan; + return NS_OK; +} + +SafeRefPtr<InternalResponse> FetchDriver::BeginAndGetFilteredResponse( + SafeRefPtr<InternalResponse> aResponse, bool aFoundOpaqueRedirect) { + MOZ_ASSERT(aResponse); + AutoTArray<nsCString, 4> reqURLList; + mRequest->GetURLListWithoutFragment(reqURLList); + MOZ_ASSERT(!reqURLList.IsEmpty()); + aResponse->SetURLList(reqURLList); + SafeRefPtr<InternalResponse> filteredResponse; + if (aFoundOpaqueRedirect) { + filteredResponse = aResponse->OpaqueRedirectResponse(); + } else { + switch (mRequest->GetResponseTainting()) { + case LoadTainting::Basic: + filteredResponse = aResponse->BasicResponse(); + break; + case LoadTainting::CORS: + filteredResponse = aResponse->CORSResponse(); + break; + case LoadTainting::Opaque: { + filteredResponse = aResponse->OpaqueResponse(); + nsresult rv = filteredResponse->GeneratePaddingInfo(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + break; + } + default: + MOZ_CRASH("Unexpected case"); + } + } + + MOZ_ASSERT(filteredResponse); + MOZ_ASSERT(mObserver); + MOZ_ASSERT(filteredResponse); + if (!ShouldCheckSRI(*mRequest, *filteredResponse)) { + // Need to keep mObserver alive. + RefPtr<FetchDriverObserver> observer = mObserver; + observer->OnResponseAvailable(filteredResponse.clonePtr()); +#ifdef DEBUG + mResponseAvailableCalled = true; +#endif + } + + return filteredResponse; +} + +void FetchDriver::FailWithNetworkError(nsresult rv) { + AssertIsOnMainThread(); + if (mObserver) { + // Need to keep mObserver alive. + RefPtr<FetchDriverObserver> observer = mObserver; + observer->OnResponseAvailable(InternalResponse::NetworkError(rv)); +#ifdef DEBUG + mResponseAvailableCalled = true; +#endif + } + + // mObserver could be null after OnResponseAvailable(). + if (mObserver) { + mObserver->OnReportPerformanceTiming(); + mObserver->OnResponseEnd(FetchDriverObserver::eByNetworking, + JS::UndefinedHandleValue); + mObserver = nullptr; + } + + mChannel = nullptr; + Unfollow(); +} + +NS_IMETHODIMP +FetchDriver::OnStartRequest(nsIRequest* aRequest) { + AssertIsOnMainThread(); + + // Note, this can be called multiple times if we are doing an opaqueredirect. + // In that case we will get a simulated OnStartRequest() and then the real + // channel will call in with an errored OnStartRequest(). + + if (mFromPreload && mAborted) { + aRequest->CancelWithReason(NS_BINDING_ABORTED, + "FetchDriver::OnStartRequest aborted"_ns); + return NS_BINDING_ABORTED; + } + + if (!mChannel) { + // if the request is aborted, we remove the mObserver reference in + // OnStopRequest or ~FetchDriver() + MOZ_ASSERT_IF(!mAborted, !mObserver); + return NS_BINDING_ABORTED; + } + + nsresult rv; + aRequest->GetStatus(&rv); + if (NS_FAILED(rv)) { + FailWithNetworkError(rv); + return rv; + } + + // We should only get to the following code once. + MOZ_ASSERT(!mPipeOutputStream); + + if (!mObserver) { + MOZ_ASSERT(false, "We should have mObserver here."); + FailWithNetworkError(NS_ERROR_UNEXPECTED); + return NS_ERROR_UNEXPECTED; + } + + mNeedToObserveOnDataAvailable = mObserver->NeedOnDataAvailable(); + + SafeRefPtr<InternalResponse> response; + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest); + + // On a successful redirect we perform the following substeps of HTTP Fetch, + // step 5, "redirect status", step 11. + + bool foundOpaqueRedirect = false; + + nsAutoCString contentType; + channel->GetContentType(contentType); + + int64_t contentLength = InternalResponse::UNKNOWN_BODY_SIZE; + rv = channel->GetContentLength(&contentLength); + MOZ_ASSERT_IF(NS_FAILED(rv), + contentLength == InternalResponse::UNKNOWN_BODY_SIZE); + + if (httpChannel) { + uint32_t responseStatus = 0; + rv = httpChannel->GetResponseStatus(&responseStatus); + if (NS_FAILED(rv)) { + FailWithNetworkError(rv); + return rv; + } + + if (mozilla::net::nsHttpChannel::IsRedirectStatus(responseStatus)) { + if (mRequest->GetRedirectMode() == RequestRedirect::Error) { + FailWithNetworkError(NS_BINDING_ABORTED); + return NS_BINDING_FAILED; + } + if (mRequest->GetRedirectMode() == RequestRedirect::Manual) { + foundOpaqueRedirect = true; + } + } + + nsAutoCString statusText; + rv = httpChannel->GetResponseStatusText(statusText); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + response = MakeSafeRefPtr<InternalResponse>(responseStatus, statusText, + mRequest->GetCredentialsMode()); + + UniquePtr<mozilla::ipc::PrincipalInfo> principalInfo( + new mozilla::ipc::PrincipalInfo()); + nsresult rv = PrincipalToPrincipalInfo(mPrincipal, principalInfo.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + response->SetPrincipalInfo(std::move(principalInfo)); + + response->Headers()->FillResponseHeaders(httpChannel); + + // If Content-Encoding or Transfer-Encoding headers are set, then the actual + // Content-Length (which refer to the decoded data) is obscured behind the + // encodings. + ErrorResult result; + if (response->Headers()->Has("content-encoding"_ns, result) || + response->Headers()->Has("transfer-encoding"_ns, result)) { + // We cannot trust the content-length when content-encoding or + // transfer-encoding are set. There are many servers which just + // get this wrong. + contentLength = InternalResponse::UNKNOWN_BODY_SIZE; + } + MOZ_ASSERT(!result.Failed()); + } else { + response = MakeSafeRefPtr<InternalResponse>(200, "OK"_ns, + mRequest->GetCredentialsMode()); + + if (!contentType.IsEmpty()) { + nsAutoCString contentCharset; + channel->GetContentCharset(contentCharset); + if (NS_SUCCEEDED(rv) && !contentCharset.IsEmpty()) { + contentType += ";charset="_ns + contentCharset; + } + } + + IgnoredErrorResult result; + response->Headers()->Append("Content-Type"_ns, contentType, result); + MOZ_ASSERT(!result.Failed()); + + if (contentLength >= 0) { + nsAutoCString contentLenStr; + contentLenStr.AppendInt(contentLength); + + IgnoredErrorResult result; + response->Headers()->Append("Content-Length"_ns, contentLenStr, result); + MOZ_ASSERT(!result.Failed()); + } + } + + nsCOMPtr<nsICacheInfoChannel> cic = do_QueryInterface(aRequest); + if (cic) { + if (mAltDataListener) { + // Skip the case that mAltDataListener->Status() equals to FALLBACK, that + // means the opened channel for alternative data loading is reused for + // loading the main data. + if (mAltDataListener->Status() != + AlternativeDataStreamListener::FALLBACK) { + // Verify the cache ID is the same with from alternative data cache. + // If the cache ID is different, droping the alternative data loading, + // otherwise setup the response's alternative body and cacheInfoChannel. + uint64_t cacheEntryId = 0; + if (NS_SUCCEEDED(cic->GetCacheEntryId(&cacheEntryId)) && + cacheEntryId != + mAltDataListener->GetAlternativeDataCacheEntryId()) { + mAltDataListener->Cancel(); + } else { + // AlternativeDataStreamListener::OnStartRequest had already been + // called, the alternative data input stream and cacheInfo channel + // must be created. + nsCOMPtr<nsICacheInfoChannel> cacheInfo = + mAltDataListener->GetCacheInfoChannel(); + nsCOMPtr<nsIInputStream> altInputStream = + mAltDataListener->GetAlternativeInputStream(); + MOZ_ASSERT(altInputStream && cacheInfo); + response->SetAlternativeBody(altInputStream); + nsMainThreadPtrHandle<nsICacheInfoChannel> handle( + new nsMainThreadPtrHolder<nsICacheInfoChannel>( + "nsICacheInfoChannel", cacheInfo, false)); + response->SetCacheInfoChannel(handle); + } + } else if (!mAltDataListener->GetAlternativeDataType().IsEmpty()) { + // If the status is FALLBACK and the + // mAltDataListener::mAlternativeDataType is not empty, that means the + // data need to be saved into cache, setup the response's + // nsICacheInfoChannel for caching the data after loading. + nsMainThreadPtrHandle<nsICacheInfoChannel> handle( + new nsMainThreadPtrHolder<nsICacheInfoChannel>( + "nsICacheInfoChannel", cic, false)); + response->SetCacheInfoChannel(handle); + } + } else if (!cic->PreferredAlternativeDataTypes().IsEmpty()) { + MOZ_ASSERT(cic->PreferredAlternativeDataTypes().Length() == 1); + MOZ_ASSERT(cic->PreferredAlternativeDataTypes()[0].type().Equals( + FetchUtil::WasmAltDataType)); + MOZ_ASSERT( + cic->PreferredAlternativeDataTypes()[0].contentType().EqualsLiteral( + WASM_CONTENT_TYPE)); + + if (contentType.EqualsLiteral(WASM_CONTENT_TYPE)) { + // We want to attach the CacheInfoChannel to the response object such + // that we can track its origin when the Response object is manipulated + // by JavaScript code. This is important for WebAssembly, which uses + // fetch to query its sources in JavaScript and transfer the Response + // object to other function responsible for storing the alternate data + // using the CacheInfoChannel. + nsMainThreadPtrHandle<nsICacheInfoChannel> handle( + new nsMainThreadPtrHolder<nsICacheInfoChannel>( + "nsICacheInfoChannel", cic, false)); + response->SetCacheInfoChannel(handle); + } + } + } + + // We open a pipe so that we can immediately set the pipe's read end as the + // response's body. Setting the segment size to UINT32_MAX means that the + // pipe has infinite space. The nsIChannel will continue to buffer data in + // xpcom events even if we block on a fixed size pipe. It might be possible + // to suspend the channel and then resume when there is space available, but + // for now use an infinite pipe to avoid blocking. + nsCOMPtr<nsIInputStream> pipeInputStream; + NS_NewPipe(getter_AddRefs(pipeInputStream), getter_AddRefs(mPipeOutputStream), + 0, /* default segment size */ + UINT32_MAX /* infinite pipe */, + true /* non-blocking input, otherwise you deadlock */, + false /* blocking output, since the pipe is 'in'finite */); + response->SetBody(pipeInputStream, contentLength); + + // If the request is a file channel, then remember the local path to + // that file so we can later create File blobs rather than plain ones. + nsCOMPtr<nsIFileChannel> fc = do_QueryInterface(aRequest); + if (fc) { + nsCOMPtr<nsIFile> file; + rv = fc->GetFile(getter_AddRefs(file)); + if (!NS_WARN_IF(NS_FAILED(rv))) { + nsAutoString path; + file->GetPath(path); + response->SetBodyLocalPath(path); + } + } else { + // If the request is a blob URI, then remember that URI so that we + // can later just use that blob instance instead of cloning it. + nsCString blobURISpec; + GetBlobURISpecFromChannel(aRequest, blobURISpec); + if (!blobURISpec.IsVoid()) { + response->SetBodyBlobURISpec(blobURISpec); + } + } + + response->InitChannelInfo(channel); + + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + // Propagate any tainting from the channel back to our response here. This + // step is not reflected in the spec because the spec is written such that + // FetchEvent.respondWith() just passes the already-tainted Response back to + // the outer fetch(). In gecko, however, we serialize the Response through + // the channel and must regenerate the tainting from the channel in the + // interception case. + mRequest->MaybeIncreaseResponseTainting(loadInfo->GetTainting()); + + // Resolves fetch() promise which may trigger code running in a worker. Make + // sure the Response is fully initialized before calling this. + mResponse = + BeginAndGetFilteredResponse(std::move(response), foundOpaqueRedirect); + if (NS_WARN_IF(!mResponse)) { + // Fail to generate a paddingInfo for opaque response. + MOZ_DIAGNOSTIC_ASSERT(mRequest->GetResponseTainting() == + LoadTainting::Opaque && + !foundOpaqueRedirect); + FailWithNetworkError(NS_ERROR_UNEXPECTED); + return NS_ERROR_UNEXPECTED; + } + + // From "Main Fetch" step 19: SRI-part1. + if (ShouldCheckSRI(*mRequest, *mResponse) && mSRIMetadata.IsEmpty()) { + nsIConsoleReportCollector* reporter = nullptr; + if (mObserver) { + reporter = mObserver->GetReporter(); + } + + nsAutoCString sourceUri; + if (mDocument && mDocument->GetDocumentURI()) { + mDocument->GetDocumentURI()->GetAsciiSpec(sourceUri); + } else if (!mWorkerScript.IsEmpty()) { + sourceUri.Assign(mWorkerScript); + } + SRICheck::IntegrityMetadata(mRequest->GetIntegrity(), sourceUri, reporter, + &mSRIMetadata); + mSRIDataVerifier = + MakeUnique<SRICheckDataVerifier>(mSRIMetadata, sourceUri, reporter); + + // Do not retarget off main thread when using SRI API. + return NS_OK; + } + + nsCOMPtr<nsIEventTarget> sts = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailWithNetworkError(rv); + // Cancel request. + return rv; + } + + // Try to retarget off main thread. + if (nsCOMPtr<nsIThreadRetargetableRequest> rr = do_QueryInterface(aRequest)) { + RefPtr<TaskQueue> queue = + TaskQueue::Create(sts.forget(), "FetchDriver STS Delivery Queue"); + Unused << NS_WARN_IF(NS_FAILED(rr->RetargetDeliveryTo(queue))); + } + return NS_OK; +} + +namespace { + +// Runnable to call the observer OnDataAvailable on the main-thread. +class DataAvailableRunnable final : public Runnable { + RefPtr<FetchDriverObserver> mObserver; + + public: + explicit DataAvailableRunnable(FetchDriverObserver* aObserver) + : Runnable("dom::DataAvailableRunnable"), mObserver(aObserver) { + MOZ_ASSERT(aObserver); + } + + NS_IMETHOD + Run() override { + mObserver->OnDataAvailable(); + mObserver = nullptr; + return NS_OK; + } +}; + +struct SRIVerifierAndOutputHolder { + SRIVerifierAndOutputHolder(SRICheckDataVerifier* aVerifier, + nsIOutputStream* aOutputStream) + : mVerifier(aVerifier), mOutputStream(aOutputStream) {} + + SRICheckDataVerifier* mVerifier; + nsIOutputStream* mOutputStream; + + private: + SRIVerifierAndOutputHolder() = delete; +}; + +// Just like NS_CopySegmentToStream, but also sends the data into an +// SRICheckDataVerifier. +nsresult CopySegmentToStreamAndSRI(nsIInputStream* aInStr, void* aClosure, + const char* aBuffer, uint32_t aOffset, + uint32_t aCount, uint32_t* aCountWritten) { + auto holder = static_cast<SRIVerifierAndOutputHolder*>(aClosure); + MOZ_DIAGNOSTIC_ASSERT(holder && holder->mVerifier && holder->mOutputStream, + "Bogus holder"); + nsresult rv = holder->mVerifier->Update( + aCount, reinterpret_cast<const uint8_t*>(aBuffer)); + NS_ENSURE_SUCCESS(rv, rv); + + // The rest is just like NS_CopySegmentToStream. + *aCountWritten = 0; + while (aCount) { + uint32_t n = 0; + rv = holder->mOutputStream->Write(aBuffer, aCount, &n); + if (NS_FAILED(rv)) { + return rv; + } + aBuffer += n; + aCount -= n; + *aCountWritten += n; + } + return NS_OK; +} + +} // anonymous namespace + +NS_IMETHODIMP +FetchDriver::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aInputStream, + uint64_t aOffset, uint32_t aCount) { + // NB: This can be called on any thread! But we're guaranteed that it is + // called between OnStartRequest and OnStopRequest, so we don't need to worry + // about races for accesses in OnStartRequest, OnStopRequest and + // member functions accessed before opening the channel. + // However, we have a possibility of a race from FetchDriverAbortActions. + // Hence, we need to ensure that we are not modifying any members accessed by + // FetchDriver::FetchDriverAbortActions + + if (mNeedToObserveOnDataAvailable) { + mNeedToObserveOnDataAvailable = false; + if (mObserver) { + // Need to keep mObserver alive. + RefPtr<FetchDriverObserver> observer = mObserver; + if (NS_IsMainThread()) { + observer->OnDataAvailable(); + } else { + RefPtr<Runnable> runnable = new DataAvailableRunnable(observer); + nsresult rv = mMainThreadEventTarget->Dispatch(runnable.forget(), + NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + } + + if (!mResponse) { + MOZ_ASSERT(false); + return NS_ERROR_UNEXPECTED; + } + + // Needs to be initialized to 0 because in some cases nsStringInputStream may + // not write to aRead. + uint32_t aRead = 0; + MOZ_ASSERT(mPipeOutputStream); + + // From "Main Fetch" step 19: SRI-part2. + // Note: Avoid checking the hidden opaque body. + nsresult rv; + if (mResponse->Type() != ResponseType::Opaque && + ShouldCheckSRI(*mRequest, *mResponse)) { + MOZ_ASSERT(mSRIDataVerifier); + + SRIVerifierAndOutputHolder holder(mSRIDataVerifier.get(), + mPipeOutputStream); + rv = aInputStream->ReadSegments(CopySegmentToStreamAndSRI, &holder, aCount, + &aRead); + } else { + rv = aInputStream->ReadSegments(NS_CopySegmentToStream, mPipeOutputStream, + aCount, &aRead); + } + + // If no data was read, it's possible the output stream is closed but the + // ReadSegments call followed its contract of returning NS_OK despite write + // errors. Unfortunately, nsIOutputStream has an ill-conceived contract when + // taken together with ReadSegments' contract, because the pipe will just + // NS_OK if we try and invoke its Write* functions ourselves with a 0 count. + // So we must just assume the pipe is broken. + if (aRead == 0 && aCount != 0) { + return NS_BASE_STREAM_CLOSED; + } + return rv; +} + +NS_IMETHODIMP +FetchDriver::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) { + AssertIsOnMainThread(); + + MOZ_DIAGNOSTIC_ASSERT(!mOnStopRequestCalled); + mOnStopRequestCalled = true; + + if (mObserver && mAborted) { + // fetch request was aborted. + // We have already sent the observer + // notification that request has been aborted in FetchDriverAbortActions. + // Remove the observer reference and don't push anymore notifications. + mObserver = nullptr; + } + + // main data loading is going to finish, breaking the reference cycle. + RefPtr<AlternativeDataStreamListener> altDataListener = + std::move(mAltDataListener); + + // For PFetch and ServiceWorker navigationPreload, resource timing should be + // reported before the body stream closing. + if (mObserver) { + mObserver->OnReportPerformanceTiming(); + } + + // We need to check mObserver, which is nulled by FailWithNetworkError(), + // because in the case of "error" redirect mode, aStatusCode may be NS_OK but + // mResponse will definitely be null so we must not take the else branch. + if (NS_FAILED(aStatusCode) || !mObserver) { + nsCOMPtr<nsIAsyncOutputStream> outputStream = + do_QueryInterface(mPipeOutputStream); + if (outputStream) { + outputStream->CloseWithStatus(NS_FAILED(aStatusCode) ? aStatusCode + : NS_BINDING_FAILED); + } + if (altDataListener) { + altDataListener->Cancel(); + } + + // We proceed as usual here, since we've already created a successful + // response from OnStartRequest. + } else { + MOZ_ASSERT(mResponse); + MOZ_ASSERT(!mResponse->IsError()); + + // From "Main Fetch" step 19: SRI-part3. + if (ShouldCheckSRI(*mRequest, *mResponse)) { + MOZ_ASSERT(mSRIDataVerifier); + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + + nsIConsoleReportCollector* reporter = nullptr; + if (mObserver) { + reporter = mObserver->GetReporter(); + } + + nsAutoCString sourceUri; + if (mDocument && mDocument->GetDocumentURI()) { + mDocument->GetDocumentURI()->GetAsciiSpec(sourceUri); + } else if (!mWorkerScript.IsEmpty()) { + sourceUri.Assign(mWorkerScript); + } + nsresult rv = + mSRIDataVerifier->Verify(mSRIMetadata, channel, sourceUri, reporter); + if (NS_FAILED(rv)) { + if (altDataListener) { + altDataListener->Cancel(); + } + FailWithNetworkError(rv); + // Cancel request. + return rv; + } + } + + if (mPipeOutputStream) { + mPipeOutputStream->Close(); + } + } + + FinishOnStopRequest(altDataListener); + return NS_OK; +} + +void FetchDriver::FinishOnStopRequest( + AlternativeDataStreamListener* aAltDataListener) { + AssertIsOnMainThread(); + // OnStopRequest is not called from channel, that means the main data loading + // does not finish yet. Reaching here since alternative data loading finishes. + if (!mOnStopRequestCalled) { + return; + } + + MOZ_DIAGNOSTIC_ASSERT(!mAltDataListener); + // Wait for alternative data loading finish if we needed it. + if (aAltDataListener && + aAltDataListener->Status() == AlternativeDataStreamListener::LOADING) { + // For LOADING case, channel holds the reference of altDataListener, no need + // to restore it to mAltDataListener. + return; + } + + if (mObserver) { + // From "Main Fetch" step 19.1, 19.2: Process response. + if (ShouldCheckSRI(*mRequest, *mResponse)) { + MOZ_ASSERT(mResponse); + // Need to keep mObserver alive. + RefPtr<FetchDriverObserver> observer = mObserver; + observer->OnResponseAvailable(mResponse.clonePtr()); +#ifdef DEBUG + mResponseAvailableCalled = true; +#endif + } + } + + if (mObserver) { + mObserver->OnResponseEnd(FetchDriverObserver::eByNetworking, + JS::UndefinedHandleValue); + mObserver = nullptr; + } + + mChannel = nullptr; + Unfollow(); +} + +NS_IMETHODIMP +FetchDriver::ShouldPrepareForIntercept(nsIURI* aURI, nsIChannel* aChannel, + bool* aShouldIntercept) { + MOZ_ASSERT(aChannel); + + if (mInterceptController) { + MOZ_ASSERT(XRE_IsParentProcess()); + return mInterceptController->ShouldPrepareForIntercept(aURI, aChannel, + aShouldIntercept); + } + + nsCOMPtr<nsINetworkInterceptController> controller; + NS_QueryNotificationCallbacks(nullptr, mLoadGroup, + NS_GET_IID(nsINetworkInterceptController), + getter_AddRefs(controller)); + if (controller) { + return controller->ShouldPrepareForIntercept(aURI, aChannel, + aShouldIntercept); + } + + *aShouldIntercept = false; + return NS_OK; +} + +NS_IMETHODIMP +FetchDriver::ChannelIntercepted(nsIInterceptedChannel* aChannel) { + if (mInterceptController) { + MOZ_ASSERT(XRE_IsParentProcess()); + return mInterceptController->ChannelIntercepted(aChannel); + } + + nsCOMPtr<nsINetworkInterceptController> controller; + NS_QueryNotificationCallbacks(nullptr, mLoadGroup, + NS_GET_IID(nsINetworkInterceptController), + getter_AddRefs(controller)); + if (controller) { + return controller->ChannelIntercepted(aChannel); + } + + return NS_OK; +} + +void FetchDriver::EnableNetworkInterceptControl() { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mInterceptController); + mInterceptController = new ServiceWorkerInterceptController(); +} + +NS_IMETHODIMP +FetchDriver::AsyncOnChannelRedirect(nsIChannel* aOldChannel, + nsIChannel* aNewChannel, uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* aCallback) { + nsCOMPtr<nsIHttpChannel> oldHttpChannel = do_QueryInterface(aOldChannel); + nsCOMPtr<nsIHttpChannel> newHttpChannel = do_QueryInterface(aNewChannel); + if (oldHttpChannel && newHttpChannel) { + nsAutoCString method; + mRequest->GetMethod(method); + + // Fetch 4.4.11 + bool rewriteToGET = false; + Unused << oldHttpChannel->ShouldStripRequestBodyHeader(method, + &rewriteToGET); + + // we need to strip Authentication headers for cross-origin requests + // Ref: https://fetch.spec.whatwg.org/#http-redirect-fetch + bool skipAuthHeader = + (StaticPrefs::network_fetch_redirect_stripAuthHeader() && + NS_ShouldRemoveAuthHeaderOnRedirect(aOldChannel, aNewChannel, aFlags)); + + SetRequestHeaders(newHttpChannel, rewriteToGET, skipAuthHeader); + } + + // "HTTP-redirect fetch": step 14 "Append locationURL to request's URL list." + // However, ignore internal redirects here. We don't want to flip + // Response.redirected to true if an internal redirect occurs. These + // should be transparent to script. + nsCOMPtr<nsIURI> uri; + MOZ_ALWAYS_SUCCEEDS(NS_GetFinalChannelURI(aNewChannel, getter_AddRefs(uri))); + + nsCOMPtr<nsIURI> uriClone; + nsresult rv = NS_GetURIWithoutRef(uri, getter_AddRefs(uriClone)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + nsCString spec; + rv = uriClone->GetSpec(spec); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + nsCString fragment; + rv = uri->GetRef(fragment); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!(aFlags & nsIChannelEventSink::REDIRECT_INTERNAL)) { + mRequest->AddURL(spec, fragment); + } else { + // Overwrite the URL only when the request is redirected by a service + // worker. + mRequest->SetURLForInternalRedirect(aFlags, spec, fragment); + } + + // In redirect, httpChannel already took referrer-policy into account, so + // updates request’s associated referrer policy from channel. + UpdateReferrerInfoFromNewChannel(aNewChannel); + + aCallback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +NS_IMETHODIMP +FetchDriver::CheckListenerChain() { return NS_OK; } + +NS_IMETHODIMP +FetchDriver::GetInterface(const nsIID& aIID, void** aResult) { + if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { + *aResult = static_cast<nsIChannelEventSink*>(this); + NS_ADDREF_THIS(); + return NS_OK; + } + if (aIID.Equals(NS_GET_IID(nsIStreamListener))) { + *aResult = static_cast<nsIStreamListener*>(this); + NS_ADDREF_THIS(); + return NS_OK; + } + if (aIID.Equals(NS_GET_IID(nsIRequestObserver))) { + *aResult = static_cast<nsIRequestObserver*>(this); + NS_ADDREF_THIS(); + return NS_OK; + } + + return QueryInterface(aIID, aResult); +} + +void FetchDriver::SetDocument(Document* aDocument) { + // Cannot set document after Fetch() has been called. + MOZ_ASSERT(!mFetchCalled); + mDocument = aDocument; +} + +void FetchDriver::SetCSPEventListener(nsICSPEventListener* aCSPEventListener) { + MOZ_ASSERT(!mFetchCalled); + mCSPEventListener = aCSPEventListener; +} + +void FetchDriver::SetClientInfo(const ClientInfo& aClientInfo) { + MOZ_ASSERT(!mFetchCalled); + mClientInfo.emplace(aClientInfo); +} + +void FetchDriver::SetController( + const Maybe<ServiceWorkerDescriptor>& aController) { + MOZ_ASSERT(!mFetchCalled); + mController = aController; +} + +PerformanceTimingData* FetchDriver::GetPerformanceTimingData( + nsAString& aInitiatorType, nsAString& aEntryName) { + MOZ_ASSERT(XRE_IsParentProcess()); + if (!mChannel) { + return nullptr; + } + + nsCOMPtr<nsITimedChannel> timedChannel = do_QueryInterface(mChannel); + if (!timedChannel) { + return nullptr; + } + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel); + if (!httpChannel) { + return nullptr; + } + return dom::PerformanceTimingData::Create(timedChannel, httpChannel, 0, + aInitiatorType, aEntryName); +} + +void FetchDriver::SetRequestHeaders(nsIHttpChannel* aChannel, + bool aStripRequestBodyHeader, + bool aStripAuthHeader) const { + MOZ_ASSERT(aChannel); + + // nsIHttpChannel has a set of pre-configured headers (Accept, + // Accept-Languages, ...) and we don't want to merge the Request's headers + // with them. This array is used to know if the current header has been aleady + // set, if yes, we ask necko to merge it with the previous one, otherwise, we + // don't want the merge. + nsTArray<nsCString> headersSet; + + AutoTArray<InternalHeaders::Entry, 5> headers; + mRequest->Headers()->GetEntries(headers); + for (uint32_t i = 0; i < headers.Length(); ++i) { + if (aStripRequestBodyHeader && + (headers[i].mName.LowerCaseEqualsASCII("content-type") || + headers[i].mName.LowerCaseEqualsASCII("content-encoding") || + headers[i].mName.LowerCaseEqualsASCII("content-language") || + headers[i].mName.LowerCaseEqualsASCII("content-location"))) { + continue; + } + + if (aStripAuthHeader && + headers[i].mName.LowerCaseEqualsASCII("authorization")) { + continue; + } + + bool alreadySet = headersSet.Contains(headers[i].mName); + if (!alreadySet) { + headersSet.AppendElement(headers[i].mName); + } + + if (headers[i].mValue.IsEmpty()) { + DebugOnly<nsresult> rv = + aChannel->SetEmptyRequestHeader(headers[i].mName); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } else { + DebugOnly<nsresult> rv = aChannel->SetRequestHeader( + headers[i].mName, headers[i].mValue, alreadySet /* merge */); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } +} + +void FetchDriver::RunAbortAlgorithm() { FetchDriverAbortActions(Signal()); } + +void FetchDriver::FetchDriverAbortActions(AbortSignalImpl* aSignalImpl) { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); + + if (mObserver) { +#ifdef DEBUG + mResponseAvailableCalled = true; +#endif + JS::Rooted<JS::Value> reason(RootingCx()); + if (aSignalImpl) { + reason.set(aSignalImpl->RawReason()); + } + mObserver->OnResponseEnd(FetchDriverObserver::eAborted, reason); + // As a part of cleanup, we are not removing the mObserver reference as it + // could race with mObserver access in OnDataAvailable when it runs OMT. + // We will be removing the reference in the OnStopRequest which guaranteed + // to run after cancelling the channel. + } + + if (mChannel) { + mChannel->CancelWithReason(NS_BINDING_ABORTED, + "FetchDriver::RunAbortAlgorithm"_ns); + mChannel = nullptr; + } + + mAborted = true; +} + +} // namespace mozilla::dom diff --git a/dom/fetch/FetchDriver.h b/dom/fetch/FetchDriver.h new file mode 100644 index 0000000000..84391ef951 --- /dev/null +++ b/dom/fetch/FetchDriver.h @@ -0,0 +1,232 @@ +/* -*- 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_FetchDriver_h +#define mozilla_dom_FetchDriver_h + +#include "nsIChannelEventSink.h" +#include "nsIInterfaceRequestor.h" +#include "nsINetworkInterceptController.h" +#include "nsIStreamListener.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "mozilla/ConsoleReportCollector.h" +#include "mozilla/dom/AbortSignal.h" +#include "mozilla/dom/SafeRefPtr.h" +#include "mozilla/dom/SerializedStackHolder.h" +#include "mozilla/dom/SRIMetadata.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" + +#include "mozilla/DebugOnly.h" + +class nsIConsoleReportCollector; +class nsICookieJarSettings; +class nsICSPEventListener; +class nsIEventTarget; +class nsIOutputStream; +class nsILoadGroup; +class nsIPrincipal; + +namespace mozilla { +class PreloaderBase; + +namespace dom { + +class Document; +class InternalRequest; +class InternalResponse; +class PerformanceStorage; +class PerformanceTimingData; + +/** + * Provides callbacks to be called when response is available or on error. + * Implemenations usually resolve or reject the promise returned from fetch(). + * The callbacks can be called synchronously or asynchronously from + * FetchDriver::Fetch. + */ +class FetchDriverObserver { + public: + FetchDriverObserver() + : mReporter(new ConsoleReportCollector()), mGotResponseAvailable(false) {} + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FetchDriverObserver); + void OnResponseAvailable(SafeRefPtr<InternalResponse> aResponse); + + enum EndReason { + eAborted, + eByNetworking, + }; + + virtual void OnResponseEnd(EndReason aReason, + JS::Handle<JS::Value> aReasonDetails){}; + + nsIConsoleReportCollector* GetReporter() const { return mReporter; } + + virtual void FlushConsoleReport() = 0; + + // Called in OnStartRequest() to determine if the OnDataAvailable() method + // needs to be called. Invoking that method may generate additional main + // thread runnables. + virtual bool NeedOnDataAvailable() = 0; + + // Called once when the first byte of data is received iff + // NeedOnDataAvailable() returned true when called in OnStartRequest(). + virtual void OnDataAvailable() = 0; + + virtual void OnReportPerformanceTiming() {} + + virtual void OnNotifyNetworkMonitorAlternateStack(uint64_t aChannelID) {} + + protected: + virtual ~FetchDriverObserver() = default; + + virtual void OnResponseAvailableInternal( + SafeRefPtr<InternalResponse> aResponse) = 0; + + nsCOMPtr<nsIConsoleReportCollector> mReporter; + + private: + bool mGotResponseAvailable; +}; + +class AlternativeDataStreamListener; + +class FetchDriver final : public nsIStreamListener, + public nsIChannelEventSink, + public nsIInterfaceRequestor, + public nsINetworkInterceptController, + public nsIThreadRetargetableStreamListener, + public AbortFollower { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSINETWORKINTERCEPTCONTROLLER + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + + FetchDriver(SafeRefPtr<InternalRequest> aRequest, nsIPrincipal* aPrincipal, + nsILoadGroup* aLoadGroup, nsIEventTarget* aMainThreadEventTarget, + nsICookieJarSettings* aCookieJarSettings, + PerformanceStorage* aPerformanceStorage, bool aIsTrackingFetch); + + nsresult Fetch(AbortSignalImpl* aSignalImpl, FetchDriverObserver* aObserver); + + void SetDocument(Document* aDocument); + + void SetCSPEventListener(nsICSPEventListener* aCSPEventListener); + + void SetClientInfo(const ClientInfo& aClientInfo); + + void SetController(const Maybe<ServiceWorkerDescriptor>& aController); + + void SetWorkerScript(const nsACString& aWorkerScript) { + MOZ_ASSERT(!aWorkerScript.IsEmpty()); + mWorkerScript = aWorkerScript; + } + + void SetOriginStack(UniquePtr<SerializedStackHolder>&& aOriginStack) { + mOriginStack = std::move(aOriginStack); + } + + PerformanceTimingData* GetPerformanceTimingData(nsAString& aInitiatorType, + nsAString& aEntryName); + + // AbortFollower + void RunAbortAlgorithm() override; + void FetchDriverAbortActions(AbortSignalImpl* aSignalImpl); + + void EnableNetworkInterceptControl(); + + void SetAssociatedBrowsingContextID(uint64_t aID) { + mAssociatedBrowsingContextID = aID; + } + + private: + nsCOMPtr<nsIPrincipal> mPrincipal; + nsCOMPtr<nsILoadGroup> mLoadGroup; + SafeRefPtr<InternalRequest> mRequest; + SafeRefPtr<InternalResponse> mResponse; + nsCOMPtr<nsIOutputStream> mPipeOutputStream; + // Access to mObserver can be racy from OnDataAvailable and + // FetchAbortActions. This must not be modified + // in either of these functions. + RefPtr<FetchDriverObserver> mObserver; + RefPtr<Document> mDocument; + nsCOMPtr<nsICSPEventListener> mCSPEventListener; + Maybe<ClientInfo> mClientInfo; + Maybe<ServiceWorkerDescriptor> mController; + nsCOMPtr<nsIChannel> mChannel; + UniquePtr<SRICheckDataVerifier> mSRIDataVerifier; + nsCOMPtr<nsIEventTarget> mMainThreadEventTarget; + + nsCOMPtr<nsICookieJarSettings> mCookieJarSettings; + + // This is set only when Fetch is used in workers. + RefPtr<PerformanceStorage> mPerformanceStorage; + + SRIMetadata mSRIMetadata; + nsCString mWorkerScript; + UniquePtr<SerializedStackHolder> mOriginStack; + + // This is written once in OnStartRequest on the main thread and then + // written/read in OnDataAvailable() on any thread. Necko guarantees + // that these do not overlap. + bool mNeedToObserveOnDataAvailable; + + bool mIsTrackingFetch; + + RefPtr<AlternativeDataStreamListener> mAltDataListener; + bool mOnStopRequestCalled; + + // This flag is true when this fetch has found a matching preload and is being + // satisfied by a its response. + bool mFromPreload = false; + // This flag is set in call to Abort() and spans the possible window this + // fetch doesn't have mChannel (to be cancelled) between reuse of the matching + // preload, that has already finished and dropped reference to its channel, + // and OnStartRequest notification. It let's us cancel the load when we get + // the channel in OnStartRequest. + bool mAborted = false; + +#ifdef DEBUG + bool mResponseAvailableCalled; + bool mFetchCalled; +#endif + nsCOMPtr<nsINetworkInterceptController> mInterceptController; + + uint64_t mAssociatedBrowsingContextID{0}; + + friend class AlternativeDataStreamListener; + + FetchDriver() = delete; + FetchDriver(const FetchDriver&) = delete; + FetchDriver& operator=(const FetchDriver&) = delete; + ~FetchDriver(); + + already_AddRefed<PreloaderBase> FindPreload(nsIURI* aURI); + + void UpdateReferrerInfoFromNewChannel(nsIChannel* aChannel); + + nsresult HttpFetch(const nsACString& aPreferredAlternativeDataType = ""_ns); + // Returns the filtered response sent to the observer. + SafeRefPtr<InternalResponse> BeginAndGetFilteredResponse( + SafeRefPtr<InternalResponse> aResponse, bool aFoundOpaqueRedirect); + // Utility since not all cases need to do any post processing of the filtered + // response. + void FailWithNetworkError(nsresult rv); + + void SetRequestHeaders(nsIHttpChannel* aChannel, bool aStripRequestBodyHeader, + bool aStripAuthHeader) const; + + void FinishOnStopRequest(AlternativeDataStreamListener* aAltDataListener); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_FetchDriver_h diff --git a/dom/fetch/FetchIPCTypes.h b/dom/fetch/FetchIPCTypes.h new file mode 100644 index 0000000000..343a5acb3f --- /dev/null +++ b/dom/fetch/FetchIPCTypes.h @@ -0,0 +1,63 @@ +/* -*- 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_fetch_IPCUtils_h +#define mozilla_dom_fetch_IPCUtils_h + +#include "ipc/EnumSerializer.h" + +#include "mozilla/dom/HeadersBinding.h" +#include "mozilla/dom/RequestBinding.h" +#include "mozilla/dom/ResponseBinding.h" +#include "mozilla/dom/FetchDriver.h" + +namespace IPC { +template <> +struct ParamTraits<mozilla::dom::HeadersGuardEnum> + : public ContiguousEnumSerializer< + mozilla::dom::HeadersGuardEnum, mozilla::dom::HeadersGuardEnum::None, + mozilla::dom::HeadersGuardEnum::EndGuard_> {}; +template <> +struct ParamTraits<mozilla::dom::ReferrerPolicy> + : public ContiguousEnumSerializer<mozilla::dom::ReferrerPolicy, + mozilla::dom::ReferrerPolicy::_empty, + mozilla::dom::ReferrerPolicy::EndGuard_> { +}; +template <> +struct ParamTraits<mozilla::dom::RequestMode> + : public ContiguousEnumSerializer<mozilla::dom::RequestMode, + mozilla::dom::RequestMode::Same_origin, + mozilla::dom::RequestMode::EndGuard_> {}; +template <> +struct ParamTraits<mozilla::dom::RequestCredentials> + : public ContiguousEnumSerializer< + mozilla::dom::RequestCredentials, + mozilla::dom::RequestCredentials::Omit, + mozilla::dom::RequestCredentials::EndGuard_> {}; +template <> +struct ParamTraits<mozilla::dom::RequestCache> + : public ContiguousEnumSerializer<mozilla::dom::RequestCache, + mozilla::dom::RequestCache::Default, + mozilla::dom::RequestCache::EndGuard_> {}; +template <> +struct ParamTraits<mozilla::dom::RequestRedirect> + : public ContiguousEnumSerializer< + mozilla::dom::RequestRedirect, mozilla::dom::RequestRedirect::Follow, + mozilla::dom::RequestRedirect::EndGuard_> {}; +template <> +struct ParamTraits<mozilla::dom::ResponseType> + : public ContiguousEnumSerializer<mozilla::dom::ResponseType, + mozilla::dom::ResponseType::Basic, + mozilla::dom::ResponseType::EndGuard_> {}; +template <> +struct ParamTraits<mozilla::dom::FetchDriverObserver::EndReason> + : public ContiguousEnumSerializerInclusive< + mozilla::dom::FetchDriverObserver::EndReason, + mozilla::dom::FetchDriverObserver::eAborted, + mozilla::dom::FetchDriverObserver::eByNetworking> {}; +} // namespace IPC + +#endif // mozilla_dom_fetch_IPCUtils_h diff --git a/dom/fetch/FetchLog.h b/dom/fetch/FetchLog.h new file mode 100644 index 0000000000..37ef66e989 --- /dev/null +++ b/dom/fetch/FetchLog.h @@ -0,0 +1,19 @@ +/* -*- 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_FetchLog_h +#define _mozilla_dom_FetchLog_h + +#include "mozilla/Logging.h" + +namespace mozilla::dom { + +extern mozilla::LazyLogModule gFetchLog; + +#define FETCH_LOG(args) MOZ_LOG(gFetchLog, LogLevel::Debug, args) + +} // namespace mozilla::dom +#endif diff --git a/dom/fetch/FetchObserver.cpp b/dom/fetch/FetchObserver.cpp new file mode 100644 index 0000000000..0c9d1b9f7a --- /dev/null +++ b/dom/fetch/FetchObserver.cpp @@ -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/. */ + +#include "FetchObserver.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/EventBinding.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(FetchObserver) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(FetchObserver, + DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(FetchObserver, + DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FetchObserver) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(FetchObserver, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(FetchObserver, DOMEventTargetHelper) + +FetchObserver::FetchObserver(nsIGlobalObject* aGlobal, + AbortSignalImpl* aSignalImpl) + : DOMEventTargetHelper(aGlobal), mState(FetchState::Requesting) { + if (aSignalImpl) { + Follow(aSignalImpl); + } +} + +JSObject* FetchObserver::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return FetchObserver_Binding::Wrap(aCx, this, aGivenProto); +} + +FetchState FetchObserver::State() const { return mState; } + +void FetchObserver::RunAbortAlgorithm() { SetState(FetchState::Aborted); } + +void FetchObserver::SetState(FetchState aState) { + MOZ_ASSERT(mState < aState); + + if (mState == FetchState::Aborted || mState == FetchState::Errored || + mState == FetchState::Complete) { + // We are already in a final state. + return; + } + + // We cannot pass from Requesting to Complete directly. + if (mState == FetchState::Requesting && aState == FetchState::Complete) { + SetState(FetchState::Responding); + } + + mState = aState; + + if (mState == FetchState::Aborted || mState == FetchState::Errored || + mState == FetchState::Complete) { + Unfollow(); + } + + EventInit init; + init.mBubbles = false; + init.mCancelable = false; + + // TODO which kind of event should we dispatch here? + + RefPtr<Event> event = Event::Constructor(this, u"statechange"_ns, init); + event->SetTrusted(true); + + DispatchEvent(*event); +} + +} // namespace mozilla::dom diff --git a/dom/fetch/FetchObserver.h b/dom/fetch/FetchObserver.h new file mode 100644 index 0000000000..a80342a2fd --- /dev/null +++ b/dom/fetch/FetchObserver.h @@ -0,0 +1,45 @@ +/* -*- 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_FetchObserver_h +#define mozilla_dom_FetchObserver_h + +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/FetchObserverBinding.h" +#include "mozilla/dom/AbortSignal.h" + +namespace mozilla::dom { + +class FetchObserver final : public DOMEventTargetHelper, public AbortFollower { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(FetchObserver, DOMEventTargetHelper) + + FetchObserver(nsIGlobalObject* aGlobal, AbortSignalImpl* aSignalImpl); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + FetchState State() const; + + IMPL_EVENT_HANDLER(statechange); + IMPL_EVENT_HANDLER(requestprogress); + IMPL_EVENT_HANDLER(responseprogress); + + // AbortFollower + void RunAbortAlgorithm() override; + + void SetState(FetchState aState); + + private: + ~FetchObserver() = default; + + FetchState mState; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_FetchObserver_h diff --git a/dom/fetch/FetchParent.cpp b/dom/fetch/FetchParent.cpp new file mode 100644 index 0000000000..93e9b9e43e --- /dev/null +++ b/dom/fetch/FetchParent.cpp @@ -0,0 +1,340 @@ +/* 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 "FetchLog.h" +#include "FetchParent.h" +#include "FetchService.h" +#include "InternalRequest.h" +#include "InternalResponse.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/ClientInfo.h" +#include "mozilla/dom/FetchTypes.h" +#include "mozilla/dom/PerformanceTimingTypes.h" +#include "mozilla/dom/ServiceWorkerDescriptor.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "nsThreadUtils.h" + +using namespace mozilla::ipc; + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS(FetchParent::FetchParentCSPEventListener, nsICSPEventListener) + +FetchParent::FetchParentCSPEventListener::FetchParentCSPEventListener( + const nsID& aActorID, nsCOMPtr<nsISerialEventTarget> aEventTarget) + : mActorID(aActorID), mEventTarget(aEventTarget) { + MOZ_ASSERT(mEventTarget); + FETCH_LOG(("FetchParentCSPEventListener [%p] actor ID: %s", this, + mActorID.ToString().get())); +} + +NS_IMETHODIMP FetchParent::FetchParentCSPEventListener::OnCSPViolationEvent( + const nsAString& aJSON) { + AssertIsOnMainThread(); + FETCH_LOG(("FetchParentCSPEventListener::OnCSPViolationEvent [%p]", this)); + + nsAutoString json(aJSON); + nsCOMPtr<nsIRunnable> r = + NS_NewRunnableFunction(__func__, [actorID = mActorID, json]() mutable { + FETCH_LOG( + ("FetchParentCSPEventListener::OnCSPViolationEvent, Runnale")); + RefPtr<FetchParent> actor = FetchParent::GetActorByID(actorID); + if (actor) { + actor->OnCSPViolationEvent(json); + } + }); + + MOZ_ALWAYS_SUCCEEDS(mEventTarget->Dispatch(r, nsIThread::DISPATCH_NORMAL)); + return NS_OK; +} + +nsTHashMap<nsIDHashKey, RefPtr<FetchParent>> FetchParent::sActorTable; + +/*static*/ +RefPtr<FetchParent> FetchParent::GetActorByID(const nsID& aID) { + AssertIsOnBackgroundThread(); + auto entry = sActorTable.Lookup(aID); + if (entry) { + return entry.Data(); + } + return nullptr; +} + +FetchParent::FetchParent() : mID(nsID::GenerateUUID()) { + FETCH_LOG(("FetchParent::FetchParent [%p]", this)); + AssertIsOnBackgroundThread(); + mBackgroundEventTarget = GetCurrentSerialEventTarget(); + MOZ_ASSERT(mBackgroundEventTarget); + if (!sActorTable.WithEntryHandle(mID, [&](auto&& entry) { + if (entry.HasEntry()) { + return false; + } + entry.Insert(this); + return true; + })) { + FETCH_LOG(("FetchParent::FetchParent entry[%p] already exists", this)); + } +} + +FetchParent::~FetchParent() { + FETCH_LOG(("FetchParent::~FetchParent [%p]", this)); + // MOZ_ASSERT(!mBackgroundEventTarget); + MOZ_ASSERT(!mResponsePromises); + MOZ_ASSERT(mActorDestroyed && mIsDone); +} + +IPCResult FetchParent::RecvFetchOp(FetchOpArgs&& aArgs) { + FETCH_LOG(("FetchParent::RecvFetchOp [%p]", this)); + AssertIsOnBackgroundThread(); + + MOZ_ASSERT(!mIsDone); + if (mActorDestroyed) { + return IPC_OK(); + } + + mRequest = MakeSafeRefPtr<InternalRequest>(std::move(aArgs.request())); + mPrincipalInfo = std::move(aArgs.principalInfo()); + mWorkerScript = aArgs.workerScript(); + mClientInfo = Some(ClientInfo(aArgs.clientInfo())); + if (aArgs.controller().isSome()) { + mController = Some(ServiceWorkerDescriptor(aArgs.controller().ref())); + } + mCookieJarSettings = aArgs.cookieJarSettings(); + mNeedOnDataAvailable = aArgs.needOnDataAvailable(); + mHasCSPEventListener = aArgs.hasCSPEventListener(); + + if (mHasCSPEventListener) { + mCSPEventListener = + MakeRefPtr<FetchParentCSPEventListener>(mID, mBackgroundEventTarget); + } + mAssociatedBrowsingContextID = aArgs.associatedBrowsingContextID(); + + MOZ_ASSERT(!mPromise); + mPromise = new GenericPromise::Private(__func__); + + RefPtr<FetchParent> self = this; + mPromise->Then( + mBackgroundEventTarget, __func__, + [self](const bool&& result) mutable { + FETCH_LOG( + ("FetchParent::RecvFetchOp [%p] Success Callback", self.get())); + AssertIsOnBackgroundThread(); + self->mPromise = nullptr; + if (self->mIsDone) { + FETCH_LOG(("FetchParent::RecvFetchOp [%p] Fetch has already aborted", + self.get())); + if (!self->mActorDestroyed) { + Unused << NS_WARN_IF( + !self->Send__delete__(self, NS_ERROR_DOM_ABORT_ERR)); + } + return; + } + self->mIsDone = true; + if (!self->mActorDestroyed && !self->mExtendForCSPEventListener) { + FETCH_LOG(("FetchParent::RecvFetchOp [%p] Send__delete__(NS_OK)", + self.get())); + Unused << NS_WARN_IF(!self->Send__delete__(self, NS_OK)); + } + }, + [self](const nsresult&& aErr) mutable { + FETCH_LOG( + ("FetchParent::RecvFetchOp [%p] Failure Callback", self.get())); + AssertIsOnBackgroundThread(); + self->mIsDone = true; + self->mPromise = nullptr; + if (!self->mActorDestroyed) { + FETCH_LOG(("FetchParent::RecvFetchOp [%p] Send__delete__(aErr)", + self.get())); + Unused << NS_WARN_IF(!self->Send__delete__(self, aErr)); + } + }); + + RefPtr<nsIRunnable> r = NS_NewRunnableFunction(__func__, [self]() mutable { + FETCH_LOG( + ("FetchParent::RecvFetchOp [%p], Main Thread Runnable", self.get())); + AssertIsOnMainThread(); + if (self->mIsDone) { + MOZ_ASSERT(!self->mResponsePromises); + MOZ_ASSERT(self->mPromise); + FETCH_LOG( + ("FetchParent::RecvFetchOp [%p], Main Thread Runnable, " + "already aborted", + self.get())); + self->mPromise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__); + return; + } + RefPtr<FetchService> fetchService = FetchService::GetInstance(); + MOZ_ASSERT(fetchService); + MOZ_ASSERT(!self->mResponsePromises); + self->mResponsePromises = + fetchService->Fetch(AsVariant(FetchService::WorkerFetchArgs( + {self->mRequest.clonePtr(), self->mPrincipalInfo, + self->mWorkerScript, self->mClientInfo, self->mController, + self->mCookieJarSettings, self->mNeedOnDataAvailable, + self->mCSPEventListener, self->mAssociatedBrowsingContextID, + self->mBackgroundEventTarget, self->mID}))); + + self->mResponsePromises->GetResponseEndPromise()->Then( + GetMainThreadSerialEventTarget(), __func__, + [self](ResponseEndArgs&& aArgs) mutable { + AssertIsOnMainThread(); + MOZ_ASSERT(self->mPromise); + self->mPromise->Resolve(true, __func__); + self->mResponsePromises = nullptr; + }, + [self](CopyableErrorResult&& aErr) mutable { + AssertIsOnMainThread(); + MOZ_ASSERT(self->mPromise); + self->mPromise->Reject(aErr.StealNSResult(), __func__); + self->mResponsePromises = nullptr; + }); + }); + + MOZ_ALWAYS_SUCCEEDS( + NS_DispatchToMainThread(r.forget(), nsIThread::DISPATCH_NORMAL)); + + return IPC_OK(); +} + +IPCResult FetchParent::RecvAbortFetchOp() { + FETCH_LOG(("FetchParent::RecvAbortFetchOp [%p]", this)); + AssertIsOnBackgroundThread(); + + if (mIsDone) { + FETCH_LOG(("FetchParent::RecvAbortFetchOp [%p], Already aborted", this)); + return IPC_OK(); + } + mIsDone = true; + + RefPtr<FetchParent> self = this; + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(__func__, [self]() mutable { + FETCH_LOG(("FetchParent::RecvAbortFetchOp Runnable")); + AssertIsOnMainThread(); + if (self->mResponsePromises) { + RefPtr<FetchService> fetchService = FetchService::GetInstance(); + MOZ_ASSERT(fetchService); + fetchService->CancelFetch(std::move(self->mResponsePromises)); + } + }); + + MOZ_ALWAYS_SUCCEEDS( + NS_DispatchToMainThread(r.forget(), nsIThread::DISPATCH_NORMAL)); + + return IPC_OK(); +} + +void FetchParent::OnResponseAvailableInternal( + SafeRefPtr<InternalResponse>&& aResponse) { + FETCH_LOG(("FetchParent::OnResponseAvailableInternal [%p]", this)); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(aResponse); + MOZ_ASSERT(!mActorDestroyed); + + if (mIsDone && aResponse->Type() != ResponseType::Error) { + FETCH_LOG( + ("FetchParent::OnResponseAvailableInternal [%p] " + "Fetch has already aborted", + this)); + return; + } + + // To monitor the stream status between processes, response's body can not be + // serialized as RemoteLazyInputStream. Such that stream close can be + // propagated to FetchDriver in the parent process. + aResponse->SetSerializeAsLazy(false); + + // CSP violation notification is asynchronous. Extending the FetchParent's + // life cycle for the notificaiton. + if (aResponse->Type() == ResponseType::Error && + aResponse->GetErrorCode() == NS_ERROR_CONTENT_BLOCKED && + mCSPEventListener) { + FETCH_LOG( + ("FetchParent::OnResponseAvailableInternal [%p] " + "NS_ERROR_CONTENT_BLOCKED", + this)); + mExtendForCSPEventListener = true; + } + + Unused << SendOnResponseAvailableInternal( + aResponse->ToParentToChildInternalResponse(WrapNotNull(Manager()))); +} + +void FetchParent::OnResponseEnd(const ResponseEndArgs& aArgs) { + FETCH_LOG(("FetchParent::OnResponseEnd [%p]", this)); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + if (mIsDone && aArgs.endReason() != FetchDriverObserver::eAborted) { + FETCH_LOG( + ("FetchParent::OnResponseEnd [%p] " + "Fetch has already aborted", + this)); + return; + } + + Unused << SendOnResponseEnd(aArgs); +} + +void FetchParent::OnDataAvailable() { + FETCH_LOG(("FetchParent::OnDataAvailable [%p]", this)); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + Unused << SendOnDataAvailable(); +} + +void FetchParent::OnFlushConsoleReport( + const nsTArray<net::ConsoleReportCollected>& aReports) { + FETCH_LOG(("FetchParent::OnFlushConsoleReport [%p]", this)); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + Unused << SendOnFlushConsoleReport(aReports); +} + +void FetchParent::OnReportPerformanceTiming(const ResponseTiming&& aTiming) { + FETCH_LOG(("FetchParent::OnReportPerformanceTiming [%p]", this)); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + Unused << SendOnReportPerformanceTiming(aTiming); +} + +void FetchParent::OnNotifyNetworkMonitorAlternateStack(uint64_t aChannelID) { + FETCH_LOG(("FetchParent::OnNotifyNetworkMonitorAlternateStack [%p]", this)); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(!mActorDestroyed); + + Unused << SendOnNotifyNetworkMonitorAlternateStack(aChannelID); +} + +void FetchParent::ActorDestroy(ActorDestroyReason aReason) { + FETCH_LOG(("FetchParent::ActorDestroy [%p]", this)); + AssertIsOnBackgroundThread(); + mActorDestroyed = true; + auto entry = sActorTable.Lookup(mID); + if (entry) { + entry.Remove(); + FETCH_LOG(("FetchParent::ActorDestroy entry [%p] removed", this)); + } + // Force to abort the existing fetch. + // Actor can be destoried by shutdown when still fetching. + RecvAbortFetchOp(); + // mBackgroundEventTarget = nullptr; +} + +nsICSPEventListener* FetchParent::GetCSPEventListener() { + return mCSPEventListener; +} + +void FetchParent::OnCSPViolationEvent(const nsAString& aJSON) { + FETCH_LOG(("FetchParent::OnCSPViolationEvent [%p]", this)); + AssertIsOnBackgroundThread(); + MOZ_ASSERT(mHasCSPEventListener); + MOZ_ASSERT(!mActorDestroyed); + + Unused << SendOnCSPViolationEvent(aJSON); +} + +} // namespace mozilla::dom diff --git a/dom/fetch/FetchParent.h b/dom/fetch/FetchParent.h new file mode 100644 index 0000000000..e373d93b73 --- /dev/null +++ b/dom/fetch/FetchParent.h @@ -0,0 +1,109 @@ +/* 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_fetchParent_h__ +#define mozilla_dom_fetchParent_h__ + +#include "mozilla/Maybe.h" +#include "mozilla/MozPromise.h" +#include "mozilla/Mutex.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/PFetchParent.h" +#include "mozilla/dom/SafeRefPtr.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "nsCOMPtr.h" +#include "nsIContentSecurityPolicy.h" +#include "nsID.h" +#include "nsISerialEventTarget.h" +#include "nsString.h" +#include "nsTHashMap.h" + +namespace mozilla::dom { + +class ClientInfo; +class FetchServicePromises; +class InternalRequest; +class InternalResponse; +class ServiceWorkerDescriptor; + +class FetchParent final : public PFetchParent { + friend class PFetchParent; + + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FetchParent, override); + + mozilla::ipc::IPCResult RecvFetchOp(FetchOpArgs&& aArgs); + + mozilla::ipc::IPCResult RecvAbortFetchOp(); + + FetchParent(); + + static RefPtr<FetchParent> GetActorByID(const nsID& aID); + + void OnResponseAvailableInternal(SafeRefPtr<InternalResponse>&& aResponse); + + void OnResponseEnd(const ResponseEndArgs& aArgs); + + void OnDataAvailable(); + + void OnFlushConsoleReport( + const nsTArray<net::ConsoleReportCollected>& aReports); + + class FetchParentCSPEventListener final : public nsICSPEventListener { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSICSPEVENTLISTENER + + FetchParentCSPEventListener(const nsID& aActorID, + nsCOMPtr<nsISerialEventTarget> aEventTarget); + + private: + ~FetchParentCSPEventListener() = default; + + nsID mActorID; + nsCOMPtr<nsISerialEventTarget> mEventTarget; + }; + + nsICSPEventListener* GetCSPEventListener(); + + void OnCSPViolationEvent(const nsAString& aJSON); + + void OnReportPerformanceTiming(const ResponseTiming&& aTiming); + + void OnNotifyNetworkMonitorAlternateStack(uint64_t aChannelID); + + private: + ~FetchParent(); + + void ActorDestroy(ActorDestroyReason aReason) override; + + // The map of FetchParent and ID. Should only access in background thread. + static nsTHashMap<nsIDHashKey, RefPtr<FetchParent>> sActorTable; + + // The unique ID of the FetchParent + nsID mID; + SafeRefPtr<InternalRequest> mRequest; + RefPtr<FetchServicePromises> mResponsePromises; + RefPtr<GenericPromise::Private> mPromise; + PrincipalInfo mPrincipalInfo; + nsCString mWorkerScript; + Maybe<ClientInfo> mClientInfo; + Maybe<ServiceWorkerDescriptor> mController; + Maybe<CookieJarSettingsArgs> mCookieJarSettings; + nsCOMPtr<nsICSPEventListener> mCSPEventListener; + bool mNeedOnDataAvailable{false}; + bool mHasCSPEventListener{false}; + bool mExtendForCSPEventListener{false}; + uint64_t mAssociatedBrowsingContextID{0}; + + Atomic<bool> mIsDone{false}; + Atomic<bool> mActorDestroyed{false}; + + nsCOMPtr<nsISerialEventTarget> mBackgroundEventTarget; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/fetch/FetchService.cpp b/dom/fetch/FetchService.cpp new file mode 100644 index 0000000000..b285d65692 --- /dev/null +++ b/dom/fetch/FetchService.cpp @@ -0,0 +1,663 @@ +/* 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 "FetchLog.h" +#include "FetchParent.h" +#include "nsContentUtils.h" +#include "nsIContentSecurityPolicy.h" +#include "nsICookieJarSettings.h" +#include "nsILoadGroup.h" +#include "nsILoadInfo.h" +#include "nsIIOService.h" +#include "nsIObserverService.h" +#include "nsIPrincipal.h" +#include "nsIScriptSecurityManager.h" +#include "nsNetUtil.h" +#include "nsThreadUtils.h" +#include "nsXULAppAPI.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/ClientInfo.h" +#include "mozilla/dom/FetchService.h" +#include "mozilla/dom/InternalRequest.h" +#include "mozilla/dom/InternalResponse.h" +#include "mozilla/dom/PerformanceStorage.h" +#include "mozilla/dom/PerformanceTiming.h" +#include "mozilla/dom/ServiceWorkerDescriptor.h" +#include "mozilla/ipc/BackgroundUtils.h" +#include "mozilla/net/CookieJarSettings.h" + +namespace mozilla::dom { + +mozilla::LazyLogModule gFetchLog("Fetch"); + +// FetchServicePromises + +FetchServicePromises::FetchServicePromises() + : mAvailablePromise( + MakeRefPtr<FetchServiceResponseAvailablePromise::Private>(__func__)), + mTimingPromise( + MakeRefPtr<FetchServiceResponseTimingPromise::Private>(__func__)), + mEndPromise( + MakeRefPtr<FetchServiceResponseEndPromise::Private>(__func__)) { + mAvailablePromise->UseSynchronousTaskDispatch(__func__); + mTimingPromise->UseSynchronousTaskDispatch(__func__); + mEndPromise->UseSynchronousTaskDispatch(__func__); +} + +RefPtr<FetchServiceResponseAvailablePromise> +FetchServicePromises::GetResponseAvailablePromise() { + return mAvailablePromise; +} + +RefPtr<FetchServiceResponseTimingPromise> +FetchServicePromises::GetResponseTimingPromise() { + return mTimingPromise; +} + +RefPtr<FetchServiceResponseEndPromise> +FetchServicePromises::GetResponseEndPromise() { + return mEndPromise; +} + +void FetchServicePromises::ResolveResponseAvailablePromise( + FetchServiceResponse&& aResponse, const char* aMethodName) { + if (mAvailablePromise) { + mAvailablePromise->Resolve(std::move(aResponse), aMethodName); + } +} + +void FetchServicePromises::RejectResponseAvailablePromise( + const CopyableErrorResult&& aError, const char* aMethodName) { + if (mAvailablePromise) { + mAvailablePromise->Reject(aError, aMethodName); + } +} + +void FetchServicePromises::ResolveResponseTimingPromise( + ResponseTiming&& aTiming, const char* aMethodName) { + if (mTimingPromise) { + mTimingPromise->Resolve(std::move(aTiming), aMethodName); + } +} + +void FetchServicePromises::RejectResponseTimingPromise( + const CopyableErrorResult&& aError, const char* aMethodName) { + if (mTimingPromise) { + mTimingPromise->Reject(aError, aMethodName); + } +} + +void FetchServicePromises::ResolveResponseEndPromise(ResponseEndArgs&& aArgs, + const char* aMethodName) { + if (mEndPromise) { + mEndPromise->Resolve(std::move(aArgs), aMethodName); + } +} + +void FetchServicePromises::RejectResponseEndPromise( + const CopyableErrorResult&& aError, const char* aMethodName) { + if (mEndPromise) { + mEndPromise->Reject(aError, aMethodName); + } +} + +// FetchInstance + +nsresult FetchService::FetchInstance::Initialize(FetchArgs&& aArgs) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!aArgs.is<UnknownArgs>() && mArgs.is<UnknownArgs>()); + + mArgs = std::move(aArgs); + + // Get needed information for FetchDriver from passed-in channel. + if (mArgs.is<NavigationPreloadArgs>()) { + mRequest = mArgs.as<NavigationPreloadArgs>().mRequest.clonePtr(); + nsIChannel* channel = mArgs.as<NavigationPreloadArgs>().mChannel; + FETCH_LOG(("FetchInstance::Initialize [%p] request[%p], channel[%p]", this, + mRequest.unsafeGetRawPtr(), channel)); + + nsresult rv; + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + MOZ_ASSERT(loadInfo); + + nsCOMPtr<nsIURI> channelURI; + rv = channel->GetURI(getter_AddRefs(channelURI)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsIScriptSecurityManager* securityManager = + nsContentUtils::GetSecurityManager(); + if (securityManager) { + securityManager->GetChannelResultPrincipal(channel, + getter_AddRefs(mPrincipal)); + } + + if (!mPrincipal) { + return NS_ERROR_UNEXPECTED; + } + + // Get loadGroup from channel + rv = channel->GetLoadGroup(getter_AddRefs(mLoadGroup)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (!mLoadGroup) { + rv = NS_NewLoadGroup(getter_AddRefs(mLoadGroup), mPrincipal); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + // Get CookieJarSettings from channel + rv = loadInfo->GetCookieJarSettings(getter_AddRefs(mCookieJarSettings)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Get PerformanceStorage from channel + mPerformanceStorage = loadInfo->GetPerformanceStorage(); + } else { + mIsWorkerFetch = true; + mRequest = mArgs.as<WorkerFetchArgs>().mRequest.clonePtr(); + + FETCH_LOG(("FetchInstance::Initialize [%p] request[%p]", this, + mRequest.unsafeGetRawPtr())); + + auto principalOrErr = + PrincipalInfoToPrincipal(mArgs.as<WorkerFetchArgs>().mPrincipalInfo); + if (principalOrErr.isErr()) { + return principalOrErr.unwrapErr(); + } + mPrincipal = principalOrErr.unwrap(); + nsresult rv = NS_NewLoadGroup(getter_AddRefs(mLoadGroup), mPrincipal); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (mArgs.as<WorkerFetchArgs>().mCookieJarSettings.isSome()) { + net::CookieJarSettings::Deserialize( + mArgs.as<WorkerFetchArgs>().mCookieJarSettings.ref(), + getter_AddRefs(mCookieJarSettings)); + } + } + + return NS_OK; +} + +RefPtr<FetchServicePromises> FetchService::FetchInstance::Fetch() { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + MOZ_ASSERT(mPrincipal); + MOZ_ASSERT(mLoadGroup); + + nsAutoCString principalSpec; + MOZ_ALWAYS_SUCCEEDS(mPrincipal->GetAsciiSpec(principalSpec)); + nsAutoCString requestURL; + mRequest->GetURL(requestURL); + FETCH_LOG(("FetchInstance::Fetch [%p], mRequest URL: %s mPrincipal: %s", this, + requestURL.BeginReading(), principalSpec.BeginReading())); + + nsresult rv; + + // Create a FetchDriver instance + mFetchDriver = MakeRefPtr<FetchDriver>( + mRequest.clonePtr(), // Fetch Request + mPrincipal, // Principal + mLoadGroup, // LoadGroup + GetMainThreadSerialEventTarget(), // MainThreadEventTarget + mCookieJarSettings, // CookieJarSettings + mPerformanceStorage, // PerformanceStorage + false // IsTrackingFetch + ); + + if (mIsWorkerFetch) { + auto& args = mArgs.as<WorkerFetchArgs>(); + mFetchDriver->SetWorkerScript(args.mWorkerScript); + MOZ_ASSERT(args.mClientInfo.isSome()); + mFetchDriver->SetClientInfo(args.mClientInfo.ref()); + mFetchDriver->SetController(args.mController); + if (args.mCSPEventListener) { + mFetchDriver->SetCSPEventListener(args.mCSPEventListener); + } + mFetchDriver->SetAssociatedBrowsingContextID( + args.mAssociatedBrowsingContextID); + } + + mFetchDriver->EnableNetworkInterceptControl(); + + mPromises = MakeRefPtr<FetchServicePromises>(); + + // Call FetchDriver::Fetch to start fetching. + // Pass AbortSignalImpl as nullptr since we no need support AbortSignalImpl + // with FetchService. AbortSignalImpl related information should be passed + // through PFetch or InterceptedHttpChannel, then call + // FetchService::CancelFetch() to abort the running fetch. + rv = mFetchDriver->Fetch(nullptr, this); + if (NS_WARN_IF(NS_FAILED(rv))) { + FETCH_LOG( + ("FetchInstance::Fetch FetchDriver::Fetch failed(0x%X)", (uint32_t)rv)); + return FetchService::NetworkErrorResponse(rv); + } + + return mPromises; +} + +void FetchService::FetchInstance::Cancel() { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + FETCH_LOG(("FetchInstance::Cancel() [%p]", this)); + + // If mFetchDriver is not null here, FetchInstance::Fetch() has already + // started, let mFetchDriver::RunAbortAlgorithm() to call + // FetchInstance::OnResponseEnd() to resolve the pending promises. + // Otherwise, resolving the pending promises here. + if (mFetchDriver) { + mFetchDriver->RunAbortAlgorithm(); + return; + } + + MOZ_ASSERT(mPromises); + + mPromises->ResolveResponseAvailablePromise( + InternalResponse::NetworkError(NS_ERROR_DOM_ABORT_ERR), __func__); + + mPromises->ResolveResponseTimingPromise(ResponseTiming(), __func__); + + mPromises->ResolveResponseEndPromise( + ResponseEndArgs(FetchDriverObserver::eAborted), __func__); +} + +void FetchService::FetchInstance::OnResponseEnd( + FetchDriverObserver::EndReason aReason, + JS::Handle<JS::Value> aReasonDetails) { + FETCH_LOG(("FetchInstance::OnResponseEnd [%p] %s", this, + aReason == eAborted ? "eAborted" : "eNetworking")); + + if (mIsWorkerFetch) { + FlushConsoleReport(); + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [endArgs = ResponseEndArgs(aReason), + actorID = mArgs.as<WorkerFetchArgs>().mActorID]() { + FETCH_LOG(("FetchInstance::OnResponseEnd, Runnable")); + RefPtr<FetchParent> actor = FetchParent::GetActorByID(actorID); + if (actor) { + actor->OnResponseEnd(std::move(endArgs)); + } + }); + MOZ_ALWAYS_SUCCEEDS(mArgs.as<WorkerFetchArgs>().mEventTarget->Dispatch( + r, nsIThread::DISPATCH_NORMAL)); + } + + MOZ_ASSERT(mPromises); + + if (aReason == eAborted) { + // If ResponseAvailablePromise has not resolved yet, resolved with + // NS_ERROR_DOM_ABORT_ERR response. + if (!mPromises->GetResponseAvailablePromise()->IsResolved()) { + mPromises->ResolveResponseAvailablePromise( + InternalResponse::NetworkError(NS_ERROR_DOM_ABORT_ERR), __func__); + } + + // If ResponseTimingPromise has not resolved yet, resolved with empty + // ResponseTiming. + if (!mPromises->GetResponseTimingPromise()->IsResolved()) { + mPromises->ResolveResponseTimingPromise(ResponseTiming(), __func__); + } + // Resolve the ResponseEndPromise + mPromises->ResolveResponseEndPromise(ResponseEndArgs(aReason), __func__); + return; + } + + MOZ_ASSERT(mPromises->GetResponseAvailablePromise()->IsResolved() && + mPromises->GetResponseTimingPromise()->IsResolved()); + + // Resolve the ResponseEndPromise + mPromises->ResolveResponseEndPromise(ResponseEndArgs(aReason), __func__); + + // Remove the FetchInstance from FetchInstanceTable + RefPtr<FetchService> fetchService = FetchService::GetInstance(); + MOZ_ASSERT(fetchService); + auto entry = fetchService->mFetchInstanceTable.Lookup(mPromises); + if (entry) { + entry.Remove(); + FETCH_LOG( + ("FetchInstance::OnResponseEnd entry of responsePromise[%p] is " + "removed", + mPromises.get())); + } +} + +void FetchService::FetchInstance::OnResponseAvailableInternal( + SafeRefPtr<InternalResponse> aResponse) { + FETCH_LOG(("FetchInstance::OnResponseAvailableInternal [%p]", this)); + mResponse = std::move(aResponse); + + nsCOMPtr<nsIInputStream> body; + mResponse->GetUnfilteredBody(getter_AddRefs(body)); + FETCH_LOG( + ("FetchInstance::OnResponseAvailableInternal [%p] response body: %p", + this, body.get())); + + if (mIsWorkerFetch) { + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [response = mResponse.clonePtr(), + actorID = mArgs.as<WorkerFetchArgs>().mActorID]() mutable { + FETCH_LOG(("FetchInstance::OnResponseAvailableInternal Runnable")); + RefPtr<FetchParent> actor = FetchParent::GetActorByID(actorID); + if (actor) { + actor->OnResponseAvailableInternal(std::move(response)); + } + }); + MOZ_ALWAYS_SUCCEEDS(mArgs.as<WorkerFetchArgs>().mEventTarget->Dispatch( + r, nsIThread::DISPATCH_NORMAL)); + } + + MOZ_ASSERT(mPromises); + + // Resolve the ResponseAvailablePromise + mPromises->ResolveResponseAvailablePromise(mResponse.clonePtr(), __func__); +} + +bool FetchService::FetchInstance::NeedOnDataAvailable() { + if (mArgs.is<WorkerFetchArgs>()) { + return mArgs.as<WorkerFetchArgs>().mNeedOnDataAvailable; + } + return false; +} + +void FetchService::FetchInstance::OnDataAvailable() { + FETCH_LOG(("FetchInstance::OnDataAvailable [%p]", this)); + + if (!NeedOnDataAvailable()) { + return; + } + + if (mIsWorkerFetch) { + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [actorID = mArgs.as<WorkerFetchArgs>().mActorID]() { + FETCH_LOG(("FetchInstance::OnDataAvailable, Runnable")); + RefPtr<FetchParent> actor = FetchParent::GetActorByID(actorID); + if (actor) { + actor->OnDataAvailable(); + } + }); + MOZ_ALWAYS_SUCCEEDS(mArgs.as<WorkerFetchArgs>().mEventTarget->Dispatch( + r, nsIThread::DISPATCH_NORMAL)); + } +} + +void FetchService::FetchInstance::FlushConsoleReport() { + FETCH_LOG(("FetchInstance::FlushConsoleReport [%p]", this)); + + if (mIsWorkerFetch) { + if (!mReporter) { + return; + } + nsTArray<net::ConsoleReportCollected> reports; + mReporter->StealConsoleReports(reports); + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [actorID = mArgs.as<WorkerFetchArgs>().mActorID, + consoleReports = std::move(reports)]() { + FETCH_LOG(("FetchInstance::FlushConsolReport, Runnable")); + RefPtr<FetchParent> actor = FetchParent::GetActorByID(actorID); + if (actor) { + actor->OnFlushConsoleReport(std::move(consoleReports)); + } + }); + MOZ_ALWAYS_SUCCEEDS(mArgs.as<WorkerFetchArgs>().mEventTarget->Dispatch( + r, nsIThread::DISPATCH_NORMAL)); + } +} + +void FetchService::FetchInstance::OnReportPerformanceTiming() { + FETCH_LOG(("FetchInstance::OnReportPerformanceTiming [%p]", this)); + MOZ_ASSERT(mFetchDriver); + MOZ_ASSERT(mPromises); + + if (mPromises->GetResponseTimingPromise()->IsResolved()) { + return; + } + + ResponseTiming timing; + UniquePtr<PerformanceTimingData> performanceTiming( + mFetchDriver->GetPerformanceTimingData(timing.initiatorType(), + timing.entryName())); + // FetchDriver has no corresponding performance timing when fetch() failed. + // Resolve the ResponseTimingPromise with empty timing. + if (!performanceTiming) { + mPromises->ResolveResponseTimingPromise(ResponseTiming(), __func__); + return; + } + timing.timingData() = performanceTiming->ToIPC(); + // Force replace initiatorType for ServiceWorkerNavgationPreload. + if (!mIsWorkerFetch) { + timing.initiatorType() = u"navigation"_ns; + } else { + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, + [actorID = mArgs.as<WorkerFetchArgs>().mActorID, timing = timing]() { + FETCH_LOG(("FetchInstance::OnReportPerformanceTiming, Runnable")); + RefPtr<FetchParent> actor = FetchParent::GetActorByID(actorID); + if (actor) { + actor->OnReportPerformanceTiming(std::move(timing)); + } + }); + MOZ_ALWAYS_SUCCEEDS(mArgs.as<WorkerFetchArgs>().mEventTarget->Dispatch( + r, nsIThread::DISPATCH_NORMAL)); + } + + mPromises->ResolveResponseTimingPromise(std::move(timing), __func__); +} + +void FetchService::FetchInstance::OnNotifyNetworkMonitorAlternateStack( + uint64_t aChannelID) { + FETCH_LOG(("FetchInstance::OnNotifyNetworkMonitorAlternateStack [%p]", this)); + MOZ_ASSERT(mFetchDriver); + MOZ_ASSERT(mPromises); + if (!mIsWorkerFetch) { + return; + } + + nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction( + __func__, [actorID = mArgs.as<WorkerFetchArgs>().mActorID, + channelID = aChannelID]() { + FETCH_LOG( + ("FetchInstance::NotifyNetworkMonitorAlternateStack, Runnable")); + RefPtr<FetchParent> actor = FetchParent::GetActorByID(actorID); + if (actor) { + actor->OnNotifyNetworkMonitorAlternateStack(channelID); + } + }); + + MOZ_ALWAYS_SUCCEEDS(mArgs.as<WorkerFetchArgs>().mEventTarget->Dispatch( + r, nsIThread::DISPATCH_NORMAL)); +} + +// FetchService + +NS_IMPL_ISUPPORTS(FetchService, nsIObserver) + +StaticRefPtr<FetchService> gInstance; + +/*static*/ +already_AddRefed<FetchService> FetchService::GetInstance() { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + if (!gInstance) { + gInstance = MakeRefPtr<FetchService>(); + nsresult rv = gInstance->RegisterNetworkObserver(); + if (NS_WARN_IF(NS_FAILED(rv))) { + gInstance = nullptr; + return nullptr; + } + ClearOnShutdown(&gInstance); + } + RefPtr<FetchService> service = gInstance; + return service.forget(); +} + +/*static*/ +RefPtr<FetchServicePromises> FetchService::NetworkErrorResponse(nsresult aRv) { + RefPtr<FetchServicePromises> promises = MakeRefPtr<FetchServicePromises>(); + promises->ResolveResponseAvailablePromise(InternalResponse::NetworkError(aRv), + __func__); + promises->ResolveResponseTimingPromise(ResponseTiming(), __func__); + promises->ResolveResponseEndPromise( + ResponseEndArgs(FetchDriverObserver::eAborted), __func__); + return promises; +} + +FetchService::FetchService() { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); +} + +FetchService::~FetchService() { + MOZ_ALWAYS_SUCCEEDS(UnregisterNetworkObserver()); +} + +nsresult FetchService::RegisterNetworkObserver() { + AssertIsOnMainThread(); + nsCOMPtr<nsIObserverService> observerService = services::GetObserverService(); + if (!observerService) { + return NS_ERROR_UNEXPECTED; + } + + nsCOMPtr<nsIIOService> ioService = services::GetIOService(); + if (!ioService) { + return NS_ERROR_UNEXPECTED; + } + + nsresult rv = observerService->AddObserver( + this, NS_IOSERVICE_OFFLINE_STATUS_TOPIC, false); + NS_ENSURE_SUCCESS(rv, rv); + + rv = observerService->AddObserver(this, "xpcom-shutdown", false); + NS_ENSURE_SUCCESS(rv, rv); + + rv = ioService->GetOffline(&mOffline); + NS_ENSURE_SUCCESS(rv, rv); + mObservingNetwork = true; + + return NS_OK; +} + +nsresult FetchService::UnregisterNetworkObserver() { + AssertIsOnMainThread(); + nsresult rv; + if (mObservingNetwork) { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + rv = observerService->RemoveObserver(this, + NS_IOSERVICE_OFFLINE_STATUS_TOPIC); + NS_ENSURE_SUCCESS(rv, rv); + rv = observerService->RemoveObserver(this, "xpcom-shutdown"); + NS_ENSURE_SUCCESS(rv, rv); + } + mObservingNetwork = false; + } + return NS_OK; +} + +NS_IMETHODIMP FetchService::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + FETCH_LOG(("FetchService::Observe topic: %s", aTopic)); + AssertIsOnMainThread(); + MOZ_ASSERT(!strcmp(aTopic, NS_IOSERVICE_OFFLINE_STATUS_TOPIC) || + !strcmp(aTopic, "xpcom-shutdown")); + + if (!strcmp(aTopic, "xpcom-shutdown")) { + // Going to shutdown, unregister the network status observer to avoid + // receiving + nsresult rv = UnregisterNetworkObserver(); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; + } + + if (nsDependentString(aData).EqualsLiteral(NS_IOSERVICE_ONLINE)) { + mOffline = false; + } else { + mOffline = true; + // Network is offline, cancel running fetchs. + for (auto it = mFetchInstanceTable.begin(), end = mFetchInstanceTable.end(); + it != end; ++it) { + it->GetData()->Cancel(); + } + mFetchInstanceTable.Clear(); + } + return NS_OK; +} + +RefPtr<FetchServicePromises> FetchService::Fetch(FetchArgs&& aArgs) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + FETCH_LOG(("FetchService::Fetch (%s)", aArgs.is<NavigationPreloadArgs>() + ? "NavigationPreload" + : "WorkerFetch")); + if (mOffline) { + FETCH_LOG(("FetchService::Fetch network offline")); + return NetworkErrorResponse(NS_ERROR_OFFLINE); + } + + // Create FetchInstance + RefPtr<FetchInstance> fetch = MakeRefPtr<FetchInstance>(); + + // Call FetchInstance::Initialize() to get needed information for FetchDriver, + nsresult rv = fetch->Initialize(std::move(aArgs)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NetworkErrorResponse(rv); + } + + // Call FetchInstance::Fetch() to start an asynchronous fetching. + RefPtr<FetchServicePromises> promises = fetch->Fetch(); + MOZ_ASSERT(promises); + + if (!promises->GetResponseAvailablePromise()->IsResolved()) { + // Insert the created FetchInstance into FetchInstanceTable. + if (!mFetchInstanceTable.WithEntryHandle(promises, [&](auto&& entry) { + if (entry.HasEntry()) { + return false; + } + entry.Insert(fetch); + return true; + })) { + FETCH_LOG( + ("FetchService::Fetch entry[%p] already exists", promises.get())); + return NetworkErrorResponse(NS_ERROR_UNEXPECTED); + } + FETCH_LOG(("FetchService::Fetch entry[%p] of FetchInstance[%p] added", + promises.get(), fetch.get())); + } + return promises; +} + +void FetchService::CancelFetch(const RefPtr<FetchServicePromises>&& aPromises) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aPromises); + FETCH_LOG(("FetchService::CancelFetch aPromises[%p]", aPromises.get())); + + auto entry = mFetchInstanceTable.Lookup(aPromises); + if (entry) { + // Notice any modifications here before entry.Remove() probably should be + // reflected to Observe() offline case. + entry.Data()->Cancel(); + entry.Remove(); + FETCH_LOG( + ("FetchService::CancelFetch entry [%p] removed", aPromises.get())); + } +} + +} // namespace mozilla::dom diff --git a/dom/fetch/FetchService.h b/dom/fetch/FetchService.h new file mode 100644 index 0000000000..3032c4d2a9 --- /dev/null +++ b/dom/fetch/FetchService.h @@ -0,0 +1,186 @@ +/* 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_FetchService_h +#define _mozilla_dom_FetchService_h + +#include "nsIChannel.h" +#include "nsIObserver.h" +#include "nsTHashMap.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/MozPromise.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/FetchDriver.h" +#include "mozilla/dom/FetchTypes.h" +#include "mozilla/dom/PerformanceTimingTypes.h" +#include "mozilla/dom/SafeRefPtr.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/net/NeckoChannelParams.h" + +class nsILoadGroup; +class nsIPrincipal; +class nsICookieJarSettings; +class PerformanceStorage; + +namespace mozilla::dom { + +class InternalRequest; +class InternalResponse; +class ClientInfo; +class ServiceWorkerDescriptor; + +using FetchServiceResponse = SafeRefPtr<InternalResponse>; +using FetchServiceResponseAvailablePromise = + MozPromise<FetchServiceResponse, CopyableErrorResult, true>; +using FetchServiceResponseTimingPromise = + MozPromise<ResponseTiming, CopyableErrorResult, true>; +using FetchServiceResponseEndPromise = + MozPromise<ResponseEndArgs, CopyableErrorResult, true>; + +class FetchServicePromises final { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FetchServicePromises); + + public: + FetchServicePromises(); + + RefPtr<FetchServiceResponseAvailablePromise> GetResponseAvailablePromise(); + RefPtr<FetchServiceResponseTimingPromise> GetResponseTimingPromise(); + RefPtr<FetchServiceResponseEndPromise> GetResponseEndPromise(); + + void ResolveResponseAvailablePromise(FetchServiceResponse&& aResponse, + const char* aMethodName); + void RejectResponseAvailablePromise(const CopyableErrorResult&& aError, + const char* aMethodName); + void ResolveResponseTimingPromise(ResponseTiming&& aTiming, + const char* aMethodName); + void RejectResponseTimingPromise(const CopyableErrorResult&& aError, + const char* aMethodName); + void ResolveResponseEndPromise(ResponseEndArgs&& aArgs, + const char* aMethodName); + void RejectResponseEndPromise(const CopyableErrorResult&& aError, + const char* aMethodName); + + private: + ~FetchServicePromises() = default; + + RefPtr<FetchServiceResponseAvailablePromise::Private> mAvailablePromise; + RefPtr<FetchServiceResponseTimingPromise::Private> mTimingPromise; + RefPtr<FetchServiceResponseEndPromise::Private> mEndPromise; +}; + +/** + * FetchService is a singleton object which designed to be used in parent + * process main thread only. It is used to handle the special fetch requests + * from ServiceWorkers(by Navigation Preload) and PFetch. + * + * FetchService creates FetchInstance internally to represent each Fetch + * request. It supports an asynchronous fetching, FetchServicePromises is + * created when a Fetch starts, once the response is ready or any error happens, + * the FetchServicePromises would be resolved or rejected. The promises + * consumers can set callbacks to handle the Fetch result. + */ +class FetchService final : public nsIObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + struct NavigationPreloadArgs { + SafeRefPtr<InternalRequest> mRequest; + nsCOMPtr<nsIChannel> mChannel; + }; + + struct WorkerFetchArgs { + SafeRefPtr<InternalRequest> mRequest; + mozilla::ipc::PrincipalInfo mPrincipalInfo; + nsCString mWorkerScript; + Maybe<ClientInfo> mClientInfo; + Maybe<ServiceWorkerDescriptor> mController; + Maybe<net::CookieJarSettingsArgs> mCookieJarSettings; + bool mNeedOnDataAvailable; + nsCOMPtr<nsICSPEventListener> mCSPEventListener; + uint64_t mAssociatedBrowsingContextID; + nsCOMPtr<nsISerialEventTarget> mEventTarget; + nsID mActorID; + }; + + struct UnknownArgs {}; + + using FetchArgs = + Variant<NavigationPreloadArgs, WorkerFetchArgs, UnknownArgs>; + + static already_AddRefed<FetchService> GetInstance(); + + static RefPtr<FetchServicePromises> NetworkErrorResponse(nsresult aRv); + + FetchService(); + + // This method creates a FetchInstance to trigger fetch. + // The created FetchInstance is saved in mFetchInstanceTable + RefPtr<FetchServicePromises> Fetch(FetchArgs&& aArgs); + + void CancelFetch(const RefPtr<FetchServicePromises>&& aPromises); + + private: + /** + * FetchInstance is an internal representation for each Fetch created by + * FetchService. + * FetchInstance is also a FetchDriverObserver which has responsibility to + * resolve/reject the FetchServicePromises. + * FetchInstance triggers fetch by instancing a FetchDriver with proper + * initialization. The general usage flow of FetchInstance is as follows + * + * RefPtr<FetchInstance> fetch = MakeRefPtr<FetchInstance>(); + * fetch->Initialize(FetchArgs args); + * RefPtr<FetchServicePromises> fetch->Fetch(); + */ + class FetchInstance final : public FetchDriverObserver { + public: + FetchInstance() = default; + + nsresult Initialize(FetchArgs&& aArgs); + + RefPtr<FetchServicePromises> Fetch(); + + void Cancel(); + + /* FetchDriverObserver interface */ + void OnResponseEnd(FetchDriverObserver::EndReason aReason, + JS::Handle<JS::Value> aReasonDetails) override; + void OnResponseAvailableInternal( + SafeRefPtr<InternalResponse> aResponse) override; + bool NeedOnDataAvailable() override; + void OnDataAvailable() override; + void FlushConsoleReport() override; + void OnReportPerformanceTiming() override; + void OnNotifyNetworkMonitorAlternateStack(uint64_t aChannelID) override; + + private: + ~FetchInstance() = default; + + SafeRefPtr<InternalRequest> mRequest; + nsCOMPtr<nsIPrincipal> mPrincipal; + nsCOMPtr<nsILoadGroup> mLoadGroup; + nsCOMPtr<nsICookieJarSettings> mCookieJarSettings; + RefPtr<PerformanceStorage> mPerformanceStorage; + FetchArgs mArgs{AsVariant(FetchService::UnknownArgs())}; + RefPtr<FetchDriver> mFetchDriver; + SafeRefPtr<InternalResponse> mResponse; + RefPtr<FetchServicePromises> mPromises; + bool mIsWorkerFetch{false}; + }; + + ~FetchService(); + + nsresult RegisterNetworkObserver(); + nsresult UnregisterNetworkObserver(); + + // This is a container to manage the generated fetches. + nsTHashMap<nsRefPtrHashKey<FetchServicePromises>, RefPtr<FetchInstance> > + mFetchInstanceTable; + bool mObservingNetwork{false}; + bool mOffline{false}; +}; + +} // namespace mozilla::dom + +#endif // _mozilla_dom_FetchService_h diff --git a/dom/fetch/FetchStreamReader.cpp b/dom/fetch/FetchStreamReader.cpp new file mode 100644 index 0000000000..de5a2cfefe --- /dev/null +++ b/dom/fetch/FetchStreamReader.cpp @@ -0,0 +1,441 @@ +/* -*- 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 "FetchStreamReader.h" +#include "InternalResponse.h" +#include "mozilla/ConsoleReportCollector.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/StaticAnalysisFunctions.h" +#include "mozilla/dom/AutoEntryScript.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseBinding.h" +#include "mozilla/dom/ReadableStream.h" +#include "mozilla/dom/ReadableStreamDefaultController.h" +#include "mozilla/dom/ReadableStreamDefaultReader.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRef.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/TaskCategory.h" +#include "nsContentUtils.h" +#include "nsDebug.h" +#include "nsIAsyncInputStream.h" +#include "nsIPipe.h" +#include "nsIScriptError.h" +#include "nsPIDOMWindow.h" +#include "jsapi.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTING_ADDREF(FetchStreamReader) +NS_IMPL_CYCLE_COLLECTING_RELEASE(FetchStreamReader) + +NS_IMPL_CYCLE_COLLECTION_CLASS(FetchStreamReader) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(FetchStreamReader) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mGlobal) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mReader) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(FetchStreamReader) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReader) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(FetchStreamReader) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FetchStreamReader) + NS_INTERFACE_MAP_ENTRY(nsIOutputStreamCallback) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIOutputStreamCallback) +NS_INTERFACE_MAP_END + +/* static */ +nsresult FetchStreamReader::Create(JSContext* aCx, nsIGlobalObject* aGlobal, + FetchStreamReader** aStreamReader, + nsIInputStream** aInputStream) { + MOZ_ASSERT(aCx); + MOZ_ASSERT(aGlobal); + MOZ_ASSERT(aStreamReader); + MOZ_ASSERT(aInputStream); + + RefPtr<FetchStreamReader> streamReader = new FetchStreamReader(aGlobal); + + nsCOMPtr<nsIAsyncInputStream> pipeIn; + + NS_NewPipe2(getter_AddRefs(pipeIn), getter_AddRefs(streamReader->mPipeOut), + true, true, 0, 0); + + if (!NS_IsMainThread()) { + WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(aCx); + MOZ_ASSERT(workerPrivate); + + RefPtr<StrongWorkerRef> workerRef = StrongWorkerRef::Create( + workerPrivate, "FetchStreamReader", [streamReader]() { + MOZ_ASSERT(streamReader); + + // mAsyncWaitWorkerRef may keep the (same) StrongWorkerRef alive even + // when mWorkerRef has already been nulled out by a previous call to + // CloseAndRelease, we can just safely ignore this callback then + // (as would the CloseAndRelease do on a second call). + if (streamReader->mWorkerRef) { + streamReader->CloseAndRelease( + streamReader->mWorkerRef->Private()->GetJSContext(), + NS_ERROR_DOM_INVALID_STATE_ERR); + } else { + MOZ_DIAGNOSTIC_ASSERT(streamReader->mAsyncWaitWorkerRef); + } + }); + + if (NS_WARN_IF(!workerRef)) { + streamReader->mPipeOut->CloseWithStatus(NS_ERROR_DOM_INVALID_STATE_ERR); + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + // These 2 objects create a ref-cycle here that is broken when the stream is + // closed or the worker shutsdown. + streamReader->mWorkerRef = std::move(workerRef); + } + + pipeIn.forget(aInputStream); + streamReader.forget(aStreamReader); + return NS_OK; +} + +FetchStreamReader::FetchStreamReader(nsIGlobalObject* aGlobal) + : mGlobal(aGlobal), + mOwningEventTarget(mGlobal->EventTargetFor(TaskCategory::Other)) { + MOZ_ASSERT(aGlobal); + + mozilla::HoldJSObjects(this); +} + +FetchStreamReader::~FetchStreamReader() { + CloseAndRelease(nullptr, NS_BASE_STREAM_CLOSED); + + mozilla::DropJSObjects(this); +} + +// If a context is provided, an attempt will be made to cancel the reader. The +// only situation where we don't expect to have a context is when closure is +// being triggered from the destructor or the WorkerRef is notifying. If +// we're at the destructor, it's far too late to cancel anything. And if the +// WorkerRef is being notified, the global is going away, so there's also +// no need to do further JS work. +void FetchStreamReader::CloseAndRelease(JSContext* aCx, nsresult aStatus) { + NS_ASSERT_OWNINGTHREAD(FetchStreamReader); + + if (mStreamClosed) { + // Already closed. + return; + } + + RefPtr<FetchStreamReader> kungFuDeathGrip = this; + if (aCx && mReader) { + ErrorResult rv; + if (aStatus == NS_ERROR_DOM_WRONG_TYPE_ERR) { + rv.ThrowTypeError<MSG_FETCH_BODY_WRONG_TYPE>(); + } else { + rv = aStatus; + } + JS::Rooted<JS::Value> errorValue(aCx); + if (ToJSValue(aCx, std::move(rv), &errorValue)) { + IgnoredErrorResult ignoredError; + // It's currently safe to cancel an already closed reader because, per the + // comments in ReadableStream::cancel() conveying the spec, step 2 of + // 3.4.3 that specified ReadableStreamCancel is: If stream.[[state]] is + // "closed", return a new promise resolved with undefined. + RefPtr<Promise> cancelResultPromise = + MOZ_KnownLive(mReader)->Cancel(aCx, errorValue, ignoredError); + NS_WARNING_ASSERTION(!ignoredError.Failed(), + "Failed to cancel stream during close and release"); + if (cancelResultPromise) { + bool setHandled = cancelResultPromise->SetAnyPromiseIsHandled(); + NS_WARNING_ASSERTION(setHandled, + "Failed to mark cancel promise as handled."); + (void)setHandled; + } + } + + // We don't want to propagate exceptions during the cleanup. + JS_ClearPendingException(aCx); + } + + mStreamClosed = true; + + mGlobal = nullptr; + + if (mPipeOut) { + mPipeOut->CloseWithStatus(aStatus); + } + mPipeOut = nullptr; + + mWorkerRef = nullptr; + + mReader = nullptr; + mBuffer.Clear(); +} + +// https://fetch.spec.whatwg.org/#body-incrementally-read +void FetchStreamReader::StartConsuming(JSContext* aCx, ReadableStream* aStream, + ErrorResult& aRv) { + MOZ_DIAGNOSTIC_ASSERT(!mReader); + MOZ_DIAGNOSTIC_ASSERT(aStream); + + // Step 2: Let reader be the result of getting a reader for body’s stream. + RefPtr<ReadableStreamDefaultReader> reader = aStream->GetReader(aRv); + if (aRv.Failed()) { + CloseAndRelease(aCx, NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + mReader = reader; + + mAsyncWaitWorkerRef = mWorkerRef; + aRv = mPipeOut->AsyncWait(this, 0, 0, mOwningEventTarget); + if (NS_WARN_IF(aRv.Failed())) { + mAsyncWaitWorkerRef = nullptr; + CloseAndRelease(aCx, NS_ERROR_DOM_INVALID_STATE_ERR); + } +} + +struct FetchReadRequest : public ReadRequest { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(FetchReadRequest, ReadRequest) + + explicit FetchReadRequest(FetchStreamReader* aReader) + : mFetchStreamReader(aReader) {} + + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void ChunkSteps(JSContext* aCx, JS::Handle<JS::Value> aChunk, + ErrorResult& aRv) override { + mFetchStreamReader->ChunkSteps(aCx, aChunk, aRv); + } + + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void CloseSteps(JSContext* aCx, ErrorResult& aRv) override { + mFetchStreamReader->CloseSteps(aCx, aRv); + } + + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void ErrorSteps(JSContext* aCx, JS::Handle<JS::Value> aError, + ErrorResult& aRv) override { + mFetchStreamReader->ErrorSteps(aCx, aError, aRv); + } + + protected: + virtual ~FetchReadRequest() = default; + + MOZ_KNOWN_LIVE RefPtr<FetchStreamReader> mFetchStreamReader; +}; + +NS_IMPL_CYCLE_COLLECTION_INHERITED(FetchReadRequest, ReadRequest, + mFetchStreamReader) +NS_IMPL_ADDREF_INHERITED(FetchReadRequest, ReadRequest) +NS_IMPL_RELEASE_INHERITED(FetchReadRequest, ReadRequest) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FetchReadRequest) +NS_INTERFACE_MAP_END_INHERITING(ReadRequest) + +// nsIOutputStreamCallback interface +MOZ_CAN_RUN_SCRIPT_BOUNDARY +NS_IMETHODIMP +FetchStreamReader::OnOutputStreamReady(nsIAsyncOutputStream* aStream) { + NS_ASSERT_OWNINGTHREAD(FetchStreamReader); + if (mStreamClosed) { + mAsyncWaitWorkerRef = nullptr; + return NS_OK; + } + + AutoEntryScript aes(mGlobal, "ReadableStreamReader.read", !mWorkerRef); + if (!Process(aes.cx())) { + // We're done processing data, and haven't queued up a new AsyncWait - we + // can clear our mAsyncWaitWorkerRef. + mAsyncWaitWorkerRef = nullptr; + } + return NS_OK; +} + +bool FetchStreamReader::Process(JSContext* aCx) { + NS_ASSERT_OWNINGTHREAD(FetchStreamReader); + MOZ_ASSERT(mReader); + + if (!mBuffer.IsEmpty()) { + nsresult rv = WriteBuffer(); + if (NS_WARN_IF(NS_FAILED(rv))) { + CloseAndRelease(aCx, NS_ERROR_DOM_ABORT_ERR); + return false; + } + return true; + } + + // Check if the output stream has already been closed. This lets us propagate + // errors eagerly, and detect output stream closures even when we have no data + // to write. + if (NS_WARN_IF(NS_FAILED(mPipeOut->StreamStatus()))) { + CloseAndRelease(aCx, NS_ERROR_DOM_ABORT_ERR); + return false; + } + + // We're waiting on new data - set up a WAIT_CLOSURE_ONLY callback so we + // notice if the reader closes. + nsresult rv = mPipeOut->AsyncWait( + this, nsIAsyncOutputStream::WAIT_CLOSURE_ONLY, 0, mOwningEventTarget); + if (NS_WARN_IF(NS_FAILED(rv))) { + CloseAndRelease(aCx, NS_ERROR_DOM_INVALID_STATE_ERR); + return false; + } + + // If we already have an outstanding read request, don't start another one + // concurrently. + if (!mHasOutstandingReadRequest) { + // https://fetch.spec.whatwg.org/#incrementally-read-loop + // The below very loosely tries to implement the incrementally-read-loop + // from the fetch spec. + // Step 2: Read a chunk from reader given readRequest. + RefPtr<ReadRequest> readRequest = new FetchReadRequest(this); + RefPtr<ReadableStreamDefaultReader> reader = mReader; + mHasOutstandingReadRequest = true; + + IgnoredErrorResult err; + reader->ReadChunk(aCx, *readRequest, err); + if (NS_WARN_IF(err.Failed())) { + // Let's close the stream. + mHasOutstandingReadRequest = false; + CloseAndRelease(aCx, NS_ERROR_DOM_INVALID_STATE_ERR); + // Don't return false, as we've already called `AsyncWait`. + } + } + return true; +} + +void FetchStreamReader::ChunkSteps(JSContext* aCx, JS::Handle<JS::Value> aChunk, + ErrorResult& aRv) { + // This roughly implements the chunk steps from + // https://fetch.spec.whatwg.org/#incrementally-read-loop. + + mHasOutstandingReadRequest = false; + + // Step 2. If chunk is not a Uint8Array object, then set continueAlgorithm to + // this step: run processBodyError given a TypeError. + RootedSpiderMonkeyInterface<Uint8Array> chunk(aCx); + if (!aChunk.isObject() || !chunk.Init(&aChunk.toObject())) { + CloseAndRelease(aCx, NS_ERROR_DOM_WRONG_TYPE_ERR); + return; + } + chunk.ComputeState(); + + MOZ_DIAGNOSTIC_ASSERT(mBuffer.IsEmpty()); + + // Let's take a copy of the data. + // FIXME: We could sometimes avoid this copy by trying to write `chunk` + // directly into `mPipeOut` eagerly, and only filling `mBuffer` if there isn't + // enough space in the pipe's buffer. + if (!mBuffer.AppendElements(chunk.Data(), chunk.Length(), fallible)) { + CloseAndRelease(aCx, NS_ERROR_OUT_OF_MEMORY); + return; + } + + mBufferOffset = 0; + mBufferRemaining = chunk.Length(); + + nsresult rv = WriteBuffer(); + if (NS_WARN_IF(NS_FAILED(rv))) { + CloseAndRelease(aCx, NS_ERROR_DOM_ABORT_ERR); + } +} + +void FetchStreamReader::CloseSteps(JSContext* aCx, ErrorResult& aRv) { + mHasOutstandingReadRequest = false; + CloseAndRelease(aCx, NS_BASE_STREAM_CLOSED); +} + +void FetchStreamReader::ErrorSteps(JSContext* aCx, JS::Handle<JS::Value> aError, + ErrorResult& aRv) { + mHasOutstandingReadRequest = false; + ReportErrorToConsole(aCx, aError); + CloseAndRelease(aCx, NS_ERROR_FAILURE); +} + +nsresult FetchStreamReader::WriteBuffer() { + MOZ_ASSERT(mBuffer.Length() == (mBufferOffset + mBufferRemaining)); + + char* data = reinterpret_cast<char*>(mBuffer.Elements()); + + while (mBufferRemaining > 0) { + uint32_t written = 0; + nsresult rv = + mPipeOut->Write(data + mBufferOffset, mBufferRemaining, &written); + + if (rv == NS_BASE_STREAM_WOULD_BLOCK) { + break; + } + + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(written <= mBufferRemaining); + mBufferRemaining -= written; + mBufferOffset += written; + + if (mBufferRemaining == 0) { + mBuffer.Clear(); + break; + } + } + + nsresult rv = mPipeOut->AsyncWait(this, 0, 0, mOwningEventTarget); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +void FetchStreamReader::ReportErrorToConsole(JSContext* aCx, + JS::Handle<JS::Value> aValue) { + nsCString sourceSpec; + uint32_t line = 0; + uint32_t column = 0; + nsString valueString; + + nsContentUtils::ExtractErrorValues(aCx, aValue, sourceSpec, &line, &column, + valueString); + + nsTArray<nsString> params; + params.AppendElement(valueString); + + RefPtr<ConsoleReportCollector> reporter = new ConsoleReportCollector(); + reporter->AddConsoleReport(nsIScriptError::errorFlag, + "ReadableStreamReader.read"_ns, + nsContentUtils::eDOM_PROPERTIES, sourceSpec, line, + column, "ReadableStreamReadingFailed"_ns, params); + + uint64_t innerWindowId = 0; + + if (NS_IsMainThread()) { + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(mGlobal); + if (window) { + innerWindowId = window->WindowID(); + } + reporter->FlushReportsToConsole(innerWindowId); + return; + } + + WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(aCx); + if (workerPrivate) { + innerWindowId = workerPrivate->WindowID(); + } + + RefPtr<Runnable> r = NS_NewRunnableFunction( + "FetchStreamReader::ReportErrorToConsole", [reporter, innerWindowId]() { + reporter->FlushReportsToConsole(innerWindowId); + }); + + workerPrivate->DispatchToMainThread(r.forget()); +} + +} // namespace mozilla::dom diff --git a/dom/fetch/FetchStreamReader.h b/dom/fetch/FetchStreamReader.h new file mode 100644 index 0000000000..65d94d8cb0 --- /dev/null +++ b/dom/fetch/FetchStreamReader.h @@ -0,0 +1,94 @@ +/* -*- 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_FetchStreamReader_h +#define mozilla_dom_FetchStreamReader_h + +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/FetchBinding.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "nsIAsyncOutputStream.h" +#include "nsIGlobalObject.h" + +namespace mozilla::dom { + +class ReadableStream; +class ReadableStreamDefaultReader; +class StrongWorkerRef; + +class FetchStreamReader final : public nsIOutputStreamCallback { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_AMBIGUOUS( + FetchStreamReader, nsIOutputStreamCallback) + NS_DECL_NSIOUTPUTSTREAMCALLBACK + + // This creates a nsIInputStream able to retrieve data from the ReadableStream + // object. The reading starts when StartConsuming() is called. + static nsresult Create(JSContext* aCx, nsIGlobalObject* aGlobal, + FetchStreamReader** aStreamReader, + nsIInputStream** aInputStream); + + MOZ_CAN_RUN_SCRIPT + void ChunkSteps(JSContext* aCx, JS::Handle<JS::Value> aChunk, + ErrorResult& aRv); + MOZ_CAN_RUN_SCRIPT + void CloseSteps(JSContext* aCx, ErrorResult& aRv); + MOZ_CAN_RUN_SCRIPT + void ErrorSteps(JSContext* aCx, JS::Handle<JS::Value> aError, + ErrorResult& aRv); + + // Idempotently close the output stream and null out all state. If aCx is + // provided, the reader will also be canceled. aStatus must be a DOM error + // as understood by DOMException because it will be provided as the + // cancellation reason. + // + // This is a script boundary minimize annotation changes required while + // we figure out how to handle some more tricky annotation cases (for + // example, the destructor of this class. Tracking under Bug 1750656) + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void CloseAndRelease(JSContext* aCx, nsresult aStatus); + + void StartConsuming(JSContext* aCx, ReadableStream* aStream, + ErrorResult& aRv); + + private: + explicit FetchStreamReader(nsIGlobalObject* aGlobal); + ~FetchStreamReader(); + + nsresult WriteBuffer(); + + // Attempt to copy data from mBuffer into mPipeOut. Returns `true` if data was + // written, and AsyncWait callbacks or FetchReadRequest calls have been set up + // to write more data in the future, and `false` otherwise. + MOZ_CAN_RUN_SCRIPT + bool Process(JSContext* aCx); + + void ReportErrorToConsole(JSContext* aCx, JS::Handle<JS::Value> aValue); + + nsCOMPtr<nsIGlobalObject> mGlobal; + nsCOMPtr<nsIEventTarget> mOwningEventTarget; + + nsCOMPtr<nsIAsyncOutputStream> mPipeOut; + + RefPtr<StrongWorkerRef> mWorkerRef; + RefPtr<StrongWorkerRef> mAsyncWaitWorkerRef; + + RefPtr<ReadableStreamDefaultReader> mReader; + + nsTArray<uint8_t> mBuffer; + uint32_t mBufferRemaining = 0; + uint32_t mBufferOffset = 0; + + bool mHasOutstandingReadRequest = false; + bool mStreamClosed = false; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_FetchStreamReader_h diff --git a/dom/fetch/FetchStreamUtils.cpp b/dom/fetch/FetchStreamUtils.cpp new file mode 100644 index 0000000000..d9cb762526 --- /dev/null +++ b/dom/fetch/FetchStreamUtils.cpp @@ -0,0 +1,83 @@ +/* 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 "FetchStreamUtils.h" + +#include "mozilla/NotNull.h" +#include "mozilla/RemoteLazyInputStreamChild.h" +#include "mozilla/RemoteLazyInputStreamStorage.h" +#include "mozilla/dom/FetchTypes.h" +#include "mozilla/dom/IPCBlob.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "nsContentUtils.h" +#include "nsXULAppAPI.h" + +namespace mozilla::dom { + +namespace { + +RefPtr<RemoteLazyInputStreamStorage> GetRemoteLazyInputStreamStorage() { + auto storageOrErr = RemoteLazyInputStreamStorage::Get(); + MOZ_ASSERT(storageOrErr.isOk()); + return storageOrErr.unwrap(); +} + +} // namespace + +NotNull<nsCOMPtr<nsIInputStream>> ToInputStream( + const ParentToParentStream& aStream) { + MOZ_ASSERT(XRE_IsParentProcess()); + return WrapNotNull( + GetRemoteLazyInputStreamStorage()->ForgetStream(aStream.uuid())); +} + +NotNull<nsCOMPtr<nsIInputStream>> ToInputStream( + const ParentToChildStream& aStream) { + MOZ_ASSERT(XRE_IsContentProcess()); + nsCOMPtr<nsIInputStream> result; + if (aStream.type() == ParentToChildStream::TRemoteLazyInputStream) { + result = aStream.get_RemoteLazyInputStream(); + } else { + result = DeserializeIPCStream(aStream.get_IPCStream()); + } + return WrapNotNull(result); +} + +ParentToParentStream ToParentToParentStream( + const NotNull<nsCOMPtr<nsIInputStream>>& aStream, int64_t aStreamSize) { + MOZ_ASSERT(XRE_IsParentProcess()); + + ParentToParentStream stream; + stream.uuid() = nsID::GenerateUUID(); + GetRemoteLazyInputStreamStorage()->AddStream(aStream.get(), stream.uuid()); + return stream; +} + +ParentToChildStream ToParentToChildStream( + const NotNull<nsCOMPtr<nsIInputStream>>& aStream, int64_t aStreamSize, + NotNull<mozilla::ipc::PBackgroundParent*> aBackgroundParent, + bool aSerializeAsLazy) { + MOZ_ASSERT(XRE_IsParentProcess()); + + ParentToChildStream result; + if (aSerializeAsLazy) { + result = RemoteLazyInputStream::WrapStream(aStream.get()); + } else { + nsCOMPtr<nsIInputStream> stream(aStream.get()); + mozilla::ipc::IPCStream ipcStream; + Unused << NS_WARN_IF( + !mozilla::ipc::SerializeIPCStream(stream.forget(), ipcStream, false)); + result = ipcStream; + } + return result; +} + +ParentToChildStream ToParentToChildStream( + const ParentToParentStream& aStream, int64_t aStreamSize, + NotNull<mozilla::ipc::PBackgroundParent*> aBackgroundParent) { + return ToParentToChildStream(ToInputStream(aStream), aStreamSize, + aBackgroundParent); +} + +} // namespace mozilla::dom diff --git a/dom/fetch/FetchStreamUtils.h b/dom/fetch/FetchStreamUtils.h new file mode 100644 index 0000000000..407339df41 --- /dev/null +++ b/dom/fetch/FetchStreamUtils.h @@ -0,0 +1,56 @@ +/* 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_fetch_FetchStreamUtils_h +#define _mozilla_dom_fetch_FetchStreamUtils_h + +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/NotNull.h" +#include "mozilla/dom/FetchTypes.h" + +#include "nsIInputStream.h" + +#include <cstdint> + +namespace mozilla { + +namespace ipc { +class PBackgroundParent; +} + +namespace dom { + +// Convert a ParentToParentStream received over IPC to an nsIInputStream. Can +// only be called in the parent process. +NotNull<nsCOMPtr<nsIInputStream>> ToInputStream( + const ParentToParentStream& aStream); + +// Convert a ParentToChildStream received over IPC to an nsIInputStream. Can +// only be called in a content process. +NotNull<nsCOMPtr<nsIInputStream>> ToInputStream( + const ParentToChildStream& aStream); + +// Serialize an nsIInputStream for IPC inside the parent process. Can only be +// called in the parent process. +ParentToParentStream ToParentToParentStream( + const NotNull<nsCOMPtr<nsIInputStream>>& aStream, int64_t aStreamSize); + +// Serialize an nsIInputStream for IPC from the parent process to a content +// process. Can only be called in the parent process. +ParentToChildStream ToParentToChildStream( + const NotNull<nsCOMPtr<nsIInputStream>>& aStream, int64_t aStreamSize, + NotNull<mozilla::ipc::PBackgroundParent*> aBackgroundParent, + bool aSerializeAsLazy = true); + +// Convert a ParentToParentStream to a ParentToChildStream. Can only be called +// in the parent process. +ParentToChildStream ToParentToChildStream( + const ParentToParentStream& aStream, int64_t aStreamSize, + NotNull<mozilla::ipc::PBackgroundParent*> aBackgroundParent); + +} // namespace dom + +} // namespace mozilla + +#endif // _mozilla_dom_fetch_FetchStreamUtils_h diff --git a/dom/fetch/FetchTypes.ipdlh b/dom/fetch/FetchTypes.ipdlh new file mode 100644 index 0000000000..f8c594bf1a --- /dev/null +++ b/dom/fetch/FetchTypes.ipdlh @@ -0,0 +1,127 @@ +/* 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 IPCStream; +include PBackgroundSharedTypes; +include PerformanceTimingTypes; +include NeckoChannelParams; + +include "mozilla/dom/FetchIPCTypes.h"; +include "mozilla/ipc/TransportSecurityInfoUtils.h"; + +using mozilla::dom::HeadersGuardEnum from "mozilla/dom/HeadersBinding.h"; +using mozilla::dom::ReferrerPolicy from "mozilla/dom/ReferrerPolicyBinding.h"; +using mozilla::dom::RequestCache from "mozilla/dom/RequestBinding.h"; +using mozilla::dom::RequestCredentials from "mozilla/dom/RequestBinding.h"; +using mozilla::dom::RequestRedirect from "mozilla/dom/RequestBinding.h"; +using mozilla::dom::ResponseType from "mozilla/dom/ResponseBinding.h"; +using struct nsID from "nsID.h"; +using mozilla::dom::FetchDriverObserver::EndReason from "mozilla/dom/FetchDriver.h"; +using nsILoadInfo::CrossOriginEmbedderPolicy from "nsILoadInfo.h"; +[RefCounted] using class mozilla::RemoteLazyInputStream from "mozilla/RemoteLazyInputStream.h"; +[RefCounted] using class nsITransportSecurityInfo from "nsITransportSecurityInfo.h"; + +namespace mozilla { +namespace dom { + +struct HeadersEntry { + nsCString name; + nsCString value; +}; + +struct ParentToParentStream { + // Used as a key for RemoteLazyInputStreamStorage + nsID uuid; +}; + +union ParentToChildStream { + nullable RemoteLazyInputStream; + IPCStream; +}; + +struct ChildToParentStream { + IPCStream stream; +}; + +union BodyStreamVariant { + ParentToParentStream; + ParentToChildStream; + ChildToParentStream; +}; + +struct IPCInternalRequest { + nsCString method; + nsCString[] urlList; + HeadersGuardEnum headersGuard; + HeadersEntry[] headers; + BodyStreamVariant? body; + int64_t bodySize; + nsCString preferredAlternativeDataType; + uint32_t contentPolicyType; + nsString referrer; + ReferrerPolicy referrerPolicy; + ReferrerPolicy environmentReferrerPolicy; + RequestMode requestMode; + RequestCredentials requestCredentials; + RequestCache cacheMode; + RequestRedirect requestRedirect; + nsString integrity; + nsCString fragment; + PrincipalInfo? principalInfo; + PrincipalInfo? interceptionTriggeringPrincipalInfo; + uint32_t interceptionContentPolicyType; + RedirectHistoryEntryInfo[] interceptionRedirectChain; + bool interceptionFromThirdParty; + CrossOriginEmbedderPolicy embedderPolicy; +}; + +struct InternalResponseMetadata { + ResponseType type; + nsCString[] urlList; + uint16_t status; + nsCString statusText; + HeadersGuardEnum headersGuard; + HeadersEntry[] headers; + nsresult errorCode; + nsCString alternativeDataType; + nullable nsITransportSecurityInfo securityInfo; + PrincipalInfo? principalInfo; + nsCString bodyBlobURISpec; + nsString bodyLocalPath; + RequestCredentials credentialsMode; +}; + +struct ParentToParentInternalResponse { + InternalResponseMetadata metadata; + ParentToParentStream? body; + int64_t bodySize; + ParentToParentStream? alternativeBody; +}; + +struct ParentToChildInternalResponse { + InternalResponseMetadata metadata; + ParentToChildStream? body; + int64_t bodySize; + ParentToChildStream? alternativeBody; +}; + +struct ChildToParentInternalResponse { + InternalResponseMetadata metadata; + ChildToParentStream? body; + int64_t bodySize; + ChildToParentStream? alternativeBody; +}; + +struct ResponseTiming { + IPCPerformanceTimingData timingData; + nsString initiatorType; + nsString entryName; +}; + +struct ResponseEndArgs { + EndReason endReason; +}; + +} // namespace ipc +} // namespace mozilla diff --git a/dom/fetch/FetchUtil.cpp b/dom/fetch/FetchUtil.cpp new file mode 100644 index 0000000000..48281df321 --- /dev/null +++ b/dom/fetch/FetchUtil.cpp @@ -0,0 +1,814 @@ +/* -*- 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 "FetchUtil.h" + +#include "zlib.h" + +#include "js/friend/ErrorMessages.h" // JSMSG_* +#include "nsCRT.h" +#include "nsError.h" +#include "nsIAsyncInputStream.h" +#include "nsICloneableInputStream.h" +#include "nsIHttpChannel.h" +#include "nsNetUtil.h" +#include "nsStreamUtils.h" +#include "nsString.h" +#include "js/BuildId.h" +#include "mozilla/dom/Document.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/dom/DOMException.h" +#include "mozilla/dom/InternalRequest.h" +#include "mozilla/dom/Response.h" +#include "mozilla/dom/ReferrerInfo.h" +#include "mozilla/dom/WorkerRef.h" + +namespace mozilla::dom { + +// static +nsresult FetchUtil::GetValidRequestMethod(const nsACString& aMethod, + nsCString& outMethod) { + nsAutoCString upperCaseMethod(aMethod); + ToUpperCase(upperCaseMethod); + if (!NS_IsValidHTTPToken(aMethod)) { + outMethod.SetIsVoid(true); + return NS_ERROR_DOM_SYNTAX_ERR; + } + + if (upperCaseMethod.EqualsLiteral("CONNECT") || + upperCaseMethod.EqualsLiteral("TRACE") || + upperCaseMethod.EqualsLiteral("TRACK")) { + outMethod.SetIsVoid(true); + return NS_ERROR_DOM_SECURITY_ERR; + } + + if (upperCaseMethod.EqualsLiteral("DELETE") || + upperCaseMethod.EqualsLiteral("GET") || + upperCaseMethod.EqualsLiteral("HEAD") || + upperCaseMethod.EqualsLiteral("OPTIONS") || + upperCaseMethod.EqualsLiteral("POST") || + upperCaseMethod.EqualsLiteral("PUT")) { + outMethod = upperCaseMethod; + } else { + outMethod = aMethod; // Case unchanged for non-standard methods + } + return NS_OK; +} + +static bool FindCRLF(nsACString::const_iterator& aStart, + nsACString::const_iterator& aEnd) { + nsACString::const_iterator end(aEnd); + return FindInReadable("\r\n"_ns, aStart, end); +} + +// Reads over a CRLF and positions start after it. +static bool PushOverLine(nsACString::const_iterator& aStart, + const nsACString::const_iterator& aEnd) { + if (*aStart == nsCRT::CR && (aEnd - aStart > 1) && *(++aStart) == nsCRT::LF) { + ++aStart; // advance to after CRLF + return true; + } + + return false; +} + +// static +bool FetchUtil::ExtractHeader(nsACString::const_iterator& aStart, + nsACString::const_iterator& aEnd, + nsCString& aHeaderName, nsCString& aHeaderValue, + bool* aWasEmptyHeader) { + MOZ_ASSERT(aWasEmptyHeader); + // Set it to a valid value here so we don't forget later. + *aWasEmptyHeader = false; + + const char* beginning = aStart.get(); + nsACString::const_iterator end(aEnd); + if (!FindCRLF(aStart, end)) { + return false; + } + + if (aStart.get() == beginning) { + *aWasEmptyHeader = true; + return true; + } + + nsAutoCString header(beginning, aStart.get() - beginning); + + nsACString::const_iterator headerStart, iter, headerEnd; + header.BeginReading(headerStart); + header.EndReading(headerEnd); + iter = headerStart; + if (!FindCharInReadable(':', iter, headerEnd)) { + return false; + } + + aHeaderName.Assign(StringHead(header, iter - headerStart)); + aHeaderName.CompressWhitespace(); + if (!NS_IsValidHTTPToken(aHeaderName)) { + return false; + } + + aHeaderValue.Assign(Substring(++iter, headerEnd)); + if (!NS_IsReasonableHTTPHeaderValue(aHeaderValue)) { + return false; + } + aHeaderValue.CompressWhitespace(); + + return PushOverLine(aStart, aEnd); +} + +// static +nsresult FetchUtil::SetRequestReferrer(nsIPrincipal* aPrincipal, Document* aDoc, + nsIHttpChannel* aChannel, + InternalRequest& aRequest) { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv = NS_OK; + nsAutoString referrer; + aRequest.GetReferrer(referrer); + + ReferrerPolicy policy = aRequest.ReferrerPolicy_(); + nsCOMPtr<nsIReferrerInfo> referrerInfo; + if (referrer.IsEmpty()) { + // This is the case request’s referrer is "no-referrer" + referrerInfo = new ReferrerInfo(nullptr, ReferrerPolicy::No_referrer); + } else if (referrer.EqualsLiteral(kFETCH_CLIENT_REFERRER_STR)) { + referrerInfo = ReferrerInfo::CreateForFetch(aPrincipal, aDoc); + // In the first step, we should use referrer info from requetInit + referrerInfo = static_cast<ReferrerInfo*>(referrerInfo.get()) + ->CloneWithNewPolicy(policy); + } else { + // From "Determine request's Referrer" step 3 + // "If request's referrer is a URL, let referrerSource be request's + // referrer." + nsCOMPtr<nsIURI> referrerURI; + rv = NS_NewURI(getter_AddRefs(referrerURI), referrer); + NS_ENSURE_SUCCESS(rv, rv); + referrerInfo = new ReferrerInfo(referrerURI, policy); + } + + rv = aChannel->SetReferrerInfoWithoutClone(referrerInfo); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString computedReferrerSpec; + referrerInfo = aChannel->GetReferrerInfo(); + if (referrerInfo) { + Unused << referrerInfo->GetComputedReferrerSpec(computedReferrerSpec); + } + + // Step 8 https://fetch.spec.whatwg.org/#main-fetch + // If request’s referrer is not "no-referrer", set request’s referrer to + // the result of invoking determine request’s referrer. + aRequest.SetReferrer(computedReferrerSpec); + + return NS_OK; +} + +class StoreOptimizedEncodingRunnable final : public Runnable { + nsMainThreadPtrHandle<nsICacheInfoChannel> mCache; + Vector<uint8_t> mBytes; + + public: + StoreOptimizedEncodingRunnable( + nsMainThreadPtrHandle<nsICacheInfoChannel>&& aCache, + Vector<uint8_t>&& aBytes) + : Runnable("StoreOptimizedEncodingRunnable"), + mCache(std::move(aCache)), + mBytes(std::move(aBytes)) {} + + NS_IMETHOD Run() override { + nsresult rv; + + nsCOMPtr<nsIAsyncOutputStream> stream; + rv = mCache->OpenAlternativeOutputStream(FetchUtil::WasmAltDataType, + int64_t(mBytes.length()), + getter_AddRefs(stream)); + if (NS_FAILED(rv)) { + return rv; + } + + auto closeStream = MakeScopeExit([&]() { stream->CloseWithStatus(rv); }); + + uint32_t written; + rv = stream->Write((char*)mBytes.begin(), mBytes.length(), &written); + if (NS_FAILED(rv)) { + return rv; + } + + MOZ_RELEASE_ASSERT(mBytes.length() == written); + return NS_OK; + }; +}; + +class WindowStreamOwner final : public nsIObserver, + public nsSupportsWeakReference { + // Read from any thread but only set/cleared on the main thread. The lifecycle + // of WindowStreamOwner prevents concurrent read/clear. + nsCOMPtr<nsIAsyncInputStream> mStream; + + nsCOMPtr<nsIGlobalObject> mGlobal; + + ~WindowStreamOwner() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->RemoveObserver(this, DOM_WINDOW_DESTROYED_TOPIC); + } + } + + public: + NS_DECL_ISUPPORTS + + WindowStreamOwner(nsIAsyncInputStream* aStream, nsIGlobalObject* aGlobal) + : mStream(aStream), mGlobal(aGlobal) { + MOZ_DIAGNOSTIC_ASSERT(mGlobal); + MOZ_ASSERT(NS_IsMainThread()); + } + + static already_AddRefed<WindowStreamOwner> Create( + nsIAsyncInputStream* aStream, nsIGlobalObject* aGlobal) { + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (NS_WARN_IF(!os)) { + return nullptr; + } + + RefPtr<WindowStreamOwner> self = new WindowStreamOwner(aStream, aGlobal); + + // Holds nsIWeakReference to self. + nsresult rv = os->AddObserver(self, DOM_WINDOW_DESTROYED_TOPIC, true); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + return self.forget(); + } + + // nsIObserver: + + NS_IMETHOD + Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) override { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(strcmp(aTopic, DOM_WINDOW_DESTROYED_TOPIC) == 0); + + if (!mStream) { + return NS_OK; + } + + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(mGlobal); + if (!SameCOMIdentity(aSubject, window)) { + return NS_OK; + } + + // mStream->Close() will call JSStreamConsumer::OnInputStreamReady which may + // then destory itself, dropping the last reference to 'this'. + RefPtr<WindowStreamOwner> keepAlive(this); + + mStream->Close(); + mStream = nullptr; + mGlobal = nullptr; + return NS_OK; + } +}; + +NS_IMPL_ISUPPORTS(WindowStreamOwner, nsIObserver, nsISupportsWeakReference) + +inline nsISupports* ToSupports(WindowStreamOwner* aObj) { + return static_cast<nsIObserver*>(aObj); +} + +class WorkerStreamOwner final { + public: + NS_INLINE_DECL_REFCOUNTING(WorkerStreamOwner) + + explicit WorkerStreamOwner(nsIAsyncInputStream* aStream, + nsCOMPtr<nsIEventTarget>&& target) + : mStream(aStream), mOwningEventTarget(std::move(target)) {} + + static already_AddRefed<WorkerStreamOwner> Create( + nsIAsyncInputStream* aStream, WorkerPrivate* aWorker, + nsCOMPtr<nsIEventTarget>&& target) { + RefPtr<WorkerStreamOwner> self = + new WorkerStreamOwner(aStream, std::move(target)); + + self->mWorkerRef = + StrongWorkerRef::Create(aWorker, "JSStreamConsumer", [self]() { + if (self->mStream) { + // If this Close() calls JSStreamConsumer::OnInputStreamReady and + // drops the last reference to the JSStreamConsumer, 'this' will not + // be destroyed since ~JSStreamConsumer() only enqueues a release + // proxy. + self->mStream->Close(); + self->mStream = nullptr; + } + }); + + if (!self->mWorkerRef) { + return nullptr; + } + + return self.forget(); + } + + static void ProxyRelease(already_AddRefed<WorkerStreamOwner> aDoomed) { + RefPtr<WorkerStreamOwner> doomed = aDoomed; + nsIEventTarget* target = doomed->mOwningEventTarget; + NS_ProxyRelease("WorkerStreamOwner", target, doomed.forget(), + /* aAlwaysProxy = */ true); + } + + private: + ~WorkerStreamOwner() = default; + + // Read from any thread but only set/cleared on the worker thread. The + // lifecycle of WorkerStreamOwner prevents concurrent read/clear. + nsCOMPtr<nsIAsyncInputStream> mStream; + RefPtr<StrongWorkerRef> mWorkerRef; + nsCOMPtr<nsIEventTarget> mOwningEventTarget; +}; + +class JSStreamConsumer final : public nsIInputStreamCallback, + public JS::OptimizedEncodingListener { + // A LengthPrefixType is stored at the start of the compressed optimized + // encoding, allowing the decompressed buffer to be allocated to exactly + // the right size. + using LengthPrefixType = uint32_t; + static const unsigned PrefixBytes = sizeof(LengthPrefixType); + + RefPtr<WindowStreamOwner> mWindowStreamOwner; + RefPtr<WorkerStreamOwner> mWorkerStreamOwner; + nsMainThreadPtrHandle<nsICacheInfoChannel> mCache; + const bool mOptimizedEncoding; + z_stream mZStream; + bool mZStreamInitialized; + Vector<uint8_t> mOptimizedEncodingBytes; + JS::StreamConsumer* mConsumer; + bool mConsumerAborted; + + JSStreamConsumer(already_AddRefed<WindowStreamOwner> aWindowStreamOwner, + nsIGlobalObject* aGlobal, JS::StreamConsumer* aConsumer, + nsMainThreadPtrHandle<nsICacheInfoChannel>&& aCache, + bool aOptimizedEncoding) + : mWindowStreamOwner(aWindowStreamOwner), + mCache(std::move(aCache)), + mOptimizedEncoding(aOptimizedEncoding), + mZStreamInitialized(false), + mConsumer(aConsumer), + mConsumerAborted(false) { + MOZ_DIAGNOSTIC_ASSERT(mWindowStreamOwner); + MOZ_DIAGNOSTIC_ASSERT(mConsumer); + } + + JSStreamConsumer(RefPtr<WorkerStreamOwner> aWorkerStreamOwner, + nsIGlobalObject* aGlobal, JS::StreamConsumer* aConsumer, + nsMainThreadPtrHandle<nsICacheInfoChannel>&& aCache, + bool aOptimizedEncoding) + : mWorkerStreamOwner(std::move(aWorkerStreamOwner)), + mCache(std::move(aCache)), + mOptimizedEncoding(aOptimizedEncoding), + mZStreamInitialized(false), + mConsumer(aConsumer), + mConsumerAborted(false) { + MOZ_DIAGNOSTIC_ASSERT(mWorkerStreamOwner); + MOZ_DIAGNOSTIC_ASSERT(mConsumer); + } + + ~JSStreamConsumer() { + if (mZStreamInitialized) { + inflateEnd(&mZStream); + } + + // Both WindowStreamOwner and WorkerStreamOwner need to be destroyed on + // their global's event target thread. + + if (mWindowStreamOwner) { + MOZ_DIAGNOSTIC_ASSERT(!mWorkerStreamOwner); + NS_ReleaseOnMainThread("JSStreamConsumer::mWindowStreamOwner", + mWindowStreamOwner.forget(), + /* aAlwaysProxy = */ true); + } else { + MOZ_DIAGNOSTIC_ASSERT(mWorkerStreamOwner); + WorkerStreamOwner::ProxyRelease(mWorkerStreamOwner.forget()); + } + + // Bug 1733674: these annotations currently do nothing, because they are + // member variables and the annotation mechanism only applies to locals. But + // the analysis could be extended so that these could replace the big-hammer + // ~JSStreamConsumer annotation and thus the analysis could check that + // nothing is added that might GC for a different reason. + JS_HAZ_VALUE_IS_GC_SAFE(mWindowStreamOwner); + JS_HAZ_VALUE_IS_GC_SAFE(mWorkerStreamOwner); + } + + static nsresult WriteSegment(nsIInputStream* aStream, void* aClosure, + const char* aFromSegment, uint32_t aToOffset, + uint32_t aCount, uint32_t* aWriteCount) { + JSStreamConsumer* self = reinterpret_cast<JSStreamConsumer*>(aClosure); + MOZ_DIAGNOSTIC_ASSERT(!self->mConsumerAborted); + + if (self->mOptimizedEncoding) { + if (!self->mZStreamInitialized) { + // mOptimizedEncodingBytes is used as temporary storage until we have + // the full prefix. + MOZ_ASSERT(self->mOptimizedEncodingBytes.length() < PrefixBytes); + uint32_t remain = PrefixBytes - self->mOptimizedEncodingBytes.length(); + uint32_t consume = std::min(remain, aCount); + + if (!self->mOptimizedEncodingBytes.append(aFromSegment, consume)) { + return NS_ERROR_UNEXPECTED; + } + + if (consume == remain) { + // Initialize zlib once all prefix bytes are loaded. + LengthPrefixType length; + memcpy(&length, self->mOptimizedEncodingBytes.begin(), PrefixBytes); + + if (!self->mOptimizedEncodingBytes.resizeUninitialized(length)) { + return NS_ERROR_UNEXPECTED; + } + + memset(&self->mZStream, 0, sizeof(self->mZStream)); + self->mZStream.avail_out = length; + self->mZStream.next_out = self->mOptimizedEncodingBytes.begin(); + + if (inflateInit(&self->mZStream) != Z_OK) { + return NS_ERROR_UNEXPECTED; + } + self->mZStreamInitialized = true; + } + + *aWriteCount = consume; + return NS_OK; + } + + // Zlib is initialized, overwrite the prefix with the inflated data. + + MOZ_DIAGNOSTIC_ASSERT(aCount > 0); + self->mZStream.avail_in = aCount; + self->mZStream.next_in = (uint8_t*)aFromSegment; + + int ret = inflate(&self->mZStream, Z_NO_FLUSH); + + MOZ_DIAGNOSTIC_ASSERT(ret == Z_OK || ret == Z_STREAM_END, + "corrupt optimized wasm cache file: data"); + MOZ_DIAGNOSTIC_ASSERT(self->mZStream.avail_in == 0, + "corrupt optimized wasm cache file: input"); + MOZ_DIAGNOSTIC_ASSERT_IF(ret == Z_STREAM_END, + self->mZStream.avail_out == 0); + // Gracefully handle corruption in release. + bool ok = + (ret == Z_OK || ret == Z_STREAM_END) && self->mZStream.avail_in == 0; + if (!ok) { + return NS_ERROR_UNEXPECTED; + } + } else { + // This callback can be called on any thread which is explicitly allowed + // by this particular JS API call. + if (!self->mConsumer->consumeChunk((const uint8_t*)aFromSegment, + aCount)) { + self->mConsumerAborted = true; + return NS_ERROR_UNEXPECTED; + } + } + + *aWriteCount = aCount; + return NS_OK; + } + + public: + NS_DECL_THREADSAFE_ISUPPORTS + + static bool Start(nsCOMPtr<nsIInputStream> aStream, nsIGlobalObject* aGlobal, + WorkerPrivate* aMaybeWorker, JS::StreamConsumer* aConsumer, + nsMainThreadPtrHandle<nsICacheInfoChannel>&& aCache, + bool aOptimizedEncoding) { + nsCOMPtr<nsIAsyncInputStream> asyncStream; + nsresult rv = NS_MakeAsyncNonBlockingInputStream( + aStream.forget(), getter_AddRefs(asyncStream)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + RefPtr<JSStreamConsumer> consumer; + if (aMaybeWorker) { + RefPtr<WorkerStreamOwner> owner = WorkerStreamOwner::Create( + asyncStream, aMaybeWorker, + aGlobal->EventTargetFor(TaskCategory::Other)); + if (!owner) { + return false; + } + + consumer = new JSStreamConsumer(std::move(owner), aGlobal, aConsumer, + std::move(aCache), aOptimizedEncoding); + } else { + RefPtr<WindowStreamOwner> owner = + WindowStreamOwner::Create(asyncStream, aGlobal); + if (!owner) { + return false; + } + + consumer = new JSStreamConsumer(owner.forget(), aGlobal, aConsumer, + std::move(aCache), aOptimizedEncoding); + } + + // This AsyncWait() creates a ref-cycle between asyncStream and consumer: + // + // asyncStream -> consumer -> (Window|Worker)StreamOwner -> asyncStream + // + // The cycle is broken when the stream completes or errors out and + // asyncStream drops its reference to consumer. + return NS_SUCCEEDED(asyncStream->AsyncWait(consumer, 0, 0, nullptr)); + } + + // nsIInputStreamCallback: + + NS_IMETHOD + OnInputStreamReady(nsIAsyncInputStream* aStream) override { + // Can be called on any stream. The JS API calls made below explicitly + // support being called from any thread. + MOZ_DIAGNOSTIC_ASSERT(!mConsumerAborted); + + nsresult rv; + + uint64_t available = 0; + rv = aStream->Available(&available); + if (NS_SUCCEEDED(rv) && available == 0) { + rv = NS_BASE_STREAM_CLOSED; + } + + if (rv == NS_BASE_STREAM_CLOSED) { + if (mOptimizedEncoding) { + // Gracefully handle corruption of compressed data stream in release. + // From on investigations in bug 1738987, the incomplete data cases + // mostly happen during shutdown. Some corruptions in the cache entry + // can still happen and will be handled in the WriteSegment above. + bool ok = mZStreamInitialized && mZStream.avail_out == 0; + if (!ok) { + mConsumer->streamError(size_t(NS_ERROR_UNEXPECTED)); + return NS_OK; + } + + mConsumer->consumeOptimizedEncoding(mOptimizedEncodingBytes.begin(), + mOptimizedEncodingBytes.length()); + } else { + // If there is cache entry associated with this stream, then listen for + // an optimized encoding so we can store it in the alt data. By JS API + // contract, the compilation process will hold a refcount to 'this' + // until it's done, optionally calling storeOptimizedEncoding(). + mConsumer->streamEnd(mCache ? this : nullptr); + } + return NS_OK; + } + + if (NS_FAILED(rv)) { + mConsumer->streamError(size_t(rv)); + return NS_OK; + } + + // Check mConsumerAborted before NS_FAILED to avoid calling streamError() + // if consumeChunk() returned false per JS API contract. + uint32_t written = 0; + rv = aStream->ReadSegments(WriteSegment, this, available, &written); + if (mConsumerAborted) { + return NS_OK; + } + if (NS_WARN_IF(NS_FAILED(rv))) { + mConsumer->streamError(size_t(rv)); + return NS_OK; + } + + rv = aStream->AsyncWait(this, 0, 0, nullptr); + if (NS_WARN_IF(NS_FAILED(rv))) { + mConsumer->streamError(size_t(rv)); + return NS_OK; + } + + return NS_OK; + } + + // JS::OptimizedEncodingListener + + void storeOptimizedEncoding(const uint8_t* aSrcBytes, + size_t aSrcLength) override { + MOZ_ASSERT(mCache, "we only listen if there's a cache entry"); + + z_stream zstream; + memset(&zstream, 0, sizeof(zstream)); + zstream.avail_in = aSrcLength; + zstream.next_in = (uint8_t*)aSrcBytes; + + // The wins from increasing compression levels are tiny, while the time + // to compress increases drastically. For example, for a 148mb alt-data + // produced by a 40mb .wasm file, the level 2 takes 2.5s to get a 3.7x size + // reduction while level 9 takes 22.5s to get a 4x size reduction. Read-time + // wins from smaller compressed cache files are not found to be + // significant, thus the fastest compression level is used. (On test + // workloads, level 2 actually was faster *and* smaller than level 1.) + const int COMPRESSION = 2; + if (deflateInit(&zstream, COMPRESSION) != Z_OK) { + return; + } + auto autoDestroy = MakeScopeExit([&]() { deflateEnd(&zstream); }); + + Vector<uint8_t> dstBytes; + if (!dstBytes.resizeUninitialized(PrefixBytes + + deflateBound(&zstream, aSrcLength))) { + return; + } + + MOZ_RELEASE_ASSERT(LengthPrefixType(aSrcLength) == aSrcLength); + LengthPrefixType srcLength = aSrcLength; + memcpy(dstBytes.begin(), &srcLength, PrefixBytes); + + uint8_t* compressBegin = dstBytes.begin() + PrefixBytes; + zstream.next_out = compressBegin; + zstream.avail_out = dstBytes.length() - PrefixBytes; + + int ret = deflate(&zstream, Z_FINISH); + if (ret == Z_MEM_ERROR) { + return; + } + MOZ_RELEASE_ASSERT(ret == Z_STREAM_END); + + dstBytes.shrinkTo(zstream.next_out - dstBytes.begin()); + + NS_DispatchToMainThread(new StoreOptimizedEncodingRunnable( + std::move(mCache), std::move(dstBytes))); + } +}; + +NS_IMPL_ISUPPORTS(JSStreamConsumer, nsIInputStreamCallback) + +// static +const nsCString FetchUtil::WasmAltDataType; + +// static +void FetchUtil::InitWasmAltDataType() { + nsCString& type = const_cast<nsCString&>(WasmAltDataType); + MOZ_ASSERT(type.IsEmpty()); + + RunOnShutdown([]() { + // Avoid nsStringBuffer leak tests failures. + const_cast<nsCString&>(WasmAltDataType).Truncate(); + }); + + type.Append(nsLiteralCString("wasm-")); + + JS::BuildIdCharVector buildId; + if (!JS::GetOptimizedEncodingBuildId(&buildId)) { + MOZ_CRASH("build id oom"); + } + + type.Append(buildId.begin(), buildId.length()); +} + +static bool ThrowException(JSContext* aCx, unsigned errorNumber) { + JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr, errorNumber); + return false; +} + +// static +bool FetchUtil::StreamResponseToJS(JSContext* aCx, JS::Handle<JSObject*> aObj, + JS::MimeType aMimeType, + JS::StreamConsumer* aConsumer, + WorkerPrivate* aMaybeWorker) { + MOZ_ASSERT(!WasmAltDataType.IsEmpty()); + MOZ_ASSERT(!aMaybeWorker == NS_IsMainThread()); + + RefPtr<Response> response; + nsresult rv = UNWRAP_OBJECT(Response, aObj, response); + if (NS_FAILED(rv)) { + return ThrowException(aCx, JSMSG_WASM_BAD_RESPONSE_VALUE); + } + + const char* requiredMimeType = nullptr; + switch (aMimeType) { + case JS::MimeType::Wasm: + requiredMimeType = WASM_CONTENT_TYPE; + break; + } + + nsAutoCString mimeType; + nsAutoCString mixedCaseMimeType; // unused + response->GetMimeType(mimeType, mixedCaseMimeType); + + if (!mimeType.EqualsASCII(requiredMimeType)) { + JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr, + JSMSG_WASM_BAD_RESPONSE_MIME_TYPE, mimeType.get(), + requiredMimeType); + return false; + } + + if (response->Type() != ResponseType::Basic && + response->Type() != ResponseType::Cors && + response->Type() != ResponseType::Default) { + return ThrowException(aCx, JSMSG_WASM_BAD_RESPONSE_CORS_SAME_ORIGIN); + } + + if (!response->Ok()) { + return ThrowException(aCx, JSMSG_WASM_BAD_RESPONSE_STATUS); + } + + if (response->BodyUsed()) { + return ThrowException(aCx, JSMSG_WASM_RESPONSE_ALREADY_CONSUMED); + } + + switch (aMimeType) { + case JS::MimeType::Wasm: + nsAutoString url; + response->GetUrl(url); + + IgnoredErrorResult result; + nsCString sourceMapUrl; + response->GetInternalHeaders()->Get("SourceMap"_ns, sourceMapUrl, result); + if (NS_WARN_IF(result.Failed())) { + return ThrowException(aCx, JSMSG_WASM_ERROR_CONSUMING_RESPONSE); + } + NS_ConvertUTF16toUTF8 urlUTF8(url); + aConsumer->noteResponseURLs( + urlUTF8.get(), sourceMapUrl.IsVoid() ? nullptr : sourceMapUrl.get()); + break; + } + + SafeRefPtr<InternalResponse> ir = response->GetInternalResponse(); + if (NS_WARN_IF(!ir)) { + return ThrowException(aCx, JSMSG_OUT_OF_MEMORY); + } + + nsCOMPtr<nsIInputStream> stream; + + nsMainThreadPtrHandle<nsICacheInfoChannel> cache; + bool optimizedEncoding = false; + if (ir->HasCacheInfoChannel()) { + cache = ir->TakeCacheInfoChannel(); + + nsAutoCString altDataType; + if (NS_SUCCEEDED(cache->GetAlternativeDataType(altDataType)) && + WasmAltDataType.Equals(altDataType)) { + optimizedEncoding = true; + rv = cache->GetAlternativeDataInputStream(getter_AddRefs(stream)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return ThrowException(aCx, JSMSG_OUT_OF_MEMORY); + } + if (ir->HasBeenCloned()) { + // If `Response` is cloned, clone alternative data stream instance. + // The cache entry does not clone automatically, and multiple + // JSStreamConsumer instances will collide during read if not cloned. + nsCOMPtr<nsICloneableInputStream> original = do_QueryInterface(stream); + if (NS_WARN_IF(!original)) { + return ThrowException(aCx, JSMSG_OUT_OF_MEMORY); + } + rv = original->Clone(getter_AddRefs(stream)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return ThrowException(aCx, JSMSG_OUT_OF_MEMORY); + } + } + } + } + + if (!optimizedEncoding) { + ir->GetUnfilteredBody(getter_AddRefs(stream)); + if (!stream) { + aConsumer->streamEnd(); + return true; + } + } + + MOZ_ASSERT(stream); + + IgnoredErrorResult error; + response->SetBodyUsed(aCx, error); + if (NS_WARN_IF(error.Failed())) { + return ThrowException(aCx, JSMSG_WASM_ERROR_CONSUMING_RESPONSE); + } + + nsIGlobalObject* global = xpc::NativeGlobal(js::UncheckedUnwrap(aObj)); + + if (!JSStreamConsumer::Start(stream, global, aMaybeWorker, aConsumer, + std::move(cache), optimizedEncoding)) { + return ThrowException(aCx, JSMSG_OUT_OF_MEMORY); + } + + return true; +} + +// static +void FetchUtil::ReportJSStreamError(JSContext* aCx, size_t aErrorCode) { + // For now, convert *all* errors into AbortError. + + RefPtr<DOMException> e = DOMException::Create(NS_ERROR_DOM_ABORT_ERR); + + JS::Rooted<JS::Value> value(aCx); + if (!GetOrCreateDOMReflector(aCx, e, &value)) { + return; + } + + JS_SetPendingException(aCx, value); +} + +} // namespace mozilla::dom diff --git a/dom/fetch/FetchUtil.h b/dom/fetch/FetchUtil.h new file mode 100644 index 0000000000..c0c9157a43 --- /dev/null +++ b/dom/fetch/FetchUtil.h @@ -0,0 +1,82 @@ +/* -*- 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_FetchUtil_h +#define mozilla_dom_FetchUtil_h + +#include "nsString.h" +#include "nsError.h" + +#include "mozilla/dom/File.h" +#include "mozilla/dom/FormData.h" + +#define WASM_CONTENT_TYPE "application/wasm" + +class nsIPrincipal; +class nsIHttpChannel; + +namespace mozilla::dom { + +class Document; +class InternalRequest; +class WorkerPrivate; + +class FetchUtil final { + private: + FetchUtil() = delete; + + public: + /** + * Sets outMethod to a valid HTTP request method string based on an input + * method. Implements checks and normalization as specified by the Fetch + * specification. Returns NS_ERROR_DOM_SECURITY_ERR if the method is invalid. + * Otherwise returns NS_OK and the normalized method via outMethod. + */ + static nsresult GetValidRequestMethod(const nsACString& aMethod, + nsCString& outMethod); + /** + * Extracts an HTTP header from a substring range. + */ + static bool ExtractHeader(nsACString::const_iterator& aStart, + nsACString::const_iterator& aEnd, + nsCString& aHeaderName, nsCString& aHeaderValue, + bool* aWasEmptyHeader); + + static nsresult SetRequestReferrer(nsIPrincipal* aPrincipal, Document* aDoc, + nsIHttpChannel* aChannel, + InternalRequest& aRequest); + + /** + * The WebAssembly alt data type includes build-id, cpu-id and other relevant + * state that is necessary to ensure the validity of caching machine code and + * metadata in alt data. InitWasmAltDataType() must be called during startup + * before the first fetch(), ensuring that !WasmAltDataType.IsEmpty(). + */ + static const nsCString WasmAltDataType; + static void InitWasmAltDataType(); + + /** + * Check that the given object is a Response and, if so, stream to the given + * JS consumer. On any failure, this function will report an error on the + * given JSContext before returning false. If executing in a worker, the + * WorkerPrivate must be given. + */ + static bool StreamResponseToJS(JSContext* aCx, JS::Handle<JSObject*> aObj, + JS::MimeType aMimeType, + JS::StreamConsumer* aConsumer, + WorkerPrivate* aMaybeWorker); + + /** + * Called by JS to report (i.e., throw) an error that was passed to the + * JS::StreamConsumer::streamError() method on a random stream thread. + * This method is passed by function pointer to the JS engine hence the + * untyped 'size_t' instead of Gecko 'nsresult'. + */ + static void ReportJSStreamError(JSContext* aCx, size_t aErrorCode); +}; + +} // namespace mozilla::dom +#endif diff --git a/dom/fetch/Headers.cpp b/dom/fetch/Headers.cpp new file mode 100644 index 0000000000..b8edbe1ed7 --- /dev/null +++ b/dom/fetch/Headers.cpp @@ -0,0 +1,87 @@ +/* -*- 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 "mozilla/dom/Headers.h" + +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/Preferences.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTING_ADDREF(Headers) +NS_IMPL_CYCLE_COLLECTING_RELEASE(Headers) +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Headers, mOwner) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Headers) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +// static +already_AddRefed<Headers> Headers::Constructor( + const GlobalObject& aGlobal, + const Optional<ByteStringSequenceSequenceOrByteStringByteStringRecord>& + aInit, + ErrorResult& aRv) { + RefPtr<InternalHeaders> ih = new InternalHeaders(); + RefPtr<Headers> headers = new Headers(aGlobal.GetAsSupports(), ih); + + if (!aInit.WasPassed()) { + return headers.forget(); + } + + if (aInit.Value().IsByteStringSequenceSequence()) { + ih->Fill(aInit.Value().GetAsByteStringSequenceSequence(), aRv); + } else if (aInit.Value().IsByteStringByteStringRecord()) { + ih->Fill(aInit.Value().GetAsByteStringByteStringRecord(), aRv); + } + + if (aRv.Failed()) { + return nullptr; + } + + return headers.forget(); +} + +// static +already_AddRefed<Headers> Headers::Constructor( + const GlobalObject& aGlobal, + const OwningByteStringSequenceSequenceOrByteStringByteStringRecord& aInit, + ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + return Create(global, aInit, aRv); +} + +/* static */ +already_AddRefed<Headers> Headers::Create( + nsIGlobalObject* aGlobal, + const OwningByteStringSequenceSequenceOrByteStringByteStringRecord& aInit, + ErrorResult& aRv) { + RefPtr<InternalHeaders> ih = new InternalHeaders(); + RefPtr<Headers> headers = new Headers(aGlobal, ih); + + if (aInit.IsByteStringSequenceSequence()) { + ih->Fill(aInit.GetAsByteStringSequenceSequence(), aRv); + } else if (aInit.IsByteStringByteStringRecord()) { + ih->Fill(aInit.GetAsByteStringByteStringRecord(), aRv); + } + + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return headers.forget(); +} + +JSObject* Headers::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return mozilla::dom::Headers_Binding::Wrap(aCx, this, aGivenProto); +} + +Headers::~Headers() = default; + +} // namespace mozilla::dom diff --git a/dom/fetch/Headers.h b/dom/fetch/Headers.h new file mode 100644 index 0000000000..ea3b24405d --- /dev/null +++ b/dom/fetch/Headers.h @@ -0,0 +1,132 @@ +/* -*- 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_Headers_h +#define mozilla_dom_Headers_h + +#include "mozilla/dom/HeadersBinding.h" + +#include "nsClassHashtable.h" +#include "nsWrapperCache.h" + +#include "InternalHeaders.h" + +namespace mozilla { + +class ErrorResult; + +namespace dom { + +template <typename K, typename V> +class Record; +class ByteStringSequenceSequenceOrByteStringByteStringRecord; +class OwningByteStringSequenceSequenceOrByteStringByteStringRecord; + +/** + * This Headers class is only used to represent the content facing Headers + * object. It is actually backed by an InternalHeaders implementation. Gecko + * code should NEVER use this, except in the Request and Response + * implementations, where they must always be created from the backing + * InternalHeaders object. + */ +class Headers final : public nsISupports, public nsWrapperCache { + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Headers) + + friend class Request; + friend class Response; + + private: + nsCOMPtr<nsISupports> mOwner; + RefPtr<InternalHeaders> mInternalHeaders; + + public: + explicit Headers(nsISupports* aOwner, InternalHeaders* aInternalHeaders) + : mOwner(aOwner), mInternalHeaders(aInternalHeaders) {} + + explicit Headers(const Headers& aOther) = delete; + + static bool PrefEnabled(JSContext* cx, JSObject* obj); + + static already_AddRefed<Headers> Constructor( + const GlobalObject& aGlobal, + const Optional<ByteStringSequenceSequenceOrByteStringByteStringRecord>& + aInit, + ErrorResult& aRv); + + static already_AddRefed<Headers> Constructor( + const GlobalObject& aGlobal, + const OwningByteStringSequenceSequenceOrByteStringByteStringRecord& aInit, + ErrorResult& aRv); + + static already_AddRefed<Headers> Create( + nsIGlobalObject* aGlobalObject, + const OwningByteStringSequenceSequenceOrByteStringByteStringRecord& aInit, + ErrorResult& aRv); + + void Append(const nsACString& aName, const nsACString& aValue, + ErrorResult& aRv) { + mInternalHeaders->Append(aName, aValue, aRv); + } + + void Delete(const nsACString& aName, ErrorResult& aRv) { + mInternalHeaders->Delete(aName, aRv); + } + + void Get(const nsACString& aName, nsACString& aValue, + ErrorResult& aRv) const { + mInternalHeaders->Get(aName, aValue, aRv); + } + + void GetSetCookie(nsTArray<nsCString>& aValues) const { + mInternalHeaders->GetSetCookie(aValues); + } + + void GetFirst(const nsACString& aName, nsACString& aValue, + ErrorResult& aRv) const { + mInternalHeaders->GetFirst(aName, aValue, aRv); + } + + bool Has(const nsACString& aName, ErrorResult& aRv) const { + return mInternalHeaders->Has(aName, aRv); + } + + void Set(const nsACString& aName, const nsACString& aValue, + ErrorResult& aRv) { + mInternalHeaders->Set(aName, aValue, aRv); + } + + uint32_t GetIterableLength() const { + return mInternalHeaders->GetIterableLength(); + } + const nsString GetKeyAtIndex(unsigned aIndex) const { + return mInternalHeaders->GetKeyAtIndex(aIndex); + } + const nsString GetValueAtIndex(unsigned aIndex) const { + return mInternalHeaders->GetValueAtIndex(aIndex); + } + + // ChromeOnly + HeadersGuardEnum Guard() const { return mInternalHeaders->Guard(); } + + void SetGuard(HeadersGuardEnum aGuard, ErrorResult& aRv) { + mInternalHeaders->SetGuard(aGuard, aRv); + } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + nsISupports* GetParentObject() const { return mOwner; } + + private: + virtual ~Headers(); + + InternalHeaders* GetInternalHeaders() const { return mInternalHeaders; } +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_Headers_h diff --git a/dom/fetch/InternalHeaders.cpp b/dom/fetch/InternalHeaders.cpp new file mode 100644 index 0000000000..af24432b62 --- /dev/null +++ b/dom/fetch/InternalHeaders.cpp @@ -0,0 +1,654 @@ +/* -*- 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 "mozilla/dom/InternalHeaders.h" + +#include "FetchUtil.h" +#include "mozilla/dom/FetchTypes.h" +#include "mozilla/ErrorResult.h" + +#include "nsCharSeparatedTokenizer.h" +#include "nsContentUtils.h" +#include "nsIHttpChannel.h" +#include "nsIHttpHeaderVisitor.h" +#include "nsNetUtil.h" +#include "nsReadableUtils.h" + +namespace mozilla::dom { + +InternalHeaders::InternalHeaders(nsTArray<Entry>&& aHeaders, + HeadersGuardEnum aGuard) + : mGuard(aGuard), mList(std::move(aHeaders)), mListDirty(true) {} + +InternalHeaders::InternalHeaders( + const nsTArray<HeadersEntry>& aHeadersEntryList, HeadersGuardEnum aGuard) + : mGuard(aGuard), mListDirty(true) { + for (const HeadersEntry& headersEntry : aHeadersEntryList) { + mList.AppendElement(Entry(headersEntry.name(), headersEntry.value())); + } +} + +void InternalHeaders::ToIPC(nsTArray<HeadersEntry>& aIPCHeaders, + HeadersGuardEnum& aGuard) { + aGuard = mGuard; + + aIPCHeaders.Clear(); + for (Entry& entry : mList) { + aIPCHeaders.AppendElement(HeadersEntry(entry.mName, entry.mValue)); + } +} + +bool InternalHeaders::IsValidHeaderValue(const nsCString& aLowerName, + const nsCString& aNormalizedValue, + ErrorResult& aRv) { + // Steps 2 to 6 for ::Set() and ::Append() in the spec. + + // Step 2 + if (IsInvalidName(aLowerName, aRv) || IsInvalidValue(aNormalizedValue, aRv)) { + return false; + } + + // Step 3 + if (IsImmutable(aRv)) { + return false; + } + + // Step 4 + if (mGuard == HeadersGuardEnum::Request) { + if (IsForbiddenRequestHeader(aLowerName, aNormalizedValue)) { + return false; + } + } + // Step 5 + if (mGuard == HeadersGuardEnum::Request_no_cors) { + nsAutoCString tempValue; + Get(aLowerName, tempValue, aRv); + + if (tempValue.IsVoid()) { + tempValue = aNormalizedValue; + } else { + tempValue.Append(", "); + tempValue.Append(aNormalizedValue); + } + + if (!nsContentUtils::IsCORSSafelistedRequestHeader(aLowerName, tempValue)) { + return false; + } + } + + // Step 6 + else if (IsForbiddenResponseHeader(aLowerName)) { + return false; + } + + return true; +} + +void InternalHeaders::Append(const nsACString& aName, const nsACString& aValue, + ErrorResult& aRv) { + // Step 1 + nsAutoCString trimValue; + NS_TrimHTTPWhitespace(aValue, trimValue); + + // Steps 2 to 6 + nsAutoCString lowerName; + ToLowerCase(aName, lowerName); + if (!IsValidHeaderValue(lowerName, trimValue, aRv)) { + return; + } + + // Step 7 + nsAutoCString name(aName); + ReuseExistingNameIfExists(name); + SetListDirty(); + mList.AppendElement(Entry(name, trimValue)); + + // Step 8 + if (mGuard == HeadersGuardEnum::Request_no_cors) { + RemovePrivilegedNoCorsRequestHeaders(); + } +} + +void InternalHeaders::RemovePrivilegedNoCorsRequestHeaders() { + bool dirty = false; + + // remove in reverse order to minimize copying + for (int32_t i = mList.Length() - 1; i >= 0; --i) { + if (IsPrivilegedNoCorsRequestHeaderName(mList[i].mName)) { + mList.RemoveElementAt(i); + dirty = true; + } + } + + if (dirty) { + SetListDirty(); + } +} + +bool InternalHeaders::DeleteInternal(const nsCString& aLowerName, + ErrorResult& aRv) { + bool dirty = false; + + // remove in reverse order to minimize copying + for (int32_t i = mList.Length() - 1; i >= 0; --i) { + if (mList[i].mName.EqualsIgnoreCase(aLowerName.get())) { + mList.RemoveElementAt(i); + dirty = true; + } + } + + if (dirty) { + SetListDirty(); + } + + return dirty; +} + +void InternalHeaders::Delete(const nsACString& aName, ErrorResult& aRv) { + nsAutoCString lowerName; + ToLowerCase(aName, lowerName); + + // Step 1 + if (IsInvalidName(lowerName, aRv)) { + return; + } + + // Step 2 + if (IsImmutable(aRv)) { + return; + } + + // Step 3 + nsAutoCString value; + GetInternal(lowerName, value, aRv); + if (IsForbiddenRequestHeader(lowerName, value)) { + return; + } + + // Step 4 + if (mGuard == HeadersGuardEnum::Request_no_cors && + !IsNoCorsSafelistedRequestHeaderName(lowerName) && + !IsPrivilegedNoCorsRequestHeaderName(lowerName)) { + return; + } + + // Step 5 + if (IsForbiddenResponseHeader(lowerName)) { + return; + } + + // Steps 6 and 7 + if (!DeleteInternal(lowerName, aRv)) { + return; + } + + // Step 8 + if (mGuard == HeadersGuardEnum::Request_no_cors) { + RemovePrivilegedNoCorsRequestHeaders(); + } +} + +void InternalHeaders::Get(const nsACString& aName, nsACString& aValue, + ErrorResult& aRv) const { + nsAutoCString lowerName; + ToLowerCase(aName, lowerName); + + if (IsInvalidName(lowerName, aRv)) { + return; + } + + GetInternal(lowerName, aValue, aRv); +} + +void InternalHeaders::GetInternal(const nsCString& aLowerName, + nsACString& aValue, ErrorResult& aRv) const { + const char* delimiter = ", "; + bool firstValueFound = false; + + for (uint32_t i = 0; i < mList.Length(); ++i) { + if (mList[i].mName.EqualsIgnoreCase(aLowerName.get())) { + if (firstValueFound) { + aValue += delimiter; + } + aValue += mList[i].mValue; + firstValueFound = true; + } + } + + // No value found, so return null to content + if (!firstValueFound) { + aValue.SetIsVoid(true); + } +} + +void InternalHeaders::GetSetCookie(nsTArray<nsCString>& aValues) const { + for (uint32_t i = 0; i < mList.Length(); ++i) { + if (mList[i].mName.EqualsIgnoreCase("Set-Cookie")) { + aValues.AppendElement(mList[i].mValue); + } + } +} + +void InternalHeaders::GetFirst(const nsACString& aName, nsACString& aValue, + ErrorResult& aRv) const { + nsAutoCString lowerName; + ToLowerCase(aName, lowerName); + + if (IsInvalidName(lowerName, aRv)) { + return; + } + + for (uint32_t i = 0; i < mList.Length(); ++i) { + if (mList[i].mName.EqualsIgnoreCase(lowerName.get())) { + aValue = mList[i].mValue; + return; + } + } + + // No value found, so return null to content + aValue.SetIsVoid(true); +} + +bool InternalHeaders::Has(const nsACString& aName, ErrorResult& aRv) const { + nsAutoCString lowerName; + ToLowerCase(aName, lowerName); + + if (IsInvalidName(lowerName, aRv)) { + return false; + } + + for (uint32_t i = 0; i < mList.Length(); ++i) { + if (mList[i].mName.EqualsIgnoreCase(lowerName.get())) { + return true; + } + } + return false; +} + +void InternalHeaders::Set(const nsACString& aName, const nsACString& aValue, + ErrorResult& aRv) { + // Step 1 + nsAutoCString trimValue; + NS_TrimHTTPWhitespace(aValue, trimValue); + + // Steps 2 to 6 + nsAutoCString lowerName; + ToLowerCase(aName, lowerName); + if (!IsValidHeaderValue(lowerName, trimValue, aRv)) { + return; + } + + // Step 7 + SetListDirty(); + + int32_t firstIndex = INT32_MAX; + + // remove in reverse order to minimize copying + for (int32_t i = mList.Length() - 1; i >= 0; --i) { + if (mList[i].mName.EqualsIgnoreCase(lowerName.get())) { + firstIndex = std::min(firstIndex, i); + mList.RemoveElementAt(i); + } + } + + if (firstIndex < INT32_MAX) { + Entry* entry = mList.InsertElementAt(firstIndex); + entry->mName = aName; + entry->mValue = trimValue; + } else { + mList.AppendElement(Entry(aName, trimValue)); + } + + // Step 8 + if (mGuard == HeadersGuardEnum::Request_no_cors) { + RemovePrivilegedNoCorsRequestHeaders(); + } +} + +void InternalHeaders::Clear() { + SetListDirty(); + mList.Clear(); +} + +void InternalHeaders::SetGuard(HeadersGuardEnum aGuard, ErrorResult& aRv) { + // The guard is only checked during ::Set() and ::Append() in the spec. It + // does not require revalidating headers already set. + mGuard = aGuard; +} + +InternalHeaders::~InternalHeaders() = default; + +// static +bool InternalHeaders::IsNoCorsSafelistedRequestHeaderName( + const nsCString& aName) { + return aName.EqualsIgnoreCase("accept") || + aName.EqualsIgnoreCase("accept-language") || + aName.EqualsIgnoreCase("content-language") || + aName.EqualsIgnoreCase("content-type"); +} + +// static +bool InternalHeaders::IsPrivilegedNoCorsRequestHeaderName( + const nsCString& aName) { + return aName.EqualsIgnoreCase("range"); +} + +// static +bool InternalHeaders::IsSimpleHeader(const nsCString& aName, + const nsACString& aValue) { + if (aValue.Length() > 128) { + return false; + } + // Note, we must allow a null content-type value here to support + // get("content-type"), but the IsInvalidValue() check will prevent null + // from being set or appended. + return (aName.EqualsIgnoreCase("accept") && + nsContentUtils::IsAllowedNonCorsAccept(aValue)) || + (aName.EqualsIgnoreCase("accept-language") && + nsContentUtils::IsAllowedNonCorsLanguage(aValue)) || + (aName.EqualsIgnoreCase("content-language") && + nsContentUtils::IsAllowedNonCorsLanguage(aValue)) || + (aName.EqualsIgnoreCase("content-type") && + nsContentUtils::IsAllowedNonCorsContentType(aValue)); +} + +// static +bool InternalHeaders::IsRevalidationHeader(const nsCString& aName) { + return aName.EqualsIgnoreCase("if-modified-since") || + aName.EqualsIgnoreCase("if-none-match") || + aName.EqualsIgnoreCase("if-unmodified-since") || + aName.EqualsIgnoreCase("if-match") || + aName.EqualsIgnoreCase("if-range"); +} + +// static +bool InternalHeaders::IsInvalidName(const nsACString& aName, ErrorResult& aRv) { + if (!NS_IsValidHTTPToken(aName)) { + aRv.ThrowTypeError<MSG_INVALID_HEADER_NAME>(aName); + return true; + } + + return false; +} + +// static +bool InternalHeaders::IsInvalidValue(const nsACString& aValue, + ErrorResult& aRv) { + if (!NS_IsReasonableHTTPHeaderValue(aValue)) { + aRv.ThrowTypeError<MSG_INVALID_HEADER_VALUE>(aValue); + return true; + } + return false; +} + +bool InternalHeaders::IsImmutable(ErrorResult& aRv) const { + if (mGuard == HeadersGuardEnum::Immutable) { + aRv.ThrowTypeError("Headers are immutable and cannot be modified."); + return true; + } + return false; +} + +bool InternalHeaders::IsForbiddenRequestHeader(const nsCString& aName, + const nsACString& aValue) const { + return mGuard == HeadersGuardEnum::Request && + nsContentUtils::IsForbiddenRequestHeader(aName, aValue); +} + +bool InternalHeaders::IsForbiddenRequestNoCorsHeader( + const nsCString& aName) const { + return mGuard == HeadersGuardEnum::Request_no_cors && + !IsSimpleHeader(aName, ""_ns); +} + +bool InternalHeaders::IsForbiddenRequestNoCorsHeader( + const nsCString& aName, const nsACString& aValue) const { + return mGuard == HeadersGuardEnum::Request_no_cors && + !IsSimpleHeader(aName, aValue); +} + +bool InternalHeaders::IsForbiddenResponseHeader(const nsCString& aName) const { + return mGuard == HeadersGuardEnum::Response && + nsContentUtils::IsForbiddenResponseHeader(aName); +} + +void InternalHeaders::Fill(const InternalHeaders& aInit, ErrorResult& aRv) { + const nsTArray<Entry>& list = aInit.mList; + for (uint32_t i = 0; i < list.Length() && !aRv.Failed(); ++i) { + const Entry& entry = list[i]; + Append(entry.mName, entry.mValue, aRv); + } +} + +void InternalHeaders::Fill(const Sequence<Sequence<nsCString>>& aInit, + ErrorResult& aRv) { + for (uint32_t i = 0; i < aInit.Length() && !aRv.Failed(); ++i) { + const Sequence<nsCString>& tuple = aInit[i]; + if (tuple.Length() != 2) { + aRv.ThrowTypeError( + "Headers require name/value tuples when being initialized by a " + "sequence."); + return; + } + Append(tuple[0], tuple[1], aRv); + } +} + +void InternalHeaders::Fill(const Record<nsCString, nsCString>& aInit, + ErrorResult& aRv) { + for (auto& entry : aInit.Entries()) { + Append(entry.mKey, entry.mValue, aRv); + if (aRv.Failed()) { + return; + } + } +} + +namespace { + +class FillHeaders final : public nsIHttpHeaderVisitor { + RefPtr<InternalHeaders> mInternalHeaders; + + ~FillHeaders() = default; + + public: + NS_DECL_ISUPPORTS + + explicit FillHeaders(InternalHeaders* aInternalHeaders) + : mInternalHeaders(aInternalHeaders) { + MOZ_DIAGNOSTIC_ASSERT(mInternalHeaders); + } + + NS_IMETHOD + VisitHeader(const nsACString& aHeader, const nsACString& aValue) override { + mInternalHeaders->Append(aHeader, aValue, IgnoreErrors()); + return NS_OK; + } +}; + +NS_IMPL_ISUPPORTS(FillHeaders, nsIHttpHeaderVisitor) + +} // namespace + +void InternalHeaders::FillResponseHeaders(nsIRequest* aRequest) { + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest); + if (!httpChannel) { + return; + } + + RefPtr<FillHeaders> visitor = new FillHeaders(this); + nsresult rv = httpChannel->VisitResponseHeaders(visitor); + if (NS_FAILED(rv)) { + NS_WARNING("failed to fill headers"); + } +} + +bool InternalHeaders::HasOnlySimpleHeaders() const { + for (uint32_t i = 0; i < mList.Length(); ++i) { + if (!IsSimpleHeader(mList[i].mName, mList[i].mValue)) { + return false; + } + } + + return true; +} + +bool InternalHeaders::HasRevalidationHeaders() const { + for (uint32_t i = 0; i < mList.Length(); ++i) { + if (IsRevalidationHeader(mList[i].mName)) { + return true; + } + } + + return false; +} + +// static +already_AddRefed<InternalHeaders> InternalHeaders::BasicHeaders( + InternalHeaders* aHeaders) { + RefPtr<InternalHeaders> basic = new InternalHeaders(*aHeaders); + ErrorResult result; + // The Set-Cookie headers cannot be invalid mutable headers, so the Delete + // must succeed. + basic->Delete("Set-Cookie"_ns, result); + MOZ_ASSERT(!result.Failed()); + basic->Delete("Set-Cookie2"_ns, result); + MOZ_ASSERT(!result.Failed()); + return basic.forget(); +} + +// static +already_AddRefed<InternalHeaders> InternalHeaders::CORSHeaders( + InternalHeaders* aHeaders, RequestCredentials aCredentialsMode) { + RefPtr<InternalHeaders> cors = new InternalHeaders(aHeaders->mGuard); + ErrorResult result; + + nsAutoCString acExposedNames; + aHeaders->Get("Access-Control-Expose-Headers"_ns, acExposedNames, result); + MOZ_ASSERT(!result.Failed()); + + bool allowAllHeaders = false; + AutoTArray<nsCString, 5> exposeNamesArray; + for (const nsACString& token : + nsCCharSeparatedTokenizer(acExposedNames, ',').ToRange()) { + if (token.IsEmpty()) { + continue; + } + + if (!NS_IsValidHTTPToken(token)) { + NS_WARNING( + "Got invalid HTTP token in Access-Control-Expose-Headers. Header " + "value is:"); + NS_WARNING(acExposedNames.get()); + exposeNamesArray.Clear(); + break; + } + + if (token.EqualsLiteral("*") && + aCredentialsMode != RequestCredentials::Include) { + allowAllHeaders = true; + } + + exposeNamesArray.AppendElement(token); + } + + nsCaseInsensitiveCStringArrayComparator comp; + for (uint32_t i = 0; i < aHeaders->mList.Length(); ++i) { + const Entry& entry = aHeaders->mList[i]; + if (allowAllHeaders) { + cors->Append(entry.mName, entry.mValue, result); + MOZ_ASSERT(!result.Failed()); + } else if (entry.mName.EqualsIgnoreCase("cache-control") || + entry.mName.EqualsIgnoreCase("content-language") || + entry.mName.EqualsIgnoreCase("content-type") || + entry.mName.EqualsIgnoreCase("content-length") || + entry.mName.EqualsIgnoreCase("expires") || + entry.mName.EqualsIgnoreCase("last-modified") || + entry.mName.EqualsIgnoreCase("pragma") || + exposeNamesArray.Contains(entry.mName, comp)) { + cors->Append(entry.mName, entry.mValue, result); + MOZ_ASSERT(!result.Failed()); + } + } + + return cors.forget(); +} + +void InternalHeaders::GetEntries( + nsTArray<InternalHeaders::Entry>& aEntries) const { + MOZ_ASSERT(aEntries.IsEmpty()); + aEntries.AppendElements(mList); +} + +void InternalHeaders::GetUnsafeHeaders(nsTArray<nsCString>& aNames) const { + MOZ_ASSERT(aNames.IsEmpty()); + for (uint32_t i = 0; i < mList.Length(); ++i) { + const Entry& header = mList[i]; + if (!InternalHeaders::IsSimpleHeader(header.mName, header.mValue)) { + aNames.AppendElement(header.mName); + } + } +} + +void InternalHeaders::MaybeSortList() { + class Comparator { + public: + bool Equals(const Entry& aA, const Entry& aB) const { + return aA.mName == aB.mName; + } + + bool LessThan(const Entry& aA, const Entry& aB) const { + return aA.mName < aB.mName; + } + }; + + if (!mListDirty) { + return; + } + + mListDirty = false; + + Comparator comparator; + + mSortedList.Clear(); + for (const Entry& entry : mList) { + bool found = false; + + // We combine every header but Set-Cookie. + if (!entry.mName.EqualsIgnoreCase("Set-Cookie")) { + for (Entry& sortedEntry : mSortedList) { + if (sortedEntry.mName.EqualsIgnoreCase(entry.mName.get())) { + sortedEntry.mValue += ", "; + sortedEntry.mValue += entry.mValue; + found = true; + break; + } + } + } + + if (!found) { + Entry newEntry = entry; + ToLowerCase(newEntry.mName); + mSortedList.InsertElementSorted(newEntry, comparator); + } + } +} + +void InternalHeaders::SetListDirty() { + mSortedList.Clear(); + mListDirty = true; +} + +void InternalHeaders::ReuseExistingNameIfExists(nsCString& aName) const { + for (const Entry& entry : mList) { + if (entry.mName.EqualsIgnoreCase(aName.get())) { + aName = entry.mName; + break; + } + } +} + +} // namespace mozilla::dom diff --git a/dom/fetch/InternalHeaders.h b/dom/fetch/InternalHeaders.h new file mode 100644 index 0000000000..258725e32e --- /dev/null +++ b/dom/fetch/InternalHeaders.h @@ -0,0 +1,180 @@ +/* -*- 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_InternalHeaders_h +#define mozilla_dom_InternalHeaders_h + +// needed for HeadersGuardEnum. +#include "mozilla/dom/HeadersBinding.h" +#include "mozilla/dom/RequestBinding.h" +#include "mozilla/dom/UnionTypes.h" + +#include "nsClassHashtable.h" +#include "nsWrapperCache.h" + +class nsIRequest; + +namespace mozilla { + +class ErrorResult; + +namespace dom { + +template <typename K, typename V> +class Record; +class HeadersEntry; + +class InternalHeaders final { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(InternalHeaders) + + public: + struct Entry { + Entry(const nsACString& aName, const nsACString& aValue) + : mName(aName), mValue(aValue) {} + + Entry() = default; + + nsCString mName; + nsCString mValue; + }; + + private: + HeadersGuardEnum mGuard; + nsTArray<Entry> mList; + + nsTArray<Entry> mSortedList; + + // This boolean is set to true at any writing operation to mList. It's set to + // false when mSortedList is regenerated. This happens when the header is + // iterated. + bool mListDirty; + + public: + explicit InternalHeaders(HeadersGuardEnum aGuard = HeadersGuardEnum::None) + : mGuard(aGuard), mListDirty(false) {} + + explicit InternalHeaders(const InternalHeaders& aOther) + : mGuard(HeadersGuardEnum::None), mListDirty(true) { + ErrorResult result; + Fill(aOther, result); + MOZ_ASSERT(!result.Failed()); + // Note that it's important to set the guard after Fill(), to make sure + // that Fill() doesn't fail if aOther is immutable. + mGuard = aOther.mGuard; + } + + explicit InternalHeaders(nsTArray<Entry>&& aHeaders, + HeadersGuardEnum aGuard = HeadersGuardEnum::None); + + InternalHeaders(const nsTArray<HeadersEntry>& aHeadersEntryList, + HeadersGuardEnum aGuard); + + void ToIPC(nsTArray<HeadersEntry>& aIPCHeaders, HeadersGuardEnum& aGuard); + + void Append(const nsACString& aName, const nsACString& aValue, + ErrorResult& aRv); + void Delete(const nsACString& aName, ErrorResult& aRv); + void Get(const nsACString& aName, nsACString& aValue, ErrorResult& aRv) const; + void GetSetCookie(nsTArray<nsCString>& aValues) const; + void GetFirst(const nsACString& aName, nsACString& aValue, + ErrorResult& aRv) const; + bool Has(const nsACString& aName, ErrorResult& aRv) const; + void Set(const nsACString& aName, const nsACString& aValue, ErrorResult& aRv); + + uint32_t GetIterableLength() { + MaybeSortList(); + return mSortedList.Length(); + } + const NS_ConvertASCIItoUTF16 GetKeyAtIndex(unsigned aIndex) { + MaybeSortList(); + MOZ_ASSERT(aIndex < mSortedList.Length()); + return NS_ConvertASCIItoUTF16(mSortedList[aIndex].mName); + } + const NS_ConvertASCIItoUTF16 GetValueAtIndex(unsigned aIndex) { + MaybeSortList(); + MOZ_ASSERT(aIndex < mSortedList.Length()); + return NS_ConvertASCIItoUTF16(mSortedList[aIndex].mValue); + } + + void Clear(); + + HeadersGuardEnum Guard() const { return mGuard; } + void SetGuard(HeadersGuardEnum aGuard, ErrorResult& aRv); + + void Fill(const InternalHeaders& aInit, ErrorResult& aRv); + void Fill(const Sequence<Sequence<nsCString>>& aInit, ErrorResult& aRv); + void Fill(const Record<nsCString, nsCString>& aInit, ErrorResult& aRv); + void FillResponseHeaders(nsIRequest* aRequest); + + bool HasOnlySimpleHeaders() const; + + bool HasRevalidationHeaders() const; + + static already_AddRefed<InternalHeaders> BasicHeaders( + InternalHeaders* aHeaders); + + static already_AddRefed<InternalHeaders> CORSHeaders( + InternalHeaders* aHeaders, + RequestCredentials aCredentialsMode = RequestCredentials::Omit); + + void GetEntries(nsTArray<InternalHeaders::Entry>& aEntries) const; + + void GetUnsafeHeaders(nsTArray<nsCString>& aNames) const; + + private: + virtual ~InternalHeaders(); + + static bool IsInvalidName(const nsACString& aName, ErrorResult& aRv); + static bool IsInvalidValue(const nsACString& aValue, ErrorResult& aRv); + bool IsValidHeaderValue(const nsCString& aLowerName, + const nsCString& aNormalizedValue, ErrorResult& aRv); + bool IsImmutable(ErrorResult& aRv) const; + bool IsForbiddenRequestHeader(const nsCString& aName, + const nsACString& aValue) const; + bool IsForbiddenRequestNoCorsHeader(const nsCString& aName) const; + bool IsForbiddenRequestNoCorsHeader(const nsCString& aName, + const nsACString& aValue) const; + bool IsForbiddenResponseHeader(const nsCString& aName) const; + + bool IsInvalidMutableHeader(const nsCString& aName, ErrorResult& aRv) const { + return IsInvalidMutableHeader(aName, ""_ns, aRv); + } + + bool IsInvalidMutableHeader(const nsCString& aName, const nsACString& aValue, + ErrorResult& aRv) const { + return IsInvalidName(aName, aRv) || IsInvalidValue(aValue, aRv) || + IsImmutable(aRv) || IsForbiddenRequestHeader(aName, aValue) || + IsForbiddenRequestNoCorsHeader(aName, aValue) || + IsForbiddenResponseHeader(aName); + } + + // This method updates the passed name to match the capitalization of a header + // with the same name (ignoring case, per the spec). + void ReuseExistingNameIfExists(nsCString& aName) const; + + void RemovePrivilegedNoCorsRequestHeaders(); + + void GetInternal(const nsCString& aLowerName, nsACString& aValue, + ErrorResult& aRv) const; + + bool DeleteInternal(const nsCString& aLowerName, ErrorResult& aRv); + + static bool IsNoCorsSafelistedRequestHeaderName(const nsCString& aName); + + static bool IsPrivilegedNoCorsRequestHeaderName(const nsCString& aName); + + static bool IsSimpleHeader(const nsCString& aName, const nsACString& aValue); + + static bool IsRevalidationHeader(const nsCString& aName); + + void MaybeSortList(); + void SetListDirty(); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_InternalHeaders_h diff --git a/dom/fetch/InternalRequest.cpp b/dom/fetch/InternalRequest.cpp new file mode 100644 index 0000000000..41797d3e82 --- /dev/null +++ b/dom/fetch/InternalRequest.cpp @@ -0,0 +1,478 @@ +/* -*- 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 "InternalRequest.h" + +#include "InternalResponse.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/FetchTypes.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/RemoteLazyInputStreamChild.h" +#include "nsIContentPolicy.h" +#include "nsStreamUtils.h" + +namespace mozilla::dom { +// The global is used to extract the principal. +SafeRefPtr<InternalRequest> InternalRequest::GetRequestConstructorCopy( + nsIGlobalObject* aGlobal, ErrorResult& aRv) const { + MOZ_RELEASE_ASSERT(!mURLList.IsEmpty(), + "Internal Request's urlList should not be empty when " + "copied from constructor."); + auto copy = + MakeSafeRefPtr<InternalRequest>(mURLList.LastElement(), mFragment); + copy->SetMethod(mMethod); + copy->mHeaders = new InternalHeaders(*mHeaders); + copy->SetUnsafeRequest(); + copy->mBodyStream = mBodyStream; + copy->mBodyLength = mBodyLength; + // The "client" is not stored in our implementation. Fetch API users should + // use the appropriate window/document/principal and other Gecko security + // mechanisms as appropriate. + copy->mReferrer = mReferrer; + copy->mReferrerPolicy = mReferrerPolicy; + copy->mEnvironmentReferrerPolicy = mEnvironmentReferrerPolicy; + copy->mIntegrity = mIntegrity; + copy->mMozErrors = mMozErrors; + + copy->mContentPolicyType = mContentPolicyTypeOverridden + ? mContentPolicyType + : nsIContentPolicy::TYPE_FETCH; + copy->mMode = mMode; + copy->mCredentialsMode = mCredentialsMode; + copy->mCacheMode = mCacheMode; + copy->mRedirectMode = mRedirectMode; + copy->mContentPolicyTypeOverridden = mContentPolicyTypeOverridden; + + copy->mPreferredAlternativeDataType = mPreferredAlternativeDataType; + copy->mSkipWasmCaching = mSkipWasmCaching; + copy->mEmbedderPolicy = mEmbedderPolicy; + return copy; +} + +SafeRefPtr<InternalRequest> InternalRequest::Clone() { + auto clone = MakeSafeRefPtr<InternalRequest>(*this, ConstructorGuard{}); + + if (!mBodyStream) { + return clone; + } + + nsCOMPtr<nsIInputStream> clonedBody; + nsCOMPtr<nsIInputStream> replacementBody; + + nsresult rv = NS_CloneInputStream(mBodyStream, getter_AddRefs(clonedBody), + getter_AddRefs(replacementBody)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + clone->mBodyStream.swap(clonedBody); + if (replacementBody) { + mBodyStream.swap(replacementBody); + } + return clone; +} +InternalRequest::InternalRequest(const nsACString& aURL, + const nsACString& aFragment) + : mMethod("GET"), + mHeaders(new InternalHeaders(HeadersGuardEnum::None)), + mBodyLength(InternalResponse::UNKNOWN_BODY_SIZE), + mContentPolicyType(nsIContentPolicy::TYPE_FETCH), + mReferrer(NS_LITERAL_STRING_FROM_CSTRING(kFETCH_CLIENT_REFERRER_STR)), + mReferrerPolicy(ReferrerPolicy::_empty), + mEnvironmentReferrerPolicy(ReferrerPolicy::_empty), + mMode(RequestMode::No_cors), + mCredentialsMode(RequestCredentials::Omit), + mCacheMode(RequestCache::Default), + mRedirectMode(RequestRedirect::Follow) { + MOZ_ASSERT(!aURL.IsEmpty()); + AddURL(aURL, aFragment); +} +InternalRequest::InternalRequest( + const nsACString& aURL, const nsACString& aFragment, + const nsACString& aMethod, already_AddRefed<InternalHeaders> aHeaders, + RequestCache aCacheMode, RequestMode aMode, + RequestRedirect aRequestRedirect, RequestCredentials aRequestCredentials, + const nsAString& aReferrer, ReferrerPolicy aReferrerPolicy, + nsContentPolicyType aContentPolicyType, const nsAString& aIntegrity) + : mMethod(aMethod), + mHeaders(aHeaders), + mBodyLength(InternalResponse::UNKNOWN_BODY_SIZE), + mContentPolicyType(aContentPolicyType), + mReferrer(aReferrer), + mReferrerPolicy(aReferrerPolicy), + mEnvironmentReferrerPolicy(ReferrerPolicy::_empty), + mMode(aMode), + mCredentialsMode(aRequestCredentials), + mCacheMode(aCacheMode), + mRedirectMode(aRequestRedirect), + mIntegrity(aIntegrity) { + MOZ_ASSERT(!aURL.IsEmpty()); + AddURL(aURL, aFragment); +} +InternalRequest::InternalRequest(const InternalRequest& aOther, + ConstructorGuard) + : mMethod(aOther.mMethod), + mURLList(aOther.mURLList.Clone()), + mHeaders(new InternalHeaders(*aOther.mHeaders)), + mBodyLength(InternalResponse::UNKNOWN_BODY_SIZE), + mContentPolicyType(aOther.mContentPolicyType), + mReferrer(aOther.mReferrer), + mReferrerPolicy(aOther.mReferrerPolicy), + mEnvironmentReferrerPolicy(aOther.mEnvironmentReferrerPolicy), + mMode(aOther.mMode), + mCredentialsMode(aOther.mCredentialsMode), + mResponseTainting(aOther.mResponseTainting), + mCacheMode(aOther.mCacheMode), + mRedirectMode(aOther.mRedirectMode), + mIntegrity(aOther.mIntegrity), + mMozErrors(aOther.mMozErrors), + mFragment(aOther.mFragment), + mSkipServiceWorker(aOther.mSkipServiceWorker), + mSkipWasmCaching(aOther.mSkipWasmCaching), + mSynchronous(aOther.mSynchronous), + mUnsafeRequest(aOther.mUnsafeRequest), + mUseURLCredentials(aOther.mUseURLCredentials), + mContentPolicyTypeOverridden(aOther.mContentPolicyTypeOverridden), + mEmbedderPolicy(aOther.mEmbedderPolicy), + mInterceptionContentPolicyType(aOther.mInterceptionContentPolicyType), + mInterceptionRedirectChain(aOther.mInterceptionRedirectChain), + mInterceptionFromThirdParty(aOther.mInterceptionFromThirdParty) { + // NOTE: does not copy body stream... use the fallible Clone() for that + + if (aOther.GetInterceptionTriggeringPrincipalInfo()) { + mInterceptionTriggeringPrincipalInfo = + MakeUnique<mozilla::ipc::PrincipalInfo>( + *(aOther.GetInterceptionTriggeringPrincipalInfo().get())); + } +} + +InternalRequest::InternalRequest(const IPCInternalRequest& aIPCRequest) + : mMethod(aIPCRequest.method()), + mURLList(aIPCRequest.urlList().Clone()), + mHeaders(new InternalHeaders(aIPCRequest.headers(), + aIPCRequest.headersGuard())), + mBodyLength(aIPCRequest.bodySize()), + mPreferredAlternativeDataType(aIPCRequest.preferredAlternativeDataType()), + mContentPolicyType( + static_cast<nsContentPolicyType>(aIPCRequest.contentPolicyType())), + mReferrer(aIPCRequest.referrer()), + mReferrerPolicy(aIPCRequest.referrerPolicy()), + mEnvironmentReferrerPolicy(aIPCRequest.environmentReferrerPolicy()), + mMode(aIPCRequest.requestMode()), + mCredentialsMode(aIPCRequest.requestCredentials()), + mCacheMode(aIPCRequest.cacheMode()), + mRedirectMode(aIPCRequest.requestRedirect()), + mIntegrity(aIPCRequest.integrity()), + mFragment(aIPCRequest.fragment()), + mEmbedderPolicy(aIPCRequest.embedderPolicy()), + mInterceptionContentPolicyType(static_cast<nsContentPolicyType>( + aIPCRequest.interceptionContentPolicyType())), + mInterceptionRedirectChain(aIPCRequest.interceptionRedirectChain()), + mInterceptionFromThirdParty(aIPCRequest.interceptionFromThirdParty()) { + if (aIPCRequest.principalInfo()) { + mPrincipalInfo = MakeUnique<mozilla::ipc::PrincipalInfo>( + aIPCRequest.principalInfo().ref()); + } + + if (aIPCRequest.interceptionTriggeringPrincipalInfo()) { + mInterceptionTriggeringPrincipalInfo = + MakeUnique<mozilla::ipc::PrincipalInfo>( + aIPCRequest.interceptionTriggeringPrincipalInfo().ref()); + } + + const Maybe<BodyStreamVariant>& body = aIPCRequest.body(); + + if (body) { + if (body->type() == BodyStreamVariant::TParentToChildStream) { + mBodyStream = body->get_ParentToChildStream().get_RemoteLazyInputStream(); + } + if (body->type() == BodyStreamVariant::TChildToParentStream) { + mBodyStream = + DeserializeIPCStream(body->get_ChildToParentStream().stream()); + } + } +} + +void InternalRequest::ToIPCInternalRequest( + IPCInternalRequest* aIPCRequest, mozilla::ipc::PBackgroundChild* aManager) { + aIPCRequest->method() = mMethod; + for (const auto& url : mURLList) { + aIPCRequest->urlList().AppendElement(url); + } + mHeaders->ToIPC(aIPCRequest->headers(), aIPCRequest->headersGuard()); + aIPCRequest->bodySize() = mBodyLength; + aIPCRequest->preferredAlternativeDataType() = mPreferredAlternativeDataType; + aIPCRequest->contentPolicyType() = mContentPolicyType; + aIPCRequest->referrer() = mReferrer; + aIPCRequest->referrerPolicy() = mReferrerPolicy; + aIPCRequest->environmentReferrerPolicy() = mEnvironmentReferrerPolicy; + aIPCRequest->requestMode() = mMode; + aIPCRequest->requestCredentials() = mCredentialsMode; + aIPCRequest->cacheMode() = mCacheMode; + aIPCRequest->requestRedirect() = mRedirectMode; + aIPCRequest->integrity() = mIntegrity; + aIPCRequest->fragment() = mFragment; + aIPCRequest->embedderPolicy() = mEmbedderPolicy; + + if (mPrincipalInfo) { + aIPCRequest->principalInfo() = Some(*mPrincipalInfo); + } + + if (mBodyStream) { + nsCOMPtr<nsIInputStream> body = mBodyStream; + aIPCRequest->body().emplace(ChildToParentStream()); + DebugOnly<bool> ok = mozilla::ipc::SerializeIPCStream( + body.forget(), aIPCRequest->body()->get_ChildToParentStream().stream(), + /* aAllowLazy */ false); + MOZ_ASSERT(ok); + } +} + +InternalRequest::~InternalRequest() = default; + +void InternalRequest::SetContentPolicyType( + nsContentPolicyType aContentPolicyType) { + mContentPolicyType = aContentPolicyType; +} + +void InternalRequest::OverrideContentPolicyType( + nsContentPolicyType aContentPolicyType) { + SetContentPolicyType(aContentPolicyType); + mContentPolicyTypeOverridden = true; +} + +void InternalRequest::SetInterceptionContentPolicyType( + nsContentPolicyType aContentPolicyType) { + mInterceptionContentPolicyType = aContentPolicyType; +} + +/* static */ +RequestDestination InternalRequest::MapContentPolicyTypeToRequestDestination( + nsContentPolicyType aContentPolicyType) { + switch (aContentPolicyType) { + case nsIContentPolicy::TYPE_OTHER: + return RequestDestination::_empty; + case nsIContentPolicy::TYPE_INTERNAL_SCRIPT: + case nsIContentPolicy::TYPE_INTERNAL_SCRIPT_PRELOAD: + case nsIContentPolicy::TYPE_INTERNAL_MODULE: + case nsIContentPolicy::TYPE_INTERNAL_MODULE_PRELOAD: + case nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER: + case nsIContentPolicy::TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS: + case nsIContentPolicy::TYPE_INTERNAL_CHROMEUTILS_COMPILED_SCRIPT: + case nsIContentPolicy::TYPE_INTERNAL_FRAME_MESSAGEMANAGER_SCRIPT: + case nsIContentPolicy::TYPE_SCRIPT: + return RequestDestination::Script; + case nsIContentPolicy::TYPE_INTERNAL_WORKER: + case nsIContentPolicy::TYPE_INTERNAL_WORKER_STATIC_MODULE: + return RequestDestination::Worker; + case nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER: + return RequestDestination::Sharedworker; + case nsIContentPolicy::TYPE_IMAGESET: + case nsIContentPolicy::TYPE_INTERNAL_IMAGE: + case nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD: + case nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON: + case nsIContentPolicy::TYPE_IMAGE: + return RequestDestination::Image; + case nsIContentPolicy::TYPE_STYLESHEET: + case nsIContentPolicy::TYPE_INTERNAL_STYLESHEET: + case nsIContentPolicy::TYPE_INTERNAL_STYLESHEET_PRELOAD: + return RequestDestination::Style; + case nsIContentPolicy::TYPE_OBJECT: + case nsIContentPolicy::TYPE_INTERNAL_OBJECT: + return RequestDestination::Object; + case nsIContentPolicy::TYPE_INTERNAL_EMBED: + return RequestDestination::Embed; + case nsIContentPolicy::TYPE_DOCUMENT: + return RequestDestination::Document; + case nsIContentPolicy::TYPE_SUBDOCUMENT: + case nsIContentPolicy::TYPE_INTERNAL_IFRAME: + return RequestDestination::Iframe; + case nsIContentPolicy::TYPE_INTERNAL_FRAME: + return RequestDestination::Frame; + case nsIContentPolicy::TYPE_PING: + return RequestDestination::_empty; + case nsIContentPolicy::TYPE_XMLHTTPREQUEST: + case nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST: + return RequestDestination::_empty; + case nsIContentPolicy::TYPE_INTERNAL_EVENTSOURCE: + return RequestDestination::_empty; + case nsIContentPolicy::TYPE_OBJECT_SUBREQUEST: + return RequestDestination::_empty; + case nsIContentPolicy::TYPE_DTD: + case nsIContentPolicy::TYPE_INTERNAL_DTD: + case nsIContentPolicy::TYPE_INTERNAL_FORCE_ALLOWED_DTD: + return RequestDestination::_empty; + case nsIContentPolicy::TYPE_FONT: + case nsIContentPolicy::TYPE_INTERNAL_FONT_PRELOAD: + case nsIContentPolicy::TYPE_UA_FONT: + return RequestDestination::Font; + case nsIContentPolicy::TYPE_MEDIA: + return RequestDestination::_empty; + case nsIContentPolicy::TYPE_INTERNAL_AUDIO: + return RequestDestination::Audio; + case nsIContentPolicy::TYPE_INTERNAL_VIDEO: + return RequestDestination::Video; + case nsIContentPolicy::TYPE_INTERNAL_TRACK: + return RequestDestination::Track; + case nsIContentPolicy::TYPE_WEBSOCKET: + return RequestDestination::_empty; + case nsIContentPolicy::TYPE_CSP_REPORT: + return RequestDestination::Report; + case nsIContentPolicy::TYPE_XSLT: + return RequestDestination::Xslt; + case nsIContentPolicy::TYPE_BEACON: + return RequestDestination::_empty; + case nsIContentPolicy::TYPE_FETCH: + case nsIContentPolicy::TYPE_INTERNAL_FETCH_PRELOAD: + return RequestDestination::_empty; + case nsIContentPolicy::TYPE_WEB_MANIFEST: + return RequestDestination::Manifest; + case nsIContentPolicy::TYPE_SAVEAS_DOWNLOAD: + return RequestDestination::_empty; + case nsIContentPolicy::TYPE_SPECULATIVE: + return RequestDestination::_empty; + case nsIContentPolicy::TYPE_INTERNAL_AUDIOWORKLET: + return RequestDestination::Audioworklet; + case nsIContentPolicy::TYPE_INTERNAL_PAINTWORKLET: + return RequestDestination::Paintworklet; + case nsIContentPolicy::TYPE_PROXIED_WEBRTC_MEDIA: + return RequestDestination::_empty; + case nsIContentPolicy::TYPE_WEB_IDENTITY: + return RequestDestination::_empty; + case nsIContentPolicy::TYPE_WEB_TRANSPORT: + return RequestDestination::_empty; + case nsIContentPolicy::TYPE_INVALID: + case nsIContentPolicy::TYPE_END: + break; + // Do not add default: so that compilers can catch the missing case. + } + + MOZ_ASSERT(false, "Unhandled nsContentPolicyType value"); + return RequestDestination::_empty; +} + +// static +bool InternalRequest::IsNavigationContentPolicy( + nsContentPolicyType aContentPolicyType) { + // https://fetch.spec.whatwg.org/#navigation-request-context + // + // A navigation request context is one of "form", "frame", "hyperlink", + // "iframe", "internal" (as long as context frame type is not "none"), + // "location", "metarefresh", and "prerender". + // + // Note, all of these request types are effectively initiated by nsDocShell. + return aContentPolicyType == nsIContentPolicy::TYPE_DOCUMENT || + aContentPolicyType == nsIContentPolicy::TYPE_SUBDOCUMENT || + aContentPolicyType == nsIContentPolicy::TYPE_INTERNAL_FRAME || + aContentPolicyType == nsIContentPolicy::TYPE_INTERNAL_IFRAME; +} + +// static +bool InternalRequest::IsWorkerContentPolicy( + nsContentPolicyType aContentPolicyType) { + // https://fetch.spec.whatwg.org/#worker-request-context + // + // A worker request context is one of "serviceworker", "sharedworker", and + // "worker". + // + // Note, service workers are not included here because currently there is + // no way to generate a Request with a "serviceworker" RequestDestination. + // ServiceWorker scripts cannot be intercepted. + return aContentPolicyType == nsIContentPolicy::TYPE_INTERNAL_WORKER || + aContentPolicyType == nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER; +} + +bool InternalRequest::IsNavigationRequest() const { + return IsNavigationContentPolicy(mContentPolicyType); +} + +bool InternalRequest::IsWorkerRequest() const { + return IsWorkerContentPolicy(mContentPolicyType); +} + +bool InternalRequest::IsClientRequest() const { + return IsNavigationRequest() || IsWorkerRequest(); +} + +// static +RequestMode InternalRequest::MapChannelToRequestMode(nsIChannel* aChannel) { + MOZ_ASSERT(aChannel); + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + + nsContentPolicyType contentPolicy = loadInfo->InternalContentPolicyType(); + if (IsNavigationContentPolicy(contentPolicy)) { + return RequestMode::Navigate; + } + + // TODO: remove the worker override once securityMode is fully implemented + // (bug 1189945) + if (IsWorkerContentPolicy(contentPolicy)) { + return RequestMode::Same_origin; + } + + uint32_t securityMode = loadInfo->GetSecurityMode(); + + switch (securityMode) { + case nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT: + case nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_DATA_IS_BLOCKED: + return RequestMode::Same_origin; + case nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT: + case nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL: + return RequestMode::No_cors; + case nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT: + // TODO: Check additional flag force-preflight after bug 1199693 (bug + // 1189945) + return RequestMode::Cors; + default: + MOZ_ASSERT_UNREACHABLE("Unexpected security mode!"); + return RequestMode::Same_origin; + } +} + +// static +RequestCredentials InternalRequest::MapChannelToRequestCredentials( + nsIChannel* aChannel) { + MOZ_ASSERT(aChannel); + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->LoadInfo(); + + uint32_t cookiePolicy = loadInfo->GetCookiePolicy(); + + if (cookiePolicy == nsILoadInfo::SEC_COOKIES_INCLUDE) { + return RequestCredentials::Include; + } else if (cookiePolicy == nsILoadInfo::SEC_COOKIES_OMIT) { + return RequestCredentials::Omit; + } else if (cookiePolicy == nsILoadInfo::SEC_COOKIES_SAME_ORIGIN) { + return RequestCredentials::Same_origin; + } + + MOZ_ASSERT_UNREACHABLE("Unexpected cookie policy!"); + return RequestCredentials::Same_origin; +} + +void InternalRequest::MaybeSkipCacheIfPerformingRevalidation() { + if (mCacheMode == RequestCache::Default && + mHeaders->HasRevalidationHeaders()) { + mCacheMode = RequestCache::No_store; + } +} + +void InternalRequest::SetPrincipalInfo( + UniquePtr<mozilla::ipc::PrincipalInfo> aPrincipalInfo) { + mPrincipalInfo = std::move(aPrincipalInfo); +} + +void InternalRequest::SetInterceptionTriggeringPrincipalInfo( + UniquePtr<mozilla::ipc::PrincipalInfo> aPrincipalInfo) { + mInterceptionTriggeringPrincipalInfo = std::move(aPrincipalInfo); +} +} // namespace mozilla::dom diff --git a/dom/fetch/InternalRequest.h b/dom/fetch/InternalRequest.h new file mode 100644 index 0000000000..3dfbb9284d --- /dev/null +++ b/dom/fetch/InternalRequest.h @@ -0,0 +1,489 @@ +/* -*- 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_InternalRequest_h +#define mozilla_dom_InternalRequest_h + +#include "mozilla/dom/HeadersBinding.h" +#include "mozilla/dom/InternalHeaders.h" +#include "mozilla/dom/RequestBinding.h" +#include "mozilla/dom/SafeRefPtr.h" +#include "mozilla/LoadTainting.h" +#include "mozilla/UniquePtr.h" + +#include "nsIChannelEventSink.h" +#include "nsIInputStream.h" +#include "nsISupportsImpl.h" +#include "mozilla/net/NeckoChannelParams.h" +#ifdef DEBUG +# include "nsIURLParser.h" +# include "nsNetCID.h" +# include "nsServiceManagerUtils.h" +#endif + +using mozilla::net::RedirectHistoryEntryInfo; + +namespace mozilla { + +namespace ipc { +class PBackgroundChild; +class PrincipalInfo; +} // namespace ipc + +namespace dom { + +/* + * The mapping of RequestDestination and nsContentPolicyType is currently as the + * following. + * + * RequestDestination| nsContentPolicyType + * ------------------+-------------------- + * "audio" | TYPE_INTERNAL_AUDIO + * "audioworklet" | TYPE_INTERNAL_AUDIOWORKLET + * "document" | TYPE_DOCUMENT + * "embed" | TYPE_INTERNAL_EMBED + * "font" | TYPE_FONT, TYPE_INTERNAL_FONT_PRELOAD + * "frame" | TYPE_INTERNAL_FRAME + * "iframe" | TYPE_SUBDOCUMENT, TYPE_INTERNAL_IFRAME + * "image" | TYPE_INTERNAL_IMAGE, TYPE_INTERNAL_IMAGE_PRELOAD, + * | TYPE_IMAGE, TYPE_INTERNAL_IMAGE_FAVICON, TYPE_IMAGESET + * "manifest" | TYPE_WEB_MANIFEST + * "object" | TYPE_INTERNAL_OBJECT, TYPE_OBJECT + * "paintworklet" | TYPE_INTERNAL_PAINTWORKLET + * "report" | TYPE_CSP_REPORT + * "script" | TYPE_INTERNAL_SCRIPT, TYPE_INTERNAL_SCRIPT_PRELOAD, + * | TYPE_INTERNAL_MODULE, TYPE_INTERNAL_MODULE_PRELOAD, + * | TYPE_SCRIPT, + * | TYPE_INTERNAL_SERVICE_WORKER, + * | TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS, + * | TYPE_INTERNAL_CHROMEUTILS_COMPILED_SCRIPT + * | TYPE_INTERNAL_FRAME_MESSAGEMANAGER_SCRIPT + * "sharedworker" | TYPE_INTERNAL_SHARED_WORKER + * "serviceworker" | The spec lists this as a valid value for the enum, + * | however it is impossible to observe a request with this + * | destination value. + * "style" | TYPE_INTERNAL_STYLESHEET, + * | TYPE_INTERNAL_STYLESHEET_PRELOAD, + * | TYPE_STYLESHEET + * "track" | TYPE_INTERNAL_TRACK + * "video" | TYPE_INTERNAL_VIDEO + * "worker" | TYPE_INTERNAL_WORKER, TYPE_INTERNAL_WORKER_STATIC_MODULE + * "xslt" | TYPE_XSLT + * "" | Default for everything else. + * + */ + +class IPCInternalRequest; +class Request; + +#define kFETCH_CLIENT_REFERRER_STR "about:client" +class InternalRequest final : public AtomicSafeRefCounted<InternalRequest> { + friend class Request; + + public: + MOZ_DECLARE_REFCOUNTED_TYPENAME(InternalRequest) + InternalRequest(const nsACString& aURL, const nsACString& aFragment); + InternalRequest(const nsACString& aURL, const nsACString& aFragment, + const nsACString& aMethod, + already_AddRefed<InternalHeaders> aHeaders, + RequestCache aCacheMode, RequestMode aMode, + RequestRedirect aRequestRedirect, + RequestCredentials aRequestCredentials, + const nsAString& aReferrer, ReferrerPolicy aReferrerPolicy, + nsContentPolicyType aContentPolicyType, + const nsAString& aIntegrity); + + explicit InternalRequest(const IPCInternalRequest& aIPCRequest); + + void ToIPCInternalRequest(IPCInternalRequest* aIPCRequest, + mozilla::ipc::PBackgroundChild* aManager); + + SafeRefPtr<InternalRequest> Clone(); + + void GetMethod(nsCString& aMethod) const { aMethod.Assign(mMethod); } + + void SetMethod(const nsACString& aMethod) { mMethod.Assign(aMethod); } + + bool HasSimpleMethod() const { + return mMethod.LowerCaseEqualsASCII("get") || + mMethod.LowerCaseEqualsASCII("post") || + mMethod.LowerCaseEqualsASCII("head"); + } + // GetURL should get the request's current url with fragment. A request has + // an associated current url. It is a pointer to the last fetch URL in + // request's url list. + void GetURL(nsACString& aURL) const { + aURL.Assign(GetURLWithoutFragment()); + if (GetFragment().IsEmpty()) { + return; + } + aURL.AppendLiteral("#"); + aURL.Append(GetFragment()); + } + + const nsCString& GetURLWithoutFragment() const { + MOZ_RELEASE_ASSERT(!mURLList.IsEmpty(), + "Internal Request's urlList should not be empty."); + + return mURLList.LastElement(); + } + + // A safe guard for ensuring that request's URL is only allowed to be set in a + // sw internal redirect. + void SetURLForInternalRedirect(const uint32_t aFlag, const nsACString& aURL, + const nsACString& aFragment) { + // Only check in debug build to prevent it from being used unexpectedly. + MOZ_ASSERT(aFlag & nsIChannelEventSink::REDIRECT_INTERNAL); + + return SetURL(aURL, aFragment); + } + + // AddURL should append the url into url list. + // Normally we strip the fragment from the URL in Request::Constructor and + // pass the fragment as the second argument into it. + // If a fragment is present in the URL it must be stripped and passed in + // separately. + void AddURL(const nsACString& aURL, const nsACString& aFragment) { + MOZ_ASSERT(!aURL.IsEmpty()); + MOZ_ASSERT(!aURL.Contains('#')); + + mURLList.AppendElement(aURL); + + mFragment.Assign(aFragment); + } + // Get the URL list without their fragments. + void GetURLListWithoutFragment(nsTArray<nsCString>& aURLList) { + aURLList.Assign(mURLList); + } + void GetReferrer(nsAString& aReferrer) const { aReferrer.Assign(mReferrer); } + + void SetReferrer(const nsAString& aReferrer) { +#ifdef DEBUG + bool validReferrer = false; + if (aReferrer.IsEmpty() || + aReferrer.EqualsLiteral(kFETCH_CLIENT_REFERRER_STR)) { + validReferrer = true; + } else { + nsCOMPtr<nsIURLParser> parser = do_GetService(NS_STDURLPARSER_CONTRACTID); + if (!parser) { + NS_WARNING("Could not get parser to validate URL!"); + } else { + uint32_t schemePos; + int32_t schemeLen; + uint32_t authorityPos; + int32_t authorityLen; + uint32_t pathPos; + int32_t pathLen; + + NS_ConvertUTF16toUTF8 ref(aReferrer); + nsresult rv = + parser->ParseURL(ref.get(), ref.Length(), &schemePos, &schemeLen, + &authorityPos, &authorityLen, &pathPos, &pathLen); + if (NS_FAILED(rv)) { + NS_WARNING("Invalid referrer URL!"); + } else if (schemeLen < 0 || authorityLen < 0) { + NS_WARNING("Invalid referrer URL!"); + } else { + validReferrer = true; + } + } + } + + MOZ_ASSERT(validReferrer); +#endif + + mReferrer.Assign(aReferrer); + } + + ReferrerPolicy ReferrerPolicy_() const { return mReferrerPolicy; } + + void SetReferrerPolicy(ReferrerPolicy aReferrerPolicy) { + mReferrerPolicy = aReferrerPolicy; + } + + ReferrerPolicy GetEnvironmentReferrerPolicy() const { + return mEnvironmentReferrerPolicy; + } + + void SetEnvironmentReferrerPolicy(ReferrerPolicy aReferrerPolicy) { + mEnvironmentReferrerPolicy = aReferrerPolicy; + } + + bool SkipServiceWorker() const { return mSkipServiceWorker; } + + void SetSkipServiceWorker() { mSkipServiceWorker = true; } + + bool SkipWasmCaching() const { return mSkipWasmCaching; } + + void SetSkipWasmCaching() { mSkipWasmCaching = true; } + + bool IsSynchronous() const { return mSynchronous; } + + RequestMode Mode() const { return mMode; } + + void SetMode(RequestMode aMode) { mMode = aMode; } + + RequestCredentials GetCredentialsMode() const { return mCredentialsMode; } + + void SetCredentialsMode(RequestCredentials aCredentialsMode) { + mCredentialsMode = aCredentialsMode; + } + + LoadTainting GetResponseTainting() const { return mResponseTainting; } + + void MaybeIncreaseResponseTainting(LoadTainting aTainting) { + if (aTainting > mResponseTainting) { + mResponseTainting = aTainting; + } + } + + RequestCache GetCacheMode() const { return mCacheMode; } + + void SetCacheMode(RequestCache aCacheMode) { mCacheMode = aCacheMode; } + + RequestRedirect GetRedirectMode() const { return mRedirectMode; } + + void SetRedirectMode(RequestRedirect aRedirectMode) { + mRedirectMode = aRedirectMode; + } + + const nsString& GetIntegrity() const { return mIntegrity; } + + void SetIntegrity(const nsAString& aIntegrity) { + mIntegrity.Assign(aIntegrity); + } + + bool MozErrors() const { return mMozErrors; } + + void SetMozErrors() { mMozErrors = true; } + + const nsCString& GetFragment() const { return mFragment; } + + nsContentPolicyType ContentPolicyType() const { return mContentPolicyType; } + void SetContentPolicyType(nsContentPolicyType aContentPolicyType); + + void OverrideContentPolicyType(nsContentPolicyType aContentPolicyType); + + RequestDestination Destination() const { + return MapContentPolicyTypeToRequestDestination(mContentPolicyType); + } + + bool UnsafeRequest() const { return mUnsafeRequest; } + + void SetUnsafeRequest() { mUnsafeRequest = true; } + + InternalHeaders* Headers() const { return mHeaders; } + + void SetHeaders(InternalHeaders* aHeaders) { + MOZ_ASSERT(aHeaders); + mHeaders = aHeaders; + } + + void SetBody(nsIInputStream* aStream, int64_t aBodyLength) { + // A request's body may not be reset once set. + MOZ_ASSERT_IF(aStream, !mBodyStream); + mBodyStream = aStream; + mBodyLength = aBodyLength; + } + + // Will return the original stream! + // Use a tee or copy if you don't want to erase the original. + void GetBody(nsIInputStream** aStream, int64_t* aBodyLength = nullptr) const { + nsCOMPtr<nsIInputStream> s = mBodyStream; + s.forget(aStream); + + if (aBodyLength) { + *aBodyLength = mBodyLength; + } + } + + void SetBodyBlobURISpec(nsACString& aBlobURISpec) { + mBodyBlobURISpec = aBlobURISpec; + } + + const nsACString& BodyBlobURISpec() const { return mBodyBlobURISpec; } + + void SetBodyLocalPath(nsAString& aLocalPath) { mBodyLocalPath = aLocalPath; } + + const nsAString& BodyLocalPath() const { return mBodyLocalPath; } + + // The global is used as the client for the new object. + SafeRefPtr<InternalRequest> GetRequestConstructorCopy( + nsIGlobalObject* aGlobal, ErrorResult& aRv) const; + + bool IsNavigationRequest() const; + + bool IsWorkerRequest() const; + + bool IsClientRequest() const; + + void MaybeSkipCacheIfPerformingRevalidation(); + + bool IsContentPolicyTypeOverridden() const { + return mContentPolicyTypeOverridden; + } + + static RequestMode MapChannelToRequestMode(nsIChannel* aChannel); + + static RequestCredentials MapChannelToRequestCredentials( + nsIChannel* aChannel); + + // Takes ownership of the principal info. + void SetPrincipalInfo(UniquePtr<mozilla::ipc::PrincipalInfo> aPrincipalInfo); + const UniquePtr<mozilla::ipc::PrincipalInfo>& GetPrincipalInfo() const { + return mPrincipalInfo; + } + + const nsCString& GetPreferredAlternativeDataType() const { + return mPreferredAlternativeDataType; + } + + void SetPreferredAlternativeDataType(const nsACString& aDataType) { + mPreferredAlternativeDataType = aDataType; + } + + ~InternalRequest(); + + InternalRequest(const InternalRequest& aOther) = delete; + + void SetEmbedderPolicy(nsILoadInfo::CrossOriginEmbedderPolicy aPolicy) { + mEmbedderPolicy = aPolicy; + } + + nsILoadInfo::CrossOriginEmbedderPolicy GetEmbedderPolicy() const { + return mEmbedderPolicy; + } + + void SetInterceptionTriggeringPrincipalInfo( + UniquePtr<mozilla::ipc::PrincipalInfo> aPrincipalInfo); + + const UniquePtr<mozilla::ipc::PrincipalInfo>& + GetInterceptionTriggeringPrincipalInfo() const { + return mInterceptionTriggeringPrincipalInfo; + } + + nsContentPolicyType InterceptionContentPolicyType() const { + return mInterceptionContentPolicyType; + } + void SetInterceptionContentPolicyType(nsContentPolicyType aContentPolicyType); + + const nsTArray<RedirectHistoryEntryInfo>& InterceptionRedirectChain() const { + return mInterceptionRedirectChain; + } + + void SetInterceptionRedirectChain( + const nsTArray<RedirectHistoryEntryInfo>& aRedirectChain) { + mInterceptionRedirectChain = aRedirectChain; + } + + const bool& InterceptionFromThirdParty() const { + return mInterceptionFromThirdParty; + } + + void SetInterceptionFromThirdParty(bool aFromThirdParty) { + mInterceptionFromThirdParty = aFromThirdParty; + } + + private: + struct ConstructorGuard {}; + + public: + // Does not copy mBodyStream. Use fallible Clone() for complete copy. + InternalRequest(const InternalRequest& aOther, ConstructorGuard); + + private: + // Map the content policy type to the associated fetch destination, as defined + // by the spec at https://fetch.spec.whatwg.org/#concept-request-destination. + // Note that while the HTML spec for the "Link" element and its "as" attribute + // (https://html.spec.whatwg.org/#attr-link-as) reuse fetch's definition of + // destination. + static RequestDestination MapContentPolicyTypeToRequestDestination( + nsContentPolicyType aContentPolicyType); + + static bool IsNavigationContentPolicy(nsContentPolicyType aContentPolicyType); + + static bool IsWorkerContentPolicy(nsContentPolicyType aContentPolicyType); + + // It should only be called while there is a service-worker-internal-redirect. + void SetURL(const nsACString& aURL, const nsACString& aFragment) { + MOZ_ASSERT(!aURL.IsEmpty()); + MOZ_ASSERT(!aURL.Contains('#')); + MOZ_ASSERT(mURLList.Length() > 0); + + mURLList.LastElement() = aURL; + mFragment.Assign(aFragment); + } + + nsCString mMethod; + // mURLList: a list of one or more fetch URLs + nsTArray<nsCString> mURLList; + RefPtr<InternalHeaders> mHeaders; + nsCString mBodyBlobURISpec; + nsString mBodyLocalPath; + nsCOMPtr<nsIInputStream> mBodyStream; + int64_t mBodyLength; + + nsCString mPreferredAlternativeDataType; + + nsContentPolicyType mContentPolicyType; + + // Empty string: no-referrer + // "about:client": client (default) + // URL: an URL + nsString mReferrer; + ReferrerPolicy mReferrerPolicy; + + // This will be used for request created from Window or Worker contexts + // In case there's no Referrer Policy in Request, this will be passed to + // channel. + ReferrerPolicy mEnvironmentReferrerPolicy; + RequestMode mMode; + RequestCredentials mCredentialsMode; + LoadTainting mResponseTainting = LoadTainting::Basic; + RequestCache mCacheMode; + RequestRedirect mRedirectMode; + nsString mIntegrity; + bool mMozErrors = false; + nsCString mFragment; + bool mSkipServiceWorker = false; + bool mSkipWasmCaching = false; + bool mSynchronous = false; + bool mUnsafeRequest = false; + bool mUseURLCredentials = false; + // This is only set when Request.overrideContentPolicyType() has been set. + // It is illegal to pass such a Request object to a fetch() method unless + // if the caller has chrome privileges. + bool mContentPolicyTypeOverridden = false; + nsILoadInfo::CrossOriginEmbedderPolicy mEmbedderPolicy = + nsILoadInfo::EMBEDDER_POLICY_NULL; + + UniquePtr<mozilla::ipc::PrincipalInfo> mPrincipalInfo; + + // Following members are specific for the FetchEvent.request or + // NavigationPreload request which is extracted from the + // InterceptedHttpChannel. + // Notice that these members would not be copied when calling + // InternalRequest::GetRequestConstructorCopy() since these information should + // not be propagated when copying the Request in ServiceWorker script. + + // This is the trigging principalInfo of the InterceptedHttpChannel. + UniquePtr<mozilla::ipc::PrincipalInfo> mInterceptionTriggeringPrincipalInfo; + + // This is the contentPolicyType of the InterceptedHttpChannel. + nsContentPolicyType mInterceptionContentPolicyType{ + nsIContentPolicy::TYPE_INVALID}; + + // This is the redirect history of the InterceptedHttpChannel. + CopyableTArray<RedirectHistoryEntryInfo> mInterceptionRedirectChain; + + // This indicates that the InterceptedHttpChannel is a third party channel. + bool mInterceptionFromThirdParty{false}; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_InternalRequest_h diff --git a/dom/fetch/InternalResponse.cpp b/dom/fetch/InternalResponse.cpp new file mode 100644 index 0000000000..b54e400941 --- /dev/null +++ b/dom/fetch/InternalResponse.cpp @@ -0,0 +1,458 @@ +/* -*- 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 "InternalResponse.h" + +#include "mozilla/Assertions.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/FetchStreamUtils.h" +#include "mozilla/dom/FetchTypes.h" +#include "mozilla/dom/InternalHeaders.h" +#include "mozilla/dom/cache/CacheTypes.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/ipc/IPCStreamUtils.h" +#include "mozilla/RandomNum.h" +#include "mozilla/RemoteLazyInputStreamStorage.h" +#include "nsIRandomGenerator.h" +#include "nsStreamUtils.h" + +namespace mozilla::dom { + +namespace { + +// Const variable for generate padding size +// XXX This will be tweaked to something more meaningful in Bug 1383656. +const uint32_t kMaxRandomNumber = 102400; + +} // namespace + +InternalResponse::InternalResponse(uint16_t aStatus, + const nsACString& aStatusText, + RequestCredentials aCredentialsMode) + : mType(ResponseType::Default), + mStatus(aStatus), + mStatusText(aStatusText), + mHeaders(new InternalHeaders(HeadersGuardEnum::Response)), + mBodySize(UNKNOWN_BODY_SIZE), + mPaddingSize(UNKNOWN_PADDING_SIZE), + mErrorCode(NS_OK), + mCredentialsMode(aCredentialsMode), + mCloned(false) {} + +/* static */ SafeRefPtr<InternalResponse> InternalResponse::FromIPC( + const ParentToParentInternalResponse& aIPCResponse) { + MOZ_ASSERT(XRE_IsParentProcess()); + return FromIPCTemplate(aIPCResponse); +} + +/* static */ SafeRefPtr<InternalResponse> InternalResponse::FromIPC( + const ParentToChildInternalResponse& aIPCResponse) { + MOZ_ASSERT(XRE_IsContentProcess()); + return FromIPCTemplate(aIPCResponse); +} + +template <typename T> +/* static */ SafeRefPtr<InternalResponse> InternalResponse::FromIPCTemplate( + const T& aIPCResponse) { + if (aIPCResponse.metadata().type() == ResponseType::Error) { + return InternalResponse::NetworkError(aIPCResponse.metadata().errorCode()); + } + + SafeRefPtr<InternalResponse> response = MakeSafeRefPtr<InternalResponse>( + aIPCResponse.metadata().status(), aIPCResponse.metadata().statusText()); + + response->SetURLList(aIPCResponse.metadata().urlList()); + response->mHeaders = + new InternalHeaders(aIPCResponse.metadata().headers(), + aIPCResponse.metadata().headersGuard()); + + if (aIPCResponse.body()) { + auto bodySize = aIPCResponse.bodySize(); + auto body = ToInputStream(*aIPCResponse.body()); + response->SetBody(body.get(), bodySize); + } + + response->SetAlternativeDataType( + aIPCResponse.metadata().alternativeDataType()); + + if (aIPCResponse.alternativeBody()) { + auto alternativeBody = ToInputStream(*aIPCResponse.alternativeBody()); + response->SetAlternativeBody(alternativeBody.get()); + } + + response->InitChannelInfo(aIPCResponse.metadata().securityInfo()); + + if (aIPCResponse.metadata().principalInfo()) { + response->SetPrincipalInfo(MakeUnique<mozilla::ipc::PrincipalInfo>( + aIPCResponse.metadata().principalInfo().ref())); + } + + nsAutoCString bodyBlobURISpec(aIPCResponse.metadata().bodyBlobURISpec()); + response->SetBodyBlobURISpec(bodyBlobURISpec); + nsAutoString bodyLocalPath(aIPCResponse.metadata().bodyLocalPath()); + response->SetBodyLocalPath(bodyLocalPath); + + response->mCredentialsMode = aIPCResponse.metadata().credentialsMode(); + + switch (aIPCResponse.metadata().type()) { + case ResponseType::Basic: + response = response->BasicResponse(); + break; + case ResponseType::Cors: + response = response->CORSResponse(); + break; + case ResponseType::Default: + break; + case ResponseType::Opaque: + response = response->OpaqueResponse(); + break; + case ResponseType::Opaqueredirect: + response = response->OpaqueRedirectResponse(); + break; + default: + MOZ_CRASH("Unexpected ResponseType!"); + } + + MOZ_ASSERT(response); + + return response; +} + +InternalResponse::~InternalResponse() = default; + +InternalResponseMetadata InternalResponse::GetMetadata() { + nsTArray<HeadersEntry> headers; + HeadersGuardEnum headersGuard; + UnfilteredHeaders()->ToIPC(headers, headersGuard); + + Maybe<mozilla::ipc::PrincipalInfo> principalInfo = + mPrincipalInfo ? Some(*mPrincipalInfo) : Nothing(); + + nsAutoCString bodyBlobURISpec(BodyBlobURISpec()); + nsAutoString bodyLocalPath(BodyLocalPath()); + + // Note: all the arguments are copied rather than moved, which would be more + // efficient, because there's no move-friendly constructor generated. + nsCOMPtr<nsITransportSecurityInfo> securityInfo(mChannelInfo.SecurityInfo()); + return InternalResponseMetadata( + mType, GetUnfilteredURLList(), GetUnfilteredStatus(), + GetUnfilteredStatusText(), headersGuard, headers, mErrorCode, + GetAlternativeDataType(), securityInfo, principalInfo, bodyBlobURISpec, + bodyLocalPath, GetCredentialsMode()); +} + +void InternalResponse::ToChildToParentInternalResponse( + ChildToParentInternalResponse* aIPCResponse, + mozilla::ipc::PBackgroundChild* aManager) { + *aIPCResponse = ChildToParentInternalResponse(GetMetadata(), Nothing(), + UNKNOWN_BODY_SIZE, Nothing()); + + nsCOMPtr<nsIInputStream> body; + int64_t bodySize; + GetUnfilteredBody(getter_AddRefs(body), &bodySize); + + if (body) { + aIPCResponse->body().emplace(ChildToParentStream()); + aIPCResponse->bodySize() = bodySize; + + DebugOnly<bool> ok = mozilla::ipc::SerializeIPCStream( + body.forget(), aIPCResponse->body()->stream(), /* aAllowLazy */ false); + MOZ_ASSERT(ok); + } + + nsCOMPtr<nsIInputStream> alternativeBody = TakeAlternativeBody(); + if (alternativeBody) { + aIPCResponse->alternativeBody().emplace(ChildToParentStream()); + + DebugOnly<bool> ok = mozilla::ipc::SerializeIPCStream( + alternativeBody.forget(), aIPCResponse->alternativeBody()->stream(), + /* aAllowLazy */ false); + MOZ_ASSERT(ok); + } +} + +ParentToParentInternalResponse +InternalResponse::ToParentToParentInternalResponse() { + ParentToParentInternalResponse result(GetMetadata(), Nothing(), + UNKNOWN_BODY_SIZE, Nothing()); + + nsCOMPtr<nsIInputStream> body; + int64_t bodySize; + GetUnfilteredBody(getter_AddRefs(body), &bodySize); + + if (body) { + result.body() = Some(ToParentToParentStream(WrapNotNull(body), bodySize)); + result.bodySize() = bodySize; + } + + nsCOMPtr<nsIInputStream> alternativeBody = TakeAlternativeBody(); + if (alternativeBody) { + result.alternativeBody() = Some(ToParentToParentStream( + WrapNotNull(alternativeBody), UNKNOWN_BODY_SIZE)); + } + + return result; +} + +ParentToChildInternalResponse InternalResponse::ToParentToChildInternalResponse( + NotNull<mozilla::ipc::PBackgroundParent*> aBackgroundParent) { + ParentToChildInternalResponse result(GetMetadata(), Nothing(), + UNKNOWN_BODY_SIZE, Nothing()); + + nsCOMPtr<nsIInputStream> body; + int64_t bodySize; + GetUnfilteredBody(getter_AddRefs(body), &bodySize); + + if (body) { + ParentToChildStream bodyStream = ToParentToChildStream( + WrapNotNull(body), bodySize, aBackgroundParent, mSerializeAsLazy); + // The body stream can fail to serialize as an IPCStream. In the case, the + // IPCStream's type would be T__None. Don't set up IPCInternalResponse's + // body with the failed IPCStream. + if (mSerializeAsLazy || bodyStream.get_IPCStream().stream().type() != + mozilla::ipc::InputStreamParams::T__None) { + result.body() = Some(bodyStream); + result.bodySize() = bodySize; + } + } + + nsCOMPtr<nsIInputStream> alternativeBody = TakeAlternativeBody(); + if (alternativeBody) { + ParentToChildStream alterBodyStream = + ToParentToChildStream(WrapNotNull(alternativeBody), UNKNOWN_BODY_SIZE, + aBackgroundParent, mSerializeAsLazy); + // The body stream can fail to serialize as an IPCStream. In the case, the + // IPCStream's type would be T__None. Don't set up IPCInternalResponse's + // body with the failed IPCStream. + if (mSerializeAsLazy || alterBodyStream.get_IPCStream().stream().type() != + mozilla::ipc::InputStreamParams::T__None) { + result.alternativeBody() = Some(alterBodyStream); + } + } + + return result; +} + +SafeRefPtr<InternalResponse> InternalResponse::Clone(CloneType aCloneType) { + SafeRefPtr<InternalResponse> clone = CreateIncompleteCopy(); + clone->mCloned = (mCloned = true); + + clone->mHeaders = new InternalHeaders(*mHeaders); + + // Make sure the clone response will have the same padding size. + clone->mPaddingInfo = mPaddingInfo; + clone->mPaddingSize = mPaddingSize; + + clone->mCacheInfoChannel = mCacheInfoChannel; + clone->mCredentialsMode = mCredentialsMode; + + if (mWrappedResponse) { + clone->mWrappedResponse = mWrappedResponse->Clone(aCloneType); + MOZ_ASSERT(!mBody); + return clone; + } + + if (!mBody || aCloneType == eDontCloneInputStream) { + return clone; + } + + nsCOMPtr<nsIInputStream> clonedBody; + nsCOMPtr<nsIInputStream> replacementBody; + + nsresult rv = NS_CloneInputStream(mBody, getter_AddRefs(clonedBody), + getter_AddRefs(replacementBody)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + clone->mBody.swap(clonedBody); + if (replacementBody) { + mBody.swap(replacementBody); + } + + return clone; +} + +SafeRefPtr<InternalResponse> InternalResponse::BasicResponse() { + MOZ_ASSERT(!mWrappedResponse, + "Can't BasicResponse a already wrapped response"); + SafeRefPtr<InternalResponse> basic = CreateIncompleteCopy(); + basic->mType = ResponseType::Basic; + basic->mHeaders = InternalHeaders::BasicHeaders(Headers()); + basic->mWrappedResponse = SafeRefPtrFromThis(); + return basic; +} + +SafeRefPtr<InternalResponse> InternalResponse::CORSResponse() { + MOZ_ASSERT(!mWrappedResponse, + "Can't CORSResponse a already wrapped response"); + SafeRefPtr<InternalResponse> cors = CreateIncompleteCopy(); + cors->mType = ResponseType::Cors; + cors->mHeaders = InternalHeaders::CORSHeaders(Headers(), mCredentialsMode); + cors->mWrappedResponse = SafeRefPtrFromThis(); + return cors; +} + +uint32_t InternalResponse::GetPaddingInfo() { + // If it's an opaque response, the paddingInfo should be generated only when + // paddingSize is unknown size. + // If it's not, the paddingInfo should be nothing and the paddingSize should + // be unknown size. + MOZ_DIAGNOSTIC_ASSERT( + (mType == ResponseType::Opaque && mPaddingSize == UNKNOWN_PADDING_SIZE && + mPaddingInfo.isSome()) || + (mType == ResponseType::Opaque && mPaddingSize != UNKNOWN_PADDING_SIZE && + mPaddingInfo.isNothing()) || + (mType != ResponseType::Opaque && mPaddingSize == UNKNOWN_PADDING_SIZE && + mPaddingInfo.isNothing())); + return mPaddingInfo.isSome() ? mPaddingInfo.ref() : 0; +} + +nsresult InternalResponse::GeneratePaddingInfo() { + MOZ_DIAGNOSTIC_ASSERT(mType == ResponseType::Opaque); + MOZ_DIAGNOSTIC_ASSERT(mPaddingSize == UNKNOWN_PADDING_SIZE); + + // Utilize random generator to generator a random number + nsresult rv; + uint32_t randomNumber = 0; + nsCOMPtr<nsIRandomGenerator> randomGenerator = + do_GetService("@mozilla.org/security/random-generator;1", &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + Maybe<uint64_t> maybeRandomNum = RandomUint64(); + if (maybeRandomNum.isSome()) { + mPaddingInfo.emplace(uint32_t(maybeRandomNum.value() % kMaxRandomNumber)); + return NS_OK; + } + return rv; + } + + MOZ_DIAGNOSTIC_ASSERT(randomGenerator); + + uint8_t* buffer; + rv = randomGenerator->GenerateRandomBytes(sizeof(randomNumber), &buffer); + if (NS_WARN_IF(NS_FAILED(rv))) { + Maybe<uint64_t> maybeRandomNum = RandomUint64(); + if (maybeRandomNum.isSome()) { + mPaddingInfo.emplace(uint32_t(maybeRandomNum.value() % kMaxRandomNumber)); + return NS_OK; + } + return rv; + } + + memcpy(&randomNumber, buffer, sizeof(randomNumber)); + free(buffer); + + mPaddingInfo.emplace(randomNumber % kMaxRandomNumber); + + return rv; +} + +int64_t InternalResponse::GetPaddingSize() { + // We initialize padding size to an unknown size (-1). After cached, we only + // pad opaque response. Opaque response's padding size might be unknown before + // cached. + MOZ_DIAGNOSTIC_ASSERT(mType == ResponseType::Opaque || + mPaddingSize == UNKNOWN_PADDING_SIZE); + MOZ_DIAGNOSTIC_ASSERT(mPaddingSize == UNKNOWN_PADDING_SIZE || + mPaddingSize >= 0); + + return mPaddingSize; +} + +void InternalResponse::SetPaddingSize(int64_t aPaddingSize) { + // We should only pad the opaque response. + MOZ_DIAGNOSTIC_ASSERT( + (mType == ResponseType::Opaque) != + (aPaddingSize == InternalResponse::UNKNOWN_PADDING_SIZE)); + MOZ_DIAGNOSTIC_ASSERT(aPaddingSize == UNKNOWN_PADDING_SIZE || + aPaddingSize >= 0); + + mPaddingSize = aPaddingSize; +} + +void InternalResponse::SetPrincipalInfo( + UniquePtr<mozilla::ipc::PrincipalInfo> aPrincipalInfo) { + mPrincipalInfo = std::move(aPrincipalInfo); +} + +LoadTainting InternalResponse::GetTainting() const { + switch (mType) { + case ResponseType::Cors: + return LoadTainting::CORS; + case ResponseType::Opaque: + return LoadTainting::Opaque; + default: + return LoadTainting::Basic; + } +} + +SafeRefPtr<InternalResponse> InternalResponse::Unfiltered() { + SafeRefPtr<InternalResponse> ref = mWrappedResponse.clonePtr(); + if (!ref) { + ref = SafeRefPtrFromThis(); + } + return ref; +} + +SafeRefPtr<InternalResponse> InternalResponse::OpaqueResponse() { + MOZ_ASSERT(!mWrappedResponse, + "Can't OpaqueResponse a already wrapped response"); + SafeRefPtr<InternalResponse> response = + MakeSafeRefPtr<InternalResponse>(0, ""_ns); + response->mType = ResponseType::Opaque; + response->mChannelInfo = mChannelInfo; + if (mPrincipalInfo) { + response->mPrincipalInfo = + MakeUnique<mozilla::ipc::PrincipalInfo>(*mPrincipalInfo); + } + response->mWrappedResponse = SafeRefPtrFromThis(); + return response; +} + +SafeRefPtr<InternalResponse> InternalResponse::OpaqueRedirectResponse() { + MOZ_ASSERT(!mWrappedResponse, + "Can't OpaqueRedirectResponse a already wrapped response"); + MOZ_ASSERT(!mURLList.IsEmpty(), + "URLList should not be emtpy for internalResponse"); + SafeRefPtr<InternalResponse> response = OpaqueResponse(); + response->mType = ResponseType::Opaqueredirect; + response->mURLList = mURLList.Clone(); + return response; +} + +SafeRefPtr<InternalResponse> InternalResponse::CreateIncompleteCopy() { + SafeRefPtr<InternalResponse> copy = + MakeSafeRefPtr<InternalResponse>(mStatus, mStatusText); + copy->mType = mType; + copy->mURLList = mURLList.Clone(); + copy->mChannelInfo = mChannelInfo; + if (mPrincipalInfo) { + copy->mPrincipalInfo = + MakeUnique<mozilla::ipc::PrincipalInfo>(*mPrincipalInfo); + } + return copy; +} + +ParentToChildInternalResponse ToParentToChild( + const ParentToParentInternalResponse& aResponse, + NotNull<mozilla::ipc::PBackgroundParent*> aBackgroundParent) { + ParentToChildInternalResponse result(aResponse.metadata(), Nothing(), + aResponse.bodySize(), Nothing()); + + if (aResponse.body().isSome()) { + result.body() = Some(ToParentToChildStream( + aResponse.body().ref(), aResponse.bodySize(), aBackgroundParent)); + } + if (aResponse.alternativeBody().isSome()) { + result.alternativeBody() = Some(ToParentToChildStream( + aResponse.alternativeBody().ref(), InternalResponse::UNKNOWN_BODY_SIZE, + aBackgroundParent)); + } + + return result; +} + +} // namespace mozilla::dom diff --git a/dom/fetch/InternalResponse.h b/dom/fetch/InternalResponse.h new file mode 100644 index 0000000000..e8a492a314 --- /dev/null +++ b/dom/fetch/InternalResponse.h @@ -0,0 +1,419 @@ +/* -*- 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_InternalResponse_h +#define mozilla_dom_InternalResponse_h + +#include "mozilla/dom/FetchTypes.h" +#include "nsIInputStream.h" +#include "nsICacheInfoChannel.h" +#include "nsISupportsImpl.h" +#include "nsProxyRelease.h" + +#include "mozilla/dom/InternalHeaders.h" +#include "mozilla/dom/RequestBinding.h" +#include "mozilla/dom/ResponseBinding.h" +#include "mozilla/dom/ChannelInfo.h" +#include "mozilla/dom/SafeRefPtr.h" +#include "mozilla/NotNull.h" +#include "mozilla/UniquePtr.h" + +namespace mozilla { +namespace ipc { +class PBackgroundChild; +class PBackgroundParent; +class PrincipalInfo; +} // namespace ipc + +namespace dom { + +class ChildToParentInternalResponse; +class InternalHeaders; +class ParentToChildInternalResponse; +class ParentToParentInternalResponse; + +class InternalResponse final : public AtomicSafeRefCounted<InternalResponse> { + friend class FetchDriver; + + public: + MOZ_DECLARE_REFCOUNTED_TYPENAME(InternalResponse) + + InternalResponse( + uint16_t aStatus, const nsACString& aStatusText, + RequestCredentials aCredentialsMode = RequestCredentials::Omit); + + static SafeRefPtr<InternalResponse> FromIPC( + const ParentToChildInternalResponse& aIPCResponse); + + static SafeRefPtr<InternalResponse> FromIPC( + const ParentToParentInternalResponse& aIPCResponse); + + void ToChildToParentInternalResponse( + ChildToParentInternalResponse* aIPCResponse, + mozilla::ipc::PBackgroundChild* aManager); + + ParentToParentInternalResponse ToParentToParentInternalResponse(); + + ParentToChildInternalResponse ToParentToChildInternalResponse( + NotNull<mozilla::ipc::PBackgroundParent*> aBackgroundParent); + + enum CloneType { + eCloneInputStream, + eDontCloneInputStream, + }; + + SafeRefPtr<InternalResponse> Clone(CloneType aCloneType); + + static SafeRefPtr<InternalResponse> NetworkError(nsresult aRv) { + MOZ_DIAGNOSTIC_ASSERT(NS_FAILED(aRv)); + SafeRefPtr<InternalResponse> response = + MakeSafeRefPtr<InternalResponse>(0, ""_ns); + ErrorResult result; + response->Headers()->SetGuard(HeadersGuardEnum::Immutable, result); + MOZ_ASSERT(!result.Failed()); + response->mType = ResponseType::Error; + response->mErrorCode = aRv; + return response; + } + + SafeRefPtr<InternalResponse> OpaqueResponse(); + + SafeRefPtr<InternalResponse> OpaqueRedirectResponse(); + + SafeRefPtr<InternalResponse> BasicResponse(); + + SafeRefPtr<InternalResponse> CORSResponse(); + + ResponseType Type() const { + MOZ_ASSERT_IF(mType == ResponseType::Error, !mWrappedResponse); + MOZ_ASSERT_IF(mType == ResponseType::Default, !mWrappedResponse); + MOZ_ASSERT_IF(mType == ResponseType::Basic, mWrappedResponse); + MOZ_ASSERT_IF(mType == ResponseType::Cors, mWrappedResponse); + MOZ_ASSERT_IF(mType == ResponseType::Opaque, mWrappedResponse); + MOZ_ASSERT_IF(mType == ResponseType::Opaqueredirect, mWrappedResponse); + return mType; + } + + bool IsError() const { return Type() == ResponseType::Error; } + // GetUrl should return last fetch URL in response's url list and null if + // response's url list is the empty list. + const nsCString& GetURL() const { + // Empty urlList when response is a synthetic response. + if (mURLList.IsEmpty()) { + return EmptyCString(); + } + return mURLList.LastElement(); + } + void GetURLList(nsTArray<nsCString>& aURLList) const { + aURLList.Assign(mURLList); + } + const nsCString& GetUnfilteredURL() const { + if (mWrappedResponse) { + return mWrappedResponse->GetURL(); + } + return GetURL(); + } + void GetUnfilteredURLList(nsTArray<nsCString>& aURLList) const { + if (mWrappedResponse) { + return mWrappedResponse->GetURLList(aURLList); + } + + return GetURLList(aURLList); + } + + nsTArray<nsCString> GetUnfilteredURLList() const { + nsTArray<nsCString> list; + GetUnfilteredURLList(list); + return list; + } + + void SetURLList(const nsTArray<nsCString>& aURLList) { + mURLList.Assign(aURLList); + +#ifdef DEBUG + for (uint32_t i = 0; i < mURLList.Length(); ++i) { + MOZ_ASSERT(mURLList[i].Find("#"_ns) == kNotFound); + } +#endif + } + + uint16_t GetStatus() const { return mStatus; } + + uint16_t GetUnfilteredStatus() const { + if (mWrappedResponse) { + return mWrappedResponse->GetStatus(); + } + + return GetStatus(); + } + + const nsCString& GetStatusText() const { return mStatusText; } + + const nsCString& GetUnfilteredStatusText() const { + if (mWrappedResponse) { + return mWrappedResponse->GetStatusText(); + } + + return GetStatusText(); + } + + InternalHeaders* Headers() { return mHeaders; } + + InternalHeaders* UnfilteredHeaders() { + if (mWrappedResponse) { + return mWrappedResponse->Headers(); + }; + + return Headers(); + } + + void GetUnfilteredBody(nsIInputStream** aStream, + int64_t* aBodySize = nullptr) { + if (mWrappedResponse) { + MOZ_ASSERT(!mBody); + return mWrappedResponse->GetBody(aStream, aBodySize); + } + nsCOMPtr<nsIInputStream> stream = mBody; + stream.forget(aStream); + if (aBodySize) { + *aBodySize = mBodySize; + } + } + + void GetBody(nsIInputStream** aStream, int64_t* aBodySize = nullptr) { + if (Type() == ResponseType::Opaque || + Type() == ResponseType::Opaqueredirect) { + *aStream = nullptr; + if (aBodySize) { + *aBodySize = UNKNOWN_BODY_SIZE; + } + return; + } + + GetUnfilteredBody(aStream, aBodySize); + } + + void SetBodyBlobURISpec(nsACString& aBlobURISpec) { + mBodyBlobURISpec = aBlobURISpec; + } + + const nsACString& BodyBlobURISpec() const { + if (mWrappedResponse) { + return mWrappedResponse->BodyBlobURISpec(); + } + return mBodyBlobURISpec; + } + + void SetBodyLocalPath(nsAString& aLocalPath) { mBodyLocalPath = aLocalPath; } + + const nsAString& BodyLocalPath() const { + if (mWrappedResponse) { + return mWrappedResponse->BodyLocalPath(); + } + return mBodyLocalPath; + } + + void SetBody(nsIInputStream* aBody, int64_t aBodySize) { + if (mWrappedResponse) { + return mWrappedResponse->SetBody(aBody, aBodySize); + } + // A request's body may not be reset once set. + MOZ_ASSERT(!mBody); + MOZ_ASSERT(mBodySize == UNKNOWN_BODY_SIZE); + // Check arguments. + MOZ_ASSERT(aBodySize == UNKNOWN_BODY_SIZE || aBodySize >= 0); + // If body is not given, then size must be unknown. + MOZ_ASSERT_IF(!aBody, aBodySize == UNKNOWN_BODY_SIZE); + + mBody = aBody; + mBodySize = aBodySize; + } + + uint32_t GetPaddingInfo(); + + nsresult GeneratePaddingInfo(); + + int64_t GetPaddingSize(); + + void SetPaddingSize(int64_t aPaddingSize); + + void SetAlternativeDataType(const nsACString& aAltDataType) { + if (mWrappedResponse) { + return mWrappedResponse->SetAlternativeDataType(aAltDataType); + } + + MOZ_DIAGNOSTIC_ASSERT(mAlternativeDataType.IsEmpty()); + + mAlternativeDataType.Assign(aAltDataType); + } + + const nsCString& GetAlternativeDataType() { + if (mWrappedResponse) { + return mWrappedResponse->GetAlternativeDataType(); + } + + return mAlternativeDataType; + } + + void SetAlternativeBody(nsIInputStream* aAlternativeBody) { + if (mWrappedResponse) { + return mWrappedResponse->SetAlternativeBody(aAlternativeBody); + } + // A request's body may not be reset once set. + MOZ_DIAGNOSTIC_ASSERT(!mAlternativeBody); + + mAlternativeBody = aAlternativeBody; + } + + already_AddRefed<nsIInputStream> TakeAlternativeBody() { + if (mWrappedResponse) { + return mWrappedResponse->TakeAlternativeBody(); + } + + if (!mAlternativeBody) { + return nullptr; + } + + // cleanup the non-alternative body here. + // Once alternative data is used, the real body is no need anymore. + mBody = nullptr; + mBodySize = UNKNOWN_BODY_SIZE; + return mAlternativeBody.forget(); + } + + void SetCacheInfoChannel( + const nsMainThreadPtrHandle<nsICacheInfoChannel>& aCacheInfoChannel) { + if (mWrappedResponse) { + return mWrappedResponse->SetCacheInfoChannel(aCacheInfoChannel); + } + MOZ_ASSERT(!mCacheInfoChannel); + mCacheInfoChannel = aCacheInfoChannel; + } + + nsMainThreadPtrHandle<nsICacheInfoChannel> TakeCacheInfoChannel() { + if (mWrappedResponse) { + return mWrappedResponse->TakeCacheInfoChannel(); + } + nsMainThreadPtrHandle<nsICacheInfoChannel> rtn = mCacheInfoChannel; + mCacheInfoChannel = nullptr; + return rtn; + } + + bool HasCacheInfoChannel() const { + if (mWrappedResponse) { + return !!mWrappedResponse->HasCacheInfoChannel(); + } + return !!mCacheInfoChannel; + } + + bool HasBeenCloned() const { return mCloned; } + + void SetSerializeAsLazy(bool aAllow) { mSerializeAsLazy = aAllow; } + bool CanSerializeAsLazy() const { return mSerializeAsLazy; } + + void InitChannelInfo(nsIChannel* aChannel) { + mChannelInfo.InitFromChannel(aChannel); + } + + void InitChannelInfo(nsITransportSecurityInfo* aSecurityInfo) { + mChannelInfo.InitFromTransportSecurityInfo(aSecurityInfo); + } + + void InitChannelInfo(const ChannelInfo& aChannelInfo) { + mChannelInfo = aChannelInfo; + } + + const ChannelInfo& GetChannelInfo() const { return mChannelInfo; } + + const UniquePtr<mozilla::ipc::PrincipalInfo>& GetPrincipalInfo() const { + return mPrincipalInfo; + } + + bool IsRedirected() const { return mURLList.Length() > 1; } + + nsresult GetErrorCode() const { return mErrorCode; } + + // Takes ownership of the principal info. + void SetPrincipalInfo(UniquePtr<mozilla::ipc::PrincipalInfo> aPrincipalInfo); + + LoadTainting GetTainting() const; + + SafeRefPtr<InternalResponse> Unfiltered(); + + InternalResponseMetadata GetMetadata(); + + RequestCredentials GetCredentialsMode() const { + if (mWrappedResponse) { + return mWrappedResponse->GetCredentialsMode(); + } + return mCredentialsMode; + } + + ~InternalResponse(); + + private: + explicit InternalResponse(const InternalResponse& aOther) = delete; + InternalResponse& operator=(const InternalResponse&) = delete; + + // Returns an instance of InternalResponse which is a copy of this + // InternalResponse, except headers, body and wrapped response (if any) which + // are left uninitialized. Used for cloning and filtering. + SafeRefPtr<InternalResponse> CreateIncompleteCopy(); + + template <typename T> + static SafeRefPtr<InternalResponse> FromIPCTemplate(const T& aIPCResponse); + + ResponseType mType; + // A response has an associated url list (a list of zero or more fetch URLs). + // Unless stated otherwise, it is the empty list. The current url is the last + // element in mURLlist + nsTArray<nsCString> mURLList; + const uint16_t mStatus; + const nsCString mStatusText; + RefPtr<InternalHeaders> mHeaders; + nsCOMPtr<nsIInputStream> mBody; + nsCString mBodyBlobURISpec; + nsString mBodyLocalPath; + int64_t mBodySize; + // It's used to passed to the CacheResponse to generate padding size. Once, we + // generate the padding size for resposne, we don't need it anymore. + Maybe<uint32_t> mPaddingInfo; + int64_t mPaddingSize; + nsresult mErrorCode; + RequestCredentials mCredentialsMode; + + // For alternative data such as JS Bytecode cached in the HTTP cache. + nsCString mAlternativeDataType; + nsCOMPtr<nsIInputStream> mAlternativeBody; + nsMainThreadPtrHandle<nsICacheInfoChannel> mCacheInfoChannel; + bool mCloned; + // boolean to indicate the body/alternativeBody will be serialized as a + // RemoteLazyInputStream. + bool mSerializeAsLazy{true}; + + public: + static constexpr int64_t UNKNOWN_BODY_SIZE = -1; + static constexpr int64_t UNKNOWN_PADDING_SIZE = -1; + + private: + ChannelInfo mChannelInfo; + UniquePtr<mozilla::ipc::PrincipalInfo> mPrincipalInfo; + + // For filtered responses. + // Cache, and SW interception should always serialize/access the underlying + // unfiltered headers and when deserializing, create an InternalResponse + // with the unfiltered headers followed by wrapping it. + SafeRefPtr<InternalResponse> mWrappedResponse; +}; + +ParentToChildInternalResponse ToParentToChild( + const ParentToParentInternalResponse& aResponse, + NotNull<mozilla::ipc::PBackgroundParent*> aBackgroundParent); + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_InternalResponse_h diff --git a/dom/fetch/PFetch.ipdl b/dom/fetch/PFetch.ipdl new file mode 100644 index 0000000000..904ef0738d --- /dev/null +++ b/dom/fetch/PFetch.ipdl @@ -0,0 +1,56 @@ +/* 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 protocol PBackground; + +include ClientIPCTypes; +include FetchTypes; +include IPCServiceWorkerDescriptor; +include NeckoChannelParams; +include PBackgroundSharedTypes; +include PerformanceTimingTypes; + +namespace mozilla { +namespace dom { + +struct FetchOpArgs{ + IPCInternalRequest request; + PrincipalInfo principalInfo; + nsCString workerScript; + IPCClientInfo clientInfo; + IPCServiceWorkerDescriptor? controller; + CookieJarSettingsArgs? cookieJarSettings; + bool needOnDataAvailable; + bool hasCSPEventListener; + uint64_t associatedBrowsingContextID; +}; + +protocol PFetch { + manager PBackground; + + parent: + async FetchOp(FetchOpArgs aArgs); + + async AbortFetchOp(); + + child: + async OnResponseAvailableInternal(ParentToChildInternalResponse aResponse); + + async OnResponseEnd(ResponseEndArgs aResponseEndArgs); + + async OnDataAvailable(); + + async OnFlushConsoleReport(ConsoleReportCollected[] aReports); + + async OnCSPViolationEvent(nsString aJSON); + + async OnReportPerformanceTiming(ResponseTiming aTiming); + + async OnNotifyNetworkMonitorAlternateStack(uint64_t aChannelID); + + async __delete__(nsresult aResult); +}; + +} +} diff --git a/dom/fetch/Request.cpp b/dom/fetch/Request.cpp new file mode 100644 index 0000000000..4fcb19b6f7 --- /dev/null +++ b/dom/fetch/Request.cpp @@ -0,0 +1,658 @@ +/* -*- 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 "Request.h" + +#include "js/Value.h" +#include "nsIURI.h" +#include "nsNetUtil.h" +#include "nsPIDOMWindow.h" + +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/Headers.h" +#include "mozilla/dom/Fetch.h" +#include "mozilla/dom/FetchUtil.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/URL.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "mozilla/dom/WindowContext.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/Unused.h" + +#include "mozilla/dom/ReadableStreamDefaultReader.h" + +namespace mozilla::dom { + +NS_IMPL_ADDREF_INHERITED(Request, FetchBody<Request>) +NS_IMPL_RELEASE_INHERITED(Request, FetchBody<Request>) + +// Can't use _INHERITED macro here because FetchBody<Request> is abstract. +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Request) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(Request, FetchBody<Request>) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mHeaders) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSignal) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mFetchStreamReader) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(Request, FetchBody<Request>) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHeaders) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSignal) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFetchStreamReader) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Request) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY +NS_INTERFACE_MAP_END_INHERITING(FetchBody<Request>) + +Request::Request(nsIGlobalObject* aOwner, SafeRefPtr<InternalRequest> aRequest, + AbortSignal* aSignal) + : FetchBody<Request>(aOwner), mRequest(std::move(aRequest)) { + MOZ_ASSERT(mRequest->Headers()->Guard() == HeadersGuardEnum::Immutable || + mRequest->Headers()->Guard() == HeadersGuardEnum::Request || + mRequest->Headers()->Guard() == HeadersGuardEnum::Request_no_cors); + if (aSignal) { + // If we don't have a signal as argument, we will create it when required by + // content, otherwise the Request's signal must follow what has been passed. + JS::Rooted<JS::Value> reason(RootingCx(), aSignal->RawReason()); + mSignal = new AbortSignal(aOwner, aSignal->Aborted(), reason); + if (!mSignal->Aborted()) { + mSignal->Follow(aSignal); + } + } +} + +Request::~Request() = default; + +SafeRefPtr<InternalRequest> Request::GetInternalRequest() { + return mRequest.clonePtr(); +} + +namespace { +already_AddRefed<nsIURI> ParseURLFromDocument(Document* aDocument, + const nsAString& aInput, + ErrorResult& aRv) { + MOZ_ASSERT(aDocument); + MOZ_ASSERT(NS_IsMainThread()); + + // Don't use NS_ConvertUTF16toUTF8 because that doesn't let us handle OOM. + nsAutoCString input; + if (!AppendUTF16toUTF8(aInput, input, fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return nullptr; + } + + nsCOMPtr<nsIURI> resolvedURI; + nsresult rv = NS_NewURI(getter_AddRefs(resolvedURI), input, nullptr, + aDocument->GetBaseURI()); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.ThrowTypeError<MSG_INVALID_URL>(input); + } + return resolvedURI.forget(); +} +void GetRequestURLFromDocument(Document* aDocument, const nsAString& aInput, + nsAString& aRequestURL, nsACString& aURLfragment, + ErrorResult& aRv) { + nsCOMPtr<nsIURI> resolvedURI = ParseURLFromDocument(aDocument, aInput, aRv); + if (aRv.Failed()) { + return; + } + // This fails with URIs with weird protocols, even when they are valid, + // so we ignore the failure + nsAutoCString credentials; + Unused << resolvedURI->GetUserPass(credentials); + if (!credentials.IsEmpty()) { + aRv.ThrowTypeError<MSG_URL_HAS_CREDENTIALS>(NS_ConvertUTF16toUTF8(aInput)); + return; + } + + nsCOMPtr<nsIURI> resolvedURIClone; + aRv = NS_GetURIWithoutRef(resolvedURI, getter_AddRefs(resolvedURIClone)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + nsAutoCString spec; + aRv = resolvedURIClone->GetSpec(spec); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + CopyUTF8toUTF16(spec, aRequestURL); + + // Get the fragment from nsIURI. + aRv = resolvedURI->GetRef(aURLfragment); + if (NS_WARN_IF(aRv.Failed())) { + return; + } +} +already_AddRefed<nsIURI> ParseURLFromChrome(const nsAString& aInput, + ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + // Don't use NS_ConvertUTF16toUTF8 because that doesn't let us handle OOM. + nsAutoCString input; + if (!AppendUTF16toUTF8(aInput, input, fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return nullptr; + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), input); + if (NS_FAILED(rv)) { + aRv.ThrowTypeError<MSG_INVALID_URL>(input); + } + return uri.forget(); +} +void GetRequestURLFromChrome(const nsAString& aInput, nsAString& aRequestURL, + nsACString& aURLfragment, ErrorResult& aRv) { + nsCOMPtr<nsIURI> uri = ParseURLFromChrome(aInput, aRv); + if (aRv.Failed()) { + return; + } + // This fails with URIs with weird protocols, even when they are valid, + // so we ignore the failure + nsAutoCString credentials; + Unused << uri->GetUserPass(credentials); + if (!credentials.IsEmpty()) { + aRv.ThrowTypeError<MSG_URL_HAS_CREDENTIALS>(NS_ConvertUTF16toUTF8(aInput)); + return; + } + + nsCOMPtr<nsIURI> uriClone; + aRv = NS_GetURIWithoutRef(uri, getter_AddRefs(uriClone)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + nsAutoCString spec; + aRv = uriClone->GetSpec(spec); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + CopyUTF8toUTF16(spec, aRequestURL); + + // Get the fragment from nsIURI. + aRv = uri->GetRef(aURLfragment); + if (NS_WARN_IF(aRv.Failed())) { + return; + } +} +already_AddRefed<URL> ParseURLFromWorker(nsIGlobalObject* aGlobal, + const nsAString& aInput, + ErrorResult& aRv) { + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(worker); + worker->AssertIsOnWorkerThread(); + + NS_ConvertUTF8toUTF16 baseURL(worker->GetLocationInfo().mHref); + RefPtr<URL> url = URL::Constructor(aGlobal, aInput, baseURL, aRv); + if (NS_WARN_IF(aRv.Failed())) { + aRv.ThrowTypeError<MSG_INVALID_URL>(NS_ConvertUTF16toUTF8(aInput)); + } + return url.forget(); +} +void GetRequestURLFromWorker(nsIGlobalObject* aGlobal, const nsAString& aInput, + nsAString& aRequestURL, nsACString& aURLfragment, + ErrorResult& aRv) { + RefPtr<URL> url = ParseURLFromWorker(aGlobal, aInput, aRv); + if (aRv.Failed()) { + return; + } + nsString username; + url->GetUsername(username); + + nsString password; + url->GetPassword(password); + + if (!username.IsEmpty() || !password.IsEmpty()) { + aRv.ThrowTypeError<MSG_URL_HAS_CREDENTIALS>(NS_ConvertUTF16toUTF8(aInput)); + return; + } + + // Get the fragment from URL. + nsAutoString fragment; + url->GetHash(fragment); + + // Note: URL::GetHash() includes the "#" and we want the fragment with out + // the hash symbol. + if (!fragment.IsEmpty()) { + CopyUTF16toUTF8(Substring(fragment, 1), aURLfragment); + } + + url->SetHash(u""_ns); + url->GetHref(aRequestURL); +} + +class ReferrerSameOriginChecker final : public WorkerMainThreadRunnable { + public: + ReferrerSameOriginChecker(WorkerPrivate* aWorkerPrivate, + const nsAString& aReferrerURL, nsresult& aResult) + : WorkerMainThreadRunnable(aWorkerPrivate, + "Fetch :: Referrer same origin check"_ns), + mReferrerURL(aReferrerURL), + mResult(aResult) { + mWorkerPrivate->AssertIsOnWorkerThread(); + } + + bool MainThreadRun() override { + nsCOMPtr<nsIURI> uri; + if (NS_SUCCEEDED(NS_NewURI(getter_AddRefs(uri), mReferrerURL))) { + nsCOMPtr<nsIPrincipal> principal = mWorkerPrivate->GetPrincipal(); + if (principal) { + mResult = principal->CheckMayLoad(uri, + /* allowIfInheritsPrincipal */ false); + } + } + return true; + } + + private: + const nsString mReferrerURL; + nsresult& mResult; +}; + +} // namespace + +/*static*/ +SafeRefPtr<Request> Request::Constructor(const GlobalObject& aGlobal, + const RequestOrUSVString& aInput, + const RequestInit& aInit, + ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + return Constructor(global, aGlobal.Context(), aInput, aInit, aRv); +} + +/*static*/ +SafeRefPtr<Request> Request::Constructor(nsIGlobalObject* aGlobal, + JSContext* aCx, + const RequestOrUSVString& aInput, + const RequestInit& aInit, + ErrorResult& aRv) { + bool hasCopiedBody = false; + SafeRefPtr<InternalRequest> request; + + RefPtr<AbortSignal> signal; + + if (aInput.IsRequest()) { + RefPtr<Request> inputReq = &aInput.GetAsRequest(); + nsCOMPtr<nsIInputStream> body; + inputReq->GetBody(getter_AddRefs(body)); + if (inputReq->BodyUsed()) { + aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>(); + return nullptr; + } + + // The body will be copied when GetRequestConstructorCopy() is executed. + if (body) { + hasCopiedBody = true; + } + + request = inputReq->GetInternalRequest(); + signal = inputReq->GetOrCreateSignal(); + } else { + // aInput is USVString. + // We need to get url before we create a InternalRequest. + nsAutoString input; + input.Assign(aInput.GetAsUSVString()); + nsAutoString requestURL; + nsCString fragment; + if (NS_IsMainThread()) { + nsCOMPtr<nsPIDOMWindowInner> inner(do_QueryInterface(aGlobal)); + Document* doc = inner ? inner->GetExtantDoc() : nullptr; + if (doc) { + GetRequestURLFromDocument(doc, input, requestURL, fragment, aRv); + } else { + // If we don't have a document, we must assume that this is a full URL. + GetRequestURLFromChrome(input, requestURL, fragment, aRv); + } + } else { + GetRequestURLFromWorker(aGlobal, input, requestURL, fragment, aRv); + } + if (aRv.Failed()) { + return nullptr; + } + request = MakeSafeRefPtr<InternalRequest>(NS_ConvertUTF16toUTF8(requestURL), + fragment); + } + request = request->GetRequestConstructorCopy(aGlobal, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + RequestMode fallbackMode = RequestMode::EndGuard_; + RequestCredentials fallbackCredentials = RequestCredentials::EndGuard_; + RequestCache fallbackCache = RequestCache::EndGuard_; + if (aInput.IsUSVString()) { + fallbackMode = RequestMode::Cors; + fallbackCredentials = RequestCredentials::Same_origin; + fallbackCache = RequestCache::Default; + } + + RequestMode mode = + aInit.mMode.WasPassed() ? aInit.mMode.Value() : fallbackMode; + RequestCredentials credentials = aInit.mCredentials.WasPassed() + ? aInit.mCredentials.Value() + : fallbackCredentials; + + if (mode == RequestMode::Navigate) { + aRv.ThrowTypeError<MSG_INVALID_REQUEST_MODE>("navigate"); + return nullptr; + } + if (aInit.IsAnyMemberPresent() && request->Mode() == RequestMode::Navigate) { + mode = RequestMode::Same_origin; + } + + if (aInit.IsAnyMemberPresent()) { + request->SetReferrer( + NS_LITERAL_STRING_FROM_CSTRING(kFETCH_CLIENT_REFERRER_STR)); + request->SetReferrerPolicy(ReferrerPolicy::_empty); + } + if (aInit.mReferrer.WasPassed()) { + const nsString& referrer = aInit.mReferrer.Value(); + if (referrer.IsEmpty()) { + request->SetReferrer(u""_ns); + } else { + nsAutoString referrerURL; + if (NS_IsMainThread()) { + nsCOMPtr<nsPIDOMWindowInner> inner(do_QueryInterface(aGlobal)); + Document* doc = inner ? inner->GetExtantDoc() : nullptr; + nsCOMPtr<nsIURI> uri; + if (doc) { + uri = ParseURLFromDocument(doc, referrer, aRv); + } else { + // If we don't have a document, we must assume that this is a full + // URL. + uri = ParseURLFromChrome(referrer, aRv); + } + if (NS_WARN_IF(aRv.Failed())) { + aRv.ThrowTypeError<MSG_INVALID_REFERRER_URL>( + NS_ConvertUTF16toUTF8(referrer)); + return nullptr; + } + nsAutoCString spec; + uri->GetSpec(spec); + CopyUTF8toUTF16(spec, referrerURL); + if (!referrerURL.EqualsLiteral(kFETCH_CLIENT_REFERRER_STR)) { + nsCOMPtr<nsIPrincipal> principal = aGlobal->PrincipalOrNull(); + if (principal) { + nsresult rv = + principal->CheckMayLoad(uri, + /* allowIfInheritsPrincipal */ false); + if (NS_FAILED(rv)) { + referrerURL.AssignLiteral(kFETCH_CLIENT_REFERRER_STR); + } + } + } + } else { + RefPtr<URL> url = ParseURLFromWorker(aGlobal, referrer, aRv); + if (NS_WARN_IF(aRv.Failed())) { + aRv.ThrowTypeError<MSG_INVALID_REFERRER_URL>( + NS_ConvertUTF16toUTF8(referrer)); + return nullptr; + } + url->GetHref(referrerURL); + if (!referrerURL.EqualsLiteral(kFETCH_CLIENT_REFERRER_STR)) { + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + nsresult rv = NS_OK; + // ReferrerSameOriginChecker uses a sync loop to get the main thread + // to perform the same-origin check. Overall, on Workers this method + // can create 3 sync loops (two for constructing URLs and one here) so + // in the future we may want to optimize it all by off-loading all of + // this work in a single sync loop. + RefPtr<ReferrerSameOriginChecker> checker = + new ReferrerSameOriginChecker(worker, referrerURL, rv); + IgnoredErrorResult error; + checker->Dispatch(Canceling, error); + if (error.Failed() || NS_FAILED(rv)) { + referrerURL.AssignLiteral(kFETCH_CLIENT_REFERRER_STR); + } + } + } + request->SetReferrer(referrerURL); + } + } + + if (aInit.mReferrerPolicy.WasPassed()) { + request->SetReferrerPolicy(aInit.mReferrerPolicy.Value()); + } + + if (aInit.mSignal.WasPassed()) { + signal = aInit.mSignal.Value(); + } + + UniquePtr<mozilla::ipc::PrincipalInfo> principalInfo; + nsILoadInfo::CrossOriginEmbedderPolicy coep = + nsILoadInfo::EMBEDDER_POLICY_NULL; + + if (NS_IsMainThread()) { + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal); + if (window) { + nsCOMPtr<Document> doc; + doc = window->GetExtantDoc(); + if (doc) { + request->SetEnvironmentReferrerPolicy(doc->GetReferrerPolicy()); + + principalInfo.reset(new mozilla::ipc::PrincipalInfo()); + nsresult rv = + PrincipalToPrincipalInfo(doc->NodePrincipal(), principalInfo.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>(); + return nullptr; + } + } + if (window->GetWindowContext()) { + coep = window->GetWindowContext()->GetEmbedderPolicy(); + } + } + } else { + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + if (worker) { + worker->AssertIsOnWorkerThread(); + request->SetEnvironmentReferrerPolicy(worker->GetReferrerPolicy()); + principalInfo = + MakeUnique<mozilla::ipc::PrincipalInfo>(worker->GetPrincipalInfo()); + coep = worker->GetEmbedderPolicy(); + // For dedicated worker, the response must respect the owner's COEP. + if (coep == nsILoadInfo::EMBEDDER_POLICY_NULL && + worker->IsDedicatedWorker()) { + coep = worker->GetOwnerEmbedderPolicy(); + } + } + } + + request->SetPrincipalInfo(std::move(principalInfo)); + request->SetEmbedderPolicy(coep); + + if (mode != RequestMode::EndGuard_) { + request->SetMode(mode); + } + + if (credentials != RequestCredentials::EndGuard_) { + request->SetCredentialsMode(credentials); + } + + RequestCache cache = + aInit.mCache.WasPassed() ? aInit.mCache.Value() : fallbackCache; + if (cache != RequestCache::EndGuard_) { + if (cache == RequestCache::Only_if_cached && + request->Mode() != RequestMode::Same_origin) { + nsCString modeString(RequestModeValues::GetString(request->Mode())); + aRv.ThrowTypeError<MSG_ONLY_IF_CACHED_WITHOUT_SAME_ORIGIN>(modeString); + return nullptr; + } + request->SetCacheMode(cache); + } + + if (aInit.mRedirect.WasPassed()) { + request->SetRedirectMode(aInit.mRedirect.Value()); + } + + if (aInit.mIntegrity.WasPassed()) { + request->SetIntegrity(aInit.mIntegrity.Value()); + } + + if (aInit.mMozErrors.WasPassed() && aInit.mMozErrors.Value()) { + request->SetMozErrors(); + } + + // Request constructor step 14. + if (aInit.mMethod.WasPassed()) { + nsAutoCString method(aInit.mMethod.Value()); + + // Step 14.1. Disallow forbidden methods, and anything that is not a HTTP + // token, since HTTP states that Method may be any of the defined values or + // a token (extension method). + nsAutoCString outMethod; + nsresult rv = FetchUtil::GetValidRequestMethod(method, outMethod); + if (NS_FAILED(rv)) { + aRv.ThrowTypeError<MSG_INVALID_REQUEST_METHOD>(method); + return nullptr; + } + + // Step 14.2 + request->SetMethod(outMethod); + } + + RefPtr<InternalHeaders> requestHeaders = request->Headers(); + + RefPtr<InternalHeaders> headers; + if (aInit.mHeaders.WasPassed()) { + RefPtr<Headers> h = Headers::Create(aGlobal, aInit.mHeaders.Value(), aRv); + if (aRv.Failed()) { + return nullptr; + } + headers = h->GetInternalHeaders(); + } else { + headers = new InternalHeaders(*requestHeaders); + } + + requestHeaders->Clear(); + // From "Let r be a new Request object associated with request and a new + // Headers object whose guard is "request"." + requestHeaders->SetGuard(HeadersGuardEnum::Request, aRv); + MOZ_ASSERT(!aRv.Failed()); + + if (request->Mode() == RequestMode::No_cors) { + if (!request->HasSimpleMethod()) { + nsAutoCString method; + request->GetMethod(method); + aRv.ThrowTypeError<MSG_INVALID_REQUEST_METHOD>(method); + return nullptr; + } + + requestHeaders->SetGuard(HeadersGuardEnum::Request_no_cors, aRv); + if (aRv.Failed()) { + return nullptr; + } + } + + requestHeaders->Fill(*headers, aRv); + if (aRv.Failed()) { + return nullptr; + } + + if ((aInit.mBody.WasPassed() && !aInit.mBody.Value().IsNull()) || + hasCopiedBody) { + // HEAD and GET are not allowed to have a body. + nsAutoCString method; + request->GetMethod(method); + // method is guaranteed to be uppercase due to step 14.2 above. + if (method.EqualsLiteral("HEAD") || method.EqualsLiteral("GET")) { + aRv.ThrowTypeError("HEAD or GET Request cannot have a body."); + return nullptr; + } + } + + if (aInit.mBody.WasPassed()) { + const Nullable<fetch::OwningBodyInit>& bodyInitNullable = + aInit.mBody.Value(); + if (!bodyInitNullable.IsNull()) { + const fetch::OwningBodyInit& bodyInit = bodyInitNullable.Value(); + nsCOMPtr<nsIInputStream> stream; + nsAutoCString contentTypeWithCharset; + uint64_t contentLength = 0; + aRv = ExtractByteStreamFromBody(bodyInit, getter_AddRefs(stream), + contentTypeWithCharset, contentLength); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + nsCOMPtr<nsIInputStream> temporaryBody = stream; + + if (!contentTypeWithCharset.IsVoid() && + !requestHeaders->Has("Content-Type"_ns, aRv)) { + requestHeaders->Append("Content-Type"_ns, contentTypeWithCharset, aRv); + } + + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + if (hasCopiedBody) { + request->SetBody(nullptr, 0); + } + + request->SetBody(temporaryBody, contentLength); + } + } + + auto domRequest = + MakeSafeRefPtr<Request>(aGlobal, std::move(request), signal); + + if (aInput.IsRequest()) { + RefPtr<Request> inputReq = &aInput.GetAsRequest(); + nsCOMPtr<nsIInputStream> body; + inputReq->GetBody(getter_AddRefs(body)); + if (body) { + inputReq->SetBody(nullptr, 0); + inputReq->SetBodyUsed(aCx, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } + } + return domRequest; +} + +SafeRefPtr<Request> Request::Clone(ErrorResult& aRv) { + if (BodyUsed()) { + aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>(); + return nullptr; + } + + SafeRefPtr<InternalRequest> ir = mRequest->Clone(); + if (!ir) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + return MakeSafeRefPtr<Request>(mOwner, std::move(ir), GetOrCreateSignal()); +} + +Headers* Request::Headers_() { + if (!mHeaders) { + mHeaders = new Headers(mOwner, mRequest->Headers()); + } + + return mHeaders; +} + +AbortSignal* Request::GetOrCreateSignal() { + if (!mSignal) { + mSignal = new AbortSignal(mOwner, false, JS::UndefinedHandleValue); + } + + return mSignal; +} + +AbortSignalImpl* Request::GetSignalImpl() const { return mSignal; } + +AbortSignalImpl* Request::GetSignalImplToConsumeBody() const { + // This is a hack, see Response::GetSignalImplToConsumeBody. + return nullptr; +} + +} // namespace mozilla::dom diff --git a/dom/fetch/Request.h b/dom/fetch/Request.h new file mode 100644 index 0000000000..0086e076de --- /dev/null +++ b/dom/fetch/Request.h @@ -0,0 +1,142 @@ +/* -*- 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_Request_h +#define mozilla_dom_Request_h + +#include "nsISupportsImpl.h" +#include "nsWrapperCache.h" + +#include "mozilla/dom/Fetch.h" +#include "mozilla/dom/InternalRequest.h" +// Required here due to certain WebIDL enums/classes being declared in both +// files. +#include "mozilla/dom/RequestBinding.h" +#include "mozilla/dom/SafeRefPtr.h" + +namespace mozilla::dom { + +class Headers; +class InternalHeaders; +class RequestOrUSVString; + +class Request final : public FetchBody<Request>, public nsWrapperCache { + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS_INHERITED(Request, + FetchBody<Request>) + + public: + Request(nsIGlobalObject* aOwner, SafeRefPtr<InternalRequest> aRequest, + AbortSignal* aSignal); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override { + return Request_Binding::Wrap(aCx, this, aGivenProto); + } + + void GetUrl(nsAString& aUrl) const { + nsAutoCString url; + mRequest->GetURL(url); + CopyUTF8toUTF16(url, aUrl); + } + + void GetMethod(nsCString& aMethod) const { aMethod = mRequest->mMethod; } + + RequestMode Mode() const { return mRequest->mMode; } + + RequestCredentials Credentials() const { return mRequest->mCredentialsMode; } + + RequestCache Cache() const { return mRequest->GetCacheMode(); } + + RequestRedirect Redirect() const { return mRequest->GetRedirectMode(); } + + void GetIntegrity(nsAString& aIntegrity) const { + aIntegrity = mRequest->GetIntegrity(); + } + + bool MozErrors() const { return mRequest->MozErrors(); } + + RequestDestination Destination() const { return mRequest->Destination(); } + + void OverrideContentPolicyType(uint32_t aContentPolicyType) { + mRequest->OverrideContentPolicyType( + static_cast<nsContentPolicyType>(aContentPolicyType)); + } + + bool IsContentPolicyTypeOverridden() const { + return mRequest->IsContentPolicyTypeOverridden(); + } + + void GetReferrer(nsAString& aReferrer) const { + mRequest->GetReferrer(aReferrer); + } + + ReferrerPolicy ReferrerPolicy_() const { return mRequest->ReferrerPolicy_(); } + + InternalHeaders* GetInternalHeaders() const { return mRequest->Headers(); } + + Headers* Headers_(); + + using FetchBody::GetBody; + + void GetBody(nsIInputStream** aStream, int64_t* aBodyLength = nullptr) { + mRequest->GetBody(aStream, aBodyLength); + } + + void SetBody(nsIInputStream* aStream, int64_t aBodyLength) { + mRequest->SetBody(aStream, aBodyLength); + } + + using FetchBody::BodyBlobURISpec; + + const nsACString& BodyBlobURISpec() const { + return mRequest->BodyBlobURISpec(); + } + + using FetchBody::BodyLocalPath; + + const nsAString& BodyLocalPath() const { return mRequest->BodyLocalPath(); } + + static SafeRefPtr<Request> Constructor(const GlobalObject& aGlobal, + const RequestOrUSVString& aInput, + const RequestInit& aInit, + ErrorResult& rv); + + static SafeRefPtr<Request> Constructor(nsIGlobalObject* aGlobal, + JSContext* aCx, + const RequestOrUSVString& aInput, + const RequestInit& aInit, + ErrorResult& rv); + + nsIGlobalObject* GetParentObject() const { return mOwner; } + + SafeRefPtr<Request> Clone(ErrorResult& aRv); + + SafeRefPtr<InternalRequest> GetInternalRequest(); + + const UniquePtr<mozilla::ipc::PrincipalInfo>& GetPrincipalInfo() const { + return mRequest->GetPrincipalInfo(); + } + + AbortSignal* GetOrCreateSignal(); + + // This can return a null AbortSignalImpl. + AbortSignalImpl* GetSignalImpl() const override; + AbortSignalImpl* GetSignalImplToConsumeBody() const final; + + private: + ~Request(); + + SafeRefPtr<InternalRequest> mRequest; + + // Lazily created. + RefPtr<Headers> mHeaders; + RefPtr<AbortSignal> mSignal; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_Request_h diff --git a/dom/fetch/Response.cpp b/dom/fetch/Response.cpp new file mode 100644 index 0000000000..fbdc5b0d20 --- /dev/null +++ b/dom/fetch/Response.cpp @@ -0,0 +1,477 @@ +/* -*- 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 "Response.h" + +#include "nsISupportsImpl.h" +#include "nsIURI.h" +#include "nsNetUtil.h" +#include "nsPIDOMWindow.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/FetchBinding.h" +#include "mozilla/dom/ResponseBinding.h" +#include "mozilla/dom/Headers.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/URL.h" +#include "mozilla/dom/WorkerPrivate.h" + +#include "nsDOMString.h" + +#include "BodyExtractor.h" +#include "FetchStreamReader.h" +#include "InternalResponse.h" + +#include "mozilla/dom/ReadableStreamDefaultReader.h" + +namespace mozilla::dom { + +NS_IMPL_ADDREF_INHERITED(Response, FetchBody<Response>) +NS_IMPL_RELEASE_INHERITED(Response, FetchBody<Response>) + +NS_IMPL_CYCLE_COLLECTION_CLASS(Response) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(Response, FetchBody<Response>) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mHeaders) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSignalImpl) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mFetchStreamReader) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(Response, FetchBody<Response>) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHeaders) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSignalImpl) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFetchStreamReader) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(Response, FetchBody<Response>) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Response) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY +NS_INTERFACE_MAP_END_INHERITING(FetchBody<Response>) + +Response::Response(nsIGlobalObject* aGlobal, + SafeRefPtr<InternalResponse> aInternalResponse, + AbortSignalImpl* aSignalImpl) + : FetchBody<Response>(aGlobal), + mInternalResponse(std::move(aInternalResponse)), + mSignalImpl(aSignalImpl) { + MOZ_ASSERT( + mInternalResponse->Headers()->Guard() == HeadersGuardEnum::Immutable || + mInternalResponse->Headers()->Guard() == HeadersGuardEnum::Response); + + mozilla::HoldJSObjects(this); +} + +Response::~Response() { mozilla::DropJSObjects(this); } + +/* static */ +already_AddRefed<Response> Response::Error(const GlobalObject& aGlobal) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<Response> r = new Response( + global, InternalResponse::NetworkError(NS_ERROR_FAILURE), nullptr); + return r.forget(); +} + +/* static */ +already_AddRefed<Response> Response::Redirect(const GlobalObject& aGlobal, + const nsAString& aUrl, + uint16_t aStatus, + ErrorResult& aRv) { + nsAutoString parsedURL; + + if (NS_IsMainThread()) { + nsIURI* baseURI = nullptr; + nsCOMPtr<nsPIDOMWindowInner> inner( + do_QueryInterface(aGlobal.GetAsSupports())); + Document* doc = inner ? inner->GetExtantDoc() : nullptr; + if (doc) { + baseURI = doc->GetBaseURI(); + } + // Don't use NS_ConvertUTF16toUTF8 because that doesn't let us handle OOM. + nsAutoCString url; + if (!AppendUTF16toUTF8(aUrl, url, fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return nullptr; + } + + nsCOMPtr<nsIURI> resolvedURI; + nsresult rv = NS_NewURI(getter_AddRefs(resolvedURI), url, nullptr, baseURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.ThrowTypeError<MSG_INVALID_URL>(url); + return nullptr; + } + + nsAutoCString spec; + rv = resolvedURI->GetSpec(spec); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.ThrowTypeError<MSG_INVALID_URL>(url); + return nullptr; + } + + CopyUTF8toUTF16(spec, parsedURL); + } else { + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(worker); + worker->AssertIsOnWorkerThread(); + + NS_ConvertUTF8toUTF16 baseURL(worker->GetLocationInfo().mHref); + RefPtr<URL> url = + URL::Constructor(aGlobal.GetAsSupports(), aUrl, baseURL, aRv); + if (aRv.Failed()) { + return nullptr; + } + + url->GetHref(parsedURL); + } + + if (aStatus != 301 && aStatus != 302 && aStatus != 303 && aStatus != 307 && + aStatus != 308) { + aRv.ThrowRangeError("Invalid redirect status code."); + return nullptr; + } + + // We can't just pass nullptr for our null-valued Nullable, because the + // fetch::ResponseBodyInit is a non-temporary type due to the MOZ_RAII + // annotations on some of its members. + Nullable<fetch::ResponseBodyInit> body; + ResponseInit init; + init.mStatus = aStatus; + init.mStatusText.AssignASCII(""); + RefPtr<Response> r = Response::Constructor(aGlobal, body, init, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + r->GetInternalHeaders()->Set("Location"_ns, NS_ConvertUTF16toUTF8(parsedURL), + aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + r->GetInternalHeaders()->SetGuard(HeadersGuardEnum::Immutable, aRv); + MOZ_ASSERT(!aRv.Failed()); + + return r.forget(); +} + +/* static */ already_AddRefed<Response> Response::CreateAndInitializeAResponse( + const GlobalObject& aGlobal, const Nullable<fetch::ResponseBodyInit>& aBody, + const nsACString& aDefaultContentType, const ResponseInit& aInit, + ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + + if (NS_WARN_IF(!global)) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + // Initialize a response, Step 1. + if (aInit.mStatus < 200 || aInit.mStatus > 599) { + aRv.ThrowRangeError("Invalid response status code."); + return nullptr; + } + + // Initialize a response, Step 2: Check if the status text contains illegal + // characters + nsACString::const_iterator start, end; + aInit.mStatusText.BeginReading(start); + aInit.mStatusText.EndReading(end); + if (FindCharInReadable('\r', start, end)) { + aRv.ThrowTypeError<MSG_RESPONSE_INVALID_STATUSTEXT_ERROR>(); + return nullptr; + } + // Reset iterator since FindCharInReadable advances it. + aInit.mStatusText.BeginReading(start); + if (FindCharInReadable('\n', start, end)) { + aRv.ThrowTypeError<MSG_RESPONSE_INVALID_STATUSTEXT_ERROR>(); + return nullptr; + } + + // Initialize a response, Step 3-4. + SafeRefPtr<InternalResponse> internalResponse = + MakeSafeRefPtr<InternalResponse>(aInit.mStatus, aInit.mStatusText); + + UniquePtr<mozilla::ipc::PrincipalInfo> principalInfo; + + // Grab a valid channel info from the global so this response is 'valid' for + // interception. + if (NS_IsMainThread()) { + ChannelInfo info; + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(global); + if (window) { + Document* doc = window->GetExtantDoc(); + MOZ_ASSERT(doc); + info.InitFromDocument(doc); + + principalInfo.reset(new mozilla::ipc::PrincipalInfo()); + nsresult rv = + PrincipalToPrincipalInfo(doc->NodePrincipal(), principalInfo.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>(); + return nullptr; + } + + internalResponse->InitChannelInfo(info); + } else if (global->PrincipalOrNull()->IsSystemPrincipal()) { + info.InitFromChromeGlobal(global); + + internalResponse->InitChannelInfo(info); + } + + /** + * The channel info is left uninitialized if neither the above `if` nor + * `else if` statements are executed; this could be because we're in a + * WebExtensions content script, where the global (i.e. `global`) is a + * wrapper, and the principal is an expanded principal. In this case, + * as far as I can tell, there's no way to get the security info, but we'd + * like the `Response` to be successfully constructed. + */ + } else { + WorkerPrivate* worker = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(worker); + internalResponse->InitChannelInfo(worker->GetChannelInfo()); + principalInfo = + MakeUnique<mozilla::ipc::PrincipalInfo>(worker->GetPrincipalInfo()); + } + + internalResponse->SetPrincipalInfo(std::move(principalInfo)); + + RefPtr<Response> r = + new Response(global, internalResponse.clonePtr(), nullptr); + + if (aInit.mHeaders.WasPassed()) { + internalResponse->Headers()->Clear(); + + // Instead of using Fill, create an object to allow the constructor to + // unwrap the HeadersInit. + RefPtr<Headers> headers = + Headers::Create(global, aInit.mHeaders.Value(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + internalResponse->Headers()->Fill(*headers->GetInternalHeaders(), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } + + if (!aBody.IsNull()) { + if (aInit.mStatus == 204 || aInit.mStatus == 205 || aInit.mStatus == 304) { + aRv.ThrowTypeError("Response body is given with a null body status."); + return nullptr; + } + + nsCString contentTypeWithCharset; + nsCOMPtr<nsIInputStream> bodyStream; + int64_t bodySize = InternalResponse::UNKNOWN_BODY_SIZE; + + const fetch::ResponseBodyInit& body = aBody.Value(); + if (body.IsReadableStream()) { + JSContext* cx = aGlobal.Context(); + aRv.MightThrowJSException(); + + ReadableStream& readableStream = body.GetAsReadableStream(); + + if (readableStream.Locked() || readableStream.Disturbed()) { + aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>(); + return nullptr; + } + + r->SetReadableStreamBody(cx, &readableStream); + + // If this is a DOM generated ReadableStream, we can extract the + // inputStream directly. + if (nsIInputStream* underlyingSource = + readableStream.MaybeGetInputStreamIfUnread()) { + bodyStream = underlyingSource; + } else { + // If this is a JS-created ReadableStream, let's create a + // FetchStreamReader. + aRv = FetchStreamReader::Create(aGlobal.Context(), global, + getter_AddRefs(r->mFetchStreamReader), + getter_AddRefs(bodyStream)); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } + } else { + uint64_t size = 0; + aRv = ExtractByteStreamFromBody(body, getter_AddRefs(bodyStream), + contentTypeWithCharset, size); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + if (!aDefaultContentType.IsVoid()) { + contentTypeWithCharset = aDefaultContentType; + } + + bodySize = size; + } + + internalResponse->SetBody(bodyStream, bodySize); + + if (!contentTypeWithCharset.IsVoid() && + !internalResponse->Headers()->Has("Content-Type"_ns, aRv)) { + // Ignore Append() failing here. + ErrorResult error; + internalResponse->Headers()->Append("Content-Type"_ns, + contentTypeWithCharset, error); + error.SuppressException(); + } + + if (aRv.Failed()) { + return nullptr; + } + } + + return r.forget(); +} + +/* static */ +already_AddRefed<Response> Response::CreateFromJson(const GlobalObject& aGlobal, + JSContext* aCx, + JS::Handle<JS::Value> aData, + const ResponseInit& aInit, + ErrorResult& aRv) { + aRv.MightThrowJSException(); + nsAutoString serializedValue; + if (!nsContentUtils::StringifyJSON(aCx, aData, serializedValue, + UndefinedIsVoidString)) { + aRv.StealExceptionFromJSContext(aCx); + return nullptr; + } + if (serializedValue.IsVoid()) { + aRv.ThrowTypeError<MSG_JSON_INVALID_VALUE>(); + return nullptr; + } + Nullable<fetch::ResponseBodyInit> body; + body.SetValue().SetAsUSVString().ShareOrDependUpon(serializedValue); + return CreateAndInitializeAResponse(aGlobal, body, "application/json"_ns, + aInit, aRv); +} + +/*static*/ +already_AddRefed<Response> Response::Constructor( + const GlobalObject& aGlobal, const Nullable<fetch::ResponseBodyInit>& aBody, + const ResponseInit& aInit, ErrorResult& aRv) { + return CreateAndInitializeAResponse(aGlobal, aBody, VoidCString(), aInit, + aRv); +} + +already_AddRefed<Response> Response::Clone(JSContext* aCx, ErrorResult& aRv) { + bool bodyUsed = BodyUsed(); + + if (!bodyUsed && mReadableStreamBody) { + bool locked = mReadableStreamBody->Locked(); + bodyUsed = locked; + } + + if (bodyUsed) { + aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>(); + return nullptr; + } + + RefPtr<FetchStreamReader> streamReader; + nsCOMPtr<nsIInputStream> inputStream; + + RefPtr<ReadableStream> body; + MaybeTeeReadableStreamBody(aCx, getter_AddRefs(body), + getter_AddRefs(streamReader), + getter_AddRefs(inputStream), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + MOZ_ASSERT_IF(body, streamReader); + MOZ_ASSERT_IF(body, inputStream); + + SafeRefPtr<InternalResponse> ir = + mInternalResponse->Clone(body ? InternalResponse::eDontCloneInputStream + : InternalResponse::eCloneInputStream); + + RefPtr<Response> response = + new Response(mOwner, ir.clonePtr(), GetSignalImpl()); + + if (body) { + // Maybe we have a body, but we receive null from MaybeTeeReadableStreamBody + // if this body is a native stream. In this case the InternalResponse will + // have a clone of the native body and the ReadableStream will be created + // lazily if needed. + response->SetReadableStreamBody(aCx, body); + response->mFetchStreamReader = streamReader; + ir->SetBody(inputStream, InternalResponse::UNKNOWN_BODY_SIZE); + } + + return response.forget(); +} + +already_AddRefed<Response> Response::CloneUnfiltered(JSContext* aCx, + ErrorResult& aRv) { + if (BodyUsed()) { + aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>(); + return nullptr; + } + + RefPtr<FetchStreamReader> streamReader; + nsCOMPtr<nsIInputStream> inputStream; + + RefPtr<ReadableStream> body; + MaybeTeeReadableStreamBody(aCx, getter_AddRefs(body), + getter_AddRefs(streamReader), + getter_AddRefs(inputStream), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + MOZ_ASSERT_IF(body, streamReader); + MOZ_ASSERT_IF(body, inputStream); + + SafeRefPtr<InternalResponse> clone = + mInternalResponse->Clone(body ? InternalResponse::eDontCloneInputStream + : InternalResponse::eCloneInputStream); + + SafeRefPtr<InternalResponse> ir = clone->Unfiltered(); + RefPtr<Response> ref = new Response(mOwner, ir.clonePtr(), GetSignalImpl()); + + if (body) { + // Maybe we have a body, but we receive null from MaybeTeeReadableStreamBody + // if this body is a native stream. In this case the InternalResponse will + // have a clone of the native body and the ReadableStream will be created + // lazily if needed. + ref->SetReadableStreamBody(aCx, body); + ref->mFetchStreamReader = streamReader; + ir->SetBody(inputStream, InternalResponse::UNKNOWN_BODY_SIZE); + } + + return ref.forget(); +} + +void Response::SetBody(nsIInputStream* aBody, int64_t aBodySize) { + MOZ_ASSERT(!BodyUsed()); + mInternalResponse->SetBody(aBody, aBodySize); +} + +SafeRefPtr<InternalResponse> Response::GetInternalResponse() const { + return mInternalResponse.clonePtr(); +} + +Headers* Response::Headers_() { + if (!mHeaders) { + mHeaders = new Headers(mOwner, mInternalResponse->Headers()); + } + + return mHeaders; +} + +} // namespace mozilla::dom diff --git a/dom/fetch/Response.h b/dom/fetch/Response.h new file mode 100644 index 0000000000..1aa0e56c31 --- /dev/null +++ b/dom/fetch/Response.h @@ -0,0 +1,161 @@ +/* -*- 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_Response_h +#define mozilla_dom_Response_h + +#include "nsWrapperCache.h" +#include "nsISupportsImpl.h" + +#include "mozilla/dom/Fetch.h" +#include "mozilla/dom/ResponseBinding.h" + +#include "InternalHeaders.h" +#include "InternalResponse.h" + +namespace mozilla { +namespace ipc { +class PrincipalInfo; +} // namespace ipc + +namespace dom { + +class Headers; + +class Response final : public FetchBody<Response>, public nsWrapperCache { + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(Response, + FetchBody<Response>) + + public: + Response(nsIGlobalObject* aGlobal, + SafeRefPtr<InternalResponse> aInternalResponse, + AbortSignalImpl* aSignalImpl); + + Response(const Response& aOther) = delete; + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override { + return Response_Binding::Wrap(aCx, this, aGivenProto); + } + + ResponseType Type() const { return mInternalResponse->Type(); } + void GetUrl(nsAString& aUrl) const { + CopyUTF8toUTF16(mInternalResponse->GetURL(), aUrl); + } + bool Redirected() const { return mInternalResponse->IsRedirected(); } + uint16_t Status() const { return mInternalResponse->GetStatus(); } + + bool Ok() const { + return mInternalResponse->GetStatus() >= 200 && + mInternalResponse->GetStatus() <= 299; + } + + void GetStatusText(nsCString& aStatusText) const { + aStatusText = mInternalResponse->GetStatusText(); + } + + InternalHeaders* GetInternalHeaders() const { + return mInternalResponse->Headers(); + } + + void InitChannelInfo(nsIChannel* aChannel) { + mInternalResponse->InitChannelInfo(aChannel); + } + + const ChannelInfo& GetChannelInfo() const { + return mInternalResponse->GetChannelInfo(); + } + + const UniquePtr<mozilla::ipc::PrincipalInfo>& GetPrincipalInfo() const { + return mInternalResponse->GetPrincipalInfo(); + } + + bool HasCacheInfoChannel() const { + return mInternalResponse->HasCacheInfoChannel(); + } + + Headers* Headers_(); + + void GetBody(nsIInputStream** aStream, int64_t* aBodyLength = nullptr) { + mInternalResponse->GetBody(aStream, aBodyLength); + } + + using FetchBody::GetBody; + + using FetchBody::BodyBlobURISpec; + + const nsACString& BodyBlobURISpec() const { + return mInternalResponse->BodyBlobURISpec(); + } + + using FetchBody::BodyLocalPath; + + const nsAString& BodyLocalPath() const { + return mInternalResponse->BodyLocalPath(); + } + + static already_AddRefed<Response> Error(const GlobalObject& aGlobal); + + static already_AddRefed<Response> Redirect(const GlobalObject& aGlobal, + const nsAString& aUrl, + uint16_t aStatus, + ErrorResult& aRv); + + static already_AddRefed<Response> CreateFromJson(const GlobalObject&, + JSContext*, + JS::Handle<JS::Value>, + const ResponseInit&, + ErrorResult&); + + static already_AddRefed<Response> Constructor( + const GlobalObject& aGlobal, + const Nullable<fetch::ResponseBodyInit>& aBody, const ResponseInit& aInit, + ErrorResult& rv); + + nsIGlobalObject* GetParentObject() const { return mOwner; } + + already_AddRefed<Response> Clone(JSContext* aCx, ErrorResult& aRv); + + already_AddRefed<Response> CloneUnfiltered(JSContext* aCx, ErrorResult& aRv); + + void SetBody(nsIInputStream* aBody, int64_t aBodySize); + + SafeRefPtr<InternalResponse> GetInternalResponse() const; + + AbortSignalImpl* GetSignalImpl() const override { return mSignalImpl; } + AbortSignalImpl* GetSignalImplToConsumeBody() const final { + // XXX: BodyConsumer is supposed to work in terms of ReadableStream and + // should be affected by: https://fetch.spec.whatwg.org/#abort-fetch + // + // Step 6: If response’s body is not null and is readable, then error + // response’s body with error. + // + // But since it's written before streams work, it's currently depending on + // abort signal to be aborted. + // Please fix this when DOM ReadableStream is ready. (Bug 1730584) + return mSignalImpl; + } + + private: + static already_AddRefed<Response> CreateAndInitializeAResponse( + const GlobalObject& aGlobal, + const Nullable<fetch::ResponseBodyInit>& aBody, + const nsACString& aDefaultContentType, const ResponseInit& aInit, + ErrorResult& aRv); + + ~Response(); + + SafeRefPtr<InternalResponse> mInternalResponse; + // Lazily created + RefPtr<Headers> mHeaders; + RefPtr<AbortSignalImpl> mSignalImpl; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_Response_h diff --git a/dom/fetch/moz.build b/dom/fetch/moz.build new file mode 100644 index 0000000000..6d7aa31027 --- /dev/null +++ b/dom/fetch/moz.build @@ -0,0 +1,72 @@ +# -*- 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: Networking") + +EXPORTS.mozilla.dom += [ + "BodyExtractor.h", + "ChannelInfo.h", + "Fetch.h", + "FetchChild.h", + "FetchDriver.h", + "FetchIPCTypes.h", + "FetchObserver.h", + "FetchParent.h", + "FetchService.h", + "FetchStreamReader.h", + "FetchStreamUtils.h", + "FetchUtil.h", + "Headers.h", + "InternalHeaders.h", + "InternalRequest.h", + "InternalResponse.h", + "Request.h", + "Response.h", +] + +UNIFIED_SOURCES += [ + "BodyExtractor.cpp", + "ChannelInfo.cpp", + "Fetch.cpp", + "FetchChild.cpp", + "FetchDriver.cpp", + "FetchObserver.cpp", + "FetchParent.cpp", + "FetchService.cpp", + "FetchStreamReader.cpp", + "FetchStreamUtils.cpp", + "FetchUtil.cpp", + "Headers.cpp", + "InternalHeaders.cpp", + "InternalRequest.cpp", + "InternalResponse.cpp", + "Request.cpp", + "Response.cpp", +] + +IPDL_SOURCES += [ + "FetchTypes.ipdlh", + "PFetch.ipdl", +] + +LOCAL_INCLUDES += [ + # For nsDOMSerializer + "/dom/base", + # For HttpBaseChannel.h dependencies + "/netwerk/base", + # For nsDataHandler.h + "/netwerk/protocol/data", + # For HttpBaseChannel.h + "/netwerk/protocol/http", +] + +BROWSER_CHROME_MANIFESTS += ["tests/browser.ini"] +MOCHITEST_MANIFESTS += ["tests/mochitest.ini"] + +FINAL_LIBRARY = "xul" + +include("/ipc/chromium/chromium-config.mozbuild") diff --git a/dom/fetch/tests/browser.ini b/dom/fetch/tests/browser.ini new file mode 100644 index 0000000000..6fd8244288 --- /dev/null +++ b/dom/fetch/tests/browser.ini @@ -0,0 +1,23 @@ +[DEFAULT] +[browser_blobFromFile.js] +[browser_origin_trial_coep_credentialless_fetch_1.js] +support-files = + open_credentialless_document.sjs + store_header.sjs +[browser_origin_trial_coep_credentialless_fetch_2.js] +support-files = + open_credentialless_document.sjs + store_header.sjs +[browser_origin_trial_coep_credentialless_fetch_3.js] +support-files = + open_credentialless_document.sjs + store_header.sjs +[browser_origin_trial_coep_credentialless_worker.js] +support-files = + open_credentialless_document.sjs + store_header.sjs + credentialless_worker.sjs +[browser_origin_trial_coep_credentialless_cache.js] +support-files = + open_credentialless_document.sjs + credentialless_resource.sjs diff --git a/dom/fetch/tests/browser_blobFromFile.js b/dom/fetch/tests/browser_blobFromFile.js new file mode 100644 index 0000000000..07a8c6669e --- /dev/null +++ b/dom/fetch/tests/browser_blobFromFile.js @@ -0,0 +1,64 @@ +add_task(async function test() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.tabs.remote.separateFileUriProcess", true]], + }); + + let fileData = ""; + for (var i = 0; i < 100; ++i) { + fileData += "hello world!"; + } + + let file = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIDirectoryService) + .QueryInterface(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + file.append("file.txt"); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + + let outStream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + outStream.init( + file, + 0x02 | 0x08 | 0x20, // write, create, truncate + 0o666, + 0 + ); + outStream.write(fileData, fileData.length); + outStream.close(); + + let fileHandler = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService) + .getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler); + + let fileURL = fileHandler.getURLSpecFromActualFile(file); + + info("Opening url: " + fileURL); + let tab = BrowserTestUtils.addTab(gBrowser, fileURL); + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + let blob = await SpecialPowers.spawn( + browser, + [file.leafName], + function (fileName) { + return new content.window.Promise(resolve => { + content.window + .fetch(fileName) + .then(r => r.blob()) + .then(blob => resolve(blob)); + }); + } + ); + + ok(File.isInstance(blob), "We have a file"); + + is(blob.size, file.fileSize, "The size matches"); + is(blob.name, file.leafName, "The name is correct"); + + file.remove(false); + + gBrowser.removeTab(tab); +}); diff --git a/dom/fetch/tests/browser_origin_trial_coep_credentialless_cache.js b/dom/fetch/tests/browser_origin_trial_coep_credentialless_cache.js new file mode 100644 index 0000000000..c9b72a3393 --- /dev/null +++ b/dom/fetch/tests/browser_origin_trial_coep_credentialless_cache.js @@ -0,0 +1,139 @@ +const TOP_LEVEL_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "open_credentialless_document.sjs"; + +const SAME_ORIGIN = "https://example.com"; +const CROSS_ORIGIN = "https://test1.example.com"; + +const RESOURCE_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://test1.example.com" + ) + "credentialless_resource.sjs"; + +async function store(storer, url, requestCredentialMode) { + await SpecialPowers.spawn( + storer.linkedBrowser, + [url, requestCredentialMode], + async function (url, requestCredentialMode) { + const cache = await content.caches.open("v1"); + const fetchRequest = new content.Request(url, { + mode: "no-cors", + credentials: requestCredentialMode, + }); + + const fetchResponse = await content.fetch(fetchRequest); + content.wrappedJSObject.console.log(fetchResponse.headers); + await cache.put(fetchRequest, fetchResponse); + } + ); +} + +async function retrieve(retriever, resourceURL) { + return await SpecialPowers.spawn( + retriever.linkedBrowser, + [resourceURL], + async function (url) { + const cache = await content.caches.open("v1"); + try { + await cache.match(url); + return "retrieved"; + } catch (error) { + return "error"; + } + } + ); +} + +async function testCache( + storer, + storeRequestCredentialMode, + resourceCOEP, + retriever, + expectation +) { + const resourceURL = RESOURCE_URL + "?" + resourceCOEP; + + await store(storer, resourceURL, storeRequestCredentialMode); + const result = await retrieve(retriever, resourceURL); + + is(result, expectation); +} + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.tabs.remote.coep.credentialless", false], + ["dom.origin-trials.enabled", true], + ["dom.origin-trials.test-key.enabled", true], + ], + }); + + const noneTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TOP_LEVEL_URL + ); + const requireCorpTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TOP_LEVEL_URL + "?requirecorp" + ); + const credentiallessTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + TOP_LEVEL_URL + "?credentialless" + ); + + await testCache(noneTab, "include", "", noneTab, "retrieved"); + await testCache(noneTab, "include", "", credentiallessTab, "error"); + await testCache(noneTab, "omit", "", credentiallessTab, "retrieved"); + await testCache( + noneTab, + "include", + "corp_cross_origin", + credentiallessTab, + "retrieved" + ); + await testCache(noneTab, "include", "", requireCorpTab, "error"); + await testCache( + noneTab, + "include", + "corp_cross_origin", + requireCorpTab, + "retrieved" + ); + await testCache(credentiallessTab, "include", "", noneTab, "retrieved"); + await testCache( + credentiallessTab, + "include", + "", + credentiallessTab, + "retrieved" + ); + await testCache(credentiallessTab, "include", "", requireCorpTab, "error"); + await testCache( + requireCorpTab, + "include", + "corp_cross_origin", + noneTab, + "retrieved" + ); + await testCache( + requireCorpTab, + "include", + "corp_cross_origin", + credentiallessTab, + "retrieved" + ); + await testCache( + requireCorpTab, + "include", + "corp_cross_origin", + requireCorpTab, + "retrieved" + ); + + await BrowserTestUtils.removeTab(noneTab); + await BrowserTestUtils.removeTab(requireCorpTab); + await BrowserTestUtils.removeTab(credentiallessTab); +}); diff --git a/dom/fetch/tests/browser_origin_trial_coep_credentialless_fetch_1.js b/dom/fetch/tests/browser_origin_trial_coep_credentialless_fetch_1.js new file mode 100644 index 0000000000..773c15e72a --- /dev/null +++ b/dom/fetch/tests/browser_origin_trial_coep_credentialless_fetch_1.js @@ -0,0 +1,145 @@ +const TOP_LEVEL_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "open_credentialless_document.sjs"; + +const SAME_ORIGIN = "https://example.com"; +const CROSS_ORIGIN = "https://test1.example.com"; + +const GET_STATE_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "store_header.sjs?getstate"; + +async function addCookieToOrigin(origin) { + const fetchRequestURL = + getRootDirectory(gTestPath).replace("chrome://mochitests/content", origin) + + "store_header.sjs?addcookie"; + + const addcookieTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + fetchRequestURL + ); + + await SpecialPowers.spawn(addcookieTab.linkedBrowser, [], async function () { + content.document.cookie = "coep=credentialless; SameSite=None; Secure"; + }); + await BrowserTestUtils.removeTab(addcookieTab); +} + +async function testOrigin( + fetchOrigin, + isCredentialless, + useMetaTag, + fetchRequestMode, + fetchRequestCrendentials, + expectedCookieResult +) { + let params = []; + if (isCredentialless) { + params.push("credentialless"); + } + if (useMetaTag) { + params.push("meta"); + } + + let topLevelUrl = TOP_LEVEL_URL; + if (params.length) { + topLevelUrl += "?" + params.join("&"); + } + + const noCredentiallessTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + topLevelUrl + ); + + const fetchRequestURL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + fetchOrigin + ) + "store_header.sjs?checkheader"; + + await SpecialPowers.spawn( + noCredentiallessTab.linkedBrowser, + [ + !useMetaTag && isCredentialless, + fetchRequestURL, + fetchRequestMode, + fetchRequestCrendentials, + GET_STATE_URL, + expectedCookieResult, + ], + async function ( + sharedArrayBufferEnabled, + fetchRequestURL, + fetchRequestMode, + fetchRequestCrendentials, + getStateURL, + expectedCookieResult + ) { + if (sharedArrayBufferEnabled) { + ok(content.crossOriginIsolated); + } + // When store_header.sjs receives this request, it will store + // whether it has received the cookie as a shared state. + await content.fetch(fetchRequestURL, { + mode: fetchRequestMode, + credentials: fetchRequestCrendentials, + }); + + // This request is used to get the saved state from the + // previous fetch request. + const response = await content.fetch(getStateURL, { + mode: "cors", + }); + const text = await response.text(); + is(text, expectedCookieResult); + } + ); + + await BrowserTestUtils.removeTab(noCredentiallessTab); +} + +async function doTest( + origin, + fetchRequestMode, + fetchRequestCrendentials, + expectedCookieResultForNoCredentialless, + expectedCookieResultForCredentialless +) { + for (let credentialless of [true, false]) { + for (let meta of [true, false]) { + await testOrigin( + origin, + credentialless, + meta, + fetchRequestMode, + fetchRequestCrendentials, + credentialless + ? expectedCookieResultForCredentialless + : expectedCookieResultForNoCredentialless + ); + } + } +} + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.tabs.remote.coep.credentialless", false], + ["dom.origin-trials.enabled", true], + ["dom.origin-trials.test-key.enabled", true], + ], + }); + + await addCookieToOrigin(SAME_ORIGIN); + await addCookieToOrigin(CROSS_ORIGIN); + + // Cookies never sent with omit + await doTest(SAME_ORIGIN, "no-cors", "omit", "noCookie", "noCookie"); + await doTest(SAME_ORIGIN, "cors", "omit", "noCookie", "noCookie"); + await doTest(CROSS_ORIGIN, "no-cors", "omit", "noCookie", "noCookie"); + await doTest(CROSS_ORIGIN, "cors", "omit", "noCookie", "noCookie"); +}); diff --git a/dom/fetch/tests/browser_origin_trial_coep_credentialless_fetch_2.js b/dom/fetch/tests/browser_origin_trial_coep_credentialless_fetch_2.js new file mode 100644 index 0000000000..7a1e6879ac --- /dev/null +++ b/dom/fetch/tests/browser_origin_trial_coep_credentialless_fetch_2.js @@ -0,0 +1,145 @@ +const TOP_LEVEL_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "open_credentialless_document.sjs"; + +const SAME_ORIGIN = "https://example.com"; +const CROSS_ORIGIN = "https://test1.example.com"; + +const GET_STATE_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "store_header.sjs?getstate"; + +async function addCookieToOrigin(origin) { + const fetchRequestURL = + getRootDirectory(gTestPath).replace("chrome://mochitests/content", origin) + + "store_header.sjs?addcookie"; + + const addcookieTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + fetchRequestURL + ); + + await SpecialPowers.spawn(addcookieTab.linkedBrowser, [], async function () { + content.document.cookie = "coep=credentialless; SameSite=None; Secure"; + }); + await BrowserTestUtils.removeTab(addcookieTab); +} + +async function testOrigin( + fetchOrigin, + isCredentialless, + useMetaTag, + fetchRequestMode, + fetchRequestCrendentials, + expectedCookieResult +) { + let params = []; + if (isCredentialless) { + params.push("credentialless"); + } + if (useMetaTag) { + params.push("meta"); + } + + let topLevelUrl = TOP_LEVEL_URL; + if (params.length) { + topLevelUrl += "?" + params.join("&"); + } + + const noCredentiallessTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + topLevelUrl + ); + + const fetchRequestURL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + fetchOrigin + ) + "store_header.sjs?checkheader"; + + await SpecialPowers.spawn( + noCredentiallessTab.linkedBrowser, + [ + !useMetaTag && isCredentialless, + fetchRequestURL, + fetchRequestMode, + fetchRequestCrendentials, + GET_STATE_URL, + expectedCookieResult, + ], + async function ( + sharedArrayBufferEnabled, + fetchRequestURL, + fetchRequestMode, + fetchRequestCrendentials, + getStateURL, + expectedCookieResult + ) { + if (sharedArrayBufferEnabled) { + ok(content.crossOriginIsolated); + } + // When store_header.sjs receives this request, it will store + // whether it has received the cookie as a shared state. + await content.fetch(fetchRequestURL, { + mode: fetchRequestMode, + credentials: fetchRequestCrendentials, + }); + + // This request is used to get the saved state from the + // previous fetch request. + const response = await content.fetch(getStateURL, { + mode: "cors", + }); + const text = await response.text(); + is(text, expectedCookieResult); + } + ); + + await BrowserTestUtils.removeTab(noCredentiallessTab); +} + +async function doTest( + origin, + fetchRequestMode, + fetchRequestCrendentials, + expectedCookieResultForNoCredentialless, + expectedCookieResultForCredentialless +) { + for (let credentialless of [true, false]) { + for (let meta of [true, false]) { + await testOrigin( + origin, + credentialless, + meta, + fetchRequestMode, + fetchRequestCrendentials, + credentialless + ? expectedCookieResultForCredentialless + : expectedCookieResultForNoCredentialless + ); + } + } +} + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.tabs.remote.coep.credentialless", false], + ["dom.origin-trials.enabled", true], + ["dom.origin-trials.test-key.enabled", true], + ], + }); + + await addCookieToOrigin(SAME_ORIGIN); + await addCookieToOrigin(CROSS_ORIGIN); + + // Same-origin request contains Cookies. + await doTest(SAME_ORIGIN, "no-cors", "include", "hasCookie", "hasCookie"); + await doTest(SAME_ORIGIN, "cors", "include", "hasCookie", "hasCookie"); + await doTest(SAME_ORIGIN, "no-cors", "same-origin", "hasCookie", "hasCookie"); + await doTest(SAME_ORIGIN, "cors", "same-origin", "hasCookie", "hasCookie"); +}); diff --git a/dom/fetch/tests/browser_origin_trial_coep_credentialless_fetch_3.js b/dom/fetch/tests/browser_origin_trial_coep_credentialless_fetch_3.js new file mode 100644 index 0000000000..a1a1edb2ff --- /dev/null +++ b/dom/fetch/tests/browser_origin_trial_coep_credentialless_fetch_3.js @@ -0,0 +1,150 @@ +const TOP_LEVEL_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "open_credentialless_document.sjs"; + +const SAME_ORIGIN = "https://example.com"; +const CROSS_ORIGIN = "https://test1.example.com"; + +const GET_STATE_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "store_header.sjs?getstate"; + +async function addCookieToOrigin(origin) { + const fetchRequestURL = + getRootDirectory(gTestPath).replace("chrome://mochitests/content", origin) + + "store_header.sjs?addcookie"; + + const addcookieTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + fetchRequestURL + ); + + await SpecialPowers.spawn(addcookieTab.linkedBrowser, [], async function () { + content.document.cookie = "coep=credentialless; SameSite=None; Secure"; + }); + await BrowserTestUtils.removeTab(addcookieTab); +} + +async function testOrigin( + fetchOrigin, + isCredentialless, + useMetaTag, + fetchRequestMode, + fetchRequestCrendentials, + expectedCookieResult +) { + let params = []; + if (isCredentialless) { + params.push("credentialless"); + } + if (useMetaTag) { + params.push("meta"); + } + + let topLevelUrl = TOP_LEVEL_URL; + if (params.length) { + topLevelUrl += "?" + params.join("&"); + } + + const noCredentiallessTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + topLevelUrl + ); + + const fetchRequestURL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + fetchOrigin + ) + "store_header.sjs?checkheader"; + + await SpecialPowers.spawn( + noCredentiallessTab.linkedBrowser, + [ + !useMetaTag && isCredentialless, + fetchRequestURL, + fetchRequestMode, + fetchRequestCrendentials, + GET_STATE_URL, + expectedCookieResult, + ], + async function ( + sharedArrayBufferEnabled, + fetchRequestURL, + fetchRequestMode, + fetchRequestCrendentials, + getStateURL, + expectedCookieResult + ) { + if (sharedArrayBufferEnabled) { + ok(content.crossOriginIsolated); + } + // When store_header.sjs receives this request, it will store + // whether it has received the cookie as a shared state. + await content.fetch(fetchRequestURL, { + mode: fetchRequestMode, + credentials: fetchRequestCrendentials, + }); + + // This request is used to get the saved state from the + // previous fetch request. + const response = await content.fetch(getStateURL, { + mode: "cors", + }); + const text = await response.text(); + is(text, expectedCookieResult); + } + ); + + await BrowserTestUtils.removeTab(noCredentiallessTab); +} + +async function doTest( + origin, + fetchRequestMode, + fetchRequestCrendentials, + expectedCookieResultForNoCredentialless, + expectedCookieResultForCredentialless +) { + for (let credentialless of [true, false]) { + for (let meta of [true, false]) { + await testOrigin( + origin, + credentialless, + meta, + fetchRequestMode, + fetchRequestCrendentials, + credentialless + ? expectedCookieResultForCredentialless + : expectedCookieResultForNoCredentialless + ); + } + } +} + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.tabs.remote.coep.credentialless", false], + ["dom.origin-trials.enabled", true], + ["dom.origin-trials.test-key.enabled", true], + ], + }); + + await addCookieToOrigin(SAME_ORIGIN); + await addCookieToOrigin(CROSS_ORIGIN); + + // Cross-origin CORS requests contains Cookies, if credentials mode is set to + // 'include'. This does not depends on COEP. + await doTest(CROSS_ORIGIN, "cors", "include", "hasCookie", "hasCookie"); + await doTest(CROSS_ORIGIN, "cors", "same-origin", "noCookie", "noCookie"); + + // Cross-origin no-CORS requests includes Cookies when: + // 1. credentials mode is 'include' + // 2. COEP: is not credentialless. + await doTest(CROSS_ORIGIN, "no-cors", "include", "hasCookie", "noCookie"); + await doTest(CROSS_ORIGIN, "no-cors", "same-origin", "noCookie", "noCookie"); +}); diff --git a/dom/fetch/tests/browser_origin_trial_coep_credentialless_worker.js b/dom/fetch/tests/browser_origin_trial_coep_credentialless_worker.js new file mode 100644 index 0000000000..be77add88e --- /dev/null +++ b/dom/fetch/tests/browser_origin_trial_coep_credentialless_worker.js @@ -0,0 +1,164 @@ +const TOP_LEVEL_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "open_credentialless_document.sjs"; + +const WORKER_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "credentialless_worker.sjs"; + +const GET_STATE_URL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" + ) + "store_header.sjs?getstate"; + +const SAME_ORIGIN = "https://example.com"; +const CROSS_ORIGIN = "https://test1.example.com"; + +const WORKER_USES_CREDENTIALLESS = "credentialless"; +const WORKER_NOT_USE_CREDENTIALLESS = ""; + +async function addCookieToOrigin(origin) { + const fetchRequestURL = + getRootDirectory(gTestPath).replace("chrome://mochitests/content", origin) + + "store_header.sjs?addcookie"; + + const addcookieTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + fetchRequestURL + ); + + await SpecialPowers.spawn(addcookieTab.linkedBrowser, [], async function () { + content.document.cookie = "coep=credentialless; SameSite=None; Secure"; + }); + await BrowserTestUtils.removeTab(addcookieTab); +} + +async function testOrigin( + fetchOrigin, + isCredentialless, + workerUsesCredentialless, + expectedCookieResult +) { + let topLevelUrl = TOP_LEVEL_URL; + if (isCredentialless) { + topLevelUrl += "?credentialless"; + } + const noCredentiallessTab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + topLevelUrl + ); + + const fetchRequestURL = + getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + fetchOrigin + ) + "store_header.sjs?checkheader"; + + let workerScriptURL = WORKER_URL + "?" + workerUsesCredentialless; + + await SpecialPowers.spawn( + noCredentiallessTab.linkedBrowser, + [fetchRequestURL, GET_STATE_URL, workerScriptURL, expectedCookieResult], + async function ( + fetchRequestURL, + getStateURL, + workerScriptURL, + expectedCookieResult + ) { + const worker = new content.Worker(workerScriptURL, {}); + + // When the worker receives this message, it'll send + // a fetch request to fetchRequestURL, and fetchRequestURL + // will store whether it has received the cookie as a + // shared state. + worker.postMessage(fetchRequestURL); + + if (expectedCookieResult == "error") { + await new Promise(r => { + worker.onerror = function () { + ok(true, "worker has error"); + r(); + }; + }); + } else { + await new Promise(r => { + worker.addEventListener("message", async function () { + // This request is used to get the saved state from the + // previous fetch request. + const response = await content.fetch(getStateURL, { + mode: "cors", + }); + const text = await response.text(); + is(text, expectedCookieResult); + r(); + }); + }); + } + } + ); + await BrowserTestUtils.removeTab(noCredentiallessTab); +} + +async function dedicatedWorkerTest( + origin, + workerCOEP, + expectedCookieResultForNoCredentialless, + expectedCookieResultForCredentialless +) { + await testOrigin( + origin, + false, + workerCOEP, + expectedCookieResultForNoCredentialless + ); + await testOrigin( + origin, + true, + workerCOEP, + expectedCookieResultForCredentialless + ); +} + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["browser.tabs.remote.coep.credentialless", false], // Explicitly set credentialless to false because we want to test origin trial + ["dom.origin-trials.enabled", true], + ["dom.origin-trials.test-key.enabled", true], + ], + }); + + await addCookieToOrigin(SAME_ORIGIN); + await addCookieToOrigin(CROSS_ORIGIN); + + await dedicatedWorkerTest( + SAME_ORIGIN, + WORKER_NOT_USE_CREDENTIALLESS, + "hasCookie", + "error" + ); + await dedicatedWorkerTest( + SAME_ORIGIN, + WORKER_USES_CREDENTIALLESS, + "hasCookie", + "hasCookie" + ); + + await dedicatedWorkerTest( + CROSS_ORIGIN, + WORKER_NOT_USE_CREDENTIALLESS, + "hasCookie", + "error" + ); + await dedicatedWorkerTest( + CROSS_ORIGIN, + WORKER_USES_CREDENTIALLESS, + "noCookie", + "noCookie" + ); +}); diff --git a/dom/fetch/tests/crashtests/1577196.html b/dom/fetch/tests/crashtests/1577196.html new file mode 100644 index 0000000000..a2cdae06d0 --- /dev/null +++ b/dom/fetch/tests/crashtests/1577196.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html> +<body> +<script> + +function test() { + const xhr = new XMLHttpRequest(); + const frame = document.createElement("frame"); + document.documentElement.appendChild(frame); + const win = frame.contentWindow; + const div = document.createElement("div"); + div.insertBefore(frame, div.childNodes[0]); + SpecialPowers.forceCC(); + SpecialPowers.gc(); + xhr.open("P", "", false); + xhr.send(); + win.fetch(null, {}).then(function(arg14) {}); +} + +for (let i = 0; i < 10; i++) { + test(); +} + +</script> +</body> +</html> diff --git a/dom/fetch/tests/crashtests/1664514.html b/dom/fetch/tests/crashtests/1664514.html new file mode 100644 index 0000000000..b033d842fa --- /dev/null +++ b/dom/fetch/tests/crashtests/1664514.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script> + fetch("./url.url") +</script> diff --git a/dom/fetch/tests/crashtests/crashtests.list b/dom/fetch/tests/crashtests/crashtests.list new file mode 100644 index 0000000000..f79a122734 --- /dev/null +++ b/dom/fetch/tests/crashtests/crashtests.list @@ -0,0 +1,2 @@ +load 1577196.html +load 1664514.html diff --git a/dom/fetch/tests/crashtests/url.url b/dom/fetch/tests/crashtests/url.url new file mode 100644 index 0000000000..95e9cf8db0 --- /dev/null +++ b/dom/fetch/tests/crashtests/url.url @@ -0,0 +1,5 @@ +[{000214A0-0000-0000-C000-000000000046}]
+Prop3=19,11
+[InternetShortcut]
+IDList=
+URL=http://localhost:8000/
diff --git a/dom/fetch/tests/credentialless_resource.sjs b/dom/fetch/tests/credentialless_resource.sjs new file mode 100644 index 0000000000..72d0d738ec --- /dev/null +++ b/dom/fetch/tests/credentialless_resource.sjs @@ -0,0 +1,21 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +// small red image +const IMG_BYTES = atob( + "iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12" + + "P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" +); + +function handleRequest(request, response) { + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + response.write("Content-Type: image/png\r\n"); + if (request.queryString === "corp_cross_origin") { + response.write("Cross-Origin-Resource-Policy: cross-origin\r\n"); + } + response.write("\r\n"); + response.write(IMG_BYTES); + response.finish(); +} diff --git a/dom/fetch/tests/credentialless_worker.sjs b/dom/fetch/tests/credentialless_worker.sjs new file mode 100644 index 0000000000..a9e2197d18 --- /dev/null +++ b/dom/fetch/tests/credentialless_worker.sjs @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const WORKER = ` + onmessage = function(event) { + fetch(event.data, { + mode: "no-cors", + credentials: "include" + }).then(function() { + postMessage("fetch done"); + }); + } +`; + +function handleRequest(request, response) { + if (request.queryString === "credentialless") { + response.setHeader("Cross-Origin-Embedder-Policy", "credentialless", true); + } + + response.setHeader("Content-Type", "application/javascript", false); + response.setStatusLine(request.httpVersion, "200", "Found"); + response.write(WORKER); +} diff --git a/dom/fetch/tests/mochitest.ini b/dom/fetch/tests/mochitest.ini new file mode 100644 index 0000000000..32eb841faf --- /dev/null +++ b/dom/fetch/tests/mochitest.ini @@ -0,0 +1,2 @@ +[test_ext_response_constructor.html] +[test_invalid_header_exception.html] diff --git a/dom/fetch/tests/open_credentialless_document.sjs b/dom/fetch/tests/open_credentialless_document.sjs new file mode 100644 index 0000000000..6269e73d78 --- /dev/null +++ b/dom/fetch/tests/open_credentialless_document.sjs @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +// Created with: mktoken --origin 'https://example.com' --feature CoepCredentialless --expiry 'Wed, 01 Jan 3000 01:00:00 +0100' --sign test-keys/test-ecdsa.pkcs8 +const TOKEN = + "Az+DK2Kczk8Xz1cAlD+TkvPZmuM2uJZ2CFefbp2hLuCU9FbUqxWTyQ2tEYr50r0syKELcOZLAPaABw8aYTLHn5YAAABUeyJvcmlnaW4iOiJodHRwczovL2V4YW1wbGUuY29tIiwiZmVhdHVyZSI6IkNvZXBDcmVkZW50aWFsbGVzcyIsImV4cGlyeSI6MzI1MDM2ODAwMDB9"; + +function handleRequest(request, response) { + let params = (request.queryString || "").split("&"); + if (params.includes("credentialless")) { + response.setHeader("Cross-Origin-Embedder-Policy", "credentialless"); + // Enables the SharedArrayBuffer feature + response.setHeader("Cross-Origin-Opener-Policy", "same-origin"); + } else if (params.includes("requirecorp")) { + response.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); + } + let html = "<!doctype html>"; + if (!params.includes("meta")) { + response.setHeader("Origin-Trial", TOKEN); + } else { + html += `<meta http-equiv="origin-trial" content="${TOKEN}">`; + } + html += "<body>Hello, world!</body>"; + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + response.setStatusLine(request.httpVersion, "200", "Found"); + response.write(html); +} diff --git a/dom/fetch/tests/store_header.sjs b/dom/fetch/tests/store_header.sjs new file mode 100644 index 0000000000..3290a09995 --- /dev/null +++ b/dom/fetch/tests/store_header.sjs @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const key = "store_header"; +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/plain"); + response.setHeader("Access-Control-Allow-Origin", "https://example.com"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + + if (request.queryString === "getstate") { + response.write(getSharedState(key)); + } else if (request.queryString === "checkheader") { + if (request.hasHeader("Cookie")) { + setSharedState(key, "hasCookie"); + } else { + setSharedState(key, "noCookie"); + } + } else { + // This is the first request which sets the cookie + } +} diff --git a/dom/fetch/tests/test_ext_response_constructor.html b/dom/fetch/tests/test_ext_response_constructor.html new file mode 100644 index 0000000000..e4b6cf4eb2 --- /dev/null +++ b/dom/fetch/tests/test_ext_response_constructor.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test `Response` constructor in a WebExtension</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + add_task(async function testResponseConstructor() { + /* eslint-env webextensions */ + function contentScript() { + new Response(); + browser.test.notifyPass("done"); + } + + const extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + matches: ["<all_urls>"], + js: ["content_script.js"], + }, + ], + }, + + files: { + "content_script.js": contentScript, + }, + }); + + await extension.startup(); + + const win = window.open("https://example.com"); + await extension.awaitFinish("done"); + win.close(); + + await extension.unload(); + }); + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/dom/fetch/tests/test_invalid_header_exception.html b/dom/fetch/tests/test_invalid_header_exception.html new file mode 100644 index 0000000000..710afffcc6 --- /dev/null +++ b/dom/fetch/tests/test_invalid_header_exception.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1629390 +--> +<head> + <title>Test for Bug 1629390</title> + <meta charset="UTF-8"> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1629390">Mozilla Bug 1629390</a> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 1629390 **/ +let headerNames = [ + ['aÀªb', 'a\uFFFD\uFFFDb'], + ['Àaª', '\uFFFDa\uFFFD'], + ['Àªb', '\uFFFD\uFFFDb'], + ['\xEAa', '\uFFFDa'], + ['\xC2\x7F', '\uFFFD\x7F'], +]; +for (let [invalid, corrected] of headerNames) { + try { + new Headers([[invalid, '']]); + } catch(e) { + is(e.message, `Headers constructor: ${corrected} is an invalid header name.`); + } +} + +</script> +</pre> +</body> +</html> |