/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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/dom/FlyWebService.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/FlyWebPublishedServerIPC.h"
#include "mozilla/AddonPathService.h"
#include "nsISocketTransportService.h"
#include "mdns/libmdns/nsDNSServiceInfo.h"
#include "nsIUUIDGenerator.h"
#include "nsStandardURL.h"
#include "mozilla/Services.h"
#include "nsISupportsPrimitives.h"
#include "mozilla/dom/FlyWebDiscoveryManagerBinding.h"
#include "prnetdb.h"
#include "DNS.h"
#include "nsContentPermissionHelper.h"
#include "nsSocketTransportService2.h"
#include "nsSocketTransport2.h"
#include "nsHashPropertyBag.h"
#include "nsNetUtil.h"
#include "nsISimpleEnumerator.h"
#include "nsIProperty.h"
#include "nsICertOverrideService.h"

namespace mozilla {
namespace dom {

struct FlyWebPublishOptions;

static LazyLogModule gFlyWebServiceLog("FlyWebService");
#undef LOG_I
#define LOG_I(...) MOZ_LOG(mozilla::dom::gFlyWebServiceLog, mozilla::LogLevel::Debug, (__VA_ARGS__))

#undef LOG_E
#define LOG_E(...) MOZ_LOG(mozilla::dom::gFlyWebServiceLog, mozilla::LogLevel::Error, (__VA_ARGS__))

#undef LOG_TEST_I
#define LOG_TEST_I(...) MOZ_LOG_TEST(mozilla::dom::gFlyWebServiceLog, mozilla::LogLevel::Debug)

class FlyWebPublishServerPermissionCheck final
  : public nsIContentPermissionRequest
  , public nsIRunnable
{
public:
  NS_DECL_ISUPPORTS

  FlyWebPublishServerPermissionCheck(const nsCString& aServiceName, uint64_t aWindowID,
                                     FlyWebPublishedServer* aServer)
    : mServiceName(aServiceName)
    , mWindowID(aWindowID)
    , mServer(aServer)
  {}

  uint64_t WindowID() const
  {
    return mWindowID;
  }

  NS_IMETHOD Run() override
  {
    MOZ_ASSERT(NS_IsMainThread());

    nsGlobalWindow* globalWindow = nsGlobalWindow::GetInnerWindowWithId(mWindowID);
    if (!globalWindow) {
      return Cancel();
    }
    mWindow = globalWindow->AsInner();
    if (NS_WARN_IF(!mWindow)) {
      return Cancel();
    }

    nsCOMPtr<nsIDocument> doc = mWindow->GetDoc();
    if (NS_WARN_IF(!doc)) {
      return Cancel();
    }

    mPrincipal = doc->NodePrincipal();
    MOZ_ASSERT(mPrincipal);

    mRequester = new nsContentPermissionRequester(mWindow);
    return nsContentPermissionUtils::AskPermission(this, mWindow);
  }

  NS_IMETHOD Cancel() override
  {
    Resolve(false);
    return NS_OK;
  }

  NS_IMETHOD Allow(JS::HandleValue aChoices) override
  {
    MOZ_ASSERT(aChoices.isUndefined());
    Resolve(true);
    return NS_OK;
  }

  NS_IMETHOD GetTypes(nsIArray** aTypes) override
  {
    nsTArray<nsString> emptyOptions;
    return nsContentPermissionUtils::CreatePermissionArray(NS_LITERAL_CSTRING("flyweb-publish-server"),
                                                           NS_LITERAL_CSTRING("unused"), emptyOptions, aTypes);
  }

  NS_IMETHOD GetRequester(nsIContentPermissionRequester** aRequester) override
  {
    NS_ENSURE_ARG_POINTER(aRequester);
    nsCOMPtr<nsIContentPermissionRequester> requester = mRequester;
    requester.forget(aRequester);
    return NS_OK;
  }

  NS_IMETHOD GetPrincipal(nsIPrincipal** aRequestingPrincipal) override
  {
    NS_IF_ADDREF(*aRequestingPrincipal = mPrincipal);
    return NS_OK;
  }

  NS_IMETHOD GetWindow(mozIDOMWindow** aRequestingWindow) override
  {
    NS_IF_ADDREF(*aRequestingWindow = mWindow);
    return NS_OK;
  }

  NS_IMETHOD GetElement(nsIDOMElement** aRequestingElement) override
  {
    *aRequestingElement = nullptr;
    return NS_OK;
  }

private:
  void Resolve(bool aResolve)
  {
    mServer->PermissionGranted(aResolve);
  }

  virtual ~FlyWebPublishServerPermissionCheck() = default;

  nsCString mServiceName;
  uint64_t mWindowID;
  RefPtr<FlyWebPublishedServer> mServer;
  nsCOMPtr<nsPIDOMWindowInner> mWindow;
  nsCOMPtr<nsIPrincipal> mPrincipal;
  nsCOMPtr<nsIContentPermissionRequester> mRequester;
};

NS_IMPL_ISUPPORTS(FlyWebPublishServerPermissionCheck,
                  nsIContentPermissionRequest,
                  nsIRunnable)

class FlyWebMDNSService final
  : public nsIDNSServiceDiscoveryListener
  , public nsIDNSServiceResolveListener
  , public nsIDNSRegistrationListener
  , public nsITimerCallback
{
  friend class FlyWebService;

private:
  enum DiscoveryState {
    DISCOVERY_IDLE,
    DISCOVERY_STARTING,
    DISCOVERY_RUNNING,
    DISCOVERY_STOPPING
  };

public:
  NS_DECL_ISUPPORTS
  NS_DECL_NSIDNSSERVICEDISCOVERYLISTENER
  NS_DECL_NSIDNSSERVICERESOLVELISTENER
  NS_DECL_NSIDNSREGISTRATIONLISTENER
  NS_DECL_NSITIMERCALLBACK

  explicit FlyWebMDNSService(FlyWebService* aService,
                             const nsACString& aServiceType);

private:
  virtual ~FlyWebMDNSService() = default;

  nsresult Init();
  nsresult StartDiscovery();
  nsresult StopDiscovery();

  void ListDiscoveredServices(nsTArray<FlyWebDiscoveredService>& aServices);
  bool HasService(const nsAString& aServiceId);
  nsresult PairWithService(const nsAString& aServiceId,
                           UniquePtr<FlyWebService::PairedInfo>& aInfo);

  nsresult StartDiscoveryOf(FlyWebPublishedServerImpl* aServer);

  void EnsureDiscoveryStarted();
  void EnsureDiscoveryStopped();

  // Cycle-breaking link to manager.
  FlyWebService* mService;
  nsCString mServiceType;

  // Indicates the desired state of the system.  If mDiscoveryActive is true,
  // it indicates that backend discovery "should be happening", and discovery
  // events should be forwarded to listeners.
  // If false, the backend discovery "should be idle", and any discovery events
  // that show up should not be forwarded to listeners.
  bool mDiscoveryActive;

  uint32_t mNumConsecutiveStartDiscoveryFailures;

  // Represents the internal discovery state as it relates to nsDNSServiceDiscovery.
  // When mDiscoveryActive is true, this state will periodically loop from
  // (IDLE => STARTING => RUNNING => STOPPING => IDLE).
  DiscoveryState mDiscoveryState;

  nsCOMPtr<nsITimer> mDiscoveryStartTimer;
  nsCOMPtr<nsITimer> mDiscoveryStopTimer;
  nsCOMPtr<nsIDNSServiceDiscovery> mDNSServiceDiscovery;
  nsCOMPtr<nsICancelable> mCancelDiscovery;
  nsTHashtable<nsStringHashKey> mNewServiceSet;

  struct DiscoveredInfo
  {
    explicit DiscoveredInfo(nsIDNSServiceInfo* aDNSServiceInfo);
    FlyWebDiscoveredService mService;
    nsCOMPtr<nsIDNSServiceInfo> mDNSServiceInfo;
  };
  nsClassHashtable<nsStringHashKey, DiscoveredInfo> mServiceMap;
};

void
LogDNSInfo(nsIDNSServiceInfo* aServiceInfo, const char* aFunc)
{
  if (!LOG_TEST_I()) {
    return;
  }

  nsCString tmp;
  aServiceInfo->GetServiceName(tmp);
  LOG_I("%s: serviceName=%s", aFunc, tmp.get());

  aServiceInfo->GetHost(tmp);
  LOG_I("%s: host=%s", aFunc, tmp.get());

  aServiceInfo->GetAddress(tmp);
  LOG_I("%s: address=%s", aFunc, tmp.get());

  uint16_t port = -2;
  aServiceInfo->GetPort(&port);
  LOG_I("%s: port=%d", aFunc, (int)port);

  nsCOMPtr<nsIPropertyBag2> attributes;
  aServiceInfo->GetAttributes(getter_AddRefs(attributes));
  if (!attributes) {
    LOG_I("%s: no attributes", aFunc);
  } else {
    nsCOMPtr<nsISimpleEnumerator> enumerator;
    attributes->GetEnumerator(getter_AddRefs(enumerator));
    MOZ_ASSERT(enumerator);

    LOG_I("%s: attributes start", aFunc);

    bool hasMoreElements;
    while (NS_SUCCEEDED(enumerator->HasMoreElements(&hasMoreElements)) &&
           hasMoreElements) {
      nsCOMPtr<nsISupports> element;
      MOZ_ALWAYS_SUCCEEDS(enumerator->GetNext(getter_AddRefs(element)));
      nsCOMPtr<nsIProperty> property = do_QueryInterface(element);
      MOZ_ASSERT(property);

      nsAutoString name;
      nsCOMPtr<nsIVariant> value;
      MOZ_ALWAYS_SUCCEEDS(property->GetName(name));
      MOZ_ALWAYS_SUCCEEDS(property->GetValue(getter_AddRefs(value)));

      nsAutoCString str;
      nsresult rv = value->GetAsACString(str);
      if (NS_SUCCEEDED(rv)) {
        LOG_I("%s: attribute name=%s value=%s", aFunc,
              NS_ConvertUTF16toUTF8(name).get(), str.get());
      } else {
        uint16_t type;
        MOZ_ALWAYS_SUCCEEDS(value->GetDataType(&type));
        LOG_I("%s: attribute *unstringifiable* name=%s type=%d", aFunc,
              NS_ConvertUTF16toUTF8(name).get(), (int)type);
      }
    }

    LOG_I("%s: attributes end", aFunc);
  }
}

NS_IMPL_ISUPPORTS(FlyWebMDNSService,
                  nsIDNSServiceDiscoveryListener,
                  nsIDNSServiceResolveListener,
                  nsIDNSRegistrationListener,
                  nsITimerCallback)

FlyWebMDNSService::FlyWebMDNSService(
        FlyWebService* aService,
        const nsACString& aServiceType)
  : mService(aService)
  , mServiceType(aServiceType)
  , mDiscoveryActive(false)
  , mNumConsecutiveStartDiscoveryFailures(0)
  , mDiscoveryState(DISCOVERY_IDLE)
{}

nsresult
FlyWebMDNSService::OnDiscoveryStarted(const nsACString& aServiceType)
{
  MOZ_ASSERT(mDiscoveryState == DISCOVERY_STARTING);
  mDiscoveryState = DISCOVERY_RUNNING;
  // Reset consecutive start discovery failures.
  mNumConsecutiveStartDiscoveryFailures = 0;
  LOG_I("===========================================");
  LOG_I("MDNSService::OnDiscoveryStarted(%s)", PromiseFlatCString(aServiceType).get());
  LOG_I("===========================================");

  // Clear the new service array.
  mNewServiceSet.Clear();

  // If service discovery is inactive, then stop network discovery immediately.
  if (!mDiscoveryActive) {
    // Set the stop timer to fire immediately.
    Unused << NS_WARN_IF(NS_FAILED(mDiscoveryStopTimer->InitWithCallback(this, 0, nsITimer::TYPE_ONE_SHOT)));
    return NS_OK;
  }

  // Otherwise, set the stop timer to fire in 5 seconds.
  Unused << NS_WARN_IF(NS_FAILED(mDiscoveryStopTimer->InitWithCallback(this, 5 * 1000, nsITimer::TYPE_ONE_SHOT)));

  return NS_OK;
}

nsresult
FlyWebMDNSService::OnDiscoveryStopped(const nsACString& aServiceType)
{
  LOG_I("///////////////////////////////////////////");
  LOG_I("MDNSService::OnDiscoveryStopped(%s)", PromiseFlatCString(aServiceType).get());
  LOG_I("///////////////////////////////////////////");
  MOZ_ASSERT(mDiscoveryState == DISCOVERY_STOPPING);
  mDiscoveryState = DISCOVERY_IDLE;

  // If service discovery is inactive, then discard all results and do not proceed.
  if (!mDiscoveryActive) {
    mServiceMap.Clear();
    mNewServiceSet.Clear();
    return NS_OK;
  }

  // Process the service map, add to the pair map.
  for (auto iter = mServiceMap.Iter(); !iter.Done(); iter.Next()) {
    DiscoveredInfo* service = iter.UserData();

    if (!mNewServiceSet.Contains(service->mService.mServiceId)) {
      iter.Remove();
    }
  }

  // Notify FlyWebService of changed service list.
  mService->NotifyDiscoveredServicesChanged();

  // Start discovery again immediately.
  Unused << NS_WARN_IF(NS_FAILED(mDiscoveryStartTimer->InitWithCallback(this, 0, nsITimer::TYPE_ONE_SHOT)));

  return NS_OK;
}

nsresult
FlyWebMDNSService::OnServiceFound(nsIDNSServiceInfo* aServiceInfo)
{
  LogDNSInfo(aServiceInfo, "FlyWebMDNSService::OnServiceFound");

  // If discovery is not active, don't do anything with the result.
  // If there is no discovery underway, ignore this.
  if (!mDiscoveryActive || mDiscoveryState != DISCOVERY_RUNNING) {
    return NS_OK;
  }

  // Discovery is underway - resolve the service.
  nsresult rv = mDNSServiceDiscovery->ResolveService(aServiceInfo, this);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

nsresult
FlyWebMDNSService::OnServiceLost(nsIDNSServiceInfo* aServiceInfo)
{
  LogDNSInfo(aServiceInfo, "FlyWebMDNSService::OnServiceLost");

  return NS_OK;
}

nsresult
FlyWebMDNSService::OnStartDiscoveryFailed(const nsACString& aServiceType, int32_t aErrorCode)
{
  LOG_E("MDNSService::OnStartDiscoveryFailed(%s): %d", PromiseFlatCString(aServiceType).get(), (int) aErrorCode);

  MOZ_ASSERT(mDiscoveryState == DISCOVERY_STARTING);
  mDiscoveryState = DISCOVERY_IDLE;
  mNumConsecutiveStartDiscoveryFailures++;

  // If discovery is active, and the number of consecutive failures is < 3, try starting again.
  if (mDiscoveryActive && mNumConsecutiveStartDiscoveryFailures < 3) {
    Unused << NS_WARN_IF(NS_FAILED(mDiscoveryStartTimer->InitWithCallback(this, 0, nsITimer::TYPE_ONE_SHOT)));
  }

  return NS_OK;
}

nsresult
FlyWebMDNSService::OnStopDiscoveryFailed(const nsACString& aServiceType, int32_t aErrorCode)
{
  LOG_E("MDNSService::OnStopDiscoveryFailed(%s)", PromiseFlatCString(aServiceType).get());
  MOZ_ASSERT(mDiscoveryState == DISCOVERY_STOPPING);
  mDiscoveryState = DISCOVERY_IDLE;

  // If discovery is active, start discovery again immediately.
  if (mDiscoveryActive) {
    Unused << NS_WARN_IF(NS_FAILED(mDiscoveryStartTimer->InitWithCallback(this, 0, nsITimer::TYPE_ONE_SHOT)));
  }

  return NS_OK;
}

static bool
IsAcceptableServiceAddress(const nsCString& addr)
{
  PRNetAddr prNetAddr;
  PRStatus status = PR_StringToNetAddr(addr.get(), &prNetAddr);
  if (status == PR_FAILURE) {
    return false;
  }
  // Only allow ipv4 addreses for now.
  return prNetAddr.raw.family == PR_AF_INET;
}

nsresult
FlyWebMDNSService::OnServiceResolved(nsIDNSServiceInfo* aServiceInfo)
{
  LogDNSInfo(aServiceInfo, "FlyWebMDNSService::OnServiceResolved");

  // If discovery is not active, don't do anything with the result.
  // If there is no discovery underway, ignore this resolve.
  if (!mDiscoveryActive || mDiscoveryState != DISCOVERY_RUNNING) {
    return NS_OK;
  }

  nsresult rv;

  nsCString address;
  rv = aServiceInfo->GetAddress(address);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  if (!IsAcceptableServiceAddress(address)) {
    return NS_OK;
  }

  // Create a new serviceInfo and stuff it in the new service array.
  UniquePtr<DiscoveredInfo> svc(new DiscoveredInfo(aServiceInfo));
  mNewServiceSet.PutEntry(svc->mService.mServiceId);

  DiscoveredInfo* existingSvc =
    mServiceMap.Get(svc->mService.mServiceId);
  if (existingSvc) {
    // Update the underlying DNS service info, but leave the old object in place.
    existingSvc->mDNSServiceInfo = aServiceInfo;
  } else {
    DiscoveredInfo* info = svc.release();
    mServiceMap.Put(info->mService.mServiceId, info);
  }

  // Notify FlyWebService of changed service list.
  mService->NotifyDiscoveredServicesChanged();

  return NS_OK;
}

FlyWebMDNSService::DiscoveredInfo::DiscoveredInfo(nsIDNSServiceInfo* aDNSServiceInfo)
  : mDNSServiceInfo(aDNSServiceInfo)
{
  nsCString tmp;
  DebugOnly<nsresult> drv = aDNSServiceInfo->GetServiceName(tmp);
  MOZ_ASSERT(NS_SUCCEEDED(drv));
  CopyUTF8toUTF16(tmp, mService.mDisplayName);

  mService.mTransport = NS_LITERAL_STRING("mdns");

  drv = aDNSServiceInfo->GetServiceType(tmp);
  MOZ_ASSERT(NS_SUCCEEDED(drv));
  CopyUTF8toUTF16(tmp, mService.mServiceType);

  nsCOMPtr<nsIPropertyBag2> attrs;
  drv = aDNSServiceInfo->GetAttributes(getter_AddRefs(attrs));
  MOZ_ASSERT(NS_SUCCEEDED(drv));
  if (attrs) {
    attrs->GetPropertyAsAString(NS_LITERAL_STRING("cert"), mService.mCert);
    attrs->GetPropertyAsAString(NS_LITERAL_STRING("path"), mService.mPath);
  }

  // Construct a service id from the name, host, address, and port.
  nsCString cHost;
  drv = aDNSServiceInfo->GetHost(cHost);
  MOZ_ASSERT(NS_SUCCEEDED(drv));

  nsCString cAddress;
  drv = aDNSServiceInfo->GetAddress(cAddress);
  MOZ_ASSERT(NS_SUCCEEDED(drv));

  uint16_t port;
  drv = aDNSServiceInfo->GetPort(&port);
  MOZ_ASSERT(NS_SUCCEEDED(drv));
  nsAutoString portStr;
  portStr.AppendInt(port, 10);

  mService.mServiceId =
    NS_ConvertUTF8toUTF16(cAddress) +
    NS_LITERAL_STRING(":") +
    portStr +
    NS_LITERAL_STRING("|") +
    mService.mServiceType +
    NS_LITERAL_STRING("|") +
    NS_ConvertUTF8toUTF16(cHost) +
    NS_LITERAL_STRING("|") +
    mService.mDisplayName;
}


nsresult
FlyWebMDNSService::OnResolveFailed(nsIDNSServiceInfo* aServiceInfo, int32_t aErrorCode)
{
  LogDNSInfo(aServiceInfo, "FlyWebMDNSService::OnResolveFailed");

  return NS_OK;
}

nsresult
FlyWebMDNSService::OnServiceRegistered(nsIDNSServiceInfo* aServiceInfo)
{
  LogDNSInfo(aServiceInfo, "FlyWebMDNSService::OnServiceRegistered");

  nsCString cName;
  if (NS_WARN_IF(NS_FAILED(aServiceInfo->GetServiceName(cName)))) {
    return NS_ERROR_FAILURE;
  }

  nsString name = NS_ConvertUTF8toUTF16(cName);
  RefPtr<FlyWebPublishedServer> existingServer =
    FlyWebService::GetOrCreate()->FindPublishedServerByName(name);
  if (!existingServer) {
    return NS_ERROR_FAILURE;
  }

  existingServer->PublishedServerStarted(NS_OK);

  return NS_OK;
}

nsresult
FlyWebMDNSService::OnServiceUnregistered(nsIDNSServiceInfo* aServiceInfo)
{
  LogDNSInfo(aServiceInfo, "FlyWebMDNSService::OnServiceUnregistered");

  nsCString cName;
  if (NS_WARN_IF(NS_FAILED(aServiceInfo->GetServiceName(cName)))) {
    return NS_ERROR_FAILURE;
  }

  nsString name = NS_ConvertUTF8toUTF16(cName);
  RefPtr<FlyWebPublishedServer> existingServer =
    FlyWebService::GetOrCreate()->FindPublishedServerByName(name);
  if (!existingServer) {
    return NS_ERROR_FAILURE;
  }

  LOG_I("OnServiceRegistered(MDNS): De-advertised server with name %s.", cName.get());

  return NS_OK;
}

nsresult
FlyWebMDNSService::OnRegistrationFailed(nsIDNSServiceInfo* aServiceInfo, int32_t errorCode)
{
  LogDNSInfo(aServiceInfo, "FlyWebMDNSService::OnRegistrationFailed");

  nsCString cName;
  if (NS_WARN_IF(NS_FAILED(aServiceInfo->GetServiceName(cName)))) {
    return NS_ERROR_FAILURE;
  }

  nsString name = NS_ConvertUTF8toUTF16(cName);
  RefPtr<FlyWebPublishedServer> existingServer =
    FlyWebService::GetOrCreate()->FindPublishedServerByName(name);
  if (!existingServer) {
    return NS_ERROR_FAILURE;
  }

  LOG_I("OnServiceRegistered(MDNS): Registration of server with name %s failed.", cName.get());

  // Remove the nsICancelable from the published server.
  existingServer->PublishedServerStarted(NS_ERROR_FAILURE);
  return NS_OK;
}

nsresult
FlyWebMDNSService::OnUnregistrationFailed(nsIDNSServiceInfo* aServiceInfo, int32_t errorCode)
{
  LogDNSInfo(aServiceInfo, "FlyWebMDNSService::OnUnregistrationFailed");

  nsCString cName;
  if (NS_WARN_IF(NS_FAILED(aServiceInfo->GetServiceName(cName)))) {
    return NS_ERROR_FAILURE;
  }

  nsString name = NS_ConvertUTF8toUTF16(cName);
  RefPtr<FlyWebPublishedServer> existingServer =
    FlyWebService::GetOrCreate()->FindPublishedServerByName(name);
  if (!existingServer) {
    return NS_ERROR_FAILURE;
  }

  LOG_I("OnServiceRegistered(MDNS): Un-Advertisement of server with name %s failed.", cName.get());
  return NS_OK;
}

nsresult
FlyWebMDNSService::Notify(nsITimer* timer)
{
  if (timer == mDiscoveryStopTimer.get()) {
    LOG_I("MDNSService::Notify() got discovery stop timeout");
    // Internet discovery stop timer has fired.
    nsresult rv = StopDiscovery();
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
    return NS_OK;
  }

  if (timer == mDiscoveryStartTimer.get()) {
    LOG_I("MDNSService::Notify() got discovery start timeout");
    // Internet discovery start timer has fired.
    nsresult rv = StartDiscovery();
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
    return NS_OK;
  }

  LOG_E("MDNSService::Notify got unknown timeout.");
  return NS_OK;
}

nsresult
FlyWebMDNSService::Init()
{
  MOZ_ASSERT(mDiscoveryState == DISCOVERY_IDLE);

  mDiscoveryStartTimer = do_CreateInstance("@mozilla.org/timer;1");
  if (!mDiscoveryStartTimer) {
    return NS_ERROR_FAILURE;
  }

  mDiscoveryStopTimer = do_CreateInstance("@mozilla.org/timer;1");
  if (!mDiscoveryStopTimer) {
    return NS_ERROR_FAILURE;
  }

  nsresult rv;
  mDNSServiceDiscovery = do_GetService(DNSSERVICEDISCOVERY_CONTRACT_ID, &rv);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

nsresult
FlyWebMDNSService::StartDiscovery()
{
  nsresult rv;

  // Always cancel the timer.
  rv = mDiscoveryStartTimer->Cancel();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    LOG_E("FlyWeb failed to cancel DNS service discovery start timer.");
  }

  // If discovery is not idle, don't start it.
  if (mDiscoveryState != DISCOVERY_IDLE) {
    return NS_OK;
  }

  LOG_I("FlyWeb starting dicovery.");
  mDiscoveryState = DISCOVERY_STARTING;

  // start the discovery.
  rv = mDNSServiceDiscovery->StartDiscovery(mServiceType, this,
                                            getter_AddRefs(mCancelDiscovery));
  if (NS_WARN_IF(NS_FAILED(rv))) {
    LOG_E("FlyWeb failed to start DNS service discovery.");
    return rv;
  }

  return NS_OK;
}

nsresult
FlyWebMDNSService::StopDiscovery()
{
  nsresult rv;

  // Always cancel the timer.
  rv = mDiscoveryStopTimer->Cancel();
  if (NS_WARN_IF(NS_FAILED(rv))) {
    LOG_E("FlyWeb failed to cancel DNS service discovery stop timer.");
  }

  // If discovery is not running, do nothing.
  if (mDiscoveryState != DISCOVERY_RUNNING) {
    return NS_OK;
  }

  LOG_I("FlyWeb stopping dicovery.");

  // Mark service discovery as stopping.
  mDiscoveryState = DISCOVERY_STOPPING;

  if (mCancelDiscovery) {
    LOG_I("MDNSService::StopDiscovery() - mCancelDiscovery exists!");
    nsCOMPtr<nsICancelable> cancelDiscovery = mCancelDiscovery.forget();
    rv = cancelDiscovery->Cancel(NS_ERROR_ABORT);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      LOG_E("FlyWeb failed to cancel DNS stop service discovery.");
    }
  } else {
    LOG_I("MDNSService::StopDiscovery() - mCancelDiscovery does not exist!");
    mDiscoveryState = DISCOVERY_IDLE;
  }

  return NS_OK;
}

void
FlyWebMDNSService::ListDiscoveredServices(nsTArray<FlyWebDiscoveredService>& aServices)
{
  for (auto iter = mServiceMap.Iter(); !iter.Done(); iter.Next()) {
    aServices.AppendElement(iter.UserData()->mService);
  }
}

bool
FlyWebMDNSService::HasService(const nsAString& aServiceId)
{
  return mServiceMap.Contains(aServiceId);
}

nsresult
FlyWebMDNSService::PairWithService(const nsAString& aServiceId,
                                   UniquePtr<FlyWebService::PairedInfo>& aInfo)
{
  MOZ_ASSERT(HasService(aServiceId));

  nsresult rv;
  nsCOMPtr<nsIUUIDGenerator> uuidgen =
    do_GetService("@mozilla.org/uuid-generator;1", &rv);
  NS_ENSURE_SUCCESS(rv, rv);

  nsID id;
  rv = uuidgen->GenerateUUIDInPlace(&id);
  NS_ENSURE_SUCCESS(rv, rv);

  aInfo.reset(new FlyWebService::PairedInfo());

  char uuidChars[NSID_LENGTH];
  id.ToProvidedString(uuidChars);
  CopyUTF8toUTF16(Substring(uuidChars + 1, uuidChars + NSID_LENGTH - 2),
                  aInfo->mService.mHostname);

  DiscoveredInfo* discInfo = mServiceMap.Get(aServiceId);

  nsAutoString url;
  if (discInfo->mService.mCert.IsEmpty()) {
    url.AssignLiteral("http://");
  } else {
    url.AssignLiteral("https://");
  }
  url.Append(aInfo->mService.mHostname + NS_LITERAL_STRING("/"));
  nsCOMPtr<nsIURI> uiURL;
  NS_NewURI(getter_AddRefs(uiURL), url);
  MOZ_ASSERT(uiURL);
  if (!discInfo->mService.mPath.IsEmpty()) {
    nsCOMPtr<nsIURI> tmp = uiURL.forget();
    NS_NewURI(getter_AddRefs(uiURL), discInfo->mService.mPath, nullptr, tmp);
  }
  if (uiURL) {
    nsAutoCString spec;
    uiURL->GetSpec(spec);
    CopyUTF8toUTF16(spec, aInfo->mService.mUiUrl);
  }

  aInfo->mService.mDiscoveredService = discInfo->mService;
  aInfo->mDNSServiceInfo = discInfo->mDNSServiceInfo;

  return NS_OK;
}

nsresult
FlyWebMDNSService::StartDiscoveryOf(FlyWebPublishedServerImpl* aServer)
{

  RefPtr<FlyWebPublishedServer> existingServer =
    FlyWebService::GetOrCreate()->FindPublishedServerByName(aServer->Name());
  MOZ_ASSERT(existingServer);

  // Advertise the service via mdns.
  RefPtr<net::nsDNSServiceInfo> serviceInfo(new net::nsDNSServiceInfo());

  serviceInfo->SetPort(aServer->Port());
  serviceInfo->SetServiceType(mServiceType);

  nsCString certKey;
  aServer->GetCertKey(certKey);
  nsString uiURL;
  aServer->GetUiUrl(uiURL);

  if (!uiURL.IsEmpty() || !certKey.IsEmpty()) {
    RefPtr<nsHashPropertyBag> attrs = new nsHashPropertyBag();
    if (!uiURL.IsEmpty()) {
      attrs->SetPropertyAsAString(NS_LITERAL_STRING("path"), uiURL);
    }
    if (!certKey.IsEmpty()) {
      attrs->SetPropertyAsACString(NS_LITERAL_STRING("cert"), certKey);
    }
    serviceInfo->SetAttributes(attrs);
  }

  nsCString cstrName = NS_ConvertUTF16toUTF8(aServer->Name());
  LOG_I("MDNSService::StartDiscoveryOf() advertising service %s", cstrName.get());
  serviceInfo->SetServiceName(cstrName);

  LogDNSInfo(serviceInfo, "FlyWebMDNSService::StartDiscoveryOf");

  // Advertise the service.
  nsCOMPtr<nsICancelable> cancelRegister;
  nsresult rv = mDNSServiceDiscovery->
    RegisterService(serviceInfo, this, getter_AddRefs(cancelRegister));
  NS_ENSURE_SUCCESS(rv, rv);

  // All done.
  aServer->SetCancelRegister(cancelRegister);

  return NS_OK;
}

void
FlyWebMDNSService::EnsureDiscoveryStarted()
{
  mDiscoveryActive = true;
  // If state is idle, start discovery immediately.
  if (mDiscoveryState == DISCOVERY_IDLE) {
    StartDiscovery();
  }
}

void
FlyWebMDNSService::EnsureDiscoveryStopped()
{
  // All we need to do is set the flag to false.
  // If current state is IDLE, it's already the correct state.
  // Otherwise, the handlers for the internal state
  // transitions will check this flag and drive the state
  // towards IDLE.
  mDiscoveryActive = false;
}

static StaticRefPtr<FlyWebService> gFlyWebService;

NS_IMPL_ISUPPORTS(FlyWebService, nsIObserver)

FlyWebService::FlyWebService()
  : mMonitor("FlyWebService::mMonitor")
{
  MOZ_ASSERT(NS_IsMainThread());
  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
  if (obs) {
    obs->AddObserver(this, "inner-window-destroyed", false);
  }
}

FlyWebService::~FlyWebService()
{
}

FlyWebService*
FlyWebService::GetExisting()
{
  return gFlyWebService;
}

FlyWebService*
FlyWebService::GetOrCreate()
{
  if (!gFlyWebService) {
    gFlyWebService = new FlyWebService();
    ClearOnShutdown(&gFlyWebService);
    ErrorResult rv = gFlyWebService->Init();
    if (rv.Failed()) {
      gFlyWebService = nullptr;
      return nullptr;
    }
  }
  return gFlyWebService;
}

ErrorResult
FlyWebService::Init()
{
  // Most functions of FlyWebService should not be started in the child.
  // Instead FlyWebService in the child is mainly responsible for tracking
  // publishedServer lifetimes. Other functions are handled by the
  // FlyWebService running in the parent.
  if (XRE_GetProcessType() == GeckoProcessType_Content) {
    return ErrorResult(NS_OK);
  }

  MOZ_ASSERT(NS_IsMainThread());
  if (!mMDNSHttpService) {
    mMDNSHttpService = new FlyWebMDNSService(this, NS_LITERAL_CSTRING("_http._tcp."));
    ErrorResult rv;

    rv = mMDNSHttpService->Init();
    if (rv.Failed()) {
      LOG_E("FlyWebService failed to initialize MDNS _http._tcp.");
      mMDNSHttpService = nullptr;
      rv.SuppressException();
    }
  }

  if (!mMDNSFlywebService) {
    mMDNSFlywebService = new FlyWebMDNSService(this, NS_LITERAL_CSTRING("_flyweb._tcp."));
    ErrorResult rv;

    rv = mMDNSFlywebService->Init();
    if (rv.Failed()) {
      LOG_E("FlyWebService failed to initialize MDNS _flyweb._tcp.");
      mMDNSFlywebService = nullptr;
      rv.SuppressException();
    }
  }

  return ErrorResult(NS_OK);
}

static already_AddRefed<FlyWebPublishPromise>
MakeRejectionPromise(const char* name)
{
    MozPromiseHolder<FlyWebPublishPromise> holder;
    RefPtr<FlyWebPublishPromise> promise = holder.Ensure(name);
    holder.Reject(NS_ERROR_FAILURE, name);
    return promise.forget();
}

static bool
CheckForFlyWebAddon(const nsACString& uriString)
{
  // Before proceeding, ensure that the FlyWeb system addon exists.
  nsresult rv;
  nsCOMPtr<nsIURI> uri;
  rv = NS_NewURI(getter_AddRefs(uri), uriString);
  if (NS_FAILED(rv)) {
    return false;
  }

  JSAddonId *addonId = MapURIToAddonID(uri);
  if (!addonId) {
    return false;
  }

  JSFlatString* flat = JS_ASSERT_STRING_IS_FLAT(JS::StringOfAddonId(addonId));
  nsAutoString addonIdString;
  AssignJSFlatString(addonIdString, flat);
  if (!addonIdString.EqualsLiteral("flyweb@mozilla.org")) {
    nsCString addonIdCString = NS_ConvertUTF16toUTF8(addonIdString);
    return false;
  }

  return true;
}

already_AddRefed<FlyWebPublishPromise>
FlyWebService::PublishServer(const nsAString& aName,
                             const FlyWebPublishOptions& aOptions,
                             nsPIDOMWindowInner* aWindow)
{
  // Scan uiUrl for illegal characters

  RefPtr<FlyWebPublishedServer> existingServer =
    FlyWebService::GetOrCreate()->FindPublishedServerByName(aName);
  if (existingServer) {
    LOG_I("PublishServer: Trying to publish server with already-existing name %s.",
          NS_ConvertUTF16toUTF8(aName).get());
    return MakeRejectionPromise(__func__);
  }

  RefPtr<FlyWebPublishedServer> server;
  if (XRE_GetProcessType() == GeckoProcessType_Content) {
    server = new FlyWebPublishedServerChild(aWindow, aName, aOptions);
  } else {
    server = new FlyWebPublishedServerImpl(aWindow, aName, aOptions);

    // Before proceeding, ensure that the FlyWeb system addon exists.
    if (!CheckForFlyWebAddon(NS_LITERAL_CSTRING("chrome://flyweb/skin/icon-64.png")) &&
        !CheckForFlyWebAddon(NS_LITERAL_CSTRING("chrome://flyweb/content/icon-64.png")))
    {
      LOG_E("PublishServer: Failed to find FlyWeb system addon.");
      return MakeRejectionPromise(__func__);
    }
  }

  if (aWindow) {
    nsresult rv;

    MOZ_ASSERT(NS_IsMainThread());
    rv = NS_DispatchToCurrentThread(
      MakeAndAddRef<FlyWebPublishServerPermissionCheck>(
        NS_ConvertUTF16toUTF8(aName), aWindow->WindowID(), server));
    if (NS_WARN_IF(NS_FAILED(rv))) {
      LOG_E("PublishServer: Failed to dispatch permission check runnable for %s",
            NS_ConvertUTF16toUTF8(aName).get());
      return MakeRejectionPromise(__func__);
    }
  } else {
    // If aWindow is null, we're definitely in the e10s parent process.
    // In this case, we know that permission has already been granted
    // by the user because of content-process prompt.
    MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
    server->PermissionGranted(true);
  }

  mServers.AppendElement(server);

  return server->GetPublishPromise();
}

already_AddRefed<FlyWebPublishedServer>
FlyWebService::FindPublishedServerByName(
        const nsAString& aName)
{
  MOZ_ASSERT(NS_IsMainThread());
  for (FlyWebPublishedServer* publishedServer : mServers) {
    if (publishedServer->Name().Equals(aName)) {
      RefPtr<FlyWebPublishedServer> server = publishedServer;
      return server.forget();
    }
  }
  return nullptr;
}

void
FlyWebService::RegisterDiscoveryManager(FlyWebDiscoveryManager* aDiscoveryManager)
{
  MOZ_ASSERT(NS_IsMainThread());
  mDiscoveryManagerTable.PutEntry(aDiscoveryManager);
  if (mMDNSHttpService) {
    mMDNSHttpService->EnsureDiscoveryStarted();
  }
  if (mMDNSFlywebService) {
    mMDNSFlywebService->EnsureDiscoveryStarted();
  }
}

void
FlyWebService::UnregisterDiscoveryManager(FlyWebDiscoveryManager* aDiscoveryManager)
{
  MOZ_ASSERT(NS_IsMainThread());
  mDiscoveryManagerTable.RemoveEntry(aDiscoveryManager);
  if (mDiscoveryManagerTable.IsEmpty()) {
    if (mMDNSHttpService) {
      mMDNSHttpService->EnsureDiscoveryStopped();
    }
    if (mMDNSFlywebService) {
      mMDNSFlywebService->EnsureDiscoveryStopped();
    }
  }
}

NS_IMETHODIMP
FlyWebService::Observe(nsISupports* aSubject, const char* aTopic,
                       const char16_t* aData)
{
  MOZ_ASSERT(NS_IsMainThread());
  if (strcmp(aTopic, "inner-window-destroyed")) {
    return NS_OK;
  }

  nsCOMPtr<nsISupportsPRUint64> wrapper = do_QueryInterface(aSubject);
  NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE);

