/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "WifiCertService.h"

#include "mozilla/ClearOnShutdown.h"
#include "mozilla/ModuleUtils.h"
#include "mozilla/RefPtr.h"
#include "cert.h"
#include "certdb.h"
#include "CryptoTask.h"
#include "nsCxPusher.h"
#include "nsIDOMFile.h"
#include "nsIWifiService.h"
#include "nsNetUtil.h"
#include "nsServiceManagerUtils.h"
#include "nsXULAppAPI.h"
#include "ScopedNSSTypes.h"

#define NS_WIFICERTSERVICE_CID \
  { 0x83585afd, 0x0e11, 0x43aa, {0x83, 0x46, 0xf3, 0x4d, 0x97, 0x5e, 0x46, 0x77} }

using namespace mozilla;
using namespace mozilla::dom;

namespace mozilla {

// The singleton Wifi Cert service, to be used on the main thread.
StaticRefPtr<WifiCertService> gWifiCertService;

class ImportCertTask MOZ_FINAL: public CryptoTask
{
public:
  ImportCertTask(int32_t aId, nsIDOMBlob* aCertBlob,
                 const nsAString& aCertPassword,
                 const nsAString& aCertNickname)
    : mBlob(aCertBlob)
    , mPassword(aCertPassword)
  {
    MOZ_ASSERT(NS_IsMainThread());

    mResult.mId = aId;
    mResult.mStatus = 0;
    mResult.mUsageFlag = 0;
    mResult.mNickname = aCertNickname;
  }

private:
  virtual void ReleaseNSSResources() {}

  virtual nsresult CalculateResult() MOZ_OVERRIDE
  {
    MOZ_ASSERT(!NS_IsMainThread());

    // read data from blob.
    nsCString blobBuf;
    nsresult rv = ReadBlob(blobBuf);
    if (NS_FAILED(rv)) {
      return rv;
    }

    char* buf;
    uint32_t size = blobBuf.GetMutableData(&buf);
    if (size == 0) {
      return NS_ERROR_OUT_OF_MEMORY;
    }

    // Only support DER format now.
    return ImportDERBlob(buf, size, mResult.mNickname,
                         &mResult.mUsageFlag);
  }

  virtual void CallCallback(nsresult rv)
  {
    if (NS_FAILED(rv)) {
      mResult.mStatus = -1;
    }
    gWifiCertService->DispatchResult(mResult);
  }

  nsresult ImportDERBlob(char* buf, uint32_t size,
                         const nsAString& aNickname,
                         /*out*/ uint16_t* aUsageFlag)
  {
    NS_ENSURE_ARG_POINTER(aUsageFlag);

    // Create certificate object.
    ScopedCERTCertificate cert(CERT_DecodeCertFromPackage(buf, size));
    if (!cert) {
      return MapSECStatus(SECFailure);
    }

    // Import certificate with nickname.
    return ImportCert(cert, aNickname, aUsageFlag);
  }

  nsresult ReadBlob(/*out*/ nsCString& aBuf)
  {
    NS_ENSURE_ARG_POINTER(mBlob);

    static const uint64_t MAX_FILE_SIZE = 16384;
    uint64_t size;
    nsresult rv = mBlob->GetSize(&size);
    if (NS_FAILED(rv)) {
      return rv;
    }
    if (size > MAX_FILE_SIZE) {
      return NS_ERROR_FILE_TOO_BIG;
    }

    nsCOMPtr<nsIInputStream> inputStream;
    rv = mBlob->GetInternalStream(getter_AddRefs(inputStream));
    if (NS_FAILED(rv)) {
      return rv;
    }

    rv = NS_ReadInputStreamToString(inputStream, aBuf, (uint32_t)size);
    if (NS_FAILED(rv)) {
      return rv;
    }

    return NS_OK;
  }

  nsresult ImportCert(CERTCertificate* aCert, const nsAString& aNickname,
                      /*out*/ uint16_t* aUsageFlag)
  {
    NS_ENSURE_ARG_POINTER(aUsageFlag);

    nsCString userNickname, fullNickname;

    CopyUTF16toUTF8(aNickname, userNickname);
    // Determine certificate nickname by adding prefix according to its type.
    if (aCert->isRoot && (aCert->nsCertType & NS_CERT_TYPE_SSL_CA)) {
      // Accept self-signed SSL CA as server certificate.
      fullNickname.AssignLiteral("WIFI_SERVERCERT_");
      fullNickname += userNickname;
      *aUsageFlag |= nsIWifiCertService::WIFI_CERT_USAGE_FLAG_SERVER;
    } else {
      return NS_ERROR_ABORT;
    }

    char* nickname;
    uint32_t length;
    length = fullNickname.GetMutableData(&nickname);
    if (length == 0) {
      return NS_ERROR_UNEXPECTED;
    }

    // Import certificate, duplicated nickname will cause error.
    SECStatus srv = CERT_AddTempCertToPerm(aCert, nickname, NULL);
    if (srv != SECSuccess) {
      return MapSECStatus(srv);
    }

    return NS_OK;
  }

