diff options
Diffstat (limited to 'dom/fetch')
42 files changed, 9870 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..e612306d1d --- /dev/null +++ b/dom/fetch/BodyExtractor.h @@ -0,0 +1,44 @@ +/* -*- 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 { +namespace 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 dom +} // namespace mozilla + +#endif // mozilla_dom_BodyExtractor_h diff --git a/dom/fetch/ChannelInfo.cpp b/dom/fetch/ChannelInfo.cpp new file mode 100644 index 0000000000..62e6287b0c --- /dev/null +++ b/dom/fetch/ChannelInfo.cpp @@ -0,0 +1,113 @@ +/* -*- 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 "mozilla/ipc/ChannelInfo.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<nsISupports> 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<nsISupports> 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.Truncate(); + mInited = true; +} + +void ChannelInfo::InitFromIPCChannelInfo( + const mozilla::ipc::IPCChannelInfo& aChannelInfo) { + MOZ_ASSERT(!mInited, "Cannot initialize the object twice"); + + mSecurityInfo = aChannelInfo.securityInfo(); + + mInited = true; +} + +void ChannelInfo::SetSecurityInfo(nsISupports* aSecurityInfo) { + MOZ_ASSERT(mSecurityInfo.IsEmpty(), "security info should only be set once"); + nsCOMPtr<nsISerializable> serializable = do_QueryInterface(aSecurityInfo); + if (!serializable) { + NS_WARNING( + "A non-serializable object was passed to " + "InternalResponse::SetSecurityInfo"); + return; + } + NS_SerializeToString(serializable, mSecurityInfo); +} + +nsresult ChannelInfo::ResurrectInfoOnChannel(nsIChannel* aChannel) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mInited); + + if (!mSecurityInfo.IsEmpty()) { + nsCOMPtr<nsISupports> infoObj; + nsresult rv = NS_DeserializeObject(mSecurityInfo, getter_AddRefs(infoObj)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel); + MOZ_ASSERT(httpChannel); + net::HttpBaseChannel* httpBaseChannel = + static_cast<net::HttpBaseChannel*>(httpChannel.get()); + rv = httpBaseChannel->OverrideSecurityInfo(infoObj); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return NS_OK; +} + +mozilla::ipc::IPCChannelInfo ChannelInfo::AsIPCChannelInfo() 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. + + IPCChannelInfo ipcInfo; + + ipcInfo.securityInfo() = mSecurityInfo; + + return ipcInfo; +} diff --git a/dom/fetch/ChannelInfo.h b/dom/fetch/ChannelInfo.h new file mode 100644 index 0000000000..73dcfbf297 --- /dev/null +++ b/dom/fetch/ChannelInfo.h @@ -0,0 +1,77 @@ +/* -*- 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 "nsString.h" +#include "nsCOMPtr.h" + +class nsIChannel; +class nsIGlobalObject; +class nsIURI; + +namespace mozilla { +namespace ipc { +class IPCChannelInfo; +} // namespace ipc + +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: +// * IPCChannelInfo +// * InitFromChannel and InitFromIPCChannelInfo members +// * ResurrectInfoOnChannel member +// * AsIPCChannelInfo 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: + typedef mozilla::ipc::IPCChannelInfo IPCChannelInfo; + + 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 InitFromIPCChannelInfo(const IPCChannelInfo& aChannelInfo); + + // This restores every possible information stored from a previous channel + // object on a new one. + nsresult ResurrectInfoOnChannel(nsIChannel* aChannel); + + bool IsInitialized() const { return mInited; } + + IPCChannelInfo AsIPCChannelInfo() const; + + private: + void SetSecurityInfo(nsISupports* aSecurityInfo); + + private: + nsCString mSecurityInfo; + bool mInited; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ChannelInfo_h diff --git a/dom/fetch/ChannelInfo.ipdlh b/dom/fetch/ChannelInfo.ipdlh new file mode 100644 index 0000000000..605009e120 --- /dev/null +++ b/dom/fetch/ChannelInfo.ipdlh @@ -0,0 +1,14 @@ +/* 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/. */ + +namespace mozilla { +namespace ipc { + +struct IPCChannelInfo +{ + nsCString securityInfo; +}; + +} // namespace ipc +} // namespace mozilla diff --git a/dom/fetch/EmptyBody.cpp b/dom/fetch/EmptyBody.cpp new file mode 100644 index 0000000000..18b521ac02 --- /dev/null +++ b/dom/fetch/EmptyBody.cpp @@ -0,0 +1,84 @@ +/* -*- 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 "EmptyBody.h" + +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "nsStringStream.h" + +namespace mozilla::dom { + +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>) + AbortFollower::Unlink(static_cast<AbortFollower*>(tmp)); + 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>) + AbortFollower::Traverse(static_cast<AbortFollower*>(tmp), cb); + 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, + already_AddRefed<nsIInputStream> aBodyStream) + : FetchBody<EmptyBody>(aGlobal), + mAbortSignalImpl(aAbortSignalImpl), + mMimeType(aMimeType), + 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, + 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, + 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/EmptyBody.h b/dom/fetch/EmptyBody.h new file mode 100644 index 0000000000..dce71d4ae2 --- /dev/null +++ b/dom/fetch/EmptyBody.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_EmptyBody_h +#define mozilla_dom_EmptyBody_h + +#include "nsISupportsImpl.h" + +#include "mozilla/dom/Fetch.h" + +namespace mozilla { + +namespace ipc { +class PrincipalInfo; +} // namespace ipc + +namespace dom { + +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, + ErrorResult& aRv); + + nsIGlobalObject* GetParentObject() const { return mOwner; } + + AbortSignalImpl* GetSignalImpl() const override { return mAbortSignalImpl; } + + const UniquePtr<mozilla::ipc::PrincipalInfo>& GetPrincipalInfo() const { + return mPrincipalInfo; + } + + void GetMimeType(nsACString& aMimeType) { aMimeType = mMimeType; } + + 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, + already_AddRefed<nsIInputStream> mBodyStream); + + ~EmptyBody(); + + UniquePtr<mozilla::ipc::PrincipalInfo> mPrincipalInfo; + RefPtr<AbortSignalImpl> mAbortSignalImpl; + nsCString mMimeType; + nsCOMPtr<nsIInputStream> mBodyStream; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_EmptyBody_h diff --git a/dom/fetch/Fetch.cpp b/dom/fetch/Fetch.cpp new file mode 100644 index 0000000000..d47c09461a --- /dev/null +++ b/dom/fetch/Fetch.cpp @@ -0,0 +1,1537 @@ +/* -*- 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 "mozilla/dom/Document.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/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 "EmptyBody.h" +#include "FetchObserver.h" +#include "InternalRequest.h" +#include "InternalResponse.h" + +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRef.h" +#include "mozilla/dom/WorkerRunnable.h" +#include "mozilla/dom/WorkerScope.h" + +namespace mozilla::dom { + +namespace { + +void AbortStream(JSContext* aCx, JS::Handle<JSObject*> aStream, + ErrorResult& aRv) { + aRv.MightThrowJSException(); + + bool isReadable; + if (!JS::ReadableStreamIsReadable(aCx, aStream, &isReadable)) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + if (!isReadable) { + return; + } + + RefPtr<DOMException> e = DOMException::Create(NS_ERROR_DOM_ABORT_ERR); + + JS::Rooted<JS::Value> value(aCx); + if (!GetOrCreateDOMReflector(aCx, e, &value)) { + return; + } + + if (!JS::ReadableStreamError(aCx, aStream, value)) { + aRv.StealExceptionFromJSContext(aCx); + } +} + +} // namespace + +class AbortSignalMainThread final : public AbortSignalImpl { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(AbortSignalMainThread) + + explicit AbortSignalMainThread(bool aAborted) : AbortSignalImpl(aAborted) {} + + private: + ~AbortSignalMainThread() = default; +}; + +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_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; + +class WorkerSignalFollower final : public AbortFollower { + public: + // 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::WorkerSignalFollower::AbortSignalProxyRunnable"), + mProxy(aProxy) {} + + NS_IMETHOD Run() override; + }; + + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(WorkerSignalFollower) + + void RunAbortAlgorithm() override {} + + private: + ~WorkerSignalFollower() = default; +}; + +NS_IMPL_CYCLE_COLLECTION_CLASS(WorkerSignalFollower) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(WorkerSignalFollower) +NS_IMPL_CYCLE_COLLECTING_RELEASE(WorkerSignalFollower) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(WorkerSignalFollower) + AbortFollower::Unlink(static_cast<AbortFollower*>(tmp)); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(WorkerSignalFollower) + AbortFollower::Traverse(static_cast<AbortFollower*>(tmp), cb); +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WorkerSignalFollower) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +// 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 WorkerSignalFollower::AbortSignalProxyRunnable::Run() { + MOZ_ASSERT(NS_IsMainThread()); + AbortSignalImpl* signalImpl = mProxy->GetOrCreateSignalImplForMainThread(); + signalImpl->SignalAbort(); + return NS_OK; +} + +void AbortSignalProxy::RunAbortAlgorithm() { + MOZ_ASSERT(!NS_IsMainThread()); + using AbortSignalProxyRunnable = + WorkerSignalFollower::AbortSignalProxyRunnable; + 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; + + 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(InternalResponse* aResponse) override; + + void OnResponseEnd(FetchDriverObserver::EndReason eReason) override; + + bool NeedOnDataAvailable() override; + + void OnDataAvailable() override; + + void Shutdown(WorkerPrivate* aWorkerPrivate) { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + mIsShutdown = true; + mPromiseProxy->CleanUp(); + + 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) { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(mPromiseProxy); + } + + ~WorkerFetchResolver() = default; + + virtual void FlushConsoleReport() override; +}; + +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(InternalResponse* aResponse) override; + + void SetLoadGroup(nsILoadGroup* aLoadGroup) { mLoadGroup = aLoadGroup; } + + void OnResponseEnd(FetchDriverObserver::EndReason aReason) override { + if (aReason == eAborted) { + 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(); + RefPtr<AbortSignalImpl> signalImpl = request->GetSignalImpl(); + + if (signalImpl && signalImpl->Aborted()) { + // Already aborted signal rejects immediately. + aRv.Throw(NS_ERROR_DOM_ABORT_ERR); + return nullptr; + } + + 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(); + } + + 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(); + } + + 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() { + mPromise->MaybeResolve(mResponse); + return NS_OK; + } + RefPtr<Promise> mPromise; + RefPtr<Response> mResponse; +}; + +void MainThreadFetchResolver::OnResponseAvailableInternal( + InternalResponse* aResponse) { + NS_ASSERT_OWNINGTHREAD(MainThreadFetchResolver); + AssertIsOnMainThread(); + + if (aResponse->Type() != ResponseType::Error) { + if (mFetchObserver) { + mFetchObserver->SetState(FetchState::Complete); + } + + nsCOMPtr<nsIGlobalObject> go = mPromise->GetParentObject(); + mResponse = new Response(go, aResponse, mSignalImpl); + nsCOMPtr<nsPIDOMWindowInner> inner = do_QueryInterface(go); + 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. + RefPtr<InternalResponse> mInternalResponse; + + public: + WorkerFetchResponseRunnable(WorkerPrivate* aWorkerPrivate, + WorkerFetchResolver* aResolver, + InternalResponse* aResponse) + : MainThreadWorkerRunnable(aWorkerPrivate), + mResolver(aResolver), + mInternalResponse(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, + mResolver->GetAbortSignalForTargetThread()); + 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 { + // Execute Run anyway to make sure we cleanup our promise proxy to avoid + // leaking the worker thread + Run(); + return WorkerRunnable::Cancel(); + } +}; + +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( + InternalResponse* aResponse) { + AssertIsOnMainThread(); + + MutexAutoLock lock(mPromiseProxy->Lock()); + if (mPromiseProxy->CleanedUp()) { + return; + } + + RefPtr<WorkerFetchResponseRunnable> r = new WorkerFetchResponseRunnable( + mPromiseProxy->GetWorkerPrivate(), this, aResponse); + + if (!r->Dispatch()) { + NS_WARNING("Could not dispatch fetch response"); + } +} + +bool WorkerFetchResolver::NeedOnDataAvailable() { + AssertIsOnMainThread(); + MutexAutoLock lock(mPromiseProxy->Lock()); + return !!mFetchObserver; +} + +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) { + AssertIsOnMainThread(); + MutexAutoLock lock(mPromiseProxy->Lock()); + if (mPromiseProxy->CleanedUp()) { + return; + } + + FlushConsoleReport(); + + 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; +} + +template <class Derived> +FetchBody<Derived>::FetchBody(nsIGlobalObject* aOwner) + : mOwner(aOwner), + mWorkerPrivate(nullptr), + mReadableStreamBody(nullptr), + mReadableStreamReader(nullptr), + mBodyUsed(false) { + MOZ_ASSERT(aOwner); + + if (!NS_IsMainThread()) { + mWorkerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(mWorkerPrivate); + mMainThreadEventTarget = mWorkerPrivate->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>::GetBodyUsed(ErrorResult& aRv) const { + if (mBodyUsed) { + return true; + } + + // If this stream is disturbed, return true. + if (mReadableStreamBody) { + aRv.MightThrowJSException(); + + AutoJSAPI jsapi; + if (!jsapi.Init(mOwner)) { + aRv.Throw(NS_ERROR_FAILURE); + return true; + } + + JSContext* cx = jsapi.cx(); + JS::Rooted<JSObject*> body(cx, mReadableStreamBody); + bool disturbed; + if (!JS::ReadableStreamIsDisturbed(cx, body, &disturbed)) { + aRv.StealExceptionFromJSContext(cx); + return false; + } + + return disturbed; + } + + return false; +} + +template bool FetchBody<Request>::GetBodyUsed(ErrorResult&) const; + +template bool FetchBody<Response>::GetBodyUsed(ErrorResult&) const; + +template <class Derived> +bool FetchBody<Derived>::CheckBodyUsed() const { + IgnoredErrorResult result; + bool bodyUsed = GetBodyUsed(result); + if (result.Failed()) { + // Ignore the error. + return true; + } + return bodyUsed; +} + +template <class Derived> +void FetchBody<Derived>::SetBodyUsed(JSContext* aCx, ErrorResult& aRv) { + MOZ_ASSERT(aCx); + MOZ_ASSERT(mOwner->EventTargetFor(TaskCategory::Other)->IsOnCurrentThread()); + + if (mBodyUsed) { + 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) { + aRv.MightThrowJSException(); + + JSAutoRealm ar(aCx, mOwner->GetGlobalJSObject()); + + JS::Rooted<JSObject*> readableStreamObj(aCx, mReadableStreamBody); + + JS::ReadableStreamMode mode; + if (!JS::ReadableStreamGetMode(aCx, readableStreamObj, &mode)) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + + if (mode == JS::ReadableStreamMode::ExternalSource) { + LockStream(aCx, readableStreamObj, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } else { + // If this is not a native ReadableStream, let's activate the + // FetchStreamReader. + MOZ_ASSERT(mFetchStreamReader); + JS::Rooted<JSObject*> reader(aCx); + mFetchStreamReader->StartConsuming(aCx, readableStreamObj, &reader, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + mReadableStreamReader = reader; + } + } +} + +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()->GetSignalImpl(); + if (signalImpl && signalImpl->Aborted()) { + aRv.Throw(NS_ERROR_DOM_ABORT_ERR); + return nullptr; + } + + bool bodyUsed = GetBodyUsed(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + if (bodyUsed) { + aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>(); + return nullptr; + } + + nsAutoCString mimeType; + DerivedClass()->GetMimeType(mimeType); + + // 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, 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, 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) { + // 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); + ToLowerCase(aMimeType); + } +} + +template void FetchBody<Request>::GetMimeType(nsACString& aMimeType); +template void FetchBody<Response>::GetMimeType(nsACString& aMimeType); + +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, + JSObject* aBody) { + MOZ_ASSERT(!mReadableStreamBody); + MOZ_ASSERT(aBody); + mReadableStreamBody = aBody; + + RefPtr<AbortSignalImpl> signalImpl = DerivedClass()->GetSignalImpl(); + if (!signalImpl) { + return; + } + + bool aborted = signalImpl->Aborted(); + if (aborted) { + JS::Rooted<JSObject*> body(aCx, mReadableStreamBody); + IgnoredErrorResult result; + AbortStream(aCx, body, result); + if (NS_WARN_IF(result.Failed())) { + return; + } + } else if (!IsFollowing()) { + Follow(signalImpl); + } +} + +template void FetchBody<Request>::SetReadableStreamBody(JSContext* aCx, + JSObject* aBody); + +template void FetchBody<Response>::SetReadableStreamBody(JSContext* aCx, + JSObject* aBody); + +template <class Derived> +void FetchBody<Derived>::GetBody(JSContext* aCx, + JS::MutableHandle<JSObject*> aBodyOut, + ErrorResult& aRv) { + if (mReadableStreamBody) { + aBodyOut.set(mReadableStreamBody); + return; + } + + nsCOMPtr<nsIInputStream> inputStream; + DerivedClass()->GetBody(getter_AddRefs(inputStream)); + + if (!inputStream) { + aBodyOut.set(nullptr); + return; + } + + BodyStream::Create(aCx, this, DerivedClass()->GetParentObject(), inputStream, + aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + MOZ_ASSERT(mReadableStreamBody); + + JS::Rooted<JSObject*> body(aCx, mReadableStreamBody); + + // If the body has been already consumed, we lock the stream. + bool bodyUsed = GetBodyUsed(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + if (bodyUsed) { + LockStream(aCx, body, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + + RefPtr<AbortSignalImpl> signalImpl = DerivedClass()->GetSignalImpl(); + if (signalImpl) { + if (signalImpl->Aborted()) { + AbortStream(aCx, body, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } else if (!IsFollowing()) { + Follow(signalImpl); + } + } + + aBodyOut.set(mReadableStreamBody); +} + +template void FetchBody<Request>::GetBody(JSContext* aCx, + JS::MutableHandle<JSObject*> aMessage, + ErrorResult& aRv); + +template void FetchBody<Response>::GetBody( + JSContext* aCx, JS::MutableHandle<JSObject*> aMessage, ErrorResult& aRv); + +template <class Derived> +void FetchBody<Derived>::LockStream(JSContext* aCx, JS::HandleObject aStream, + ErrorResult& aRv) { + aRv.MightThrowJSException(); + +#if DEBUG + JS::ReadableStreamMode streamMode; + if (!JS::ReadableStreamGetMode(aCx, aStream, &streamMode)) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + MOZ_ASSERT(streamMode == JS::ReadableStreamMode::ExternalSource); +#endif // DEBUG + + // This is native stream, creating a reader will not execute any JS code. + JS::Rooted<JSObject*> reader( + aCx, JS::ReadableStreamGetReader(aCx, aStream, + JS::ReadableStreamReaderMode::Default)); + if (!reader) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + + mReadableStreamReader = reader; +} + +template void FetchBody<Request>::LockStream(JSContext* aCx, + JS::HandleObject aStream, + ErrorResult& aRv); + +template void FetchBody<Response>::LockStream(JSContext* aCx, + JS::HandleObject aStream, + ErrorResult& aRv); + +template <class Derived> +void FetchBody<Derived>::MaybeTeeReadableStreamBody( + JSContext* aCx, JS::MutableHandle<JSObject*> aBodyOut, + FetchStreamReader** aStreamReader, nsIInputStream** aInputStream, + ErrorResult& aRv) { + MOZ_DIAGNOSTIC_ASSERT(aStreamReader); + MOZ_DIAGNOSTIC_ASSERT(aInputStream); + MOZ_DIAGNOSTIC_ASSERT(!CheckBodyUsed()); + + aBodyOut.set(nullptr); + *aStreamReader = nullptr; + *aInputStream = nullptr; + + if (!mReadableStreamBody) { + return; + } + + aRv.MightThrowJSException(); + + JSAutoRealm ar(aCx, mOwner->GetGlobalJSObject()); + + JS::Rooted<JSObject*> stream(aCx, mReadableStreamBody); + + // If this is a ReadableStream with an external source, this has been + // generated by a Fetch. In this case, Fetch will be able to recreate it + // again when GetBody() is called. + JS::ReadableStreamMode streamMode; + if (!JS::ReadableStreamGetMode(aCx, stream, &streamMode)) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + if (streamMode == JS::ReadableStreamMode::ExternalSource) { + aBodyOut.set(nullptr); + return; + } + + JS::Rooted<JSObject*> branch1(aCx); + JS::Rooted<JSObject*> branch2(aCx); + + if (!JS::ReadableStreamTee(aCx, stream, &branch1, &branch2)) { + aRv.StealExceptionFromJSContext(aCx); + return; + } + + mReadableStreamBody = branch1; + aBodyOut.set(branch2); + + aRv = FetchStreamReader::Create(aCx, mOwner, aStreamReader, aInputStream); + if (NS_WARN_IF(aRv.Failed())) { + return; + } +} + +template void FetchBody<Request>::MaybeTeeReadableStreamBody( + JSContext* aCx, JS::MutableHandle<JSObject*> aMessage, + FetchStreamReader** aStreamReader, nsIInputStream** aInputStream, + ErrorResult& aRv); + +template void FetchBody<Response>::MaybeTeeReadableStreamBody( + JSContext* aCx, JS::MutableHandle<JSObject*> aMessage, + 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(); + + JS::Rooted<JSObject*> body(cx, mReadableStreamBody); + IgnoredErrorResult result; + AbortStream(cx, body, result); +} + +template void FetchBody<Request>::RunAbortAlgorithm(); + +template void FetchBody<Response>::RunAbortAlgorithm(); + +} // namespace mozilla::dom diff --git a/dom/fetch/Fetch.h b/dom/fetch/Fetch.h new file mode 100644 index 0000000000..1689848be7 --- /dev/null +++ b/dom/fetch/Fetch.h @@ -0,0 +1,262 @@ +/* -*- 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 "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/BodyStream.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/FetchStreamReader.h" +#include "mozilla/dom/RequestBinding.h" + +class nsIGlobalObject; +class nsIEventTarget; + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class BlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrUSVString; +class + BlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrReadableStreamOrUSVString; +class BlobImpl; +class InternalRequest; +class + OwningBlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrUSVString; +struct ReadableStream; +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 { +typedef BlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrUSVString + BodyInit; +typedef BlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrReadableStreamOrUSVString + ResponseBodyInit; +typedef OwningBlobOrArrayBufferViewOrArrayBufferOrFormDataOrURLSearchParamsOrUSVString + OwningBodyInit; +}; // 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. + */ +template <class Derived> +class FetchBody : public BodyStreamHolder, public AbortFollower { + public: + using BodyStreamHolder::QueryInterface; + + NS_INLINE_DECL_REFCOUNTING_INHERITED(FetchBody, BodyStreamHolder) + + bool GetBodyUsed(ErrorResult& aRv) const; + + // For use in assertions. On success, returns true if the body is used, false + // if not. On error, this sweeps the error under the rug and returns true. + bool CheckBodyUsed() 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); + } + + void GetBody(JSContext* aCx, JS::MutableHandle<JSObject*> aBodyOut, + ErrorResult& aRv); + + void GetMimeType(nsACString& aMimeType); + + const nsACString& BodyBlobURISpec() const; + + const nsAString& BodyLocalPath() const; + + // If the body contains a ReadableStream body object, this method produces a + // tee() of it. + void MaybeTeeReadableStreamBody(JSContext* aCx, + JS::MutableHandle<JSObject*> 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); + + // BodyStreamHolder + void NullifyStream() override { + mReadableStreamBody = nullptr; + mReadableStreamReader = nullptr; + mFetchStreamReader = nullptr; + } + + void SetReadableStreamBody(JSObject* aBody) override { + mReadableStreamBody = aBody; + } + + JSObject* GetReadableStreamBody() override { return mReadableStreamBody; } + + void MarkAsRead() override { mBodyUsed = true; } + + virtual AbortSignalImpl* GetSignalImpl() const = 0; + + // AbortFollower + void RunAbortAlgorithm() override; + + already_AddRefed<Promise> ConsumeBody(JSContext* aCx, + BodyConsumer::ConsumeType aType, + ErrorResult& aRv); + + protected: + nsCOMPtr<nsIGlobalObject> mOwner; + + // Always set whenever the FetchBody is created on the worker thread. + WorkerPrivate* mWorkerPrivate; + + // This is the ReadableStream exposed to content. It's underlying source is a + // BodyStream object. + JS::Heap<JSObject*> mReadableStreamBody; + + // This is the Reader used to retrieve data from the body. + JS::Heap<JSObject*> mReadableStreamReader; + RefPtr<FetchStreamReader> mFetchStreamReader; + + explicit FetchBody(nsIGlobalObject* aOwner); + + virtual ~FetchBody(); + + void SetReadableStreamBody(JSContext* aCx, JSObject* aBody); + + private: + Derived* DerivedClass() const { + return static_cast<Derived*>(const_cast<FetchBody*>(this)); + } + + void LockStream(JSContext* aCx, JS::HandleObject aStream, ErrorResult& aRv); + + bool IsOnTargetThread() { return NS_IsMainThread() == !mWorkerPrivate; } + + void AssertIsOnTargetThread() { MOZ_ASSERT(IsOnTargetThread()); } + + // Only ever set once, always on target thread. + bool mBodyUsed; + + // The main-thread event target for runnable dispatching. + nsCOMPtr<nsIEventTarget> mMainThreadEventTarget; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_Fetch_h diff --git a/dom/fetch/FetchDriver.cpp b/dom/fetch/FetchDriver.cpp new file mode 100644 index 0000000000..31bc483d34 --- /dev/null +++ b/dom/fetch/FetchDriver.cpp @@ -0,0 +1,1601 @@ +/* -*- 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/DebugOnly.h" +#include "mozilla/dom/FetchDriver.h" + +#include "nsIAsyncVerifyRedirectCallback.h" +#include "mozilla/dom/Document.h" +#include "nsICookieJarSettings.h" +#include "nsIFile.h" +#include "nsIInputStream.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 "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/UserActivation.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/PreloaderBase.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/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 = channel->GetURI(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->Cancel(NS_BINDING_ABORTED); + 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); + nsresult rv = + 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 */); + + if (NS_FAILED(rv)) { + mFetchDriver->FailWithNetworkError(rv); + return rv; + } + + MOZ_DIAGNOSTIC_ASSERT(!mCacheInfoChannel); + mCacheInfoChannel = cic; + + // call FetchDriver::HttpFetch to load main body + MOZ_ASSERT(mFetchDriver); + return mFetchDriver->HttpFetch(); + + } else { + // 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); + } + return NS_OK; +} + +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) + +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); +} + +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()) { + RunAbortAlgorithm(); + 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(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 is 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); + } + + // 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); + + // 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); + + // Conversion between enumerations is safe due to static asserts in + // dom/workers/ServiceWorkerManager.cpp + rv = internalChan->SetCorsMode(static_cast<uint32_t>(mRequest->Mode())); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + 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 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, + true); + 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()) { + nsCOMPtr<nsICacheInfoChannel> cic = do_QueryInterface(chan); + if (cic) { + cic->PreferAlternativeDataType(nsLiteralCString(WASM_ALT_DATA_TYPE_V1), + nsLiteralCString(WASM_CONTENT_TYPE), + false); + } + } + + 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; +} +already_AddRefed<InternalResponse> FetchDriver::BeginAndGetFilteredResponse( + InternalResponse* aResponse, bool aFoundOpaqueRedirect) { + MOZ_ASSERT(aResponse); + AutoTArray<nsCString, 4> reqURLList; + mRequest->GetURLListWithoutFragment(reqURLList); + MOZ_ASSERT(!reqURLList.IsEmpty()); + aResponse->SetURLList(reqURLList); + RefPtr<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); + if (!ShouldCheckSRI(*mRequest, *filteredResponse)) { + // Need to keep mObserver alive. + RefPtr<FetchDriverObserver> observer = mObserver; + observer->OnResponseAvailable(filteredResponse); +#ifdef DEBUG + mResponseAvailableCalled = true; +#endif + } + + return filteredResponse.forget(); +} + +void FetchDriver::FailWithNetworkError(nsresult rv) { + AssertIsOnMainThread(); + RefPtr<InternalResponse> error = InternalResponse::NetworkError(rv); + if (mObserver) { + // Need to keep mObserver alive. + RefPtr<FetchDriverObserver> observer = mObserver; + observer->OnResponseAvailable(error); +#ifdef DEBUG + mResponseAvailableCalled = true; +#endif + } + + // mObserver could be null after OnResponseAvailable(). + if (mObserver) { + mObserver->OnResponseEnd(FetchDriverObserver::eByNetworking); + mObserver = nullptr; + } + + mChannel = nullptr; +} + +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->Cancel(NS_BINDING_ABORTED); + return NS_BINDING_ABORTED; + } + + if (!mChannel) { + MOZ_ASSERT(!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(); + + RefPtr<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; + rv = httpChannel->GetResponseStatus(&responseStatus); + MOZ_ASSERT(NS_SUCCEEDED(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 = new 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 = + new 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().EqualsLiteral( + WASM_ALT_DATA_TYPE_V1)); + 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; + rv = 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 */); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailWithNetworkError(rv); + // Cancel request. + return rv; + } + 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<nsIURI> channelURI; + rv = channel->GetURI(getter_AddRefs(channelURI)); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailWithNetworkError(rv); + // Cancel request. + return rv; + } + + 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(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)) { + Unused << NS_WARN_IF(NS_FAILED(rr->RetargetDeliveryTo(sts))); + } + 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. + + 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; + } + } + } + } + + // Needs to be initialized to 0 because in some cases nsStringInputStream may + // not write to aRead. + uint32_t aRead = 0; + MOZ_ASSERT(mResponse); + 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; + + // main data loading is going to finish, breaking the reference cycle. + RefPtr<AlternativeDataStreamListener> altDataListener = + std::move(mAltDataListener); + + // 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); +#ifdef DEBUG + mResponseAvailableCalled = true; +#endif + } + } + + if (mObserver) { + mObserver->OnResponseEnd(FetchDriverObserver::eByNetworking); + mObserver = nullptr; + } + + mChannel = nullptr; +} + +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); + + SetRequestHeaders(newHttpChannel, rewriteToGET); + } + + // "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(aNewChannel->GetURI(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; +} + +void FetchDriver::SetRequestHeaders(nsIHttpChannel* aChannel, + bool aStripRequestBodyHeader) 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; + } + + 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)); + } + } + nsAutoCString method; + mRequest->GetMethod(method); + if (!method.EqualsLiteral("GET") && !method.EqualsLiteral("HEAD")) { + nsAutoString origin; + if (NS_SUCCEEDED(nsContentUtils::GetUTFOrigin(mPrincipal, origin))) { + DebugOnly<nsresult> rv = aChannel->SetRequestHeader( + nsDependentCString(net::nsHttp::Origin), + NS_ConvertUTF16toUTF8(origin), false /* merge */); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } +} + +void FetchDriver::RunAbortAlgorithm() { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); + + if (mObserver) { +#ifdef DEBUG + mResponseAvailableCalled = true; +#endif + mObserver->OnResponseEnd(FetchDriverObserver::eAborted); + mObserver = nullptr; + } + + if (mChannel) { + mChannel->Cancel(NS_BINDING_ABORTED); + 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..2331b1e4ea --- /dev/null +++ b/dom/fetch/FetchDriver.h @@ -0,0 +1,210 @@ +/* -*- 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 "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; + +/** + * 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(InternalResponse* aResponse) { + MOZ_ASSERT(!mGotResponseAvailable); + mGotResponseAvailable = true; + OnResponseAvailableInternal(aResponse); + } + + enum EndReason { + eAborted, + eByNetworking, + }; + + virtual void OnResponseEnd(EndReason aReason){}; + + 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; + + protected: + virtual ~FetchDriverObserver() = default; + + virtual void OnResponseAvailableInternal(InternalResponse* aResponse) = 0; + + nsCOMPtr<nsIConsoleReportCollector> mReporter; + + private: + bool mGotResponseAvailable; +}; + +class AlternativeDataStreamListener; + +class FetchDriver final : public nsIStreamListener, + public nsIChannelEventSink, + public nsIInterfaceRequestor, + public nsIThreadRetargetableStreamListener, + public AbortFollower { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSIINTERFACEREQUESTOR + 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); + } + + // AbortFollower + void RunAbortAlgorithm() override; + + private: + nsCOMPtr<nsIPrincipal> mPrincipal; + nsCOMPtr<nsILoadGroup> mLoadGroup; + SafeRefPtr<InternalRequest> mRequest; + RefPtr<InternalResponse> mResponse; + nsCOMPtr<nsIOutputStream> mPipeOutputStream; + 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 + + 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. + already_AddRefed<InternalResponse> BeginAndGetFilteredResponse( + 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) 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..b8ae402bfa --- /dev/null +++ b/dom/fetch/FetchIPCTypes.h @@ -0,0 +1,56 @@ +/* -*- 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" + +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_> {}; +} // namespace IPC + +#endif // mozilla_dom_fetch_IPCUtils_h diff --git a/dom/fetch/FetchObserver.cpp b/dom/fetch/FetchObserver.cpp new file mode 100644 index 0000000000..7bdf6b2db8 --- /dev/null +++ b/dom/fetch/FetchObserver.cpp @@ -0,0 +1,81 @@ +/* -*- 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) + AbortFollower::Traverse(static_cast<AbortFollower*>(tmp), cb); +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(FetchObserver, + DOMEventTargetHelper) + AbortFollower::Unlink(static_cast<AbortFollower*>(tmp)); +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..ba63ed233b --- /dev/null +++ b/dom/fetch/FetchObserver.h @@ -0,0 +1,47 @@ +/* -*- 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 { +namespace 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 dom +} // namespace mozilla + +#endif // mozilla_dom_FetchObserver_h diff --git a/dom/fetch/FetchStreamReader.cpp b/dom/fetch/FetchStreamReader.cpp new file mode 100644 index 0000000000..b202dbe430 --- /dev/null +++ b/dom/fetch/FetchStreamReader.cpp @@ -0,0 +1,384 @@ +/* -*- 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 "js/Stream.h" +#include "mozilla/ConsoleReportCollector.h" +#include "mozilla/dom/DOMException.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseBinding.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/WorkerRef.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/TaskCategory.h" +#include "nsContentUtils.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_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(FetchStreamReader) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(FetchStreamReader) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mReader) +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; + + nsresult rv = + NS_NewPipe2(getter_AddRefs(pipeIn), + getter_AddRefs(streamReader->mPipeOut), true, true, 0, 0); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!NS_IsMainThread()) { + WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(aCx); + MOZ_ASSERT(workerPrivate); + + RefPtr<WeakWorkerRef> workerRef = + WeakWorkerRef::Create(workerPrivate, [streamReader]() { + MOZ_ASSERT(streamReader); + MOZ_ASSERT(streamReader->mWorkerRef); + + WorkerPrivate* workerPrivate = streamReader->mWorkerRef->GetPrivate(); + MOZ_ASSERT(workerPrivate); + + streamReader->CloseAndRelease(workerPrivate->GetJSContext(), + NS_ERROR_DOM_INVALID_STATE_ERR); + }); + + 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)), + mBufferRemaining(0), + mBufferOffset(0), + mStreamClosed(false) { + 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) { + RefPtr<DOMException> error = DOMException::Create(aStatus); + + JS::Rooted<JS::Value> errorValue(aCx); + if (ToJSValue(aCx, error, &errorValue)) { + JS::Rooted<JSObject*> reader(aCx, mReader); + // 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. + JS::ReadableStreamReaderCancel(aCx, reader, errorValue); + } + + // We don't want to propagate exceptions during the cleanup. + JS_ClearPendingException(aCx); + } + + mStreamClosed = true; + + mGlobal = nullptr; + + mPipeOut->CloseWithStatus(aStatus); + mPipeOut = nullptr; + + mWorkerRef = nullptr; + + mReader = nullptr; + mBuffer.Clear(); +} + +void FetchStreamReader::StartConsuming(JSContext* aCx, JS::HandleObject aStream, + JS::MutableHandle<JSObject*> aReader, + ErrorResult& aRv) { + MOZ_DIAGNOSTIC_ASSERT(!mReader); + MOZ_DIAGNOSTIC_ASSERT(aStream); + + aRv.MightThrowJSException(); + + // Here, by spec, we can pick any global we want. Just to avoid extra + // cross-compartment steps, we want to create the reader in the same + // compartment of the owning Fetch Body object. + // The same global will be used to retrieve data from this reader. + JSAutoRealm ar(aCx, mGlobal->GetGlobalJSObject()); + + JS::Rooted<JSObject*> reader( + aCx, JS::ReadableStreamGetReader(aCx, aStream, + JS::ReadableStreamReaderMode::Default)); + if (!reader) { + aRv.StealExceptionFromJSContext(aCx); + CloseAndRelease(aCx, NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + mReader = reader; + aReader.set(reader); + + aRv = mPipeOut->AsyncWait(this, 0, 0, mOwningEventTarget); + if (NS_WARN_IF(aRv.Failed())) { + return; + } +} + +// nsIOutputStreamCallback interface + +NS_IMETHODIMP +FetchStreamReader::OnOutputStreamReady(nsIAsyncOutputStream* aStream) { + NS_ASSERT_OWNINGTHREAD(FetchStreamReader); + MOZ_ASSERT(aStream == mPipeOut); + MOZ_ASSERT(mReader); + + if (mStreamClosed) { + return NS_OK; + } + + if (!mBuffer.IsEmpty()) { + return WriteBuffer(); + } + + // Here we can retrieve data from the reader using any global we want because + // it is not observable. We want to use the reader's global, which is also the + // Response's one. + AutoEntryScript aes(mGlobal, "ReadableStreamReader.read", !mWorkerRef); + + JS::Rooted<JSObject*> reader(aes.cx(), mReader); + JS::Rooted<JSObject*> promise( + aes.cx(), JS::ReadableStreamDefaultReaderRead(aes.cx(), reader)); + if (NS_WARN_IF(!promise)) { + // Let's close the stream. + CloseAndRelease(aes.cx(), NS_ERROR_DOM_INVALID_STATE_ERR); + return NS_ERROR_FAILURE; + } + + RefPtr<Promise> domPromise = Promise::CreateFromExisting(mGlobal, promise); + if (NS_WARN_IF(!domPromise)) { + // Let's close the stream. + CloseAndRelease(aes.cx(), NS_ERROR_DOM_INVALID_STATE_ERR); + return NS_ERROR_FAILURE; + } + + // Let's wait. + domPromise->AppendNativeHandler(this); + return NS_OK; +} + +void FetchStreamReader::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue) { + if (mStreamClosed) { + return; + } + + // This promise should be resolved with { done: boolean, value: something }, + // "value" is interesting only if done is false. + + // We don't want to play with JS api, let's WebIDL bindings doing it for us. + // FetchReadableStreamReadDataDone is a dictionary with just a boolean, if the + // parsing succeeded, we can proceed with the parsing of the "value", which it + // must be a Uint8Array. + FetchReadableStreamReadDataDone valueDone; + if (!valueDone.Init(aCx, aValue)) { + JS_ClearPendingException(aCx); + CloseAndRelease(aCx, NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + if (valueDone.mDone) { + // Stream is completed. + CloseAndRelease(aCx, NS_BASE_STREAM_CLOSED); + return; + } + + RootedDictionary<FetchReadableStreamReadDataArray> value(aCx); + if (!value.Init(aCx, aValue) || !value.mValue.WasPassed()) { + JS_ClearPendingException(aCx); + CloseAndRelease(aCx, NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + Uint8Array& array = value.mValue.Value(); + array.ComputeState(); + uint32_t len = array.Length(); + + if (len == 0) { + // If there is nothing to read, let's do another reading. + OnOutputStreamReady(mPipeOut); + return; + } + + MOZ_DIAGNOSTIC_ASSERT(mBuffer.IsEmpty()); + + // Let's take a copy of the data. + if (!mBuffer.AppendElements(array.Data(), len, fallible)) { + CloseAndRelease(aCx, NS_ERROR_OUT_OF_MEMORY); + return; + } + + mBufferOffset = 0; + mBufferRemaining = len; + + nsresult rv = WriteBuffer(); + if (NS_FAILED(rv)) { + // DOMException only understands errors from domerr.msg, so we normalize to + // identifying an abort if the write fails. + CloseAndRelease(aCx, NS_ERROR_DOM_ABORT_ERR); + } +} + +nsresult FetchStreamReader::WriteBuffer() { + MOZ_ASSERT(!mBuffer.IsEmpty()); + + char* data = reinterpret_cast<char*>(mBuffer.Elements()); + + while (1) { + 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::RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue) { + ReportErrorToConsole(aCx, aValue); + CloseAndRelease(aCx, NS_ERROR_FAILURE); +} + +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..6b28136f8f --- /dev/null +++ b/dom/fetch/FetchStreamReader.h @@ -0,0 +1,76 @@ +/* -*- 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/dom/FetchBinding.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "nsIAsyncOutputStream.h" +#include "nsIGlobalObject.h" + +namespace mozilla { +namespace dom { + +class WeakWorkerRef; + +class FetchStreamReader final : public nsIOutputStreamCallback, + public PromiseNativeHandler { + 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); + + void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override; + + void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue) override; + + // 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. + void CloseAndRelease(JSContext* aCx, nsresult aStatus); + + void StartConsuming(JSContext* aCx, JS::HandleObject aStream, + JS::MutableHandle<JSObject*> aReader, ErrorResult& aRv); + + private: + explicit FetchStreamReader(nsIGlobalObject* aGlobal); + ~FetchStreamReader(); + + nsresult WriteBuffer(); + + void ReportErrorToConsole(JSContext* aCx, JS::Handle<JS::Value> aValue); + + nsCOMPtr<nsIGlobalObject> mGlobal; + nsCOMPtr<nsIEventTarget> mOwningEventTarget; + + nsCOMPtr<nsIAsyncOutputStream> mPipeOut; + + RefPtr<WeakWorkerRef> mWorkerRef; + + JS::Heap<JSObject*> mReader; + + nsTArray<uint8_t> mBuffer; + uint32_t mBufferRemaining; + uint32_t mBufferOffset; + + bool mStreamClosed; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_FetchStreamReader_h diff --git a/dom/fetch/FetchTypes.ipdlh b/dom/fetch/FetchTypes.ipdlh new file mode 100644 index 0000000000..a01c5154e5 --- /dev/null +++ b/dom/fetch/FetchTypes.ipdlh @@ -0,0 +1,86 @@ +/* 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 ChannelInfo; +include PBackgroundSharedTypes; + +include protocol PRemoteLazyInputStream; + +include "mozilla/dom/FetchIPCTypes.h"; + +using HeadersGuardEnum from "mozilla/dom/HeadersBinding.h"; +using ReferrerPolicy from "mozilla/dom/ReferrerPolicyBinding.h"; +using RequestCache from "mozilla/dom/RequestBinding.h"; +using RequestCredentials from "mozilla/dom/RequestBinding.h"; +using RequestMode from "mozilla/dom/RequestBinding.h"; +using RequestRedirect from "mozilla/dom/RequestBinding.h"; +using ResponseType from "mozilla/dom/ResponseBinding.h"; +using struct nsID from "nsID.h"; + +namespace mozilla { +namespace dom { + +struct HeadersEntry { + nsCString name; + nsCString value; +}; + +struct ParentToParentStream { + // Used as a key for RemoteLazyInputStreamStorage + nsID uuid; +}; + +struct ParentToChildStream { + PRemoteLazyInputStream actor; +}; + +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; + RequestMode requestMode; + RequestCredentials requestCredentials; + RequestCache cacheMode; + RequestRedirect requestRedirect; + nsString integrity; + nsCString fragment; + PrincipalInfo? principalInfo; +}; + +struct IPCInternalResponse { + ResponseType type; + nsCString[] urlList; + uint16_t status; + nsCString statusText; + HeadersGuardEnum headersGuard; + HeadersEntry[] headers; + BodyStreamVariant? body; + int64_t bodySize; + nsresult errorCode; + nsCString alternativeDataType; + BodyStreamVariant? alternativeBody; + IPCChannelInfo channelInfo; + PrincipalInfo? principalInfo; +}; + +} // namespace ipc +} // namespace mozilla diff --git a/dom/fetch/FetchUtil.cpp b/dom/fetch/FetchUtil.cpp new file mode 100644 index 0000000000..330ef6dda6 --- /dev/null +++ b/dom/fetch/FetchUtil.cpp @@ -0,0 +1,569 @@ +/* -*- 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 "js/friend/ErrorMessages.h" // JSMSG_* +#include "nsCRT.h" +#include "nsError.h" +#include "nsIAsyncInputStream.h" +#include "nsIHttpChannel.h" +#include "nsNetUtil.h" +#include "nsStreamUtils.h" +#include "nsString.h" +#include "mozilla/dom/Document.h" + +#include "mozilla/dom/DOMException.h" +#include "mozilla/dom/InternalRequest.h" +#include "mozilla/dom/Response.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 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(); + } + + struct Destroyer final : Runnable { + RefPtr<WindowStreamOwner> mDoomed; + + explicit Destroyer(already_AddRefed<WindowStreamOwner> aDoomed) + : Runnable("WindowStreamOwner::Destroyer"), mDoomed(aDoomed) {} + + NS_IMETHOD + Run() override { + mDoomed = nullptr; + return NS_OK; + } + }; + + // 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) + +class WorkerStreamOwner final { + public: + NS_INLINE_DECL_REFCOUNTING(WorkerStreamOwner) + + explicit WorkerStreamOwner(nsIAsyncInputStream* aStream) : mStream(aStream) {} + + static already_AddRefed<WorkerStreamOwner> Create( + nsIAsyncInputStream* aStream, WorkerPrivate* aWorker) { + RefPtr<WorkerStreamOwner> self = new WorkerStreamOwner(aStream); + + self->mWorkerRef = WeakWorkerRef::Create(aWorker, [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 Destroyer. + self->mStream->Close(); + self->mStream = nullptr; + self->mWorkerRef = nullptr; + } + }); + + if (!self->mWorkerRef) { + return nullptr; + } + + return self.forget(); + } + + struct Destroyer final : CancelableRunnable { + RefPtr<WorkerStreamOwner> mDoomed; + + explicit Destroyer(RefPtr<WorkerStreamOwner>&& aDoomed) + : CancelableRunnable("WorkerStreamOwner::Destroyer"), + mDoomed(std::move(aDoomed)) {} + + NS_IMETHOD + Run() override { + mDoomed = nullptr; + return NS_OK; + } + + nsresult Cancel() override { return Run(); } + }; + + 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<WeakWorkerRef> mWorkerRef; +}; + +class JSStreamConsumer final : public nsIInputStreamCallback { + nsCOMPtr<nsIEventTarget> mOwningEventTarget; + RefPtr<WindowStreamOwner> mWindowStreamOwner; + RefPtr<WorkerStreamOwner> mWorkerStreamOwner; + JS::StreamConsumer* mConsumer; + bool mConsumerAborted; + + JSStreamConsumer(already_AddRefed<WindowStreamOwner> aWindowStreamOwner, + nsIGlobalObject* aGlobal, JS::StreamConsumer* aConsumer) + : mOwningEventTarget(aGlobal->EventTargetFor(TaskCategory::Other)), + mWindowStreamOwner(aWindowStreamOwner), + mConsumer(aConsumer), + mConsumerAborted(false) { + MOZ_DIAGNOSTIC_ASSERT(mWindowStreamOwner); + MOZ_DIAGNOSTIC_ASSERT(mConsumer); + } + + JSStreamConsumer(RefPtr<WorkerStreamOwner> aWorkerStreamOwner, + nsIGlobalObject* aGlobal, JS::StreamConsumer* aConsumer) + : mOwningEventTarget(aGlobal->EventTargetFor(TaskCategory::Other)), + mWorkerStreamOwner(std::move(aWorkerStreamOwner)), + mConsumer(aConsumer), + mConsumerAborted(false) { + MOZ_DIAGNOSTIC_ASSERT(mWorkerStreamOwner); + MOZ_DIAGNOSTIC_ASSERT(mConsumer); + } + + ~JSStreamConsumer() { + // Both WindowStreamOwner and WorkerStreamOwner need to be destroyed on + // their global's event target thread. + + RefPtr<Runnable> destroyer; + if (mWindowStreamOwner) { + MOZ_DIAGNOSTIC_ASSERT(!mWorkerStreamOwner); + destroyer = new WindowStreamOwner::Destroyer(mWindowStreamOwner.forget()); + } else { + MOZ_DIAGNOSTIC_ASSERT(mWorkerStreamOwner); + destroyer = + new WorkerStreamOwner::Destroyer(std::move(mWorkerStreamOwner)); + } + + MOZ_ALWAYS_SUCCEEDS(mOwningEventTarget->Dispatch(destroyer.forget())); + } + + 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); + + // 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, + JS::StreamConsumer* aConsumer, nsIGlobalObject* aGlobal, + WorkerPrivate* aMaybeWorker) { + 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); + if (!owner) { + return false; + } + + consumer = new JSStreamConsumer(std::move(owner), aGlobal, aConsumer); + } else { + RefPtr<WindowStreamOwner> owner = + WindowStreamOwner::Create(asyncStream, aGlobal); + if (!owner) { + return false; + } + + consumer = new JSStreamConsumer(owner.forget(), aGlobal, aConsumer); + } + + // 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) { + mConsumer->streamEnd(); + 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; + } +}; + +NS_IMPL_ISUPPORTS(JSStreamConsumer, nsIInputStreamCallback) + +static bool ThrowException(JSContext* aCx, unsigned errorNumber) { + JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr, errorNumber); + return false; +} + +// static +bool FetchUtil::StreamResponseToJS(JSContext* aCx, JS::HandleObject aObj, + JS::MimeType aMimeType, + JS::StreamConsumer* aConsumer, + WorkerPrivate* aMaybeWorker) { + 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; + response->GetMimeType(mimeType); + + 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); + } + + IgnoredErrorResult result; + bool used = response->GetBodyUsed(result); + if (NS_WARN_IF(result.Failed())) { + return ThrowException(aCx, JSMSG_WASM_ERROR_CONSUMING_RESPONSE); + } + if (used) { + return ThrowException(aCx, JSMSG_WASM_RESPONSE_ALREADY_CONSUMED); + } + + switch (aMimeType) { + case JS::MimeType::Wasm: + nsAutoString url; + response->GetUrl(url); + + 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; + } + + RefPtr<InternalResponse> ir = response->GetInternalResponse(); + if (NS_WARN_IF(!ir)) { + return ThrowException(aCx, JSMSG_OUT_OF_MEMORY); + } + + nsCOMPtr<nsIInputStream> body; + ir->GetUnfilteredBody(getter_AddRefs(body)); + if (!body) { + aConsumer->streamEnd(); + return true; + } + + 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(std::move(body), aConsumer, global, + aMaybeWorker)) { + 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..5907e209a4 --- /dev/null +++ b/dom/fetch/FetchUtil.h @@ -0,0 +1,77 @@ +/* -*- 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" +#define WASM_ALT_DATA_TYPE_V1 "wasm/machine-code/1" + +class nsIPrincipal; +class nsIHttpChannel; + +namespace mozilla { +namespace 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); + + /** + * 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::HandleObject 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 dom +} // namespace mozilla +#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..fdccb15406 --- /dev/null +++ b/dom/fetch/Headers.h @@ -0,0 +1,128 @@ +/* -*- 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_SCRIPT_HOLDER_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 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..59271d0f86 --- /dev/null +++ b/dom/fetch/InternalHeaders.cpp @@ -0,0 +1,637 @@ +/* -*- 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 "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 && + IsForbiddenRequestHeader(aLowerName)) { + 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 + if (IsForbiddenRequestHeader(lowerName)) { + 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::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 { + return mGuard == HeadersGuardEnum::Request && + nsContentUtils::IsForbiddenRequestHeader(aName); +} + +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("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; + 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..2fac237733 --- /dev/null +++ b/dom/fetch/InternalHeaders.h @@ -0,0 +1,178 @@ +/* -*- 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 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 mCredentialsMode = 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; + 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) || + 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..0f55a8486a --- /dev/null +++ b/dom/fetch/InternalRequest.cpp @@ -0,0 +1,399 @@ +/* -*- 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; + 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), + mSynchronous(aOther.mSynchronous), + mUnsafeRequest(aOther.mUnsafeRequest), + mUseURLCredentials(aOther.mUseURLCredentials), + mContentPolicyTypeOverridden(aOther.mContentPolicyTypeOverridden) { + // NOTE: does not copy body stream... use the fallible Clone() for that +} + +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()), + mMode(aIPCRequest.requestMode()), + mCredentialsMode(aIPCRequest.requestCredentials()), + mCacheMode(aIPCRequest.cacheMode()), + mRedirectMode(aIPCRequest.requestRedirect()), + mIntegrity(aIPCRequest.integrity()), + mFragment(aIPCRequest.fragment()) { + if (aIPCRequest.principalInfo()) { + mPrincipalInfo = MakeUnique<mozilla::ipc::PrincipalInfo>( + aIPCRequest.principalInfo().ref()); + } + + const Maybe<BodyStreamVariant>& body = aIPCRequest.body(); + + // This constructor is (currently) only used for parent -> child communication + // (constructed on the child side). + if (body) { + MOZ_ASSERT(body->type() == BodyStreamVariant::TParentToChildStream); + mBodyStream = static_cast<RemoteLazyInputStreamChild*>( + body->get_ParentToChildStream().actorChild()) + ->CreateStream(); + } +} + +InternalRequest::~InternalRequest() = default; + +void InternalRequest::SetContentPolicyType( + nsContentPolicyType aContentPolicyType) { + mContentPolicyType = aContentPolicyType; +} + +void InternalRequest::OverrideContentPolicyType( + nsContentPolicyType aContentPolicyType) { + SetContentPolicyType(aContentPolicyType); + mContentPolicyTypeOverridden = true; +} + +/* 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: + 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: + 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_INVALID: + 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); +} + +} // namespace mozilla::dom diff --git a/dom/fetch/InternalRequest.h b/dom/fetch/InternalRequest.h new file mode 100644 index 0000000000..a526089fe2 --- /dev/null +++ b/dom/fetch/InternalRequest.h @@ -0,0 +1,428 @@ +/* -*- 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" +#ifdef DEBUG +# include "nsIURLParser.h" +# include "nsNetCID.h" +# include "nsServiceManagerUtils.h" +#endif + +namespace mozilla { + +namespace ipc { +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 + * "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); + + 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 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; + } + + 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 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; +}; + +} // 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..342a88eeeb --- /dev/null +++ b/dom/fetch/InternalResponse.cpp @@ -0,0 +1,361 @@ +/* -*- 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/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; + +nsCOMPtr<nsIInputStream> TakeStreamFromStorage( + const BodyStreamVariant& aVariant, int64_t aBodySize) { + MOZ_ASSERT(aVariant.type() == BodyStreamVariant::TParentToParentStream); + const auto& uuid = aVariant.get_ParentToParentStream().uuid(); + + auto storageOrErr = RemoteLazyInputStreamStorage::Get(); + MOZ_ASSERT(storageOrErr.isOk()); + auto storage = storageOrErr.unwrap(); + auto stream = storage->ForgetStream(uuid); + MOZ_ASSERT(stream); + + return stream; +} + +} // 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) {} + +/* static */ RefPtr<InternalResponse> InternalResponse::FromIPC( + const IPCInternalResponse& aIPCResponse) { + if (aIPCResponse.type() == ResponseType::Error) { + return InternalResponse::NetworkError(aIPCResponse.errorCode()); + } + + RefPtr<InternalResponse> response = + new InternalResponse(aIPCResponse.status(), aIPCResponse.statusText()); + + response->SetURLList(aIPCResponse.urlList()); + response->mHeaders = + new InternalHeaders(aIPCResponse.headers(), aIPCResponse.headersGuard()); + + if (aIPCResponse.body()) { + auto bodySize = aIPCResponse.bodySize(); + nsCOMPtr<nsIInputStream> body = + TakeStreamFromStorage(*aIPCResponse.body(), bodySize); + response->SetBody(body, bodySize); + } + + response->SetAlternativeDataType(aIPCResponse.alternativeDataType()); + + if (aIPCResponse.alternativeBody()) { + nsCOMPtr<nsIInputStream> alternativeBody = TakeStreamFromStorage( + *aIPCResponse.alternativeBody(), UNKNOWN_BODY_SIZE); + response->SetAlternativeBody(alternativeBody); + } + + response->InitChannelInfo(aIPCResponse.channelInfo()); + + if (aIPCResponse.principalInfo()) { + response->SetPrincipalInfo(MakeUnique<mozilla::ipc::PrincipalInfo>( + aIPCResponse.principalInfo().ref())); + } + + switch (aIPCResponse.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; + +void InternalResponse::ToIPC( + IPCInternalResponse* aIPCResponse, mozilla::ipc::PBackgroundChild* aManager, + UniquePtr<mozilla::ipc::AutoIPCStream>& aAutoBodyStream, + UniquePtr<mozilla::ipc::AutoIPCStream>& aAutoAlternativeBodyStream) { + nsTArray<HeadersEntry> headers; + HeadersGuardEnum headersGuard; + UnfilteredHeaders()->ToIPC(headers, headersGuard); + + Maybe<mozilla::ipc::PrincipalInfo> principalInfo = + mPrincipalInfo ? Some(*mPrincipalInfo) : Nothing(); + + // Note: all the arguments are copied rather than moved, which would be more + // efficient, because there's no move-friendly constructor generated. + *aIPCResponse = + IPCInternalResponse(mType, GetUnfilteredURLList(), GetUnfilteredStatus(), + GetUnfilteredStatusText(), headersGuard, headers, + Nothing(), static_cast<uint64_t>(UNKNOWN_BODY_SIZE), + mErrorCode, GetAlternativeDataType(), Nothing(), + mChannelInfo.AsIPCChannelInfo(), principalInfo); + + nsCOMPtr<nsIInputStream> body; + int64_t bodySize; + GetUnfilteredBody(getter_AddRefs(body), &bodySize); + + if (body) { + aIPCResponse->body().emplace(ChildToParentStream()); + aIPCResponse->bodySize() = bodySize; + + aAutoBodyStream.reset(new mozilla::ipc::AutoIPCStream( + aIPCResponse->body()->get_ChildToParentStream().stream())); + DebugOnly<bool> ok = aAutoBodyStream->Serialize(body, aManager); + MOZ_ASSERT(ok); + } + + nsCOMPtr<nsIInputStream> alternativeBody = TakeAlternativeBody(); + if (alternativeBody) { + aIPCResponse->alternativeBody().emplace(ChildToParentStream()); + + aAutoAlternativeBodyStream.reset(new mozilla::ipc::AutoIPCStream( + aIPCResponse->alternativeBody()->get_ChildToParentStream().stream())); + DebugOnly<bool> ok = + aAutoAlternativeBodyStream->Serialize(alternativeBody, aManager); + MOZ_ASSERT(ok); + } +} + +already_AddRefed<InternalResponse> InternalResponse::Clone( + CloneType aCloneType) { + RefPtr<InternalResponse> clone = CreateIncompleteCopy(); + + 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; + + if (mWrappedResponse) { + clone->mWrappedResponse = mWrappedResponse->Clone(aCloneType); + MOZ_ASSERT(!mBody); + return clone.forget(); + } + + if (!mBody || aCloneType == eDontCloneInputStream) { + return clone.forget(); + } + + 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.forget(); +} + +already_AddRefed<InternalResponse> InternalResponse::BasicResponse() { + MOZ_ASSERT(!mWrappedResponse, + "Can't BasicResponse a already wrapped response"); + RefPtr<InternalResponse> basic = CreateIncompleteCopy(); + basic->mType = ResponseType::Basic; + basic->mHeaders = InternalHeaders::BasicHeaders(Headers()); + basic->mWrappedResponse = this; + return basic.forget(); +} + +already_AddRefed<InternalResponse> InternalResponse::CORSResponse() { + MOZ_ASSERT(!mWrappedResponse, + "Can't CORSResponse a already wrapped response"); + RefPtr<InternalResponse> cors = CreateIncompleteCopy(); + cors->mType = ResponseType::Cors; + cors->mHeaders = InternalHeaders::CORSHeaders(Headers(), mCredentialsMode); + cors->mWrappedResponse = this; + return cors.forget(); +} + +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; + } +} + +already_AddRefed<InternalResponse> InternalResponse::Unfiltered() { + RefPtr<InternalResponse> ref = mWrappedResponse; + if (!ref) { + ref = this; + } + return ref.forget(); +} + +already_AddRefed<InternalResponse> InternalResponse::OpaqueResponse() { + MOZ_ASSERT(!mWrappedResponse, + "Can't OpaqueResponse a already wrapped response"); + RefPtr<InternalResponse> response = new InternalResponse(0, ""_ns); + response->mType = ResponseType::Opaque; + response->mChannelInfo = mChannelInfo; + if (mPrincipalInfo) { + response->mPrincipalInfo = + MakeUnique<mozilla::ipc::PrincipalInfo>(*mPrincipalInfo); + } + response->mWrappedResponse = this; + return response.forget(); +} + +already_AddRefed<InternalResponse> InternalResponse::OpaqueRedirectResponse() { + MOZ_ASSERT(!mWrappedResponse, + "Can't OpaqueRedirectResponse a already wrapped response"); + MOZ_ASSERT(!mURLList.IsEmpty(), + "URLList should not be emtpy for internalResponse"); + RefPtr<InternalResponse> response = OpaqueResponse(); + response->mType = ResponseType::Opaqueredirect; + response->mURLList = mURLList.Clone(); + return response.forget(); +} + +already_AddRefed<InternalResponse> InternalResponse::CreateIncompleteCopy() { + RefPtr<InternalResponse> copy = new InternalResponse(mStatus, mStatusText); + copy->mType = mType; + copy->mURLList = mURLList.Clone(); + copy->mChannelInfo = mChannelInfo; + if (mPrincipalInfo) { + copy->mPrincipalInfo = + MakeUnique<mozilla::ipc::PrincipalInfo>(*mPrincipalInfo); + } + return copy.forget(); +} + +} // namespace mozilla::dom diff --git a/dom/fetch/InternalResponse.h b/dom/fetch/InternalResponse.h new file mode 100644 index 0000000000..f256d3ec5b --- /dev/null +++ b/dom/fetch/InternalResponse.h @@ -0,0 +1,383 @@ +/* -*- 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 "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/UniquePtr.h" + +namespace mozilla { +namespace ipc { +class AutoIPCStream; +class PBackgroundChild; +class PrincipalInfo; +} // namespace ipc + +namespace dom { + +class IPCInternalResponse; +class InternalHeaders; + +class InternalResponse final { + friend class FetchDriver; + + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(InternalResponse) + + InternalResponse( + uint16_t aStatus, const nsACString& aStatusText, + RequestCredentials aCredentialsMode = RequestCredentials::Omit); + + static RefPtr<InternalResponse> FromIPC( + const IPCInternalResponse& aIPCResponse); + + // Note: the AutoIPCStreams must outlive the IPCInternalResponse. + void ToIPC( + IPCInternalResponse* aIPCResponse, + mozilla::ipc::PBackgroundChild* aManager, + UniquePtr<mozilla::ipc::AutoIPCStream>& aAutoBodyStream, + UniquePtr<mozilla::ipc::AutoIPCStream>& aAutoAlternativeBodyStream); + + enum CloneType { + eCloneInputStream, + eDontCloneInputStream, + }; + + already_AddRefed<InternalResponse> Clone(CloneType eCloneType); + + static already_AddRefed<InternalResponse> NetworkError(nsresult aRv) { + MOZ_DIAGNOSTIC_ASSERT(NS_FAILED(aRv)); + RefPtr<InternalResponse> response = new InternalResponse(0, ""_ns); + ErrorResult result; + response->Headers()->SetGuard(HeadersGuardEnum::Immutable, result); + MOZ_ASSERT(!result.Failed()); + response->mType = ResponseType::Error; + response->mErrorCode = aRv; + return response.forget(); + } + + already_AddRefed<InternalResponse> OpaqueResponse(); + + already_AddRefed<InternalResponse> OpaqueRedirectResponse(); + + already_AddRefed<InternalResponse> BasicResponse(); + + already_AddRefed<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; + } + + void InitChannelInfo(nsIChannel* aChannel) { + mChannelInfo.InitFromChannel(aChannel); + } + + void InitChannelInfo(const mozilla::ipc::IPCChannelInfo& aChannelInfo) { + mChannelInfo.InitFromIPCChannelInfo(aChannelInfo); + } + + 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; + + already_AddRefed<InternalResponse> Unfiltered(); + + private: + ~InternalResponse(); + + 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. + already_AddRefed<InternalResponse> CreateIncompleteCopy(); + + 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; + + public: + static const int64_t UNKNOWN_BODY_SIZE = -1; + static const 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. + RefPtr<InternalResponse> mWrappedResponse; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_InternalResponse_h diff --git a/dom/fetch/Request.cpp b/dom/fetch/Request.cpp new file mode 100644 index 0000000000..62041cdc10 --- /dev/null +++ b/dom/fetch/Request.cpp @@ -0,0 +1,662 @@ +/* -*- 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 "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/Unused.h" + +namespace mozilla::dom { + +NS_IMPL_ADDREF_INHERITED(Request, FetchBody<Request>) +NS_IMPL_RELEASE_INHERITED(Request, FetchBody<Request>) + +NS_IMPL_CYCLE_COLLECTION_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) + AbortFollower::Unlink(static_cast<AbortFollower*>(tmp)); + 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) + AbortFollower::Traverse(static_cast<AbortFollower*>(tmp), cb); +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(Request, FetchBody<Request>) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mReadableStreamBody) + MOZ_DIAGNOSTIC_ASSERT(!tmp->mReadableStreamReader); + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mReadableStreamReader) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_TRACE_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. + mSignal = new AbortSignal(aOwner, aSignal->Aborted()); + 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)); + bool used = inputReq->GetBodyUsed(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + if (used) { + 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) { + bool used = GetBodyUsed(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + if (used) { + 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); + } + + return mSignal; +} + +AbortSignalImpl* Request::GetSignalImpl() const { return mSignal; } + +} // namespace mozilla::dom diff --git a/dom/fetch/Request.h b/dom/fetch/Request.h new file mode 100644 index 0000000000..4b5ce08cc6 --- /dev/null +++ b/dom/fetch/Request.h @@ -0,0 +1,143 @@ +/* -*- 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 { +namespace dom { + +class Headers; +class InternalHeaders; +class RequestOrUSVString; + +class Request final : public FetchBody<Request>, public nsWrapperCache { + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_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; + + private: + ~Request(); + + SafeRefPtr<InternalRequest> mRequest; + + // Lazily created. + RefPtr<Headers> mHeaders; + RefPtr<AbortSignal> mSignal; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_Request_h diff --git a/dom/fetch/Response.cpp b/dom/fetch/Response.cpp new file mode 100644 index 0000000000..bb2a937a27 --- /dev/null +++ b/dom/fetch/Response.cpp @@ -0,0 +1,498 @@ +/* -*- 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/BodyStream.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" + +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>) + AbortFollower::Unlink(static_cast<AbortFollower*>(tmp)); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mHeaders) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSignalImpl) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mFetchStreamReader) + + tmp->mReadableStreamBody = nullptr; + tmp->mReadableStreamReader = nullptr; + + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(Response, FetchBody<Response>) + AbortFollower::Traverse(static_cast<AbortFollower*>(tmp), cb); + 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_JS_MEMBER_CALLBACK(mReadableStreamBody) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mReadableStreamReader) + 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, + InternalResponse* aInternalResponse, + AbortSignalImpl* aSignalImpl) + : FetchBody<Response>(aGlobal), + mInternalResponse(aInternalResponse), + mSignalImpl(aSignalImpl) { + MOZ_ASSERT( + aInternalResponse->Headers()->Guard() == HeadersGuardEnum::Immutable || + aInternalResponse->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<InternalResponse> error = + InternalResponse::NetworkError(NS_ERROR_FAILURE); + RefPtr<Response> r = new Response(global, error, 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::Constructor( + const GlobalObject& aGlobal, const Nullable<fetch::ResponseBodyInit>& aBody, + 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; + } + + if (aInit.mStatus < 200 || aInit.mStatus > 599) { + aRv.ThrowRangeError("Invalid response status code."); + return nullptr; + } + + // 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; + } + + RefPtr<InternalResponse> internalResponse = + new 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, 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()) { + aRv.MightThrowJSException(); + + JSContext* cx = aGlobal.Context(); + const ReadableStream& readableStream = body.GetAsReadableStream(); + + JS::Rooted<JSObject*> readableStreamObj(cx, readableStream.Obj()); + + bool disturbed; + bool locked; + if (!JS::ReadableStreamIsDisturbed(cx, readableStreamObj, &disturbed) || + !JS::ReadableStreamIsLocked(cx, readableStreamObj, &locked)) { + aRv.StealExceptionFromJSContext(cx); + return nullptr; + } + if (disturbed || locked) { + aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>(); + return nullptr; + } + + r->SetReadableStreamBody(cx, readableStreamObj); + + JS::ReadableStreamMode streamMode; + if (!JS::ReadableStreamGetMode(cx, readableStreamObj, &streamMode)) { + aRv.StealExceptionFromJSContext(cx); + return nullptr; + } + if (streamMode == JS::ReadableStreamMode::ExternalSource) { + // If this is a DOM generated ReadableStream, we can extract the + // inputStream directly. + JS::ReadableStreamUnderlyingSource* underlyingSource = nullptr; + if (!JS::ReadableStreamGetExternalUnderlyingSource( + cx, readableStreamObj, &underlyingSource)) { + aRv.StealExceptionFromJSContext(cx); + return nullptr; + } + + MOZ_ASSERT(underlyingSource); + + aRv = BodyStream::RetrieveInputStream(underlyingSource, + getter_AddRefs(bodyStream)); + + // The releasing of the external source is needed in order to avoid an + // extra stream lock. + if (!JS::ReadableStreamReleaseExternalUnderlyingSource( + cx, readableStreamObj)) { + aRv.StealExceptionFromJSContext(cx); + return nullptr; + } + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } 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; + } + + 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(); +} + +already_AddRefed<Response> Response::Clone(JSContext* aCx, ErrorResult& aRv) { + bool bodyUsed = GetBodyUsed(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + if (!bodyUsed && mReadableStreamBody) { + aRv.MightThrowJSException(); + + AutoJSAPI jsapi; + if (!jsapi.Init(mOwner)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + JSContext* cx = jsapi.cx(); + JS::Rooted<JSObject*> body(cx, mReadableStreamBody); + bool locked; + // We just need to check the 'locked' state because GetBodyUsed() already + // checked the 'disturbed' state. + if (!JS::ReadableStreamIsLocked(cx, body, &locked)) { + aRv.StealExceptionFromJSContext(cx); + return nullptr; + } + + bodyUsed = locked; + } + + if (bodyUsed) { + aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>(); + return nullptr; + } + + RefPtr<FetchStreamReader> streamReader; + nsCOMPtr<nsIInputStream> inputStream; + + JS::Rooted<JSObject*> body(aCx); + MaybeTeeReadableStreamBody(aCx, &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); + + RefPtr<InternalResponse> ir = + mInternalResponse->Clone(body ? InternalResponse::eDontCloneInputStream + : InternalResponse::eCloneInputStream); + + RefPtr<Response> response = new Response(mOwner, ir, 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 (GetBodyUsed(aRv)) { + aRv.ThrowTypeError<MSG_FETCH_BODY_CONSUMED_ERROR>(); + return nullptr; + } + + RefPtr<FetchStreamReader> streamReader; + nsCOMPtr<nsIInputStream> inputStream; + + JS::Rooted<JSObject*> body(aCx); + MaybeTeeReadableStreamBody(aCx, &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); + + RefPtr<InternalResponse> clone = + mInternalResponse->Clone(body ? InternalResponse::eDontCloneInputStream + : InternalResponse::eCloneInputStream); + + RefPtr<InternalResponse> ir = clone->Unfiltered(); + RefPtr<Response> ref = new Response(mOwner, ir, 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(!CheckBodyUsed()); + mInternalResponse->SetBody(aBody, aBodySize); +} + +already_AddRefed<InternalResponse> Response::GetInternalResponse() const { + RefPtr<InternalResponse> ref = mInternalResponse; + return ref.forget(); +} + +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..adbe5bb164 --- /dev/null +++ b/dom/fetch/Response.h @@ -0,0 +1,136 @@ +/* -*- 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, 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> 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); + + already_AddRefed<InternalResponse> GetInternalResponse() const; + + AbortSignalImpl* GetSignalImpl() const override { return mSignalImpl; } + + private: + ~Response(); + + RefPtr<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..d6be68726a --- /dev/null +++ b/dom/fetch/moz.build @@ -0,0 +1,65 @@ +# -*- 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", + "FetchDriver.h", + "FetchIPCTypes.h", + "FetchObserver.h", + "FetchStreamReader.h", + "FetchUtil.h", + "Headers.h", + "InternalHeaders.h", + "InternalRequest.h", + "InternalResponse.h", + "Request.h", + "Response.h", +] + +UNIFIED_SOURCES += [ + "BodyExtractor.cpp", + "ChannelInfo.cpp", + "EmptyBody.cpp", + "Fetch.cpp", + "FetchDriver.cpp", + "FetchObserver.cpp", + "FetchStreamReader.cpp", + "FetchUtil.cpp", + "Headers.cpp", + "InternalHeaders.cpp", + "InternalRequest.cpp", + "InternalResponse.cpp", + "Request.cpp", + "Response.cpp", +] + +IPDL_SOURCES += [ + "ChannelInfo.ipdlh", + "FetchTypes.ipdlh", +] + +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/.eslintrc.js b/dom/fetch/tests/.eslintrc.js new file mode 100644 index 0000000000..1779fd7f1c --- /dev/null +++ b/dom/fetch/tests/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/browser-test"], +}; diff --git a/dom/fetch/tests/browser.ini b/dom/fetch/tests/browser.ini new file mode 100644 index 0000000000..8705dea1ab --- /dev/null +++ b/dom/fetch/tests/browser.ini @@ -0,0 +1,2 @@ +[DEFAULT] +[browser_blobFromFile.js] diff --git a/dom/fetch/tests/browser_blobFromFile.js b/dom/fetch/tests/browser_blobFromFile.js new file mode 100644 index 0000000000..155f420477 --- /dev/null +++ b/dom/fetch/tests/browser_blobFromFile.js @@ -0,0 +1,62 @@ +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 + 0666, + 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.getURLSpecFromFile(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(blob instanceof File, "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/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..dae325d2a7 --- /dev/null +++ b/dom/fetch/tests/crashtests/1664514.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<h3>Set <code>privacy.file_unique_origin</code> to false when testing this.</h3> +<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..b3210cd4d8 --- /dev/null +++ b/dom/fetch/tests/crashtests/crashtests.list @@ -0,0 +1,2 @@ +load 1577196.html +pref(privacy.file_unique_origin,false) 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/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/test_ext_response_constructor.html b/dom/fetch/tests/test_ext_response_constructor.html new file mode 100644 index 0000000000..adff6c78e9 --- /dev/null +++ b/dom/fetch/tests/test_ext_response_constructor.html @@ -0,0 +1,46 @@ +<!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() { + 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..274e85bb68 --- /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 ([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> |