mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-27 06:43:32 +00:00
7d95e2bfc0
Differential Revision: https://phabricator.services.mozilla.com/D217591
824 lines
27 KiB
C++
824 lines
27 KiB
C++
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||
|
||
#include "FetchUtil.h"
|
||
|
||
#include "zlib.h"
|
||
|
||
#include "js/friend/ErrorMessages.h" // JSMSG_*
|
||
#include "nsCRT.h"
|
||
#include "nsError.h"
|
||
#include "nsIAsyncInputStream.h"
|
||
#include "nsICloneableInputStream.h"
|
||
#include "nsIHttpChannel.h"
|
||
#include "nsNetUtil.h"
|
||
#include "nsStreamUtils.h"
|
||
#include "nsString.h"
|
||
#include "js/BuildId.h"
|
||
#include "mozilla/dom/Document.h"
|
||
|
||
#include "mozilla/ClearOnShutdown.h"
|
||
#include "mozilla/dom/DOMException.h"
|
||
#include "mozilla/dom/InternalRequest.h"
|
||
#include "mozilla/dom/Response.h"
|
||
#include "mozilla/dom/ReferrerInfo.h"
|
||
#include "mozilla/dom/WorkerRef.h"
|
||
|
||
namespace mozilla::dom {
|
||
|
||
// static
|
||
nsresult FetchUtil::GetValidRequestMethod(const nsACString& aMethod,
|
||
nsCString& outMethod) {
|
||
nsAutoCString upperCaseMethod(aMethod);
|
||
ToUpperCase(upperCaseMethod);
|
||
if (!NS_IsValidHTTPToken(aMethod)) {
|
||
outMethod.SetIsVoid(true);
|
||
return NS_ERROR_DOM_SYNTAX_ERR;
|
||
}
|
||
|
||
if (upperCaseMethod.EqualsLiteral("CONNECT") ||
|
||
upperCaseMethod.EqualsLiteral("TRACE") ||
|
||
upperCaseMethod.EqualsLiteral("TRACK")) {
|
||
outMethod.SetIsVoid(true);
|
||
return NS_ERROR_DOM_SECURITY_ERR;
|
||
}
|
||
|
||
if (upperCaseMethod.EqualsLiteral("DELETE") ||
|
||
upperCaseMethod.EqualsLiteral("GET") ||
|
||
upperCaseMethod.EqualsLiteral("HEAD") ||
|
||
upperCaseMethod.EqualsLiteral("OPTIONS") ||
|
||
upperCaseMethod.EqualsLiteral("POST") ||
|
||
upperCaseMethod.EqualsLiteral("PUT")) {
|
||
outMethod = upperCaseMethod;
|
||
} else {
|
||
outMethod = aMethod; // Case unchanged for non-standard methods
|
||
}
|
||
return NS_OK;
|
||
}
|
||
|
||
static bool FindCRLF(nsACString::const_iterator& aStart,
|
||
nsACString::const_iterator& aEnd) {
|
||
nsACString::const_iterator end(aEnd);
|
||
return FindInReadable("\r\n"_ns, aStart, end);
|
||
}
|
||
|
||
// Reads over a CRLF and positions start after it.
|
||
static bool PushOverLine(nsACString::const_iterator& aStart,
|
||
const nsACString::const_iterator& aEnd) {
|
||
if (*aStart == nsCRT::CR && (aEnd - aStart > 1) && *(++aStart) == nsCRT::LF) {
|
||
++aStart; // advance to after CRLF
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// static
|
||
bool FetchUtil::IncrementPendingKeepaliveRequestSize(
|
||
nsILoadGroup* aLoadGroup, const uint64_t aBodyLength) {
|
||
uint64_t pendingKeepaliveRequestSize = 0;
|
||
MOZ_ASSERT(aLoadGroup);
|
||
|
||
aLoadGroup->GetTotalKeepAliveBytes(&pendingKeepaliveRequestSize);
|
||
pendingKeepaliveRequestSize += aBodyLength;
|
||
|
||
if (pendingKeepaliveRequestSize > FETCH_KEEPALIVE_MAX_SIZE) {
|
||
return false;
|
||
}
|
||
|
||
aLoadGroup->SetTotalKeepAliveBytes(pendingKeepaliveRequestSize);
|
||
return true;
|
||
}
|
||
|
||
// static
|
||
void FetchUtil::DecrementPendingKeepaliveRequestSize(
|
||
nsILoadGroup* aLoadGroup, const uint64_t aBodyLength) {
|
||
MOZ_ASSERT(aLoadGroup);
|
||
|
||
uint64_t pendingKeepaliveRequestSize = 0;
|
||
aLoadGroup->GetTotalKeepAliveBytes(&pendingKeepaliveRequestSize);
|
||
MOZ_ASSERT(pendingKeepaliveRequestSize >= aBodyLength);
|
||
pendingKeepaliveRequestSize -= aBodyLength;
|
||
aLoadGroup->SetTotalKeepAliveBytes(pendingKeepaliveRequestSize);
|
||
}
|
||
|
||
// static
|
||
nsCOMPtr<nsILoadGroup> FetchUtil::GetLoadGroupFromGlobal(
|
||
nsIGlobalObject* aGlobalObject) {
|
||
MOZ_ASSERT(NS_IsMainThread());
|
||
nsCOMPtr<nsILoadGroup> loadGroup = nullptr;
|
||
auto* innerWindow = aGlobalObject->GetAsInnerWindow();
|
||
|
||
if (innerWindow) {
|
||
Document* doc = innerWindow->GetExtantDoc();
|
||
if (doc) {
|
||
loadGroup = doc->GetDocumentLoadGroup();
|
||
}
|
||
}
|
||
|
||
return loadGroup;
|
||
}
|
||
|
||
// 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;
|
||
nsAutoCString 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);
|
||
|
||
nsAutoCString computedReferrerSpec;
|
||
referrerInfo = aChannel->GetReferrerInfo();
|
||
if (referrerInfo) {
|
||
Unused << referrerInfo->GetComputedReferrerSpec(computedReferrerSpec);
|
||
}
|
||
|
||
// Step 8 https://fetch.spec.whatwg.org/#main-fetch
|
||
// If request’s referrer is not "no-referrer", set request’s referrer to
|
||
// the result of invoking determine request’s referrer.
|
||
aRequest.SetReferrer(computedReferrerSpec);
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
class StoreOptimizedEncodingRunnable final : public Runnable {
|
||
nsMainThreadPtrHandle<nsICacheInfoChannel> mCache;
|
||
Vector<uint8_t> mBytes;
|
||
|
||
public:
|
||
StoreOptimizedEncodingRunnable(
|
||
nsMainThreadPtrHandle<nsICacheInfoChannel>&& aCache,
|
||
Vector<uint8_t>&& aBytes)
|
||
: Runnable("StoreOptimizedEncodingRunnable"),
|
||
mCache(std::move(aCache)),
|
||
mBytes(std::move(aBytes)) {}
|
||
|
||
NS_IMETHOD Run() override {
|
||
nsresult rv;
|
||
|
||
nsCOMPtr<nsIAsyncOutputStream> stream;
|
||
rv = mCache->OpenAlternativeOutputStream(FetchUtil::WasmAltDataType,
|
||
int64_t(mBytes.length()),
|
||
getter_AddRefs(stream));
|
||
if (NS_FAILED(rv)) {
|
||
return rv;
|
||
}
|
||
|
||
auto closeStream = MakeScopeExit([&]() { stream->CloseWithStatus(rv); });
|
||
|
||
uint32_t written;
|
||
rv = stream->Write((char*)mBytes.begin(), mBytes.length(), &written);
|
||
if (NS_FAILED(rv)) {
|
||
return rv;
|
||
}
|
||
|
||
MOZ_RELEASE_ASSERT(mBytes.length() == written);
|
||
return NS_OK;
|
||
};
|
||
};
|
||
|
||
class WindowStreamOwner final : public GlobalTeardownObserver {
|
||
private:
|
||
// Read from any thread but only set/cleared on the main thread. The lifecycle
|
||
// of WindowStreamOwner prevents concurrent read/clear.
|
||
nsCOMPtr<nsIAsyncInputStream> mStream;
|
||
|
||
~WindowStreamOwner() { MOZ_ASSERT(NS_IsMainThread()); }
|
||
|
||
public:
|
||
NS_DECL_ISUPPORTS
|
||
|
||
WindowStreamOwner(nsIAsyncInputStream* aStream, nsIGlobalObject* aGlobal)
|
||
: GlobalTeardownObserver(aGlobal), mStream(aStream) {
|
||
MOZ_DIAGNOSTIC_ASSERT(aGlobal);
|
||
MOZ_ASSERT(NS_IsMainThread());
|
||
}
|
||
|
||
// GlobalTeardownObserver:
|
||
|
||
void DisconnectFromOwner() override {
|
||
MOZ_ASSERT(NS_IsMainThread());
|
||
|
||
if (!mStream) {
|
||
return;
|
||
}
|
||
|
||
// mStream->Close() will call JSStreamConsumer::OnInputStreamReady which may
|
||
// then destory itself, but GTO should be strongly grabbing us right as it's
|
||
// calling DisconnectFromOwner.
|
||
|
||
mStream->Close();
|
||
mStream = nullptr;
|
||
|
||
GlobalTeardownObserver::DisconnectFromOwner();
|
||
}
|
||
};
|
||
|
||
NS_IMPL_ISUPPORTS0(WindowStreamOwner)
|
||
|
||
inline nsISupports* ToSupports(WindowStreamOwner* aObj) {
|
||
return static_cast<GlobalTeardownObserver*>(aObj);
|
||
}
|
||
|
||
class WorkerStreamOwner final {
|
||
public:
|
||
NS_INLINE_DECL_REFCOUNTING(WorkerStreamOwner)
|
||
|
||
explicit WorkerStreamOwner(nsIAsyncInputStream* aStream,
|
||
nsCOMPtr<nsIEventTarget>&& target)
|
||
: mStream(aStream), mOwningEventTarget(std::move(target)) {}
|
||
|
||
static already_AddRefed<WorkerStreamOwner> Create(
|
||
nsIAsyncInputStream* aStream, WorkerPrivate* aWorker,
|
||
nsCOMPtr<nsIEventTarget>&& target) {
|
||
RefPtr<WorkerStreamOwner> self =
|
||
new WorkerStreamOwner(aStream, std::move(target));
|
||
|
||
self->mWorkerRef =
|
||
StrongWorkerRef::Create(aWorker, "JSStreamConsumer", [self]() {
|
||
if (self->mStream) {
|
||
// If this Close() calls JSStreamConsumer::OnInputStreamReady and
|
||
// drops the last reference to the JSStreamConsumer, 'this' will not
|
||
// be destroyed since ~JSStreamConsumer() only enqueues a release
|
||
// proxy.
|
||
self->mStream->Close();
|
||
self->mStream = nullptr;
|
||
}
|
||
});
|
||
|
||
if (!self->mWorkerRef) {
|
||
return nullptr;
|
||
}
|
||
|
||
return self.forget();
|
||
}
|
||
|
||
static void ProxyRelease(already_AddRefed<WorkerStreamOwner> aDoomed) {
|
||
RefPtr<WorkerStreamOwner> doomed = aDoomed;
|
||
nsIEventTarget* target = doomed->mOwningEventTarget;
|
||
NS_ProxyRelease("WorkerStreamOwner", target, doomed.forget(),
|
||
/* aAlwaysProxy = */ true);
|
||
}
|
||
|
||
private:
|
||
~WorkerStreamOwner() = default;
|
||
|
||
// Read from any thread but only set/cleared on the worker thread. The
|
||
// lifecycle of WorkerStreamOwner prevents concurrent read/clear.
|
||
nsCOMPtr<nsIAsyncInputStream> mStream;
|
||
RefPtr<StrongWorkerRef> mWorkerRef;
|
||
nsCOMPtr<nsIEventTarget> mOwningEventTarget;
|
||
};
|
||
|
||
class JSStreamConsumer final : public nsIInputStreamCallback,
|
||
public JS::OptimizedEncodingListener {
|
||
// A LengthPrefixType is stored at the start of the compressed optimized
|
||
// encoding, allowing the decompressed buffer to be allocated to exactly
|
||
// the right size.
|
||
using LengthPrefixType = uint32_t;
|
||
static const unsigned PrefixBytes = sizeof(LengthPrefixType);
|
||
|
||
RefPtr<WindowStreamOwner> mWindowStreamOwner;
|
||
RefPtr<WorkerStreamOwner> mWorkerStreamOwner;
|
||
nsMainThreadPtrHandle<nsICacheInfoChannel> mCache;
|
||
const bool mOptimizedEncoding;
|
||
z_stream mZStream;
|
||
bool mZStreamInitialized;
|
||
Vector<uint8_t> mOptimizedEncodingBytes;
|
||
JS::StreamConsumer* mConsumer;
|
||
bool mConsumerAborted;
|
||
|
||
JSStreamConsumer(already_AddRefed<WindowStreamOwner> aWindowStreamOwner,
|
||
nsIGlobalObject* aGlobal, JS::StreamConsumer* aConsumer,
|
||
nsMainThreadPtrHandle<nsICacheInfoChannel>&& aCache,
|
||
bool aOptimizedEncoding)
|
||
: mWindowStreamOwner(aWindowStreamOwner),
|
||
mCache(std::move(aCache)),
|
||
mOptimizedEncoding(aOptimizedEncoding),
|
||
mZStreamInitialized(false),
|
||
mConsumer(aConsumer),
|
||
mConsumerAborted(false) {
|
||
MOZ_DIAGNOSTIC_ASSERT(mWindowStreamOwner);
|
||
MOZ_DIAGNOSTIC_ASSERT(mConsumer);
|
||
}
|
||
|
||
JSStreamConsumer(RefPtr<WorkerStreamOwner> aWorkerStreamOwner,
|
||
nsIGlobalObject* aGlobal, JS::StreamConsumer* aConsumer,
|
||
nsMainThreadPtrHandle<nsICacheInfoChannel>&& aCache,
|
||
bool aOptimizedEncoding)
|
||
: mWorkerStreamOwner(std::move(aWorkerStreamOwner)),
|
||
mCache(std::move(aCache)),
|
||
mOptimizedEncoding(aOptimizedEncoding),
|
||
mZStreamInitialized(false),
|
||
mConsumer(aConsumer),
|
||
mConsumerAborted(false) {
|
||
MOZ_DIAGNOSTIC_ASSERT(mWorkerStreamOwner);
|
||
MOZ_DIAGNOSTIC_ASSERT(mConsumer);
|
||
}
|
||
|
||
~JSStreamConsumer() {
|
||
if (mZStreamInitialized) {
|
||
inflateEnd(&mZStream);
|
||
}
|
||
|
||
// Both WindowStreamOwner and WorkerStreamOwner need to be destroyed on
|
||
// their global's event target thread.
|
||
|
||
if (mWindowStreamOwner) {
|
||
MOZ_DIAGNOSTIC_ASSERT(!mWorkerStreamOwner);
|
||
NS_ReleaseOnMainThread("JSStreamConsumer::mWindowStreamOwner",
|
||
mWindowStreamOwner.forget(),
|
||
/* aAlwaysProxy = */ true);
|
||
} else {
|
||
MOZ_DIAGNOSTIC_ASSERT(mWorkerStreamOwner);
|
||
WorkerStreamOwner::ProxyRelease(mWorkerStreamOwner.forget());
|
||
}
|
||
|
||
// Bug 1733674: these annotations currently do nothing, because they are
|
||
// member variables and the annotation mechanism only applies to locals. But
|
||
// the analysis could be extended so that these could replace the big-hammer
|
||
// ~JSStreamConsumer annotation and thus the analysis could check that
|
||
// nothing is added that might GC for a different reason.
|
||
JS_HAZ_VALUE_IS_GC_SAFE(mWindowStreamOwner);
|
||
JS_HAZ_VALUE_IS_GC_SAFE(mWorkerStreamOwner);
|
||
}
|
||
|
||
static nsresult WriteSegment(nsIInputStream* aStream, void* aClosure,
|
||
const char* aFromSegment, uint32_t aToOffset,
|
||
uint32_t aCount, uint32_t* aWriteCount) {
|
||
JSStreamConsumer* self = reinterpret_cast<JSStreamConsumer*>(aClosure);
|
||
MOZ_DIAGNOSTIC_ASSERT(!self->mConsumerAborted);
|
||
|
||
if (self->mOptimizedEncoding) {
|
||
if (!self->mZStreamInitialized) {
|
||
// mOptimizedEncodingBytes is used as temporary storage until we have
|
||
// the full prefix.
|
||
MOZ_ASSERT(self->mOptimizedEncodingBytes.length() < PrefixBytes);
|
||
uint32_t remain = PrefixBytes - self->mOptimizedEncodingBytes.length();
|
||
uint32_t consume = std::min(remain, aCount);
|
||
|
||
if (!self->mOptimizedEncodingBytes.append(aFromSegment, consume)) {
|
||
return NS_ERROR_UNEXPECTED;
|
||
}
|
||
|
||
if (consume == remain) {
|
||
// Initialize zlib once all prefix bytes are loaded.
|
||
LengthPrefixType length;
|
||
memcpy(&length, self->mOptimizedEncodingBytes.begin(), PrefixBytes);
|
||
|
||
if (!self->mOptimizedEncodingBytes.resizeUninitialized(length)) {
|
||
return NS_ERROR_UNEXPECTED;
|
||
}
|
||
|
||
memset(&self->mZStream, 0, sizeof(self->mZStream));
|
||
self->mZStream.avail_out = length;
|
||
self->mZStream.next_out = self->mOptimizedEncodingBytes.begin();
|
||
|
||
if (inflateInit(&self->mZStream) != Z_OK) {
|
||
return NS_ERROR_UNEXPECTED;
|
||
}
|
||
self->mZStreamInitialized = true;
|
||
}
|
||
|
||
*aWriteCount = consume;
|
||
return NS_OK;
|
||
}
|
||
|
||
// Zlib is initialized, overwrite the prefix with the inflated data.
|
||
|
||
MOZ_DIAGNOSTIC_ASSERT(aCount > 0);
|
||
self->mZStream.avail_in = aCount;
|
||
self->mZStream.next_in = (uint8_t*)aFromSegment;
|
||
|
||
int ret = inflate(&self->mZStream, Z_NO_FLUSH);
|
||
|
||
MOZ_DIAGNOSTIC_ASSERT(ret == Z_OK || ret == Z_STREAM_END,
|
||
"corrupt optimized wasm cache file: data");
|
||
MOZ_DIAGNOSTIC_ASSERT(self->mZStream.avail_in == 0,
|
||
"corrupt optimized wasm cache file: input");
|
||
MOZ_DIAGNOSTIC_ASSERT_IF(ret == Z_STREAM_END,
|
||
self->mZStream.avail_out == 0);
|
||
// Gracefully handle corruption in release.
|
||
bool ok =
|
||
(ret == Z_OK || ret == Z_STREAM_END) && self->mZStream.avail_in == 0;
|
||
if (!ok) {
|
||
return NS_ERROR_UNEXPECTED;
|
||
}
|
||
} else {
|
||
// This callback can be called on any thread which is explicitly allowed
|
||
// by this particular JS API call.
|
||
if (!self->mConsumer->consumeChunk((const uint8_t*)aFromSegment,
|
||
aCount)) {
|
||
self->mConsumerAborted = true;
|
||
return NS_ERROR_UNEXPECTED;
|
||
}
|
||
}
|
||
|
||
*aWriteCount = aCount;
|
||
return NS_OK;
|
||
}
|
||
|
||
public:
|
||
NS_DECL_THREADSAFE_ISUPPORTS
|
||
|
||
static bool Start(nsCOMPtr<nsIInputStream> aStream, nsIGlobalObject* aGlobal,
|
||
WorkerPrivate* aMaybeWorker, JS::StreamConsumer* aConsumer,
|
||
nsMainThreadPtrHandle<nsICacheInfoChannel>&& aCache,
|
||
bool aOptimizedEncoding) {
|
||
nsCOMPtr<nsIAsyncInputStream> asyncStream;
|
||
nsresult rv = NS_MakeAsyncNonBlockingInputStream(
|
||
aStream.forget(), getter_AddRefs(asyncStream));
|
||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||
return false;
|
||
}
|
||
|
||
RefPtr<JSStreamConsumer> consumer;
|
||
if (aMaybeWorker) {
|
||
RefPtr<WorkerStreamOwner> owner = WorkerStreamOwner::Create(
|
||
asyncStream, aMaybeWorker, aGlobal->SerialEventTarget());
|
||
if (!owner) {
|
||
return false;
|
||
}
|
||
|
||
consumer = new JSStreamConsumer(std::move(owner), aGlobal, aConsumer,
|
||
std::move(aCache), aOptimizedEncoding);
|
||
} else {
|
||
RefPtr<WindowStreamOwner> owner =
|
||
new WindowStreamOwner(asyncStream, aGlobal);
|
||
if (!owner) {
|
||
return false;
|
||
}
|
||
|
||
consumer = new JSStreamConsumer(owner.forget(), aGlobal, aConsumer,
|
||
std::move(aCache), aOptimizedEncoding);
|
||
}
|
||
|
||
// This AsyncWait() creates a ref-cycle between asyncStream and consumer:
|
||
//
|
||
// asyncStream -> consumer -> (Window|Worker)StreamOwner -> asyncStream
|
||
//
|
||
// The cycle is broken when the stream completes or errors out and
|
||
// asyncStream drops its reference to consumer.
|
||
return NS_SUCCEEDED(asyncStream->AsyncWait(consumer, 0, 0, nullptr));
|
||
}
|
||
|
||
// nsIInputStreamCallback:
|
||
|
||
NS_IMETHOD
|
||
OnInputStreamReady(nsIAsyncInputStream* aStream) override {
|
||
// Can be called on any stream. The JS API calls made below explicitly
|
||
// support being called from any thread.
|
||
MOZ_DIAGNOSTIC_ASSERT(!mConsumerAborted);
|
||
|
||
nsresult rv;
|
||
|
||
uint64_t available = 0;
|
||
rv = aStream->Available(&available);
|
||
if (NS_SUCCEEDED(rv) && available == 0) {
|
||
rv = NS_BASE_STREAM_CLOSED;
|
||
}
|
||
|
||
if (rv == NS_BASE_STREAM_CLOSED) {
|
||
if (mOptimizedEncoding) {
|
||
// Gracefully handle corruption of compressed data stream in release.
|
||
// From on investigations in bug 1738987, the incomplete data cases
|
||
// mostly happen during shutdown. Some corruptions in the cache entry
|
||
// can still happen and will be handled in the WriteSegment above.
|
||
bool ok = mZStreamInitialized && mZStream.avail_out == 0;
|
||
if (!ok) {
|
||
mConsumer->streamError(size_t(NS_ERROR_UNEXPECTED));
|
||
return NS_OK;
|
||
}
|
||
|
||
mConsumer->consumeOptimizedEncoding(mOptimizedEncodingBytes.begin(),
|
||
mOptimizedEncodingBytes.length());
|
||
} else {
|
||
// If there is cache entry associated with this stream, then listen for
|
||
// an optimized encoding so we can store it in the alt data. By JS API
|
||
// contract, the compilation process will hold a refcount to 'this'
|
||
// until it's done, optionally calling storeOptimizedEncoding().
|
||
mConsumer->streamEnd(mCache ? this : nullptr);
|
||
}
|
||
return NS_OK;
|
||
}
|
||
|
||
if (NS_FAILED(rv)) {
|
||
mConsumer->streamError(size_t(rv));
|
||
return NS_OK;
|
||
}
|
||
|
||
// Check mConsumerAborted before NS_FAILED to avoid calling streamError()
|
||
// if consumeChunk() returned false per JS API contract.
|
||
uint32_t written = 0;
|
||
rv = aStream->ReadSegments(WriteSegment, this, available, &written);
|
||
if (mConsumerAborted) {
|
||
return NS_OK;
|
||
}
|
||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||
mConsumer->streamError(size_t(rv));
|
||
return NS_OK;
|
||
}
|
||
|
||
rv = aStream->AsyncWait(this, 0, 0, nullptr);
|
||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||
mConsumer->streamError(size_t(rv));
|
||
return NS_OK;
|
||
}
|
||
|
||
return NS_OK;
|
||
}
|
||
|
||
// JS::OptimizedEncodingListener
|
||
|
||
void storeOptimizedEncoding(const uint8_t* aSrcBytes,
|
||
size_t aSrcLength) override {
|
||
MOZ_ASSERT(mCache, "we only listen if there's a cache entry");
|
||
|
||
z_stream zstream;
|
||
memset(&zstream, 0, sizeof(zstream));
|
||
zstream.avail_in = aSrcLength;
|
||
zstream.next_in = (uint8_t*)aSrcBytes;
|
||
|
||
// The wins from increasing compression levels are tiny, while the time
|
||
// to compress increases drastically. For example, for a 148mb alt-data
|
||
// produced by a 40mb .wasm file, the level 2 takes 2.5s to get a 3.7x size
|
||
// reduction while level 9 takes 22.5s to get a 4x size reduction. Read-time
|
||
// wins from smaller compressed cache files are not found to be
|
||
// significant, thus the fastest compression level is used. (On test
|
||
// workloads, level 2 actually was faster *and* smaller than level 1.)
|
||
const int COMPRESSION = 2;
|
||
if (deflateInit(&zstream, COMPRESSION) != Z_OK) {
|
||
return;
|
||
}
|
||
auto autoDestroy = MakeScopeExit([&]() { deflateEnd(&zstream); });
|
||
|
||
Vector<uint8_t> dstBytes;
|
||
if (!dstBytes.resizeUninitialized(PrefixBytes +
|
||
deflateBound(&zstream, aSrcLength))) {
|
||
return;
|
||
}
|
||
|
||
MOZ_RELEASE_ASSERT(LengthPrefixType(aSrcLength) == aSrcLength);
|
||
LengthPrefixType srcLength = aSrcLength;
|
||
memcpy(dstBytes.begin(), &srcLength, PrefixBytes);
|
||
|
||
uint8_t* compressBegin = dstBytes.begin() + PrefixBytes;
|
||
zstream.next_out = compressBegin;
|
||
zstream.avail_out = dstBytes.length() - PrefixBytes;
|
||
|
||
int ret = deflate(&zstream, Z_FINISH);
|
||
if (ret == Z_MEM_ERROR) {
|
||
return;
|
||
}
|
||
MOZ_RELEASE_ASSERT(ret == Z_STREAM_END);
|
||
|
||
dstBytes.shrinkTo(zstream.next_out - dstBytes.begin());
|
||
|
||
NS_DispatchToMainThread(new StoreOptimizedEncodingRunnable(
|
||
std::move(mCache), std::move(dstBytes)));
|
||
}
|
||
};
|
||
|
||
NS_IMPL_ISUPPORTS(JSStreamConsumer, nsIInputStreamCallback)
|
||
|
||
// static
|
||
const nsCString FetchUtil::WasmAltDataType;
|
||
|
||
// static
|
||
void FetchUtil::InitWasmAltDataType() {
|
||
nsCString& type = const_cast<nsCString&>(WasmAltDataType);
|
||
MOZ_ASSERT(type.IsEmpty());
|
||
|
||
RunOnShutdown([]() {
|
||
// Avoid StringBuffer leak tests failures.
|
||
const_cast<nsCString&>(WasmAltDataType).Truncate();
|
||
});
|
||
|
||
type.Append(nsLiteralCString("wasm-"));
|
||
|
||
JS::BuildIdCharVector buildId;
|
||
if (!JS::GetOptimizedEncodingBuildId(&buildId)) {
|
||
MOZ_CRASH("build id oom");
|
||
}
|
||
|
||
type.Append(buildId.begin(), buildId.length());
|
||
}
|
||
|
||
static bool ThrowException(JSContext* aCx, unsigned errorNumber) {
|
||
JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr, errorNumber);
|
||
return false;
|
||
}
|
||
|
||
// static
|
||
bool FetchUtil::StreamResponseToJS(JSContext* aCx, JS::Handle<JSObject*> aObj,
|
||
JS::MimeType aMimeType,
|
||
JS::StreamConsumer* aConsumer,
|
||
WorkerPrivate* aMaybeWorker) {
|
||
MOZ_ASSERT(!WasmAltDataType.IsEmpty());
|
||
MOZ_ASSERT(!aMaybeWorker == NS_IsMainThread());
|
||
|
||
RefPtr<Response> response;
|
||
nsresult rv = UNWRAP_OBJECT(Response, aObj, response);
|
||
if (NS_FAILED(rv)) {
|
||
return ThrowException(aCx, JSMSG_WASM_BAD_RESPONSE_VALUE);
|
||
}
|
||
|
||
const char* requiredMimeType = nullptr;
|
||
switch (aMimeType) {
|
||
case JS::MimeType::Wasm:
|
||
requiredMimeType = WASM_CONTENT_TYPE;
|
||
break;
|
||
}
|
||
|
||
nsAutoCString mimeType;
|
||
nsAutoCString mixedCaseMimeType; // unused
|
||
response->GetMimeType(mimeType, mixedCaseMimeType);
|
||
|
||
if (!mimeType.EqualsASCII(requiredMimeType)) {
|
||
JS_ReportErrorNumberASCII(aCx, js::GetErrorMessage, nullptr,
|
||
JSMSG_WASM_BAD_RESPONSE_MIME_TYPE, mimeType.get(),
|
||
requiredMimeType);
|
||
return false;
|
||
}
|
||
|
||
if (response->Type() != ResponseType::Basic &&
|
||
response->Type() != ResponseType::Cors &&
|
||
response->Type() != ResponseType::Default) {
|
||
return ThrowException(aCx, JSMSG_WASM_BAD_RESPONSE_CORS_SAME_ORIGIN);
|
||
}
|
||
|
||
if (!response->Ok()) {
|
||
return ThrowException(aCx, JSMSG_WASM_BAD_RESPONSE_STATUS);
|
||
}
|
||
|
||
if (response->BodyUsed()) {
|
||
return ThrowException(aCx, JSMSG_WASM_RESPONSE_ALREADY_CONSUMED);
|
||
}
|
||
|
||
switch (aMimeType) {
|
||
case JS::MimeType::Wasm:
|
||
nsAutoCString url;
|
||
response->GetUrl(url);
|
||
|
||
IgnoredErrorResult result;
|
||
nsCString sourceMapUrl;
|
||
response->GetInternalHeaders()->Get("SourceMap"_ns, sourceMapUrl, result);
|
||
if (NS_WARN_IF(result.Failed())) {
|
||
return ThrowException(aCx, JSMSG_WASM_ERROR_CONSUMING_RESPONSE);
|
||
}
|
||
aConsumer->noteResponseURLs(
|
||
url.get(), sourceMapUrl.IsVoid() ? nullptr : sourceMapUrl.get());
|
||
break;
|
||
}
|
||
|
||
SafeRefPtr<InternalResponse> ir = response->GetInternalResponse();
|
||
if (NS_WARN_IF(!ir)) {
|
||
return ThrowException(aCx, JSMSG_OUT_OF_MEMORY);
|
||
}
|
||
|
||
nsCOMPtr<nsIInputStream> stream;
|
||
|
||
nsMainThreadPtrHandle<nsICacheInfoChannel> cache;
|
||
bool optimizedEncoding = false;
|
||
if (ir->HasCacheInfoChannel()) {
|
||
cache = ir->TakeCacheInfoChannel();
|
||
|
||
nsAutoCString altDataType;
|
||
if (NS_SUCCEEDED(cache->GetAlternativeDataType(altDataType)) &&
|
||
WasmAltDataType.Equals(altDataType)) {
|
||
optimizedEncoding = true;
|
||
rv = cache->GetAlternativeDataInputStream(getter_AddRefs(stream));
|
||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||
return ThrowException(aCx, JSMSG_OUT_OF_MEMORY);
|
||
}
|
||
if (ir->HasBeenCloned()) {
|
||
// If `Response` is cloned, clone alternative data stream instance.
|
||
// The cache entry does not clone automatically, and multiple
|
||
// JSStreamConsumer instances will collide during read if not cloned.
|
||
nsCOMPtr<nsICloneableInputStream> original = do_QueryInterface(stream);
|
||
if (NS_WARN_IF(!original)) {
|
||
return ThrowException(aCx, JSMSG_OUT_OF_MEMORY);
|
||
}
|
||
rv = original->Clone(getter_AddRefs(stream));
|
||
if (NS_WARN_IF(NS_FAILED(rv))) {
|
||
return ThrowException(aCx, JSMSG_OUT_OF_MEMORY);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!optimizedEncoding) {
|
||
ir->GetUnfilteredBody(getter_AddRefs(stream));
|
||
if (!stream) {
|
||
aConsumer->streamEnd();
|
||
return true;
|
||
}
|
||
}
|
||
|
||
MOZ_ASSERT(stream);
|
||
|
||
IgnoredErrorResult error;
|
||
response->SetBodyUsed(aCx, error);
|
||
if (NS_WARN_IF(error.Failed())) {
|
||
return ThrowException(aCx, JSMSG_WASM_ERROR_CONSUMING_RESPONSE);
|
||
}
|
||
|
||
nsIGlobalObject* global = xpc::NativeGlobal(js::UncheckedUnwrap(aObj));
|
||
|
||
if (!JSStreamConsumer::Start(stream, global, aMaybeWorker, aConsumer,
|
||
std::move(cache), optimizedEncoding)) {
|
||
return ThrowException(aCx, JSMSG_OUT_OF_MEMORY);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
// static
|
||
void FetchUtil::ReportJSStreamError(JSContext* aCx, size_t aErrorCode) {
|
||
// For now, convert *all* errors into AbortError.
|
||
|
||
RefPtr<DOMException> e = DOMException::Create(NS_ERROR_DOM_ABORT_ERR);
|
||
|
||
JS::Rooted<JS::Value> value(aCx);
|
||
if (!GetOrCreateDOMReflector(aCx, e, &value)) {
|
||
return;
|
||
}
|
||
|
||
JS_SetPendingException(aCx, value);
|
||
}
|
||
|
||
} // namespace mozilla::dom
|