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

#include "DNSUtils.h"
#include "ObliviousHttpChannel.h"
#include "mozilla/Base64.h"
#include "mozilla/StaticPrefs_network.h"
#include "nsIObserverService.h"
#include "nsIPrefService.h"
#include "nsNetUtil.h"
#include "nsPrintfCString.h"

namespace mozilla::net {

NS_IMPL_ISUPPORTS(ObliviousHttpService, nsIObliviousHttpService, nsIObserver,
                  nsIStreamLoaderObserver)

ObliviousHttpService::ObliviousHttpService()
    : mTRRConfig(ObliviousHttpConfig(), "ObliviousHttpService::mTRRConfig") {
  MOZ_ASSERT(NS_IsMainThread());

  nsCOMPtr<nsIPrefBranch> prefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID));
  if (prefBranch) {
    prefBranch->AddObserver("network.trr.ohttp", this, false);
  }

  if (nsCOMPtr<nsIObserverService> obs =
          mozilla::services::GetObserverService()) {
    obs->AddObserver(this, "xpcom-shutdown", false);
    obs->AddObserver(this, "network:captive-portal-connectivity-changed",
                     false);
    obs->AddObserver(this, "network:trr-confirmation", false);
  }

  ReadPrefs("*"_ns);
}

static constexpr nsLiteralCString kTRRohttpConfigURIPref =
    "network.trr.ohttp.config_uri"_ns;
static constexpr nsLiteralCString kTRRohttpRelayURIPref =
    "network.trr.ohttp.relay_uri"_ns;

void ObliviousHttpService::FetchConfig(bool aConfigURIChanged) {
  auto scopeExit = MakeScopeExit([&] {
    nsCOMPtr<nsIObserverService> obs(mozilla::services::GetObserverService());
    if (!obs) {
      return;
    }
    obs->NotifyObservers(nullptr, "ohttp-service-config-loaded", u"no-changes");
  });

  {
    auto trrConfig = mTRRConfig.Lock();
    if (aConfigURIChanged) {
      // If the config URI has changed, we need to clear the config.
      trrConfig->mEncodedConfig.Clear();
    } else if (trrConfig->mEncodedConfig.Length()) {
      // The URI hasn't changed and we already have a config. No need to reload
      return;
    }
  }

  nsAutoCString configURIString;
  nsresult rv =
      Preferences::GetCString(kTRRohttpConfigURIPref.get(), configURIString);
  if (NS_FAILED(rv)) {
    return;
  }
  nsCOMPtr<nsIURI> configURI;
  rv = NS_NewURI(getter_AddRefs(configURI), configURIString);
  if (NS_FAILED(rv)) {
    return;
  }

  nsCOMPtr<nsIChannel> channel;
  rv = DNSUtils::CreateChannelHelper(configURI, getter_AddRefs(channel));
  if (NS_FAILED(rv)) {
    return;
  }
  rv = channel->SetLoadFlags(
      nsIRequest::LOAD_ANONYMOUS | nsIRequest::INHIBIT_CACHING |
      nsIRequest::LOAD_BYPASS_CACHE | nsIChannel::LOAD_BYPASS_URL_CLASSIFIER);
  if (NS_FAILED(rv)) {
    return;
  }
  nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(channel));
  if (!httpChannel) {
    return;
  }
  // This connection should not use TRR
  rv = httpChannel->SetTRRMode(nsIRequest::TRR_DISABLED_MODE);
  if (NS_FAILED(rv)) {
    return;
  }
  nsCOMPtr<nsIStreamLoader> loader;
  rv = NS_NewStreamLoader(getter_AddRefs(loader), this);
  if (NS_FAILED(rv)) {
    return;
  }
  rv = httpChannel->AsyncOpen(loader);

  if (NS_SUCCEEDED(rv)) {
    scopeExit.release();
    return;
  }

  nsPrintfCString msg(
      "ObliviousHttpService::FetchConfig AsyncOpen failed rv=%X",
      static_cast<uint32_t>(rv));
  NS_WARNING(msg.get());
}

