/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#pragma once

#include <folly/SharedMutex.h>
#include <folly/io/async/EventBase.h>
#include <folly/io/async/SSLContext.h>

#include <glog/logging.h>
#include <wangle/acceptor/SSLContextSelectionMisc.h>
#include <wangle/ssl/PasswordInFileFactory.h>
#include <wangle/ssl/SNIConfig.h>
#include <wangle/ssl/SSLContextConfig.h>
#include <wangle/ssl/SSLSessionCacheManager.h>
#include <wangle/ssl/TLSTicketKeySeeds.h>
#include <list>
#include <memory>
#include <vector>

namespace folly {

class SocketAddress;
class SSLContext;

} // namespace folly

namespace wangle {

class ClientHelloExtStats;
struct SSLCacheOptions;
class SSLStats;
class TLSTicketKeyManager;
struct TLSTicketKeySeeds;
class ServerSSLContext;

struct SSLContextManagerSettings {
  SSLContextManagerSettings() = default;

  SSLContextManagerSettings& setStrict(bool isStrict) {
    strict = isStrict;
    return *this;
  }

  SSLContextManagerSettings& setEnableSNICallback(bool isEnabled) {
    enableOpenSSLServerNameCallback = isEnabled;
    return *this;
  }

  /**
   * If true, throws exceptions when a context creation fails.
   */
  bool strict{true};

  /**
   * If true, uses Open SSL ServerNameIndication (SNI) extension
   * callback to switch context based on SNI.
   * Set to false when
   * 1- using Fizz fizzHandshakeAttempFallback callback to select
   * context
   * 2- you don't need to support SNI e.g., manager with single context
   */
  bool enableOpenSSLServerNameCallback{true};
};

// SSLContextManager represents all of the different server side
// SSL configurations behind a VIP.
//
// One VIP can serve multiple, independent TLS configurations by
// selecting a configuration by the ServerNameIndication extension
// on the initial ClientHello.
//
// There is 1 SSLContextManager per acceptor.
class SSLContextManager {
 private:
  // SslContexts is an internal, private structure used for
  // storing all of the different TLS configurations behind
  // an acceptor.
  //
  //   1 Acceptor          : 1 SSLContextManager
  //   1 SSLContextManager : N SSLContexts.
  //
  // Declaration is in SSLContextManager.cpp
  class SslContexts;

 public:
  /**
   * Provide ability to perform explicit client certificate
   * verification
   * If this callback is used, it becomes the responsibility
   * of the callback to ensure that any verification is done (e.g. via
   * SSL_CTX_set_verify(...). We remove the verification option
   * on the SSLContext wrapper so that no other layer decides to set its own
   * verification callback overriding the callbacks behavior. E.g.
   * AsyncSSLSocket's verify callback wiping out any existing cb set.
   */
  struct ClientCertVerifyCallback {
    // no-op. Should be overridden if actual
    // verification is required. This should assign the callback functions
    // to the context, without altering the callback itself.
    virtual void attachSSLContext(
        const std::shared_ptr<folly::SSLContext>& sslCtx) const = 0;
    virtual ~ClientCertVerifyCallback() = default;
  };

  SSLContextManager(
      const std::string& vipName,
      SSLContextManagerSettings settings,
      SSLStats* stats);

  virtual ~SSLContextManager();

  /**
   * Add a new X509 to SSLContextManager.  The details of a X509
   * is passed as a SSLContextConfig object.
   *
   * @param ctxConfig     Details of a X509, its private key, password, etc.
   * @param cacheOptions  Options for how to do session caching.
   * @param ticketSeeds   If non-null, the initial ticket key seeds to use.
   * @param vipAddress    Which VIP are the X509(s) used for? It is only for
   *                      for user friendly log message
   * @param externalCache Optional external provider for the session cache;
   *                      may be null
   */
  void addSSLContextConfig(
      const SSLContextConfig& ctxConfig,
      const SSLCacheOptions& cacheOptions,
      const TLSTicketKeySeeds* ticketSeeds,
      const folly::SocketAddress& vipAddress,
      const std::shared_ptr<SSLCacheProvider>& externalCache);

  /**
   * Adds a list of domain associated with a new X509 to SSLContextManager.
   * The details of a X509 is passed as a SSLContextConfig object.
   *
   * @param snis          The list of SNIs associated with the given X509.
   * @param ctxConfig     Details of a X509, its private key, password, etc.
   * @param cacheOptions  Options for how to do session caching.
   * @param ticketSeeds   If non-null, the initial ticket key seeds to use.
   * @param vipAddress    Which VIP are the X509(s) used for? It is only for
   *                      for user friendly log message
   * @param externalCache Optional external provider for the session cache;
   *                      may be null
   */
  void addSSLContextConfig(
      const std::vector<std::string>& snis,
      const SSLContextConfig& ctxConfig,
      const SSLCacheOptions& cacheOptions,
      const TLSTicketKeySeeds* ticketSeeds,
      const folly::SocketAddress& vipAddress,
      const std::shared_ptr<SSLCacheProvider>& externalCache);

  /**
   * Resets SSLContextManager with new X509s
   *
   * @param ctxConfigs    Details of a X509s, private key, password, etc.
   * @param cacheOptions  Options for how to do session caching.
   * @param ticketSeeds   If non-null, and ticketHandler_ is a
   *                      TLSTicketKeyManager, the initial ticket key seeds to
   *                      use.
   * @param vipAddress    Which VIP are the X509(s) used for? It is only for
   *                      for user friendly log message
   * @param externalCache Optional external provider for the session cache;
   *                      may be null
   */
  void resetSSLContextConfigs(
      const std::vector<SSLContextConfig>& ctxConfigs,
      const std::vector<SNIConfig>& sniConfigs,
      const SSLCacheOptions& cacheOptions,
      const TLSTicketKeySeeds* ticketSeeds,
      const folly::SocketAddress& vipAddress,
      const std::shared_ptr<SSLCacheProvider>& externalCache);

