/* 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 "ConnectionAllowlists.h"

#include <functional>
#include <utility>

#include "mozilla/Logging.h"
#include "mozilla/StaticPrefs_security.h"
#include "mozilla/dom/ConnectionAllowlistViolationReportBody.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/ReportingUtils.h"
#include "mozilla/ipc/PBackgroundSharedTypes.h"
#include "mozilla/net/SFV.h"
#include "mozilla/net/URLPatternGlue.h"
#include "nsGlobalWindowInner.h"
#include "nsIGlobalObject.h"
#include "nsILoadInfo.h"
#include "nsNetUtil.h"
#include "nsScriptSecurityManager.h"
#include "nsString.h"

using namespace mozilla;

static LazyLogModule sConnectionAllowlistsLog("ConnectionAllowlists");
#define LOG(fmt, ...) \
  MOZ_LOG_FMT(sConnectionAllowlistsLog, LogLevel::Debug, fmt, ##__VA_ARGS__)

namespace mozilla::dom {

void ConnectionAllowlists::Allowlist::AppendPattern(
    const nsACString& aSerializedPattern) {
  UrlPatternGlue pattern = nullptr;
  UrlPatternOptions options{};
  if (!urlpattern_parse_pattern_from_string(&aSerializedPattern, nullptr,
                                            options, &pattern)) {
    LOG("Failed to parse URLPattern: {}", aSerializedPattern);
    return;
  }

  mPatterns.AppendElement(UrlPattern(std::move(pattern)));
  mSerializedPatterns.AppendElement(aSerializedPattern);
}

// https://wicg.github.io/connection-allowlists/#abstract-opdef-parse-a-connection-allowlist-header
/* static */
Maybe<ConnectionAllowlists::Allowlist>
ConnectionAllowlists::ParseConnectionAllowlistHeader(const nsACString& aHeader,
                                                     Disposition aDisposition) {
  if (aHeader.IsEmpty()) {
    return Nothing();
  }

  // 1. If list's size is 0, return null.
  auto list = net::SFV::ParseList(aHeader);
  if (!list.IsValid() || list.Length() == 0) {
    LOG("Failed to parse header as a structured field list.");
    return Nothing();
  }

  // 2. If list[0] is not an inner list, return null.
  auto innerList = list.GetInnerListAt(0);
  if (!innerList.IsValid()) {
    LOG("First list member is not an inner list.");
    return Nothing();
  }

  // 3. Let allowlist be a Connection Allowlist whose disposition is
  // disposition.
  ConnectionAllowlists::Allowlist allowlist;
  allowlist.mDisposition = aDisposition;

  // 4. For each item in list[0]:
  size_t length = innerList.Length();
  for (size_t i = 0; i < length; i++) {
    auto item = innerList.GetItemAt(i);
    if (!item.IsValid()) {
      continue;
    }

    // 4.1. Let serialized pattern be null.
    Maybe<nsAutoCString> serializedPattern;

    nsAutoCString token;
    nsAutoCString string;
    if (NS_SUCCEEDED(item.GetValue<net::SFV::Token>(token))) {
      // 4.2. If item is the token `response-origin`:
      // 4.2.1. Set serialized pattern to the ASCII serialization of
      // response-url's origin.
      //
      // Unlike the spec we remember the token instead of resolving it here,
      // see the comment on mMatchesResponseOrigin.
      if (token.EqualsLiteral("response-origin")) {
        allowlist.mMatchesResponseOrigin = true;
      }
    } else if (NS_SUCCEEDED(item.GetValue<net::SFV::SFVString>(string))) {
      // 4.3. If item is a string, set serialized pattern to item.
      serializedPattern.emplace(string);
    }

    // 4.4. If serialized pattern is null, continue.
    if (serializedPattern.isNothing()) {
      continue;
    }

    // 4.5. Let URL pattern be the result of executing build a URL pattern from
    // an HTTP structured field value given serialized pattern with null as the
    // base URL. If this step throws an error, continue.
    //
    // 4.6. Append URL pattern to allowlist's allowlist.
    allowlist.AppendPattern(*serializedPattern);
  }

  // 5. For each key → value in list[0]'s parameters:
  nsAutoCString paramValue;

  // 5.1. If key is `report-to` and value is a token, set allowlist's reporting
  // endpoint to value.
  if (NS_SUCCEEDED(
          innerList.GetParam<net::SFV::Token>("report-to"_ns, paramValue))) {
    allowlist.mReportingEndpoint = paramValue;
  }

  // 5.2. If key is `redirects` and value is a token: If value is `block`, set
  // redirects to block. Else, set redirects to allow.
  if (NS_SUCCEEDED(
          innerList.GetParam<net::SFV::Token>("redirects"_ns, paramValue))) {
    allowlist.mRedirects = paramValue.EqualsLiteral("block")
                               ? ConnectionAllowlists::Redirects::Block
                               : ConnectionAllowlists::Redirects::Allow;
  }

  // 5.3. If key is `webrtc` and value is a token: If value is `block`, set
  // webrtc to block. Else, set webrtc to allow.
  if (NS_SUCCEEDED(
          innerList.GetParam<net::SFV::Token>("webrtc"_ns, paramValue))) {
    allowlist.mWebRTC = paramValue.EqualsLiteral("block")
                            ? ConnectionAllowlists::WebRTC::Block
                            : ConnectionAllowlists::WebRTC::Allow;
  }

  // 6. Return allowlist.
  return Some(std::move(allowlist));
}

