gecko/dom/cache/FetchPut.cpp

470 lines
13 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 "mozilla/dom/cache/FetchPut.h"
#include "mozilla/dom/Fetch.h"
#include "mozilla/dom/FetchDriver.h"
#include "mozilla/dom/Headers.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/PromiseNativeHandler.h"
#include "mozilla/dom/Request.h"
#include "mozilla/dom/Response.h"
#include "mozilla/dom/ResponseBinding.h"
#include "mozilla/dom/UnionTypes.h"
#include "mozilla/dom/cache/ManagerId.h"
#include "mozilla/dom/cache/PCacheTypes.h"
#include "nsContentUtils.h"
#include "nsNetUtil.h"
#include "nsThreadUtils.h"
#include "nsCRT.h"
#include "nsHttp.h"
namespace mozilla {
namespace dom {
namespace cache {
class FetchPut::Runnable final : public nsRunnable
{
public:
explicit Runnable(FetchPut* aFetchPut)
: mFetchPut(aFetchPut)
{
MOZ_ASSERT(mFetchPut);
}
NS_IMETHOD Run() override
{
if (NS_IsMainThread())
{
mFetchPut->DoFetchOnMainThread();
return NS_OK;
}
MOZ_ASSERT(mFetchPut->mInitiatingThread == NS_GetCurrentThread());
mFetchPut->DoPutOnWorkerThread();
// The FetchPut object must ultimately be freed on the worker thread,
// so make sure we release our reference here. The runnable may end
// up getting deleted on the main thread.
mFetchPut = nullptr;
return NS_OK;
}
private:
nsRefPtr<FetchPut> mFetchPut;
};
class FetchPut::FetchObserver final : public FetchDriverObserver
{
public:
explicit FetchObserver(FetchPut* aFetchPut)
: mFetchPut(aFetchPut)
{
}
virtual void OnResponseAvailable(InternalResponse* aResponse) override
{
MOZ_ASSERT(!mInternalResponse);
mInternalResponse = aResponse;
}
virtual void OnResponseEnd() override
{
mFetchPut->FetchComplete(this, mInternalResponse);
mFetchPut = nullptr;
}
protected:
virtual ~FetchObserver() { }
private:
nsRefPtr<FetchPut> mFetchPut;
nsRefPtr<InternalResponse> mInternalResponse;
};
// static
nsresult
FetchPut::Create(Listener* aListener, Manager* aManager,
RequestId aRequestId, CacheId aCacheId,
const nsTArray<PCacheRequest>& aRequests,
const nsTArray<nsCOMPtr<nsIInputStream>>& aRequestStreams,
FetchPut** aFetchPutOut)
{
MOZ_ASSERT(aRequests.Length() == aRequestStreams.Length());
// The FetchDriver requires that all requests have a referrer already set.
#ifdef DEBUG
for (uint32_t i = 0; i < aRequests.Length(); ++i) {
if (aRequests[i].referrer() == EmptyString()) {
return NS_ERROR_UNEXPECTED;
}
}
#endif
nsRefPtr<FetchPut> ref = new FetchPut(aListener, aManager, aRequestId, aCacheId,
aRequests, aRequestStreams);
nsresult rv = ref->DispatchToMainThread();
if (NS_WARN_IF(NS_FAILED(rv))) { return rv; }
ref.forget(aFetchPutOut);
return NS_OK;
}
void
FetchPut::ClearListener()
{
MOZ_ASSERT(mListener);
mListener = nullptr;
}
FetchPut::FetchPut(Listener* aListener, Manager* aManager,
RequestId aRequestId, CacheId aCacheId,
const nsTArray<PCacheRequest>& aRequests,
const nsTArray<nsCOMPtr<nsIInputStream>>& aRequestStreams)
: mListener(aListener)
, mManager(aManager)
, mRequestId(aRequestId)
, mCacheId(aCacheId)
, mInitiatingThread(NS_GetCurrentThread())
, mStateList(aRequests.Length())
, mPendingCount(0)
, mResult(NS_OK)
{
MOZ_ASSERT(mListener);
MOZ_ASSERT(mManager);
MOZ_ASSERT(aRequests.Length() == aRequestStreams.Length());
for (uint32_t i = 0; i < aRequests.Length(); ++i) {
State* s = mStateList.AppendElement();
s->mPCacheRequest = aRequests[i];
s->mRequestStream = aRequestStreams[i];
}
mManager->AddRefCacheId(mCacheId);
}
FetchPut::~FetchPut()
{
MOZ_ASSERT(mInitiatingThread == NS_GetCurrentThread());
MOZ_ASSERT(!mListener);
mManager->RemoveListener(this);
mManager->ReleaseCacheId(mCacheId);
}
nsresult
FetchPut::DispatchToMainThread()
{
MOZ_ASSERT(!mRunnable);
nsCOMPtr<nsIRunnable> runnable = new Runnable(this);
nsresult rv = NS_DispatchToMainThread(runnable, nsIThread::DISPATCH_NORMAL);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
MOZ_ASSERT(!mRunnable);
mRunnable = runnable.forget();
return NS_OK;
}
void
FetchPut::DispatchToInitiatingThread()
{
MOZ_ASSERT(mRunnable);
nsresult rv = mInitiatingThread->Dispatch(mRunnable,
nsIThread::DISPATCH_NORMAL);
if (NS_FAILED(rv)) {
MOZ_CRASH("Failed to dispatch to worker thread after fetch completion.");
}
mRunnable = nullptr;
}
void
FetchPut::DoFetchOnMainThread()
{
MOZ_ASSERT(NS_IsMainThread());
nsRefPtr<ManagerId> managerId = mManager->GetManagerId();
nsCOMPtr<nsIPrincipal> principal = managerId->Principal();
mPendingCount = mStateList.Length();
nsCOMPtr<nsILoadGroup> loadGroup;
nsresult rv = NS_NewLoadGroup(getter_AddRefs(loadGroup), principal);
if (NS_WARN_IF(NS_FAILED(rv))) {
MaybeSetError(rv);
MaybeCompleteOnMainThread();
return;
}
for (uint32_t i = 0; i < mStateList.Length(); ++i) {
nsRefPtr<InternalRequest> internalRequest =
ToInternalRequest(mStateList[i].mPCacheRequest);
// If there is a stream we must clone it so that its still available
// to store in the cache later;
if (mStateList[i].mRequestStream) {
internalRequest->SetBody(mStateList[i].mRequestStream);
nsRefPtr<InternalRequest> clone = internalRequest->Clone();
// The copy construction clone above can change the source stream,
// so get it back out to use when we put this in the cache.
internalRequest->GetBody(getter_AddRefs(mStateList[i].mRequestStream));
internalRequest = clone;
}
nsRefPtr<FetchDriver> fetchDriver = new FetchDriver(internalRequest,
principal,
loadGroup);
mStateList[i].mFetchObserver = new FetchObserver(this);
rv = fetchDriver->Fetch(mStateList[i].mFetchObserver);
if (NS_WARN_IF(NS_FAILED(rv))) {
MaybeSetError(rv);
mStateList[i].mFetchObserver = nullptr;
mPendingCount -= 1;
continue;
}
}
// If they all failed, then we might need to complete main thread immediately
MaybeCompleteOnMainThread();
}
void
FetchPut::FetchComplete(FetchObserver* aObserver,
InternalResponse* aInternalResponse)
{
MOZ_ASSERT(NS_IsMainThread());
if (aInternalResponse->IsError() && NS_SUCCEEDED(mResult)) {
MaybeSetError(NS_ERROR_FAILURE);
}
for (uint32_t i = 0; i < mStateList.Length(); ++i) {
if (mStateList[i].mFetchObserver == aObserver) {
ErrorResult rv;
ToPCacheResponseWithoutBody(mStateList[i].mPCacheResponse,
*aInternalResponse, rv);
if (rv.Failed()) {
MaybeSetError(rv.ErrorCode());
return;
}
aInternalResponse->GetBody(getter_AddRefs(mStateList[i].mResponseStream));
mStateList[i].mFetchObserver = nullptr;
MOZ_ASSERT(mPendingCount > 0);
mPendingCount -= 1;
MaybeCompleteOnMainThread();
return;
}
}
MOZ_ASSERT_UNREACHABLE("Should never get called by unknown fetch observer.");
}
void
FetchPut::MaybeCompleteOnMainThread()
{
MOZ_ASSERT(NS_IsMainThread());
if (mPendingCount > 0) {
return;
}
DispatchToInitiatingThread();
}
void
FetchPut::DoPutOnWorkerThread()
{
MOZ_ASSERT(mInitiatingThread == NS_GetCurrentThread());
if (NS_FAILED(mResult)) {
MaybeNotifyListener();
return;
}
// These allocate ~4.5k combined on the stack
nsAutoTArray<CacheRequestResponse, 16> putList;
nsAutoTArray<nsCOMPtr<nsIInputStream>, 16> requestStreamList;
nsAutoTArray<nsCOMPtr<nsIInputStream>, 16> responseStreamList;
putList.SetCapacity(mStateList.Length());
requestStreamList.SetCapacity(mStateList.Length());
responseStreamList.SetCapacity(mStateList.Length());
for (uint32_t i = 0; i < mStateList.Length(); ++i) {
// The spec requires us to catch if content tries to insert a set of
// requests that would overwrite each other.
if (MatchInPutList(mStateList[i].mPCacheRequest, putList)) {
MaybeSetError(NS_ERROR_DOM_INVALID_STATE_ERR);
MaybeNotifyListener();
return;
}
CacheRequestResponse* entry = putList.AppendElement();
entry->request() = mStateList[i].mPCacheRequest;
entry->response() = mStateList[i].mPCacheResponse;
requestStreamList.AppendElement(mStateList[i].mRequestStream.forget());
responseStreamList.AppendElement(mStateList[i].mResponseStream.forget());
}
mStateList.Clear();
mManager->CachePutAll(this, mRequestId, mCacheId, putList, requestStreamList,
responseStreamList);
}
// static
bool
FetchPut::MatchInPutList(const PCacheRequest& aRequest,
const nsTArray<CacheRequestResponse>& aPutList)
{
// This method implements the SW spec QueryCache algorithm against an
// in memory array of Request/Response objects. This essentially the
// same algorithm that is implemented in DBSchema.cpp. Unfortunately
// we cannot unify them because when operating against the real database
// we don't want to load all request/response objects into memory.
if (!aRequest.method().LowerCaseEqualsLiteral("get") &&
!aRequest.method().LowerCaseEqualsLiteral("head")) {
return false;
}
nsRefPtr<InternalHeaders> requestHeaders =
new InternalHeaders(aRequest.headers());
for (uint32_t i = 0; i < aPutList.Length(); ++i) {
const PCacheRequest& cachedRequest = aPutList[i].request();
const PCacheResponse& cachedResponse = aPutList[i].response();
// If the URLs don't match, then just skip to the next entry.
if (aRequest.url() != cachedRequest.url()) {
continue;
}
nsRefPtr<InternalHeaders> cachedRequestHeaders =
new InternalHeaders(cachedRequest.headers());
nsRefPtr<InternalHeaders> cachedResponseHeaders =
new InternalHeaders(cachedResponse.headers());
nsAutoTArray<nsCString, 16> varyHeaders;
ErrorResult rv;
cachedResponseHeaders->GetAll(NS_LITERAL_CSTRING("vary"), varyHeaders, rv);
MOZ_ALWAYS_TRUE(!rv.Failed());
// Assume the vary headers match until we find a conflict
bool varyHeadersMatch = true;
for (uint32_t j = 0; j < varyHeaders.Length(); ++j) {
// Extract the header names inside the Vary header value.
nsAutoCString varyValue(varyHeaders[j]);
char* rawBuffer = varyValue.BeginWriting();
char* token = nsCRT::strtok(rawBuffer, NS_HTTP_HEADER_SEPS, &rawBuffer);
bool bailOut = false;
for (; token;
token = nsCRT::strtok(rawBuffer, NS_HTTP_HEADER_SEPS, &rawBuffer)) {
nsDependentCString header(token);
if (header.EqualsLiteral("*")) {
continue;
}
ErrorResult headerRv;
nsAutoCString value;
requestHeaders->Get(header, value, headerRv);
if (NS_WARN_IF(headerRv.Failed())) {
headerRv.ClearMessage();
MOZ_ASSERT(value.IsEmpty());
}
nsAutoCString cachedValue;
cachedRequestHeaders->Get(header, value, headerRv);
if (NS_WARN_IF(headerRv.Failed())) {
headerRv.ClearMessage();
MOZ_ASSERT(cachedValue.IsEmpty());
}
if (value != cachedValue) {
varyHeadersMatch = false;
bailOut = true;
break;
}
}
if (bailOut) {
break;
}
}
// URL was equal and all vary headers match!
if (varyHeadersMatch) {
return true;
}
}
return false;
}
void
FetchPut::OnCachePutAll(RequestId aRequestId, nsresult aRv)
{
MOZ_ASSERT(mInitiatingThread == NS_GetCurrentThread());
MaybeSetError(aRv);
MaybeNotifyListener();
}
void
FetchPut::MaybeSetError(nsresult aRv)
{
if (NS_FAILED(mResult) || NS_SUCCEEDED(aRv)) {
return;
}
mResult = aRv;
}
void
FetchPut::MaybeNotifyListener()
{
MOZ_ASSERT(mInitiatingThread == NS_GetCurrentThread());
if (!mListener) {
return;
}
mListener->OnFetchPut(this, mRequestId, mResult);
}
nsIGlobalObject*
FetchPut::GetGlobalObject() const
{
MOZ_CRASH("No global object in parent-size FetchPut operation!");
}
#ifdef DEBUG
void
FetchPut::AssertOwningThread() const
{
MOZ_ASSERT(mInitiatingThread == NS_GetCurrentThread());
}
#endif
CachePushStreamChild*
FetchPut::CreatePushStream(nsIAsyncInputStream* aStream)
{
MOZ_CRASH("FetchPut should never create a push stream!");
}
} // namespace cache
} // namespace dom
} // namespace mozilla