  /**
   * Remove SSLContextConfig of the given key. Note that to remove the context
   * for wildcard domain, call either
   * removeSSLContextConfigByDomainName("*.example.com") or
   * removeSSLContextConfig(SSLContextKey(".example.com")).
   */
  void removeSSLContextConfigByDomainName(const std::string& domainName);
  void removeSSLContextConfig(const SSLContextKey& key);

  /**
   * Clears all ssl contexts
   */
  void clear();

  /**
   * Get the default SSL_CTX for a VIP
   */
  std::shared_ptr<folly::SSLContext> getDefaultSSLCtx() const;

  /**
   * Gets the SSL_CTX for connection requests that does not
   * present SNI extension if a no SNI context configured.
   * Returns nullptr otherwise.
   */
  std::shared_ptr<folly::SSLContext> getNoSNICtx() const;

  /**
   * Gets the SSLContext for the given SNI.
   * Searches first by exact domain, then by one level up.
   * Returns nullptr if nothing matches.
   */
  std::shared_ptr<folly::SSLContext> getSSLCtx(const std::string& sni) const;

  /**
   * Gets the SSLContext for the given key.
   */
  std::shared_ptr<folly::SSLContext> getSSLCtx(const SSLContextKey& key) const;

  /**
   * Search by the _one_ level up subdomain
   */
  std::shared_ptr<folly::SSLContext> getSSLCtxBySuffix(
      const SSLContextKey& key) const;

  /**
   * Search by the full-string domain name
   */
  std::shared_ptr<folly::SSLContext> getSSLCtxByExactDomain(
      const SSLContextKey& key) const;

  /**
   * Updates ticket keys in ticket handler if using TLSTicketKeyManager,
   * otherwise a no-op.
   */
  void reloadTLSTicketKeys(
      const std::vector<std::string>& oldSeeds,
      const std::vector<std::string>& currentSeeds,
      const std::vector<std::string>& newSeeds);

  void setSSLStats(SSLStats* stats) {
    stats_ = stats;
  }

  /**
   * SSLContextManager only collects SNI stats now
   */
  void setClientHelloExtStats(ClientHelloExtStats* stats);

  ClientHelloExtStats* getClientHelloExtStats() const;

  /*
   * Please read class header before setting this callback, it may have
   * unintended conseqeunces, as you are now fully responsible for any
   * verification.
   */
  void setClientVerifyCallback(std::unique_ptr<ClientCertVerifyCallback> cb) {
    clientCertVerifyCallback_ = std::move(cb);
  }

  void setPasswordFactory(std::shared_ptr<PasswordInFileFactory> factory) {
    passwordFactory_ = std::move(factory);
  }

 protected:
  // Return value indicates if any certificates were loaded. If not, the cert
  // manager skips this context config. Allows for contexts using certs only
  // supported by TLS 1.3/Fizz. Note: This is different than an error
  // occurring. If an error occurs, it ought to throw in here. Returning false
  // means the context is empty *due to a policy decision*.
  virtual bool loadCertKeyPairsInSSLContext(
      const std::shared_ptr<folly::SSLContext>&,
      const SSLContextConfig&,
      std::string& commonName) const;

  virtual bool loadCertKeyPairsInSSLContextExternal(
      const std::shared_ptr<folly::SSLContext>&,
      const SSLContextConfig&,
      std::string& /* commonName */) const {
    LOG(FATAL) << "Unsupported in base SSLContextManager";
    // unreachable
    return false;
  }

  virtual void overrideConfiguration(
      const std::shared_ptr<folly::SSLContext>&,
      const SSLContextConfig&) const {}

  std::string vipName_;
  SSLStats* stats_{nullptr};

  /**
   * Insert a SSLContext by domain name.
   */
  void insertSSLCtxByDomainName(
      const std::string& dn,
      std::shared_ptr<folly::SSLContext> sslCtx,
      bool defaultFallback = false);

  void loadCertsFromFiles(
      const std::shared_ptr<folly::SSLContext>& sslCtx,
      const SSLContextConfig::CertificateInfo& cert) const;

  /**
   * Helper used to verify that all certificates installed for a single
   * `folly::SSLContext` convey the same identities (with the possibility of
   * different SubjectPublicKeyInfos)
   */
  void verifyCertNames(
      const std::shared_ptr<folly::SSLContext>& sslCtx,
      const std::string& description,
      std::string& groupIdentity,
      std::unique_ptr<std::list<std::string>>& subjectAltName,
      const std::string& lastCertPath,
      bool firstCert) const;

 private:
  SSLContextManager(const SSLContextManager&) = delete;

  std::shared_ptr<SslContexts> contexts_;
  ClientHelloExtStats* clientHelloTLSExtStats_{nullptr};
  bool strict_{true};
  bool enableOpenSSLServerNameCallback_{true};
  std::unique_ptr<ClientCertVerifyCallback> clientCertVerifyCallback_{nullptr};
  std::shared_ptr<ServerSSLContext> defaultCtx_;
  std::shared_ptr<PasswordInFileFactory> passwordFactory_{nullptr};
};

} // namespace wangle