/* static */
nsresult ConnectionAllowlists::ParseHeaders(const nsACString& aHeader,
                                            const nsACString& aReportOnlyHeader,
                                            ConnectionAllowlists** aResult) {
  *aResult = nullptr;

  if (!StaticPrefs::security_connection_allowlists_enabled()) {
    return NS_OK;
  }

  Maybe<Allowlist> enforcement =
      ParseConnectionAllowlistHeader(aHeader, Disposition::Enforce);
  Maybe<Allowlist> reportOnly =
      ParseConnectionAllowlistHeader(aReportOnlyHeader, Disposition::Report);

  if (enforcement.isNothing() && reportOnly.isNothing()) {
    return NS_OK;
  }

  RefPtr<ConnectionAllowlists> allowlists = new ConnectionAllowlists();
  allowlists->mEnforcement = std::move(enforcement);
  allowlists->mReportOnly = std::move(reportOnly);
  allowlists.forget(aResult);
  return NS_OK;
}

void ConnectionAllowlists::SetResponseURI(nsIURI* aURI) { mResponseURI = aURI; }

bool ConnectionAllowlists::ShouldLoad(nsIURI* aURI,
                                      nsILoadInfo* aLoadInfo) const {
  // TODO: We probably need to exempt some content like in SubjectToCSP.
  // TODO(Bug 2072261): WebRTC.
  // TODO: Requests vs URL.
  return !ShouldBlockURL(aURI, aLoadInfo);
}

// https://wicg.github.io/connection-allowlists/#abstract-opdef-match-a-url-to-a-connection-allowlist
bool ConnectionAllowlists::MatchURL(nsIURI* aURI,
                                    const Allowlist& aAllowlist) const {
  // 1. If url is local, return success.
  // TODO: helper? add chrome:?
  if (aURI->SchemeIs("about") || aURI->SchemeIs("data") ||
      aURI->SchemeIs("blob")) {
    return true;
  }

  // If list included the `response-origin` token, then allow all same-origin
  // URLs.
  if (aAllowlist.mMatchesResponseOrigin && mResponseURI) {
    if (nsScriptSecurityManager::SecurityCompareURIs(mResponseURI, aURI)) {
      return true;
    }
  }

  // XXX This seems like something we should have abstracted.
  nsAutoCString spec;
  if (NS_WARN_IF(NS_FAILED(aURI->GetSpec(spec)))) {
    return false;
  }
  UrlPatternInput input = net::CreateUrlPatternInput(spec);

  // 2. For each pattern in connection allowlist’s allowlist:
  for (const auto& pattern : aAllowlist.mPatterns) {
    // 2.1. If URL pattern matching given pattern and url does not return null,
    // return success.
    if (net::UrlPatternTest(pattern.get(), input, Nothing())) {
      return true;
    }
  }

  // 3. Return failure.
  return false;
}

