/* 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/ChildDNSService.h"
#include "nsDNSPrefetch.h"
#include "nsIDNSListener.h"
#include "nsIOService.h"
#include "nsThreadUtils.h"
#include "nsIXPConnect.h"
#include "nsIProtocolProxyService.h"
#include "nsNetCID.h"
#include "nsQueryObject.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/StaticPrefs_network.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/SyncRunnable.h"
#include "mozilla/net/NeckoChild.h"
#include "mozilla/net/DNSListenerProxy.h"
#include "mozilla/net/TRRServiceParent.h"
#include "nsHostResolver.h"
#include "nsServiceManagerUtils.h"
#include "prsystem.h"
#include "DNSAdditionalInfo.h"
#include "TRRService.h"

namespace mozilla {
namespace net {

//-----------------------------------------------------------------------------
// ChildDNSService
//-----------------------------------------------------------------------------

static StaticRefPtr<ChildDNSService> gChildDNSService;

already_AddRefed<ChildDNSService> ChildDNSService::GetSingleton() {
  MOZ_ASSERT_IF(nsIOService::UseSocketProcess(),
                XRE_IsContentProcess() || XRE_IsParentProcess());
  MOZ_ASSERT_IF(!nsIOService::UseSocketProcess(),
                XRE_IsContentProcess() || XRE_IsSocketProcess());

  if (!gChildDNSService) {
    if (NS_WARN_IF(!NS_IsMainThread())) {
      return nullptr;
    }
    gChildDNSService = new ChildDNSService();
    gChildDNSService->Init();
    ClearOnShutdown(&gChildDNSService);
  }

  return do_AddRef(gChildDNSService);
}

NS_IMPL_ISUPPORTS_INHERITED(ChildDNSService, DNSServiceBase, nsIDNSService,
                            nsPIDNSService)

ChildDNSService::ChildDNSService() {
  MOZ_ASSERT_IF(nsIOService::UseSocketProcess(),
                XRE_IsContentProcess() || XRE_IsParentProcess());
  MOZ_ASSERT_IF(!nsIOService::UseSocketProcess(),
                XRE_IsContentProcess() || XRE_IsSocketProcess());
  if (XRE_IsParentProcess() && nsIOService::UseSocketProcess()) {
    nsDNSPrefetch::Initialize(this);
    mTRRServiceParent = new TRRServiceParent();
    mTRRServiceParent->Init();
  }
}

void ChildDNSService::GetDNSRecordHashKey(
    const nsACString& aHost, const nsACString& aTrrServer, int32_t aPort,
    uint16_t aType, const OriginAttributes& aOriginAttributes,
    nsIDNSService::DNSFlags aFlags, uintptr_t aListenerAddr,
    nsACString& aHashKey) {
  aHashKey.Assign(aHost);
  aHashKey.Assign(aTrrServer);
  aHashKey.AppendInt(aPort);
  aHashKey.AppendInt(aType);

  nsAutoCString originSuffix;
  aOriginAttributes.CreateSuffix(originSuffix);
  aHashKey.Append(originSuffix);

  aHashKey.AppendInt(aFlags);
  aHashKey.AppendPrintf("0x%" PRIxPTR, aListenerAddr);
}

nsresult ChildDNSService::AsyncResolveInternal(
    const nsACString& hostname, uint16_t type, nsIDNSService::DNSFlags flags,
    nsIDNSAdditionalInfo* aInfo, nsIDNSListener* listener,
    nsIEventTarget* target_, const OriginAttributes& aOriginAttributes,
    nsICancelable** result) {
  if (XRE_IsContentProcess()) {
    NS_ENSURE_TRUE(gNeckoChild != nullptr, NS_ERROR_FAILURE);
  }

  if (DNSForbiddenByActiveProxy(hostname, flags)) {
    // nsHostResolver returns NS_ERROR_UNKNOWN_HOST for lots of reasons.
    // We use a different error code to differentiate this failure and to make
    // it clear(er) where this error comes from.
    return NS_ERROR_UNKNOWN_PROXY_HOST;
  }

  bool resolveDNSInSocketProcess = false;
  if (XRE_IsParentProcess() && nsIOService::UseSocketProcess()) {
    resolveDNSInSocketProcess = true;
    if (type != nsIDNSService::RESOLVE_TYPE_DEFAULT &&
        (mTRRServiceParent->Mode() != nsIDNSService::MODE_TRRFIRST &&
         mTRRServiceParent->Mode() != nsIDNSService::MODE_TRRONLY)) {
      return NS_ERROR_UNKNOWN_HOST;
    }
  }

  if (mDisablePrefetch && (flags & RESOLVE_SPECULATE)) {
    return NS_ERROR_DNS_LOOKUP_QUEUE_FULL;
  }

  // We need original listener for the pending requests hash.
  uintptr_t originalListenerAddr = reinterpret_cast<uintptr_t>(listener);

  // make sure JS callers get notification on the main thread
  nsCOMPtr<nsIEventTarget> target = target_;
  nsCOMPtr<nsIXPConnectWrappedJS> wrappedListener = do_QueryInterface(listener);
  if (wrappedListener && !target) {
    target = GetMainThreadSerialEventTarget();
  }
  if (target) {
    // Guarantee listener freed on main thread.  Not sure we need this in child
    // (or in parent in nsDNSService.cpp) but doesn't hurt.
    listener = new DNSListenerProxy(listener, target);
  }

  RefPtr<DNSRequestSender> sender = new DNSRequestSender(
      hostname, DNSAdditionalInfo::URL(aInfo), DNSAdditionalInfo::Port(aInfo),
      type, aOriginAttributes, flags, listener, target);
  RefPtr<DNSRequestActor> dnsReq;
  if (resolveDNSInSocketProcess) {
    dnsReq = new DNSRequestParent(sender);
    if (!mTRRServiceParent->TRRConnectionInfoInited()) {
      mTRRServiceParent->InitTRRConnectionInfo();
    }
  } else {
    dnsReq = new DNSRequestChild(sender);
  }

  {
    MutexAutoLock lock(mPendingRequestsLock);
    nsCString key;
    GetDNSRecordHashKey(hostname, DNSAdditionalInfo::URL(aInfo),
                        DNSAdditionalInfo::Port(aInfo), type, aOriginAttributes,
                        flags, originalListenerAddr, key);
    mPendingRequests.GetOrInsertNew(key)->AppendElement(sender);
  }

  sender->StartRequest();

  sender.forget(result);
  return NS_OK;
}

nsresult ChildDNSService::CancelAsyncResolveInternal(
    const nsACString& aHostname, uint16_t aType, nsIDNSService::DNSFlags aFlags,
    nsIDNSAdditionalInfo* aInfo, nsIDNSListener* aListener, nsresult aReason,
    const OriginAttributes& aOriginAttributes) {
  if (mDisablePrefetch && (aFlags & RESOLVE_SPECULATE)) {
    return NS_ERROR_DNS_LOOKUP_QUEUE_FULL;
  }

  MutexAutoLock lock(mPendingRequestsLock);
  nsTArray<RefPtr<DNSRequestSender>>* hashEntry;
  nsCString key;
  uintptr_t listenerAddr = reinterpret_cast<uintptr_t>(aListener);
  GetDNSRecordHashKey(aHostname, DNSAdditionalInfo::URL(aInfo),
                      DNSAdditionalInfo::Port(aInfo), aType, aOriginAttributes,
                      aFlags, listenerAddr, key);
  if (mPendingRequests.Get(key, &hashEntry)) {
    // We cancel just one.
    hashEntry->ElementAt(0)->Cancel(aReason);
  }

  return NS_OK;
}

//-----------------------------------------------------------------------------
// ChildDNSService::nsIDNSService
//-----------------------------------------------------------------------------

NS_IMETHODIMP
ChildDNSService::AsyncResolve(const nsACString& hostname,
                              nsIDNSService::ResolveType aType,
                              nsIDNSService::DNSFlags flags,
                              nsIDNSAdditionalInfo* aInfo,
                              nsIDNSListener* listener, nsIEventTarget* target_,
                              JS::Handle<JS::Value> aOriginAttributes,
                              JSContext* aCx, uint8_t aArgc,
                              nsICancelable** result) {
  OriginAttributes attrs;

  if (aArgc == 1) {
    if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) {
      return NS_ERROR_INVALID_ARG;
    }
  }

  return AsyncResolveInternal(hostname, aType, flags, aInfo, listener, target_,
                              attrs, result);
}

NS_IMETHODIMP
ChildDNSService::AsyncResolveNative(
    const nsACString& hostname, nsIDNSService::ResolveType aType,
    nsIDNSService::DNSFlags flags, nsIDNSAdditionalInfo* aInfo,
    nsIDNSListener* listener, nsIEventTarget* target_,
    const OriginAttributes& aOriginAttributes, nsICancelable** result) {
  return AsyncResolveInternal(hostname, aType, flags, aInfo, listener, target_,
                              aOriginAttributes, result);
}

NS_IMETHODIMP
ChildDNSService::NewAdditionalInfo(const nsACString& aTrrURL, int32_t aPort,
                                   nsIDNSAdditionalInfo** aInfo) {
  RefPtr<DNSAdditionalInfo> res = new DNSAdditionalInfo(aTrrURL, aPort);
  res.forget(aInfo);
  return NS_OK;
}

NS_IMETHODIMP
ChildDNSService::CancelAsyncResolve(const nsACString& aHostname,
                                    nsIDNSService::ResolveType aType,
                                    nsIDNSService::DNSFlags aFlags,
                                    nsIDNSAdditionalInfo* aInfo,
                                    nsIDNSListener* aListener, nsresult aReason,
                                    JS::Handle<JS::Value> aOriginAttributes,
                                    JSContext* aCx, uint8_t aArgc) {
  OriginAttributes attrs;

  if (aArgc == 1) {
    if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) {
      return NS_ERROR_INVALID_ARG;
    }
  }

  return CancelAsyncResolveInternal(aHostname, aType, aFlags, aInfo, aListener,
                                    aReason, attrs);
}

NS_IMETHODIMP
ChildDNSService::CancelAsyncResolveNative(
    const nsACString& aHostname, nsIDNSService::ResolveType aType,
    nsIDNSService::DNSFlags aFlags, nsIDNSAdditionalInfo* aInfo,
    nsIDNSListener* aListener, nsresult aReason,
    const OriginAttributes& aOriginAttributes) {
  return CancelAsyncResolveInternal(aHostname, aType, aFlags, aInfo, aListener,
                                    aReason, aOriginAttributes);
}

NS_IMETHODIMP
ChildDNSService::Resolve(const nsACString& hostname,
                         nsIDNSService::DNSFlags flags,
                         JS::Handle<JS::Value> aOriginAttributes,
                         JSContext* aCx, uint8_t aArgc, nsIDNSRecord** result) {
  // not planning to ever support this, since sync IPDL is evil.
  return NS_ERROR_NOT_AVAILABLE;
}

NS_IMETHODIMP
ChildDNSService::ResolveNative(const nsACString& hostname,
                               nsIDNSService::DNSFlags flags,
                               const OriginAttributes& aOriginAttributes,
                               nsIDNSRecord** result) {
  // not planning to ever support this, since sync IPDL is evil.
  return NS_ERROR_NOT_AVAILABLE;
}

NS_IMETHODIMP
ChildDNSService::GetDNSCacheEntries(
    nsTArray<mozilla::net::DNSCacheEntries>* args) {
  // Only used by networking dashboard, so may not ever need this in child.
  // (and would provide a way to spy on what hosts other apps are connecting to,
  // unless we start keeping per-app DNS caches).
  return NS_ERROR_NOT_AVAILABLE;
}

NS_IMETHODIMP
ChildDNSService::ClearCache(bool aTrrToo) {
  if (!mTRRServiceParent || !mTRRServiceParent->CanSend()) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  Unused << mTRRServiceParent->SendClearDNSCache(aTrrToo);
  return NS_OK;
}

NS_IMETHODIMP
ChildDNSService::ReloadParentalControlEnabled() {
  if (!mTRRServiceParent) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  mTRRServiceParent->UpdateParentalControlEnabled();
  return NS_OK;
}

NS_IMETHODIMP
ChildDNSService::SetDetectedTrrURI(const nsACString& aURI) {
  if (!mTRRServiceParent) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  mTRRServiceParent->SetDetectedTrrURI(aURI);
  return NS_OK;
}

NS_IMETHODIMP
ChildDNSService::GetTRRSkipReasonName(nsITRRSkipReason::value aValue,
                                      nsACString& aName) {
  return mozilla::net::GetTRRSkipReasonName(aValue, aName);
}

NS_IMETHODIMP
ChildDNSService::GetCurrentTrrURI(nsACString& aURI) {
  if (!mTRRServiceParent) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  mTRRServiceParent->GetURI(aURI);
  return NS_OK;
}

NS_IMETHODIMP
ChildDNSService::GetCurrentTrrMode(nsIDNSService::ResolverMode* aMode) {
  if (!mTRRServiceParent) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  *aMode = mTRRServiceParent->Mode();
  return NS_OK;
}

NS_IMETHODIMP
ChildDNSService::GetCurrentTrrConfirmationState(uint32_t* aConfirmationState) {
  if (!mTRRServiceParent) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  *aConfirmationState = mTRRServiceParent->GetConfirmationState();
  return NS_OK;
}

NS_IMETHODIMP
ChildDNSService::GetMyHostName(nsACString& result) {
  if (XRE_IsParentProcess()) {
    char name[100];
    if (PR_GetSystemInfo(PR_SI_HOSTNAME, name, sizeof(name)) == PR_SUCCESS) {
      result = name;
      return NS_OK;
    }

    return NS_ERROR_FAILURE;
  }
  // TODO: get value from parent during PNecko construction?
  return NS_ERROR_NOT_AVAILABLE;
}

NS_IMETHODIMP
ChildDNSService::GetODoHActivated(bool* aResult) {
  NS_ENSURE_ARG(aResult);

  *aResult = mODoHActivated;
  return NS_OK;
}

void ChildDNSService::NotifyRequestDone(DNSRequestSender* aDnsRequest) {
  // We need the original flags and listener for the pending requests hash.
  nsIDNSService::DNSFlags originalFlags =
      aDnsRequest->mFlags & ~RESOLVE_OFFLINE;
  uintptr_t originalListenerAddr =
      reinterpret_cast<uintptr_t>(aDnsRequest->mListener.get());
  RefPtr<DNSListenerProxy> wrapper = do_QueryObject(aDnsRequest->mListener);
  if (wrapper) {
    originalListenerAddr = wrapper->GetOriginalListenerAddress();
  }

  MutexAutoLock lock(mPendingRequestsLock);

  nsCString key;
  GetDNSRecordHashKey(aDnsRequest->mHost, aDnsRequest->mTrrServer,
                      aDnsRequest->mPort, aDnsRequest->mType,
                      aDnsRequest->mOriginAttributes, originalFlags,
                      originalListenerAddr, key);

  nsTArray<RefPtr<DNSRequestSender>>* hashEntry;

  if (mPendingRequests.Get(key, &hashEntry)) {
    auto idx = hashEntry->IndexOf(aDnsRequest);
    if (idx != nsTArray<RefPtr<DNSRequestSender>>::NoIndex) {
      hashEntry->RemoveElementAt(idx);
      if (hashEntry->IsEmpty()) {
        mPendingRequests.Remove(key);
      }
    }
  }
}

//-----------------------------------------------------------------------------
// ChildDNSService::nsPIDNSService
//-----------------------------------------------------------------------------

nsresult ChildDNSService::Init() {
  ReadPrefs(nullptr);

  nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
  if (prefs) {
    AddPrefObserver(prefs);
  }

  nsCOMPtr<nsIObserverService> observerService =
      mozilla::services::GetObserverService();
  if (observerService) {
    observerService->AddObserver(this, "odoh-service-activated", false);
  }

  return NS_OK;
}

nsresult ChildDNSService::Shutdown() { return NS_OK; }

NS_IMETHODIMP
ChildDNSService::GetPrefetchEnabled(bool* outVal) {
  *outVal = !mDisablePrefetch;
  return NS_OK;
}

NS_IMETHODIMP
ChildDNSService::SetPrefetchEnabled(bool inVal) {
  mDisablePrefetch = !inVal;
  return NS_OK;
}

NS_IMETHODIMP
ChildDNSService::ReportFailedSVCDomainName(const nsACString& aOwnerName,
                                           const nsACString& aSVCDomainName) {
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP
ChildDNSService::IsSVCDomainNameFailed(const nsACString& aOwnerName,
                                       const nsACString& aSVCDomainName,
                                       bool* aResult) {
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP
ChildDNSService::ResetExcludedSVCDomainName(const nsACString& aOwnerName) {
  return NS_ERROR_NOT_IMPLEMENTED;
}

//-----------------------------------------------------------------------------
// ChildDNSService::nsIObserver
//-----------------------------------------------------------------------------

NS_IMETHODIMP
ChildDNSService::Observe(nsISupports* subject, const char* topic,
                         const char16_t* data) {
  if (!strcmp(topic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
    // Reread prefs
    ReadPrefs(NS_ConvertUTF16toUTF8(data).get());
  } else if (!strcmp(topic, "odoh-service-activated")) {
    mODoHActivated = u"true"_ns.Equals(data);
  }

  return NS_OK;
}

void ChildDNSService::SetTRRDomain(const nsACString& aTRRDomain) {
  mTRRDomain = aTRRDomain;
  TRRService::SetProviderDomain(aTRRDomain);
}

void ChildDNSService::GetTRRDomainKey(nsACString& aTRRDomain) {
  aTRRDomain = TRRService::ProviderKey();
}

NS_IMETHODIMP
ChildDNSService::GetTrrDomain(nsACString& aTRRDomain) {
  aTRRDomain = mTRRDomain;
  return NS_OK;
}

}  // namespace net
}  // namespace mozilla