dom/fetch/FetchUtil.cpp
author Noemi Erli <nerli@mozilla.com>
Thu, 24 May 2018 18:30:36 +0300
changeset 476428 fae06a27c30119754a4522f9ec21d254d34cfc80
parent 464251 9fb5452d41d496242e3b0de2b99798ad362c84ea
child 477586 b54db66223586b4e04f5cb926fccdacf8a176b91
permissions -rw-r--r--
Backed out changeset c19f0dedfb15 (bug 1463794) for reftest failures in build/tests/reftest/tests/layout/reftests/w3c-css/submitted/shapes1/shape-outside-circle-056.html on a CLOSED TREE

/* -*- 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 request’s 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 request’s referrer is not "no-referrer", set request’s referrer to
  // the result of invoking determine request’s 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(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(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(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(aObj);

  if (!JSStreamConsumer::Start(body, aConsumer, global, aMaybeWorker)) {
    return ThrowException(aCx, JSMSG_OUT_OF_MEMORY);
  }

  return true;
}

} // namespace dom
} // namespace mozilla