gecko-dev/dom/fetch/FetchUtil.cpp

615 lines
17 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* -*- 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 "nsError.h"
#include "nsString.h"
#include "nsIDocument.h"
#include "mozilla/dom/InternalRequest.h"
#include "mozilla/dom/WorkerRef.h"
namespace mozilla {
namespace 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(NS_LITERAL_CSTRING("\r\n"), 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,
nsIDocument* aDoc,
nsIHttpChannel* aChannel,
InternalRequest* aRequest) {
MOZ_ASSERT(NS_IsMainThread());
nsAutoString referrer;
aRequest->GetReferrer(referrer);
net::ReferrerPolicy policy = aRequest->GetReferrerPolicy();
nsresult rv = NS_OK;
if (referrer.IsEmpty()) {
// This is the case requests referrer is "no-referrer"
rv = aChannel->SetReferrerWithPolicy(nullptr, net::RP_No_Referrer);
NS_ENSURE_SUCCESS(rv, rv);
} else if (referrer.EqualsLiteral(kFETCH_CLIENT_REFERRER_STR)) {
rv = nsContentUtils::SetFetchReferrerURIWithPolicy(aPrincipal,
aDoc,
aChannel,
policy);
NS_ENSURE_SUCCESS(rv, rv);
} 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, nullptr, nullptr);
NS_ENSURE_SUCCESS(rv, rv);
rv = aChannel->SetReferrerWithPolicy(referrerURI, policy);
NS_ENSURE_SUCCESS(rv, rv);
}
nsCOMPtr<nsIURI> referrerURI;
Unused << aChannel->GetReferrer(getter_AddRefs(referrerURI));
// Step 8 https://fetch.spec.whatwg.org/#main-fetch
// If requests referrer is not "no-referrer", set requests referrer to
// the result of invoking determine requests referrer.
if (referrerURI) {
nsAutoCString spec;
rv = referrerURI->GetSpec(spec);
NS_ENSURE_SUCCESS(rv, rv);
aRequest->SetReferrer(NS_ConvertUTF8toUTF16(spec));
} else {
aRequest->SetReferrer(EmptyString());
}
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(already_AddRefed<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(mWorkerStreamOwner.forget());
}
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(nsIInputStream* aStream,
JS::StreamConsumer* aConsumer,
nsIGlobalObject* aGlobal,
WorkerPrivate* aMaybeWorker)
{
nsresult rv;
bool nonBlocking = false;
rv = aStream->IsNonBlocking(&nonBlocking);
if (NS_WARN_IF(NS_FAILED(rv))) {
return false;
}
// Use a pipe to create an nsIAsyncInputStream if we don't already have one.
nsCOMPtr<nsIAsyncInputStream> asyncStream = do_QueryInterface(aStream);
if (!asyncStream || !nonBlocking) {
nsCOMPtr<nsIAsyncOutputStream> pipe;
rv = NS_NewPipe2(getter_AddRefs(asyncStream), getter_AddRefs(pipe),
true, true);
if (NS_WARN_IF(NS_FAILED(rv))) {
return false;
}
nsCOMPtr<nsIEventTarget> thread =
do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
rv = NS_AsyncCopy(aStream, pipe, thread,
NS_ASYNCCOPY_VIA_WRITESEGMENTS);
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->streamClosed(JS::StreamConsumer::EndOfFile);
return NS_OK;
}
if (NS_FAILED(rv)) {
mConsumer->streamClosed(JS::StreamConsumer::Error);
return NS_OK;
}
// Check mConsumerAborted before NS_FAILED to avoid calling streamClosed()
// 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->streamClosed(JS::StreamConsumer::Error);
return NS_OK;
}
rv = aStream->AsyncWait(this, 0, 0, nullptr);
if (NS_WARN_IF(NS_FAILED(rv))) {
mConsumer->streamClosed(JS::StreamConsumer::Error);
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_BAD_RESPONSE_VALUE);
}
const char* requiredMimeType = nullptr;
switch (aMimeType) {
case JS::MimeType::Wasm:
requiredMimeType = "application/wasm";
break;
}
if (strcmp(requiredMimeType, response->MimeType().Data())) {
return ThrowException(aCx, JSMSG_BAD_RESPONSE_MIME_TYPE);
}
if (response->Type() != ResponseType::Basic &&
response->Type() != ResponseType::Cors &&
response->Type() != ResponseType::Default) {
return ThrowException(aCx, JSMSG_BAD_RESPONSE_CORS_SAME_ORIGIN);
}
if (!response->Ok()) {
return ThrowException(aCx, JSMSG_BAD_RESPONSE_STATUS);
}
if (response->BodyUsed()) {
return ThrowException(aCx, JSMSG_RESPONSE_ALREADY_CONSUMED);
}
switch (aMimeType) {
case JS::MimeType::Wasm:
nsAutoString url;
response->GetUrl(url);
IgnoredErrorResult result;
nsCString sourceMapUrl;
response->GetInternalHeaders()->Get(NS_LITERAL_CSTRING("SourceMap"), sourceMapUrl, result);
if (NS_WARN_IF(result.Failed())) {
return ThrowException(aCx, JSMSG_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->streamClosed(JS::StreamConsumer::EndOfFile);
return true;
}
IgnoredErrorResult error;
response->SetBodyUsed(aCx, error);
if (NS_WARN_IF(error.Failed())) {
return ThrowException(aCx, JSMSG_ERROR_CONSUMING_RESPONSE);
}
nsIGlobalObject* global = xpc::NativeGlobal(js::UncheckedUnwrap(aObj));
if (!JSStreamConsumer::Start(body, aConsumer, global, aMaybeWorker)) {
return ThrowException(aCx, JSMSG_OUT_OF_MEMORY);
}
return true;
}
} // namespace dom
} // namespace mozilla