// https://wicg.github.io/connection-allowlists/#abstract-opdef-should-url-be-blocked-by-connection-allowlists
bool ConnectionAllowlists::ShouldBlockURL(nsIURI* aURI,
                                          nsILoadInfo* aLoadInfo) const {
  // 1. For each connection allowlist in connection allowlists:
  for (const Maybe<Allowlist>& allowlist :
       {std::cref(mEnforcement), std::cref(mReportOnly)}) {
    if (allowlist.isNothing()) {
      continue;
    }

    // 1.1. If url matches connection allowlist, continue.
    if (MatchURL(aURI, *allowlist)) {
      continue;
    }

    // 1.2. Report a violation given url, environment, and connection
    // allowlist.
    ReportViolation(AsVariant(aURI), aLoadInfo, *allowlist);

    // 1.3. If connection allowlist's disposition is enforce, return blocked.
    if (allowlist->mDisposition == Disposition::Enforce) {
      LOG("Blocking URL: {}", aURI->GetSpecOrDefault());
      return true;
    }
  }

  // 2. Return allowed.
  return false;
}

// https://wicg.github.io/connection-allowlists/#abstract-opdef-report-a-violation
/* static */
void ConnectionAllowlists::ReportViolation(
    const Variant<nsIURI*, nsCString>& aResource, nsILoadInfo* aLoadInfo,
    const Allowlist& aAllowlist) {
  // 1. If allowlist’s reporting endpoint is null, return.
  if (aAllowlist.mReportingEndpoint.IsEmpty()) {
    return;
  }

  // The report is queued on the environment that initiated the load.
  // TODO: This will fail when blocking in the parent process for e.g.
  // navigation.
  RefPtr<nsGlobalWindowInner> window =
      nsGlobalWindowInner::GetInnerWindowWithId(aLoadInfo->GetInnerWindowID());
  if (!window) {
    LOG("Not reporting a violation, no global for the load.");
    return;
  }

  Document* doc = window->GetExtantDoc();
  if (NS_WARN_IF(!doc) || NS_WARN_IF(!doc->GetDocumentURI())) {
    return;
  }

  // 2. Let violation be a new ConnectionAllowlistViolationReport, initialized
  // as follows:
  //
  // url
  //   environment’s creation URL, stripped for use in reports.
  nsAutoCString url;
  ReportingUtils::StripURL(doc->GetDocumentURI(), url);

  // connection
  //   If resource URL is a URL, then resource URL, stripped for use in reports.
  //   Otherwise, resource URL.
  nsAutoCString connection;
  if (aResource.is<nsIURI*>()) {
    nsCOMPtr<nsIURI> uri = aResource.as<nsIURI*>();
    ReportingUtils::StripURL(uri, connection);
  } else {
    connection = aResource.as<nsCString>();
  }

  // allowlist
  //   A new list containing the result of serializing each pattern in
  //   allowlist’s allowlist
  //
  // disposition
  //   allowlist’s disposition.
  RefPtr<ConnectionAllowlistViolationReportBody> violation =
      new ConnectionAllowlistViolationReportBody(
          window, url, connection, aAllowlist.mSerializedPatterns.Clone(),
          aAllowlist.mDisposition == Disposition::Enforce
              ? ConnectionAllowlistDisposition::Enforce
              : ConnectionAllowlistDisposition::Report);

  // 3. Generate and queue a report given environment as the context,
  // "connection-allowlist" as the type, allowlist’s reporting endpoint as the
  // destination, and violation as the data.
  ReportingUtils::Report(window, nsGkAtoms::connection_allowlist,
                         aAllowlist.mReportingEndpoint, url, violation);
}

