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

#include "nsIX509Cert.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/RefPtr.h"
#include "nsCRT.h"
#include "nsNSSCertHelper.h"
#include "nsIObserverService.h"
#include "nsNetUtil.h"
#include "nsISupportsPrimitives.h"
#include "nsPromiseFlatString.h"
#include "nsThreadUtils.h"
#include "nsStringBuffer.h"
#include "cert.h"
#include "nspr.h"
#include "pk11pub.h"
#include "certdb.h"
#include "sechash.h"
#include "SharedSSLState.h"

using namespace mozilla;
using namespace mozilla::psm;

NS_IMPL_ISUPPORTS(nsClientAuthRememberService,
                  nsIObserver,
                  nsISupportsWeakReference)

nsClientAuthRememberService::nsClientAuthRememberService()
  : monitor("nsClientAuthRememberService.monitor")
{
}

nsClientAuthRememberService::~nsClientAuthRememberService()
{
  RemoveAllFromMemory();
}

nsresult
nsClientAuthRememberService::Init()
{
  if (!NS_IsMainThread()) {
    NS_ERROR("nsClientAuthRememberService::Init called off the main thread");
    return NS_ERROR_NOT_SAME_THREAD;
  }

  nsCOMPtr<nsIObserverService> observerService =
      mozilla::services::GetObserverService();
  if (observerService) {
    observerService->AddObserver(this, "profile-before-change", true);
  }

  return NS_OK;
}

NS_IMETHODIMP
nsClientAuthRememberService::Observe(nsISupports* aSubject,
                                     const char* aTopic,
                                     const char16_t* aData)
{
  // check the topic
  if (!nsCRT::strcmp(aTopic, "profile-before-change")) {
    // The profile is about to change,
    // or is going away because the application is shutting down.

    ReentrantMonitorAutoEnter lock(monitor);
    RemoveAllFromMemory();
  }

  return NS_OK;
}

void nsClientAuthRememberService::ClearRememberedDecisions()
{
  ReentrantMonitorAutoEnter lock(monitor);
  RemoveAllFromMemory();
}

void nsClientAuthRememberService::ClearAllRememberedDecisions()
{
  RefPtr<nsClientAuthRememberService> svc =
    PublicSSLState()->GetClientAuthRememberService();
  svc->ClearRememberedDecisions();

  svc = PrivateSSLState()->GetClientAuthRememberService();
  svc->ClearRememberedDecisions();
}

void
nsClientAuthRememberService::RemoveAllFromMemory()
{
  mSettingsTable.Clear();
}

nsresult
nsClientAuthRememberService::RememberDecision(
  const nsACString& aHostName, const OriginAttributes& aOriginAttributes,
  CERTCertificate* aServerCert, CERTCertificate* aClientCert)
{
  // aClientCert == nullptr means: remember that user does not want to use a cert
  NS_ENSURE_ARG_POINTER(aServerCert);
  if (aHostName.IsEmpty()) {
    return NS_ERROR_INVALID_ARG;
  }

  nsAutoCString fpStr;
  nsresult rv = GetCertFingerprintByOidTag(aServerCert, SEC_OID_SHA256, fpStr);
  if (NS_FAILED(rv)) {
    return rv;
  }

  {
    ReentrantMonitorAutoEnter lock(monitor);
    if (aClientCert) {
      RefPtr<nsNSSCertificate> pipCert(new nsNSSCertificate(aClientCert));
      nsAutoCString dbkey;
      rv = pipCert->GetDbKey(dbkey);
      if (NS_SUCCEEDED(rv)) {
        AddEntryToList(aHostName, aOriginAttributes, fpStr, dbkey);
      }
    } else {
      nsCString empty;
      AddEntryToList(aHostName, aOriginAttributes, fpStr, empty);
    }
  }

  return NS_OK;
}

nsresult
nsClientAuthRememberService::HasRememberedDecision(
  const nsACString& aHostName, const OriginAttributes& aOriginAttributes,
  CERTCertificate* aCert, nsACString& aCertDBKey, bool* aRetVal)
{
  if (aHostName.IsEmpty())
    return NS_ERROR_INVALID_ARG;

  NS_ENSURE_ARG_POINTER(aCert);
  NS_ENSURE_ARG_POINTER(aRetVal);
  *aRetVal = false;

  nsresult rv;
  nsAutoCString fpStr;
  rv = GetCertFingerprintByOidTag(aCert, SEC_OID_SHA256, fpStr);
  if (NS_FAILED(rv))
    return rv;

  nsAutoCString entryKey;
  GetEntryKey(aHostName, aOriginAttributes, fpStr, entryKey);
  nsClientAuthRemember settings;

  {
    ReentrantMonitorAutoEnter lock(monitor);
    nsClientAuthRememberEntry* entry = mSettingsTable.GetEntry(entryKey.get());
    if (!entry)
      return NS_OK;
    settings = entry->mSettings; // copy
  }

  aCertDBKey = settings.mDBKey;
  *aRetVal = true;
  return NS_OK;
}

nsresult
nsClientAuthRememberService::AddEntryToList(
  const nsACString& aHostName, const OriginAttributes& aOriginAttributes,
  const nsACString& aFingerprint, const nsACString& aDBKey)
{
  nsAutoCString entryKey;
  GetEntryKey(aHostName, aOriginAttributes, aFingerprint, entryKey);

  {
    ReentrantMonitorAutoEnter lock(monitor);
    nsClientAuthRememberEntry* entry = mSettingsTable.PutEntry(entryKey.get());

    if (!entry) {
      NS_ERROR("can't insert a null entry!");
      return NS_ERROR_OUT_OF_MEMORY;
    }

    entry->mEntryKey = entryKey;

    nsClientAuthRemember& settings = entry->mSettings;
    settings.mAsciiHost = aHostName;
    settings.mFingerprint = aFingerprint;
    settings.mDBKey = aDBKey;
  }

  return NS_OK;
}

void
nsClientAuthRememberService::GetEntryKey(
  const nsACString& aHostName,
  const OriginAttributes& aOriginAttributes,
  const nsACString& aFingerprint,
  nsACString& aEntryKey)
{
  nsAutoCString hostCert(aHostName);
  nsAutoCString suffix;
  aOriginAttributes.CreateSuffix(suffix);
  hostCert.Append(suffix);
  hostCert.Append(':');
  hostCert.Append(aFingerprint);

  aEntryKey.Assign(hostCert);
}
