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

#include "DNSServiceBase.h"
#include "mozilla/ipc/FileDescriptor.h"
#include "mozilla/net/SocketProcessParent.h"
#include "mozilla/psm/PSMIPCTypes.h"
#include "mozilla/Preferences.h"
#include "nsHttpConnectionInfo.h"
#include "nsICaptivePortalService.h"
#include "nsIParentalControlsService.h"
#include "nsINetworkLinkService.h"
#include "nsIObserverService.h"
#include "nsIOService.h"
#include "nsNetCID.h"
#include "TRRService.h"

#include "DNSLogging.h"

namespace mozilla {
namespace net {

static Atomic<TRRServiceParent*> sTRRServiceParentPtr;

static const char* gTRRUriCallbackPrefs[] = {
    "network.trr.uri",  "network.trr.default_provider_uri",
    "network.trr.mode", kRolloutURIPref,
    kRolloutModePref,   nullptr,
};

NS_IMPL_ISUPPORTS_INHERITED(TRRServiceParent, TRRServiceBase, nsIObserver,
                            nsISupportsWeakReference)

TRRServiceParent::~TRRServiceParent() = default;

void TRRServiceParent::Init() {
  MOZ_ASSERT(gIOService);

  if (!gIOService->SocketProcessReady()) {
    RefPtr<TRRServiceParent> self = this;
    gIOService->CallOrWaitForSocketProcess([self]() { self->Init(); });
    return;
  }

  RefPtr<SocketProcessParent> socketParent =
      SocketProcessParent::GetSingleton();
  if (!socketParent) {
    return;
  }

  nsCOMPtr<nsIObserverService> obs =
      static_cast<nsIObserverService*>(gIOService);
  TRRService::AddObserver(this, obs);

  bool captiveIsPassed = TRRService::CheckCaptivePortalIsPassed();
  bool parentalControlEnabled =
      TRRService::GetParentalControlsEnabledInternal();

  nsCOMPtr<nsINetworkLinkService> nls =
      do_GetService(NS_NETWORK_LINK_SERVICE_CONTRACTID);
  nsTArray<nsCString> suffixList;
  if (nls) {
    nls->GetDnsSuffixList(suffixList);
  }

  Preferences::RegisterPrefixCallbacks(TRRServiceParent::PrefsChanged,
                                       gTRRUriCallbackPrefs, this);
  prefsChanged(nullptr);

  if (socketParent->SendPTRRServiceConstructor(
          this, captiveIsPassed, parentalControlEnabled, suffixList)) {
    sTRRServiceParentPtr = this;
  }
}

NS_IMETHODIMP
TRRServiceParent::Observe(nsISupports* aSubject, const char* aTopic,
                          const char16_t* aData) {
  if (!strcmp(aTopic, NS_DNS_SUFFIX_LIST_UPDATED_TOPIC) ||
      !strcmp(aTopic, NS_NETWORK_LINK_TOPIC)) {
    nsCOMPtr<nsINetworkLinkService> link = do_QueryInterface(aSubject);
    // The network link service notification normally passes itself as the
    // subject, but some unit tests will sometimes pass a null subject.
    if (link) {
      nsTArray<nsCString> suffixList;
      link->GetDnsSuffixList(suffixList);
      (void)SendUpdatePlatformDNSInformation(suffixList);
    }

    if (!strcmp(aTopic, NS_NETWORK_LINK_TOPIC) && mURISetByDetection) {
      CheckURIPrefs();
    }
  }

  return NS_OK;
}

mozilla::ipc::IPCResult
TRRServiceParent::RecvNotifyNetworkConnectivityServiceObservers(
    const nsCString& aTopic) {
  // Must match TRRServiceChild::Observe()
  const char* topic = nullptr;
  if (!strcmp(aTopic.get(),
              "network:connectivity-service:ip-checks-complete-from-socket-"
              "process")) {
    topic = "network:connectivity-service:ip-checks-complete";
  } else if (!strcmp(aTopic.get(),
                     "network:connectivity-service:dns-checks-complete-from-"
                     "socket-process")) {
    topic = "network:connectivity-service:dns-checks-complete";
  } else {
    return IPC_FAIL(this, "Unexpected notification");
  }
  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
  if (obs) {
    obs->NotifyObservers(nullptr, topic, nullptr);
  }
  return IPC_OK();
}

bool TRRServiceParent::MaybeSetPrivateURI(const nsACString& aURI) {
  nsAutoCString newURI(aURI);
  ProcessURITemplate(newURI);

  if (mPrivateURI.Equals(newURI)) {
    return false;
  }

  mPrivateURI = newURI;
  AsyncCreateTRRConnectionInfo(mPrivateURI);

  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
  if (obs) {
    obs->NotifyObservers(nullptr, NS_NETWORK_TRR_URI_CHANGED_TOPIC, nullptr);
  }
  return true;
}

void TRRServiceParent::SetDetectedTrrURI(const nsACString& aURI) {
  if (!mURIPref.IsEmpty()) {
    return;
  }

  mURISetByDetection = MaybeSetPrivateURI(aURI);
  gIOService->CallOrWaitForSocketProcess(
      [self = RefPtr{this}, uri = nsAutoCString(aURI)]() {
        (void)self->SendSetDetectedTrrURI(uri);
      });
}

void TRRServiceParent::GetURI(nsACString& aURI) {
  // We don't need a lock here, since mPrivateURI is only touched on main
  // thread.
  MOZ_ASSERT(NS_IsMainThread());
  aURI = mPrivateURI;
}

void TRRServiceParent::ReloadParentalControlsEnabled() {
  bool enabled = TRRService::ReloadParentalControlsEnabled();
  RefPtr<TRRServiceParent> self = this;
  gIOService->CallOrWaitForSocketProcess([self, enabled]() {
    (void)self->SendUpdateParentalControlEnabled(enabled);
  });
}

// static
void TRRServiceParent::PrefsChanged(const char* aName, void* aSelf) {
  static_cast<TRRServiceParent*>(aSelf)->prefsChanged(aName);
}

void TRRServiceParent::prefsChanged(const char* aName) {
  if (!aName || !strcmp(aName, "network.trr.uri") ||
      !strcmp(aName, "network.trr.default_provider_uri") ||
      !strcmp(aName, kRolloutURIPref) ||
      !strcmp(aName, "network.trr.ohttp.uri")) {
    OnTRRURIChange();
  }

  if (!aName || !strcmp(aName, "network.trr.mode") ||
      !strcmp(aName, kRolloutModePref)) {
    OnTRRModeChange();
  }
}

void TRRServiceParent::ActorDestroy(ActorDestroyReason why) {
  sTRRServiceParentPtr = nullptr;
  Preferences::UnregisterPrefixCallbacks(TRRServiceParent::PrefsChanged,
                                         gTRRUriCallbackPrefs, this);
}

NS_IMETHODIMP TRRServiceParent::OnProxyConfigChanged() {
  LOG(("TRRServiceParent::OnProxyConfigChanged"));

  AsyncCreateTRRConnectionInfo(mPrivateURI);
  return NS_OK;
}

void TRRServiceParent::SetDefaultTRRConnectionInfo(
    nsHttpConnectionInfo* aConnInfo) {
  TRRServiceBase::SetDefaultTRRConnectionInfo(aConnInfo);

  if (!CanSend()) {
    return;
  }

  if (!aConnInfo) {
    (void)SendSetDefaultTRRConnectionInfo(Nothing());
    return;
  }

  HttpConnectionInfoCloneArgs infoArgs;
  nsHttpConnectionInfo::SerializeHttpConnectionInfo(aConnInfo, infoArgs);
  (void)SendSetDefaultTRRConnectionInfo(Some(infoArgs));
}

mozilla::ipc::IPCResult TRRServiceParent::RecvInitTRRConnectionInfo(
    bool aForceReinit) {
  InitTRRConnectionInfo(aForceReinit);
  return IPC_OK();
}

mozilla::ipc::IPCResult TRRServiceParent::RecvSetConfirmationState(
    uint32_t aNewState) {
  mConfirmationState = aNewState;
  return IPC_OK();
}

void TRRServiceParent::ReadEtcHostsFile() {
  if (!sTRRServiceParentPtr) {
    return;
  }

  DNSServiceBase::DoReadEtcHostsFile(
      [](const nsTArray<nsCString>* aArray) -> bool {
        RefPtr<TRRServiceParent> service(sTRRServiceParentPtr);
        if (service && aArray) {
          nsTArray<nsCString> hosts(aArray->Clone());
          NS_DispatchToMainThread(NS_NewRunnableFunction(
              "TRRServiceParent::ReadEtcHostsFile",
              [service, hosts = std::move(hosts)]() mutable {
                if (service->CanSend()) {
                  (void)service->SendUpdateEtcHosts(hosts);
                }
              }));
        }
        return !!service;
      });
}

}  // namespace net
}  // namespace mozilla