  uint64_t innerID;
  nsresult rv = wrapper->GetData(&innerID);
  NS_ENSURE_SUCCESS(rv, rv);

  for (FlyWebPublishedServer* server : mServers) {
    if (server->OwnerWindowID() == innerID) {
      server->Close();
    }
  }

  return NS_OK;
}

void
FlyWebService::UnregisterServer(FlyWebPublishedServer* aServer)
{
  MOZ_ASSERT(NS_IsMainThread());
  DebugOnly<bool> removed = mServers.RemoveElement(aServer);
  MOZ_ASSERT(removed);
}

bool
FlyWebService::HasConnectionOrServer(uint64_t aWindowID)
{
  MOZ_ASSERT(NS_IsMainThread());
  for (FlyWebPublishedServer* server : mServers) {
    nsPIDOMWindowInner* win = server->GetOwner();
    if (win && win->WindowID() == aWindowID) {
      return true;
    }
  }

  return false;
}

void
FlyWebService::NotifyDiscoveredServicesChanged()
{
  // Process the service map, add to the pair map.
  for (auto iter = mDiscoveryManagerTable.Iter(); !iter.Done(); iter.Next()) {
    iter.Get()->GetKey()->NotifyDiscoveredServicesChanged();
  }
}

void
FlyWebService::ListDiscoveredServices(nsTArray<FlyWebDiscoveredService>& aServices)
{
  MOZ_ASSERT(NS_IsMainThread());
  if (mMDNSHttpService) {
    mMDNSHttpService->ListDiscoveredServices(aServices);
  }
  if (mMDNSFlywebService) {
    mMDNSFlywebService->ListDiscoveredServices(aServices);
  }
}

void
FlyWebService::PairWithService(const nsAString& aServiceId,
                               FlyWebPairingCallback& aCallback)
{
  MOZ_ASSERT(NS_IsMainThread());
  // See if we have already paired with this service.  If so, re-use the
  // FlyWebPairedService for that.
  {
    ReentrantMonitorAutoEnter pairedMapLock(mMonitor);
    for (auto iter = mPairedServiceTable.Iter(); !iter.Done(); iter.Next()) {
      PairedInfo* pairInfo = iter.UserData();
      if (pairInfo->mService.mDiscoveredService.mServiceId.Equals(aServiceId)) {
        ErrorResult er;
        ReentrantMonitorAutoExit pairedMapRelease(mMonitor);
        aCallback.PairingSucceeded(pairInfo->mService, er);
        ENSURE_SUCCESS_VOID(er);
        return;
      }
    }
  }

  UniquePtr<PairedInfo> pairInfo;

  nsresult rv = NS_OK;
  bool notFound = false;
  if (mMDNSHttpService && mMDNSHttpService->HasService(aServiceId)) {
    rv = mMDNSHttpService->PairWithService(aServiceId, pairInfo);
  } else if (mMDNSFlywebService && mMDNSFlywebService->HasService(aServiceId)) {
    rv = mMDNSFlywebService->PairWithService(aServiceId, pairInfo);
  } else {
    notFound = true;
  }

  if (NS_FAILED(rv)) {
    ErrorResult result;
    result.Throw(rv);
    const nsAString& reason = NS_LITERAL_STRING("Error pairing.");
    aCallback.PairingFailed(reason, result);
    ENSURE_SUCCESS_VOID(result);
    return;
  }

  if (!pairInfo) {
    ErrorResult res;
    const nsAString& reason = notFound ?
      NS_LITERAL_STRING("No such service.") :
      NS_LITERAL_STRING("Error pairing.");
    aCallback.PairingFailed(reason, res);
    ENSURE_SUCCESS_VOID(res);
    return;
  }

  // Add fingerprint to certificate override database.
  if (!pairInfo->mService.mDiscoveredService.mCert.IsEmpty()) {
    nsCOMPtr<nsICertOverrideService> override =
      do_GetService("@mozilla.org/security/certoverride;1");
    if (!override ||
        NS_FAILED(override->RememberTemporaryValidityOverrideUsingFingerprint(
          NS_ConvertUTF16toUTF8(pairInfo->mService.mHostname),
          -1,
          NS_ConvertUTF16toUTF8(pairInfo->mService.mDiscoveredService.mCert),
          nsICertOverrideService::ERROR_UNTRUSTED |
          nsICertOverrideService::ERROR_MISMATCH))) {
      ErrorResult res;
      aCallback.PairingFailed(NS_LITERAL_STRING("Error adding certificate override."), res);
      ENSURE_SUCCESS_VOID(res);
      return;
    }
  }

  // Grab a weak reference to the PairedInfo so that we can
  // use it even after ownership has been transferred to mPairedServiceTable
  PairedInfo* pairInfoWeak = pairInfo.release();

  {
    ReentrantMonitorAutoEnter pairedMapLock(mMonitor);
    mPairedServiceTable.Put(
      NS_ConvertUTF16toUTF8(pairInfoWeak->mService.mHostname), pairInfoWeak);
  }

  ErrorResult er;
  aCallback.PairingSucceeded(pairInfoWeak->mService, er);
  ENSURE_SUCCESS_VOID(er);
}

nsresult
FlyWebService::CreateTransportForHost(const char **types,
                                      uint32_t typeCount,
                                      const nsACString &host,
                                      int32_t port,
                                      const nsACString &hostRoute,
                                      int32_t portRoute,
                                      nsIProxyInfo *proxyInfo,
                                      nsISocketTransport **result)
{
  // This might be called on background threads

  *result = nullptr;

  nsCString ipAddrString;
  uint16_t discPort;

  {
    ReentrantMonitorAutoEnter pairedMapLock(mMonitor);

    PairedInfo* info = mPairedServiceTable.Get(host);

    if (!info) {
      return NS_OK;
    }

    // Get the ip address of the underlying service.
    info->mDNSServiceInfo->GetAddress(ipAddrString);
    info->mDNSServiceInfo->GetPort(&discPort);
  }

  // Parse it into an NetAddr.
  PRNetAddr prNetAddr;
  PRStatus status = PR_StringToNetAddr(ipAddrString.get(), &prNetAddr);
  NS_ENSURE_FALSE(status == PR_FAILURE, NS_ERROR_FAILURE);

  // Convert PRNetAddr to NetAddr.
  mozilla::net::NetAddr netAddr;
  PRNetAddrToNetAddr(&prNetAddr, &netAddr);
  netAddr.inet.port = htons(discPort);

  RefPtr<mozilla::net::nsSocketTransport> trans = new mozilla::net::nsSocketTransport();
  nsresult rv = trans->InitPreResolved(
    types, typeCount, host, port, hostRoute, portRoute, proxyInfo, &netAddr);
  NS_ENSURE_SUCCESS(rv, rv);

  trans.forget(result);
  return NS_OK;
}

void
FlyWebService::StartDiscoveryOf(FlyWebPublishedServerImpl* aServer)
{
  MOZ_ASSERT(NS_IsMainThread());
  nsresult rv = mMDNSFlywebService ?
    mMDNSFlywebService->StartDiscoveryOf(aServer) :
    NS_ERROR_FAILURE;

  if (NS_FAILED(rv)) {
    aServer->PublishedServerStarted(rv);
  }
}

} // namespace dom
} // namespace mozilla