void ObliviousHttpService::ReadPrefs(const nsACString& whichPref) {
  if (whichPref.Equals(kTRRohttpRelayURIPref) || whichPref.EqualsLiteral("*")) {
    nsAutoCString relayURIString;
    nsresult rv =
        Preferences::GetCString(kTRRohttpRelayURIPref.get(), relayURIString);
    if (NS_FAILED(rv)) {
      return;
    }
    nsCOMPtr<nsIURI> relayURI;
    rv = NS_NewURI(getter_AddRefs(relayURI), relayURIString);
    if (NS_FAILED(rv)) {
      return;
    }
    auto trrConfig = mTRRConfig.Lock();
    trrConfig->mRelayURI = std::move(relayURI);
  }

  if (whichPref.Equals(kTRRohttpConfigURIPref) ||
      whichPref.EqualsLiteral("*")) {
    FetchConfig(true);
  }
}

// nsIObliviousHttpService

NS_IMETHODIMP
ObliviousHttpService::NewChannel(nsIURI* relayURI, nsIURI* targetURI,
                                 const nsTArray<uint8_t>& encodedConfig,
                                 nsIChannel** result) {
  nsCOMPtr<nsIChannel> innerChannel;
  nsresult rv =
      DNSUtils::CreateChannelHelper(relayURI, getter_AddRefs(innerChannel));
  if (NS_FAILED(rv)) {
    return rv;
  }
  nsCOMPtr<nsIHttpChannel> innerHttpChannel(do_QueryInterface(innerChannel));
  if (!innerHttpChannel) {
    return NS_ERROR_FAILURE;
  }
  nsCOMPtr<nsIChannel> obliviousHttpChannel(
      new ObliviousHttpChannel(targetURI, encodedConfig, innerHttpChannel));
  obliviousHttpChannel.forget(result);
  return NS_OK;
}

NS_IMETHODIMP
ObliviousHttpService::GetTRRSettings(nsIURI** relayURI,
                                     nsTArray<uint8_t>& encodedConfig) {
  auto trrConfig = mTRRConfig.Lock();
  *relayURI = do_AddRef(trrConfig->mRelayURI).take();
  encodedConfig.Assign(trrConfig->mEncodedConfig);
  return NS_OK;
}

NS_IMETHODIMP
ObliviousHttpService::ClearTRRConfig() {
  auto trrConfig = mTRRConfig.Lock();
  trrConfig->mEncodedConfig.Clear();
  return NS_OK;
}

// nsIObserver

NS_IMETHODIMP
ObliviousHttpService::Observe(nsISupports* subject, const char* topic,
                              const char16_t* data) {
  if (!strcmp(topic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
    ReadPrefs(NS_ConvertUTF16toUTF8(data));
  } else if (!strcmp(topic, "xpcom-shutdown")) {
    if (nsCOMPtr<nsIPrefBranch> prefBranch =
            do_GetService(NS_PREFSERVICE_CONTRACTID)) {
      prefBranch->RemoveObserver("network.trr.ohttp", this);
    }

    if (nsCOMPtr<nsIObserverService> obs =
            mozilla::services::GetObserverService()) {
      obs->RemoveObserver(this, "xpcom-shutdown");
      obs->RemoveObserver(this, "network:captive-portal-connectivity-changed");
      obs->RemoveObserver(this, "network:trr-confirmation");
    }
  } else if (!strcmp(topic, "network:captive-portal-connectivity-changed") ||
             (!strcmp(topic, "network:trr-confirmation") && data &&
              u"CONFIRM_FAILED"_ns.Equals(data))) {
    FetchConfig(false);
  }
  return NS_OK;
}

// nsIStreamLoaderObserver

NS_IMETHODIMP
ObliviousHttpService::OnStreamComplete(nsIStreamLoader* aLoader,
                                       nsISupports* aContext, nsresult aStatus,
                                       uint32_t aLength,
                                       const uint8_t* aContent) {
  if (NS_SUCCEEDED(aStatus)) {
    auto trrConfig = mTRRConfig.Lock();
    trrConfig->mEncodedConfig.Clear();
    trrConfig->mEncodedConfig.AppendElements(aContent, aLength);
  }

  nsCOMPtr<nsIObserverService> observerService(
      mozilla::services::GetObserverService());
  if (!observerService) {
    return NS_ERROR_FAILURE;
  }
  nsresult rv = observerService->NotifyObservers(
      nullptr, "ohttp-service-config-loaded",
      NS_SUCCEEDED(aStatus) ? u"success" : u"failed");
  if (NS_FAILED(rv)) {
    return rv;
  }

  return NS_OK;
}

}  // namespace mozilla::net