void ConnectionAllowlists::Allowlist::ToEntryArgs(
    mozilla::ipc::ConnectionAllowlistEntry& aEntry) const {
  aEntry.patterns() = mSerializedPatterns.Clone();
  aEntry.matchesResponseOrigin() = mMatchesResponseOrigin;
  aEntry.reportingEndpoint() = mReportingEndpoint;
  aEntry.allowRedirects() = mRedirects == Redirects::Allow;
  aEntry.allowWebRTC() = mWebRTC == WebRTC::Allow;
}

/* static */
ConnectionAllowlists::Allowlist ConnectionAllowlists::Allowlist::FromEntryArgs(
    const mozilla::ipc::ConnectionAllowlistEntry& aEntry,
    Disposition aDisposition) {
  Allowlist allowlist;
  allowlist.mDisposition = aDisposition;

  for (const nsCString& serializedPattern : aEntry.patterns()) {
    allowlist.AppendPattern(serializedPattern);
  }
  allowlist.mMatchesResponseOrigin = aEntry.matchesResponseOrigin();
  allowlist.mReportingEndpoint = aEntry.reportingEndpoint();
  allowlist.mRedirects =
      aEntry.allowRedirects() ? Redirects::Allow : Redirects::Block;
  allowlist.mWebRTC = aEntry.allowWebRTC() ? WebRTC::Allow : WebRTC::Block;

  return allowlist;
}

void ConnectionAllowlists::ToArgs(
    mozilla::ipc::ConnectionAllowlistsArgs& aArgs) const {
  aArgs.enforcement() = Nothing();
  aArgs.reportOnly() = Nothing();
  aArgs.responseURISpec().Truncate();

  if (mEnforcement) {
    mozilla::ipc::ConnectionAllowlistEntry entry;
    mEnforcement->ToEntryArgs(entry);
    aArgs.enforcement() = Some(std::move(entry));
  }

  if (mReportOnly) {
    mozilla::ipc::ConnectionAllowlistEntry entry;
    mReportOnly->ToEntryArgs(entry);
    aArgs.reportOnly() = Some(std::move(entry));
  }

  if (mResponseURI &&
      NS_WARN_IF(NS_FAILED(mResponseURI->GetSpec(aArgs.responseURISpec())))) {
    aArgs.responseURISpec().Truncate();
  }
}

/* static */
already_AddRefed<ConnectionAllowlists> ConnectionAllowlists::FromArgs(
    const mozilla::ipc::ConnectionAllowlistsArgs& aArgs) {
  if (aArgs.enforcement().isNothing() && aArgs.reportOnly().isNothing()) {
    return nullptr;
  }

  RefPtr<ConnectionAllowlists> allowlists = new ConnectionAllowlists();

  if (aArgs.enforcement().isSome()) {
    allowlists->mEnforcement.emplace(
        Allowlist::FromEntryArgs(*aArgs.enforcement(), Disposition::Enforce));
  }

  if (aArgs.reportOnly().isSome()) {
    allowlists->mReportOnly.emplace(
        Allowlist::FromEntryArgs(*aArgs.reportOnly(), Disposition::Report));
  }

  if (!aArgs.responseURISpec().IsEmpty()) {
    nsCOMPtr<nsIURI> responseURI;
    if (NS_SUCCEEDED(
            NS_NewURI(getter_AddRefs(responseURI), aArgs.responseURISpec()))) {
      allowlists->mResponseURI = std::move(responseURI);
    } else {
      LOG("Failed to parse responseURISpec: {}", aArgs.responseURISpec());
    }
  }

  return allowlists.forget();
}

}  // namespace mozilla::dom

#undef LOG
