/* 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 https://mozilla.org/MPL/2.0/. */

#include "mozilla/ViaductRequest.h"

#include "mozilla/ClearOnShutdown.h"
#include "mozilla/ErrorNames.h"
#include "mozilla/ResultExtensions.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/Services.h"

#include "nsComponentManagerUtils.h"
#include "nsContentUtils.h"
#include "nsIAsyncVerifyRedirectCallback.h"
#include "nsIHttpChannel.h"
#include "nsIHttpHeaderVisitor.h"
#include "nsIInputStream.h"
#include "nsIUploadChannel2.h"
#include "nsIURI.h"
#include "nsNetUtil.h"
#include "nsPrintfCString.h"
#include "nsString.h"
#include "nsStringStream.h"
#include "nsThreadUtils.h"

namespace mozilla {

namespace {

extern "C" {
ViaductByteBuffer viaduct_alloc_bytebuffer(int32_t);
void viaduct_destroy_bytebuffer(ViaductByteBuffer);
}

}  // namespace

class HeaderVisitor final : public nsIHttpHeaderVisitor {
 public:
  NS_DECL_ISUPPORTS
  NS_DECL_NSIHTTPHEADERVISITOR

  explicit HeaderVisitor(
      google::protobuf::Map<std::string, std::string>* aHeaders)
      : mHeaders(aHeaders) {}

 private:
  google::protobuf::Map<std::string, std::string>* mHeaders;
  ~HeaderVisitor() = default;
};

NS_IMETHODIMP
HeaderVisitor::VisitHeader(const nsACString& aHeader,
                           const nsACString& aValue) {
  (*mHeaders)[aHeader.BeginReading()] = aValue.BeginReading();
  return NS_OK;
}
NS_IMPL_ISUPPORTS(HeaderVisitor, nsIHttpHeaderVisitor)

nsCString ConvertMethod(
    appservices::httpconfig::protobuf::Request_Method method);

///////////////////////////////////////////////////////////////////////////////
// ViaductRequest implementation

ViaductByteBuffer ViaductRequest::MakeRequest(ViaductByteBuffer reqBuf) {
  MOZ_ASSERT(!NS_IsMainThread(), "Background thread only!");
  auto clearBuf = MakeScopeExit([&] { viaduct_destroy_bytebuffer(reqBuf); });
  // We keep the protobuf parsing/serializing in the background thread.
  appservices::httpconfig::protobuf::Request request;
  if (!request.ParseFromArray(static_cast<const void*>(reqBuf.data),
                              reqBuf.len)) {
    // We still need to return something!
    return ViaductByteBuffer{.len = 0, .data = nullptr};
  }
  MonitorAutoLock lock(mMonitor);
  NS_DispatchToMainThread(NS_NewRunnableFunction(
      "ViaductRequest::LaunchRequest", [this, &request]() {
        nsresult rv = LaunchRequest(request);
        if (NS_WARN_IF(NS_FAILED(rv))) {
          // Something went very very wrong, but we still have to unblock
          // the calling thread.
          NotifyMonitor();
        }
      }));
  while (!mDone) {
    mMonitor.Wait();
  }
  ViaductByteBuffer respBuf =
      viaduct_alloc_bytebuffer(mResponse.ByteSizeLong());
  if (!mResponse.SerializeToArray(respBuf.data, respBuf.len)) {
    viaduct_destroy_bytebuffer(respBuf);
    return ViaductByteBuffer{.len = 0, .data = nullptr};
  }
  return respBuf;
}

nsresult ViaductRequest::LaunchRequest(
    appservices::httpconfig::protobuf::Request& request) {
  if (PastShutdownPhase(ShutdownPhase::AppShutdownNetTeardown)) {
    return NS_ERROR_FAILURE;
  }
  nsCOMPtr<nsIURI> uri;
  nsresult rv = NS_NewURI(getter_AddRefs(uri), request.url().c_str());
  NS_ENSURE_SUCCESS(rv, rv);

  nsSecurityFlags secFlags =
      nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL |
      nsILoadInfo::SEC_COOKIES_OMIT;
  uint32_t loadFlags = 0;

  if (!request.use_caches()) {
    loadFlags |= nsIRequest::LOAD_BYPASS_CACHE;
  }

  if (!request.follow_redirects()) {
    secFlags |= nsILoadInfo::SEC_DONT_FOLLOW_REDIRECTS;
  }

  rv = NS_NewChannel(getter_AddRefs(mChannel), uri,
                     nsContentUtils::GetSystemPrincipal(), secFlags,
                     nsIContentPolicy::TYPE_OTHER,
                     nullptr,  // nsICookieJarSettings
                     nullptr,  // aPerformanceStorage
                     nullptr,  // aLoadGroup
                     nullptr, loadFlags);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel);
  nsCString method = ConvertMethod(request.method());
  rv = httpChannel->SetRequestMethod(method);
  NS_ENSURE_SUCCESS(rv, rv);