  nsCOMPtr<nsIDOMBlob> mBlob;
  nsString mPassword;
  WifiCertServiceResultOptions mResult;
};

class DeleteCertTask MOZ_FINAL: public CryptoTask
{
public:
  DeleteCertTask(int32_t aId, const nsAString& aCertNickname)
  {
    MOZ_ASSERT(NS_IsMainThread());

    mResult.mId = aId;
    mResult.mStatus = 0;
    mResult.mUsageFlag = 0;
    mResult.mNickname = aCertNickname;
  }

private:
  virtual void ReleaseNSSResources() {}

  virtual nsresult CalculateResult() MOZ_OVERRIDE
  {
    MOZ_ASSERT(!NS_IsMainThread());

    nsCString userNickname;
    CopyUTF16toUTF8(mResult.mNickname, userNickname);

    // Delete server certificate.
    nsCString serverCertName("WIFI_SERVERCERT_", 16);
    serverCertName += userNickname;

    ScopedCERTCertificate cert(
      CERT_FindCertByNickname(CERT_GetDefaultCertDB(), serverCertName.get())
    );
    if (!cert) {
      return MapSECStatus(SECFailure);
    }

    SECStatus srv = SEC_DeletePermCertificate(cert);
    if (srv != SECSuccess) {
      return MapSECStatus(srv);
    }

    return NS_OK;
  }

  virtual void CallCallback(nsresult rv)
  {
    if (NS_FAILED(rv)) {
      mResult.mStatus = -1;
    }
    gWifiCertService->DispatchResult(mResult);
  }

  WifiCertServiceResultOptions mResult;
};

NS_IMPL_ISUPPORTS(WifiCertService, nsIWifiCertService)

NS_IMETHODIMP
WifiCertService::Start(nsIWifiEventListener* aListener)
{
  MOZ_ASSERT(aListener);

  nsresult rv = NS_NewThread(getter_AddRefs(mRequestThread));
  if (NS_FAILED(rv)) {
    NS_WARNING("Certn't create wifi control thread");
    Shutdown();
    return NS_ERROR_FAILURE;
  }

  mListener = aListener;

  return NS_OK;
}

NS_IMETHODIMP
WifiCertService::Shutdown()
{
  MOZ_ASSERT(NS_IsMainThread());
  if (mRequestThread) {
    mRequestThread->Shutdown();
    mRequestThread = nullptr;
  }
  return NS_OK;
}

void
WifiCertService::DispatchResult(const WifiCertServiceResultOptions& aOptions)
{
  MOZ_ASSERT(NS_IsMainThread());

  mozilla::AutoSafeJSContext cx;
  JS::RootedValue val(cx);
  nsCString dummyInterface;

  if (!aOptions.ToObject(cx, &val)) {
    return;
  }

  // Certll the listener with a JS value.
  mListener->OnCommand(val, dummyInterface);
}

WifiCertService::WifiCertService()
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(!gWifiCertService);
}

WifiCertService::~WifiCertService()
{
  MOZ_ASSERT(!gWifiCertService);
}

already_AddRefed<WifiCertService>
WifiCertService::FactoryCreate()
{
  if (XRE_GetProcessType() != GeckoProcessType_Default) {
    return nullptr;
  }

  MOZ_ASSERT(NS_IsMainThread());

  if (!gWifiCertService) {
    gWifiCertService = new WifiCertService();
    ClearOnShutdown(&gWifiCertService);
  }

  nsRefPtr<WifiCertService> service = gWifiCertService.get();
  return service.forget();
}

NS_IMETHODIMP
WifiCertService::ImportCert(int32_t aId, nsIDOMBlob* aCertBlob,
                            const nsAString& aCertPassword,
                            const nsAString& aCertNickname)
{
  RefPtr<CryptoTask> task = new ImportCertTask(aId, aCertBlob, aCertPassword,
                                               aCertNickname);
  return task->Dispatch("WifiImportCert");
}

NS_IMETHODIMP
WifiCertService::DeleteCert(int32_t aId, const nsAString& aCertNickname)
{
  RefPtr<CryptoTask> task = new DeleteCertTask(aId, aCertNickname);
  return task->Dispatch("WifiDeleteCert");
}

NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(WifiCertService,
                                         WifiCertService::FactoryCreate)

NS_DEFINE_NAMED_CID(NS_WIFICERTSERVICE_CID);

static const mozilla::Module::CIDEntry kWifiCertServiceCIDs[] = {
  { &kNS_WIFICERTSERVICE_CID, false, nullptr, WifiCertServiceConstructor },
  { nullptr }
};

static const mozilla::Module::ContractIDEntry kWifiCertServiceContracts[] = {
  { "@mozilla.org/wifi/certservice;1", &kNS_WIFICERTSERVICE_CID },
  { nullptr }
};

static const mozilla::Module kWifiCertServiceModule = {
  mozilla::Module::kVersion,
  kWifiCertServiceCIDs,
  kWifiCertServiceContracts,
  nullptr
};

} // namespace mozilla

NSMODULE_DEFN(WifiCertServiceModule) = &kWifiCertServiceModule;
