summaryrefslogtreecommitdiffstats
path: root/dom/fetch/FetchUtil.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/fetch/FetchUtil.cpp')
-rw-r--r--dom/fetch/FetchUtil.cpp569
1 files changed, 569 insertions, 0 deletions
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