  for (auto& header : request.headers()) {
    rv = httpChannel->SetRequestHeader(
        nsDependentCString(header.first.c_str(), header.first.size()),
        nsDependentCString(header.second.c_str(), header.second.size()),
        false /* merge */);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  // Body
  if (request.has_body()) {
    const std::string& body = request.body();
    nsCOMPtr<nsIStringInputStream> stream(
        do_CreateInstance(NS_STRINGINPUTSTREAM_CONTRACTID));
    rv = stream->SetData(body.data(), body.size());
    NS_ENSURE_SUCCESS(rv, rv);
    nsCOMPtr<nsIUploadChannel2> uploadChannel = do_QueryInterface(mChannel);
    uploadChannel->ExplicitSetUploadStream(stream, VoidCString(), -1, method,
                                           false /* aStreamHasHeaders */);
  }

  MOZ_TRY_VAR(
      mConnectTimeoutTimer,
      NS_NewTimerWithCallback(this, request.connect_timeout_secs() * 1000,
                              nsITimer::TYPE_ONE_SHOT));
  MOZ_TRY_VAR(mReadTimeoutTimer,
              NS_NewTimerWithCallback(this, request.read_timeout_secs() * 1000,
                                      nsITimer::TYPE_ONE_SHOT));

  rv = httpChannel->AsyncOpen(this);
  NS_ENSURE_SUCCESS(rv, rv);

  return rv;
}

nsCString ConvertMethod(
    appservices::httpconfig::protobuf::Request_Method method) {
  using appservices::httpconfig::protobuf::Request_Method;
  switch (method) {
    case Request_Method::Request_Method_GET:
      return "GET"_ns;
    case Request_Method::Request_Method_HEAD:
      return "HEAD"_ns;
    case Request_Method::Request_Method_POST:
      return "POST"_ns;
    case Request_Method::Request_Method_PUT:
      return "PUT"_ns;
    case Request_Method::Request_Method_DELETE:
      return "DELETE"_ns;
    case Request_Method::Request_Method_CONNECT:
      return "CONNECT"_ns;
    case Request_Method::Request_Method_OPTIONS:
      return "OPTIONS"_ns;
    case Request_Method::Request_Method_TRACE:
      return "TRACE"_ns;
    case Request_Method::Request_Method_PATCH:
      return "PATCH"_ns;
  }
  return "UNKNOWN"_ns;
}

void ViaductRequest::ClearTimers() {
  if (mConnectTimeoutTimer) {
    mConnectTimeoutTimer->Cancel();
    mConnectTimeoutTimer = nullptr;
  }
  if (mReadTimeoutTimer) {
    mReadTimeoutTimer->Cancel();
    mReadTimeoutTimer = nullptr;
  }
}

void ViaductRequest::NotifyMonitor() {
  MonitorAutoLock lock(mMonitor);
  mDone = true;
  mMonitor.Notify();
}

ViaductRequest::~ViaductRequest() {
  ClearTimers();
  if (mChannel) {
    mChannel->Cancel(NS_ERROR_ABORT);
    mChannel = nullptr;
  }
  NotifyMonitor();
}

NS_IMPL_ISUPPORTS(ViaductRequest, nsIStreamListener, nsITimerCallback, nsINamed,
                  nsIChannelEventSink)

///////////////////////////////////////////////////////////////////////////////
// nsIStreamListener implementation

NS_IMETHODIMP
ViaductRequest::OnStartRequest(nsIRequest* aRequest) {
  if (mConnectTimeoutTimer) {
    mConnectTimeoutTimer->Cancel();
    mConnectTimeoutTimer = nullptr;
  }
  return NS_OK;
}

static nsresult AssignResponseToBuffer(nsIInputStream* aIn, void* aClosure,
                                       const char* aFromRawSegment,
                                       uint32_t aToOffset, uint32_t aCount,
                                       uint32_t* aWriteCount) {
  nsCString* buf = static_cast<nsCString*>(aClosure);
  buf->Append(aFromRawSegment, aCount);
  *aWriteCount = aCount;
  return NS_OK;
}

NS_IMETHODIMP
ViaductRequest::OnDataAvailable(nsIRequest* aRequest,
                                nsIInputStream* aInputStream, uint64_t aOffset,
                                uint32_t aCount) {
  nsresult rv;
  uint32_t readCount;
  rv = aInputStream->ReadSegments(AssignResponseToBuffer, &mBodyBuffer, aCount,
                                  &readCount);
  NS_ENSURE_SUCCESS(rv, rv);
  return NS_OK;
}

NS_IMETHODIMP
ViaductRequest::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) {
  ClearTimers();
  auto defer = MakeScopeExit([&] {
    mChannel = nullptr;
    NotifyMonitor();
  });

  if (NS_FAILED(aStatusCode)) {
    nsCString errorName;
    GetErrorName(aStatusCode, errorName);
    nsPrintfCString msg("Request error: %s", errorName.get());
    mResponse.set_exception_message(msg.BeginReading());
  } else {
    nsresult rv;
    nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest, &rv);
    // Early return is OK because MakeScopeExit will call Notify()
    // and unblock the original calling thread.
    NS_ENSURE_SUCCESS(rv, rv);

    uint32_t httpStatus;
    rv = httpChannel->GetResponseStatus(&httpStatus);
    NS_ENSURE_SUCCESS(rv, rv);
    mResponse.set_status(httpStatus);

    nsCOMPtr<nsIURI> uri;
    httpChannel->GetURI(getter_AddRefs(uri));
    nsAutoCString uriStr;
    uri->GetSpec(uriStr);
    mResponse.set_url(uriStr.BeginReading());

    auto* headers = mResponse.mutable_headers();
    nsCOMPtr<nsIHttpHeaderVisitor> visitor = new HeaderVisitor(headers);
    rv = httpChannel->VisitResponseHeaders(visitor);
    NS_ENSURE_SUCCESS(rv, rv);

    mResponse.set_body(mBodyBuffer.BeginReading());
  }

  return NS_OK;
}

///////////////////////////////////////////////////////////////////////////////
// nsIChannelEventSink implementation

NS_IMETHODIMP
ViaductRequest::AsyncOnChannelRedirect(
    nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t flags,
    nsIAsyncVerifyRedirectCallback* callback) {
  mChannel = aNewChannel;
  callback->OnRedirectVerifyCallback(NS_OK);
  return NS_OK;
}

///////////////////////////////////////////////////////////////////////////////
// nsITimerCallback implementation

NS_IMETHODIMP
ViaductRequest::Notify(nsITimer* timer) {
  ClearTimers();
  // Cancelling the channel will trigger OnStopRequest.
  if (mChannel) {
    mChannel->Cancel(NS_ERROR_ABORT);
    mChannel = nullptr;
  }
  return NS_OK;
}

///////////////////////////////////////////////////////////////////////////////
// nsINamed implementation

NS_IMETHODIMP
ViaductRequest::GetName(nsACString& aName) {
  aName.AssignLiteral("ViaductRequest");
  return NS_OK;
}

};  // namespace mozilla
