//
// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2022
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include "td/net/SslStream.h"

#if !TD_EMSCRIPTEN
#include "td/utils/common.h"
#include "td/utils/crypto.h"
#include "td/utils/FlatHashMap.h"
#include "td/utils/logging.h"
#include "td/utils/misc.h"
#include "td/utils/port/IPAddress.h"
#include "td/utils/port/wstring_convert.h"
#include "td/utils/SliceBuilder.h"
#include "td/utils/Status.h"
#include "td/utils/Time.h"

#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/ssl.h>
#include <openssl/x509.h>
#include <openssl/x509v3.h>

#include <cstring>
#include <memory>
#include <mutex>

#if TD_PORT_WINDOWS
#include <wincrypt.h>
#endif

namespace td {

namespace detail {
namespace {
#if OPENSSL_VERSION_NUMBER < 0x10100000L
void *BIO_get_data(BIO *b) {
  return b->ptr;
}
void BIO_set_data(BIO *b, void *ptr) {
  b->ptr = ptr;
}
void BIO_set_init(BIO *b, int init) {
  b->init = init;
}

int BIO_get_new_index() {
  return 0;
}
BIO_METHOD *BIO_meth_new(int type, const char *name) {
  auto res = new BIO_METHOD();
  std::memset(res, 0, sizeof(*res));
  return res;
}

int BIO_meth_set_write(BIO_METHOD *biom, int (*bwrite)(BIO *, const char *, int)) {
  biom->bwrite = bwrite;
  return 1;
}
int BIO_meth_set_read(BIO_METHOD *biom, int (*bread)(BIO *, char *, int)) {
  biom->bread = bread;
  return 1;
}
int BIO_meth_set_ctrl(BIO_METHOD *biom, long (*ctrl)(BIO *, int, long, void *)) {
  biom->ctrl = ctrl;
  return 1;
}
int BIO_meth_set_create(BIO_METHOD *biom, int (*create)(BIO *)) {
  biom->create = create;
  return 1;
}
int BIO_meth_set_destroy(BIO_METHOD *biom, int (*destroy)(BIO *)) {
  biom->destroy = destroy;
  return 1;
}
#endif

int strm_create(BIO *b) {
  BIO_set_init(b, 1);
  return 1;
}

int strm_destroy(BIO *b) {
  return 1;
}

int strm_read(BIO *b, char *buf, int len);

int strm_write(BIO *b, const char *buf, int len);

long strm_ctrl(BIO *b, int cmd, long num, void *ptr) {
  switch (cmd) {
    case BIO_CTRL_FLUSH:
      return 1;
    case BIO_CTRL_PUSH:
    case BIO_CTRL_POP:
      return 0;
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
    case BIO_CTRL_GET_KTLS_SEND:
    case BIO_CTRL_GET_KTLS_RECV:
      return 0;
#endif
    default:
      LOG(FATAL) << b << " " << cmd << " " << num << " " << ptr;
  }
  return 1;
}

BIO_METHOD *BIO_s_sslstream() {
  static BIO_METHOD *result = [] {
    BIO_METHOD *res = BIO_meth_new(BIO_get_new_index(), "td::SslStream helper bio");
    BIO_meth_set_write(res, strm_write);
    BIO_meth_set_read(res, strm_read);
    BIO_meth_set_create(res, strm_create);
    BIO_meth_set_destroy(res, strm_destroy);
    BIO_meth_set_ctrl(res, strm_ctrl);
    return res;
  }();
  return result;
}

int verify_callback(int preverify_ok, X509_STORE_CTX *ctx) {
  if (!preverify_ok) {
    char buf[256];
    X509_NAME_oneline(X509_get_subject_name(X509_STORE_CTX_get_current_cert(ctx)), buf, 256);

    int err = X509_STORE_CTX_get_error(ctx);
    auto warning = PSTRING() << "verify error:num=" << err << ":" << X509_verify_cert_error_string(err)
                             << ":depth=" << X509_STORE_CTX_get_error_depth(ctx) << ":" << Slice(buf, std::strlen(buf));
    double now = Time::now();

    static std::mutex warning_mutex;
    {
      std::lock_guard<std::mutex> lock(warning_mutex);
      static FlatHashMap<string, double> next_warning_time;
      double &next = next_warning_time[warning];
      if (next <= now) {
        next = now + 300;  // one warning per 5 minutes
        LOG(WARNING) << warning;
      }
    }
  }

  return preverify_ok;
}

using SslCtx = std::shared_ptr<SSL_CTX>;

struct SslHandleDeleter {
  void operator()(SSL *ssl_handle) {
    auto start_time = Time::now();
    if (SSL_is_init_finished(ssl_handle)) {
      clear_openssl_errors("Before SSL_shutdown");
      SSL_set_quiet_shutdown(ssl_handle, 1);
      SSL_shutdown(ssl_handle);
      clear_openssl_errors("After SSL_shutdown");
    }
    SSL_free(ssl_handle);
    auto elapsed_time = Time::now() - start_time;
    if (elapsed_time >= 0.1) {
      LOG(ERROR) << "SSL_free took " << elapsed_time << " seconds";
    }
  }
};

using SslHandle = std::unique_ptr<SSL, SslHandleDeleter>;

Result<SslCtx> do_create_ssl_ctx(CSlice cert_file, SslStream::VerifyPeer verify_peer) {
  auto ssl_method =
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
      TLS_client_method();
#else
      SSLv23_client_method();
#endif
  if (ssl_method == nullptr) {
    return create_openssl_error(-6, "Failed to create an SSL client method");
  }
  auto ssl_ctx = SSL_CTX_new(ssl_method);
  if (!ssl_ctx) {
    return create_openssl_error(-7, "Failed to create an SSL context");
  }
  auto ssl_ctx_ptr = SslCtx(ssl_ctx, SSL_CTX_free);
  long options = 0;
#ifdef SSL_OP_NO_SSLv2
  options |= SSL_OP_NO_SSLv2;
#endif
#ifdef SSL_OP_NO_SSLv3
  options |= SSL_OP_NO_SSLv3;
#endif
  SSL_CTX_set_options(ssl_ctx, options);
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
  SSL_CTX_set_min_proto_version(ssl_ctx, TLS1_VERSION);
#endif
  SSL_CTX_set_mode(ssl_ctx, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER | SSL_MODE_ENABLE_PARTIAL_WRITE);

  if (cert_file.empty()) {
#if TD_PORT_WINDOWS
    LOG(DEBUG) << "Begin to load system store";
    auto flags = CERT_STORE_OPEN_EXISTING_FLAG | CERT_STORE_READONLY_FLAG | CERT_SYSTEM_STORE_CURRENT_USER;
    HCERTSTORE system_store =
        CertOpenStore(CERT_STORE_PROV_SYSTEM_W, X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, HCRYPTPROV_LEGACY(), flags,
                      static_cast<const void *>(to_wstring("ROOT").ok().c_str()));

    if (system_store) {
      X509_STORE *store = X509_STORE_new();

      for (PCCERT_CONTEXT cert_context = CertEnumCertificatesInStore(system_store, nullptr); cert_context != nullptr;
           cert_context = CertEnumCertificatesInStore(system_store, cert_context)) {
        const unsigned char *in = cert_context->pbCertEncoded;
        X509 *x509 = d2i_X509(nullptr, &in, static_cast<long>(cert_context->cbCertEncoded));
        if (x509 != nullptr) {
          if (X509_STORE_add_cert(store, x509) != 1) {
            auto error_code = ERR_peek_error();
            auto error = create_openssl_error(-20, "Failed to add certificate");
            if (ERR_GET_REASON(error_code) != X509_R_CERT_ALREADY_IN_HASH_TABLE) {
              LOG(ERROR) << error;
            } else {
              LOG(INFO) << error;
            }
          }

          X509_free(x509);
        } else {
          LOG(ERROR) << create_openssl_error(-21, "Failed to load X509 certificate");
        }
      }

      CertCloseStore(system_store, 0);

      SSL_CTX_set_cert_store(ssl_ctx, store);
      LOG(DEBUG) << "End to load system store";
    } else {
      LOG(ERROR) << create_openssl_error(-22, "Failed to open system certificate store");
    }
#else
    if (SSL_CTX_set_default_verify_paths(ssl_ctx) == 0) {
      auto error = create_openssl_error(-8, "Failed to load default verify paths");
      if (verify_peer == SslStream::VerifyPeer::On) {
        return std::move(error);
      } else {
        LOG(ERROR) << error;
      }
    }
#endif
  } else {
    if (SSL_CTX_load_verify_locations(ssl_ctx, cert_file.c_str(), nullptr) == 0) {
      return create_openssl_error(-8, "Failed to set custom certificate file");
    }
  }

  if (verify_peer == SslStream::VerifyPeer::On) {
    SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, verify_callback);

    constexpr int DEFAULT_VERIFY_DEPTH = 10;
    SSL_CTX_set_verify_depth(ssl_ctx, DEFAULT_VERIFY_DEPTH);
  } else {
    SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_NONE, nullptr);
  }

  string cipher_list;
  if (SSL_CTX_set_cipher_list(ssl_ctx, cipher_list.empty() ? "DEFAULT" : cipher_list.c_str()) == 0) {
    return create_openssl_error(-9, PSLICE() << "Failed to set cipher list \"" << cipher_list << '"');
  }

  return std::move(ssl_ctx_ptr);
}

Result<SslCtx> get_default_ssl_ctx() {
  static auto ctx = do_create_ssl_ctx("", SslStream::VerifyPeer::On);
  if (ctx.is_error()) {
    return ctx.error().clone();
  }

  return ctx.ok();
}

Result<SslCtx> get_default_unverified_ssl_ctx() {
  static auto ctx = do_create_ssl_ctx("", SslStream::VerifyPeer::Off);
  if (ctx.is_error()) {
    return ctx.error().clone();
  }

  return ctx.ok();
}

Result<SslCtx> create_ssl_ctx(CSlice cert_file, SslStream::VerifyPeer verify_peer) {
  if (cert_file.empty()) {
    if (verify_peer == SslStream::VerifyPeer::On) {
      return get_default_ssl_ctx();
    } else {
      return get_default_unverified_ssl_ctx();
    }
  }
  auto start_time = Time::now();
  auto result = do_create_ssl_ctx(cert_file, verify_peer);
  auto elapsed_time = Time::now() - start_time;
  if (elapsed_time >= 0.1) {
    LOG(ERROR) << "SSL context creation took " << elapsed_time << " seconds";
  }
  return result;
}

}  // namespace

class SslStreamImpl {
 public:
  Status init(CSlice host, CSlice cert_file, SslStream::VerifyPeer verify_peer, bool check_ip_address_as_host) {
    static bool init_openssl = [] {
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
      return OPENSSL_init_ssl(0, nullptr) != 0;
#else
      OpenSSL_add_all_algorithms();
      SSL_load_error_strings();
      return OpenSSL_add_ssl_algorithms() != 0;
#endif
    }();
    CHECK(init_openssl);

    clear_openssl_errors("Before SslFd::init");

    TRY_RESULT(ssl_ctx, create_ssl_ctx(cert_file, verify_peer));

    auto ssl_handle = SslHandle(SSL_new(ssl_ctx.get()));
    if (!ssl_handle) {
      return create_openssl_error(-13, "Failed to create an SSL handle");
    }

    auto r_ip_address = IPAddress::get_ip_address(host);

#if OPENSSL_VERSION_NUMBER >= 0x10002000L
    X509_VERIFY_PARAM *param = SSL_get0_param(ssl_handle.get());
    X509_VERIFY_PARAM_set_hostflags(param, 0);
    if (r_ip_address.is_ok() && !check_ip_address_as_host) {
      LOG(DEBUG) << "Set verification IP address to " << r_ip_address.ok().get_ip_str();
      X509_VERIFY_PARAM_set1_ip_asc(param, r_ip_address.ok().get_ip_str().c_str());
    } else {
      LOG(DEBUG) << "Set verification host to " << host;
      X509_VERIFY_PARAM_set1_host(param, host.c_str(), 0);
    }
#else
#warning DANGEROUS! HTTPS HOST WILL NOT BE CHECKED. INSTALL OPENSSL >= 1.0.2 OR IMPLEMENT HTTPS HOST CHECK MANUALLY
#endif

    auto *bio = BIO_new(BIO_s_sslstream());
    BIO_set_data(bio, static_cast<void *>(this));
    SSL_set_bio(ssl_handle.get(), bio, bio);

#if OPENSSL_VERSION_NUMBER >= 0x0090806fL && !defined(OPENSSL_NO_TLSEXT)
    if (r_ip_address.is_error()) {  // IP address must not be send as SNI
      LOG(DEBUG) << "Set SNI host name to " << host;
      auto host_str = host.str();
      SSL_set_tlsext_host_name(ssl_handle.get(), MutableCSlice(host_str).begin());
    }
#endif
    SSL_set_connect_state(ssl_handle.get());

    ssl_handle_ = std::move(ssl_handle);

    return Status::OK();
  }

  ByteFlowInterface &read_byte_flow() {
    return read_flow_;
  }
  ByteFlowInterface &write_byte_flow() {
    return write_flow_;
  }
  size_t flow_read(MutableSlice slice) {
    return read_flow_.read(slice);
  }
  size_t flow_write(Slice slice) {
    return write_flow_.write(slice);
  }

 private:
  SslHandle ssl_handle_;

  friend class SslReadByteFlow;
  friend class SslWriteByteFlow;

  Result<size_t> write(Slice slice) {
    clear_openssl_errors("Before SslFd::write");
    auto start_time = Time::now();
    auto size = SSL_write(ssl_handle_.get(), slice.data(), static_cast<int>(slice.size()));
    auto elapsed_time = Time::now() - start_time;
    if (elapsed_time >= 0.1) {
      LOG(ERROR) << "SSL_write of size " << slice.size() << " took " << elapsed_time << " seconds and returned " << size
                 << ' ' << SSL_get_error(ssl_handle_.get(), size);
    }
    if (size <= 0) {
      return process_ssl_error(size);
    }
    return size;
  }

  Result<size_t> read(MutableSlice slice) {
    clear_openssl_errors("Before SslFd::read");
    auto start_time = Time::now();
    auto size = SSL_read(ssl_handle_.get(), slice.data(), static_cast<int>(slice.size()));
    auto elapsed_time = Time::now() - start_time;
    if (elapsed_time >= 0.1) {
      LOG(ERROR) << "SSL_read took " << elapsed_time << " seconds and returned " << size << ' '
                 << SSL_get_error(ssl_handle_.get(), size);
    }
    if (size <= 0) {
      return process_ssl_error(size);
    }
    return size;
  }

  class SslReadByteFlow final : public ByteFlowBase {
   public:
    explicit SslReadByteFlow(SslStreamImpl *stream) : stream_(stream) {
    }
    bool loop() final {
      auto to_read = output_.prepare_append();
      auto r_size = stream_->read(to_read);
      if (r_size.is_error()) {
        finish(r_size.move_as_error());
        return false;
      }
      auto size = r_size.move_as_ok();
      if (size == 0) {
        return false;
      }
      output_.confirm_append(size);
      return true;
    }

    size_t read(MutableSlice data) {
      return input_->advance(min(data.size(), input_->size()), data);
    }

   private:
    SslStreamImpl *stream_;
  };

  class SslWriteByteFlow final : public ByteFlowBase {
   public:
    explicit SslWriteByteFlow(SslStreamImpl *stream) : stream_(stream) {
    }
    bool loop() final {
      auto to_write = input_->prepare_read();
      auto r_size = stream_->write(to_write);
      if (r_size.is_error()) {
        finish(r_size.move_as_error());
        return false;
      }
      auto size = r_size.move_as_ok();
      if (size == 0) {
        return false;
      }
      input_->confirm_read(size);
      return true;
    }

    size_t write(Slice data) {
      output_.append(data);
      return data.size();
    }

   private:
    SslStreamImpl *stream_;
  };

  SslReadByteFlow read_flow_{this};
  SslWriteByteFlow write_flow_{this};

  Result<size_t> process_ssl_error(int ret) {
    auto os_error = OS_ERROR("SSL_ERROR_SYSCALL");
    int error = SSL_get_error(ssl_handle_.get(), ret);
    switch (error) {
      case SSL_ERROR_NONE:
        LOG(ERROR) << "SSL_get_error returned no error";
        return 0;
      case SSL_ERROR_ZERO_RETURN:
        LOG(DEBUG) << "SSL_ZERO_RETURN";
        return 0;
      case SSL_ERROR_WANT_READ:
        LOG(DEBUG) << "SSL_WANT_READ";
        return 0;
      case SSL_ERROR_WANT_WRITE:
        LOG(DEBUG) << "SSL_WANT_WRITE";
        return 0;
      case SSL_ERROR_WANT_CONNECT:
      case SSL_ERROR_WANT_ACCEPT:
      case SSL_ERROR_WANT_X509_LOOKUP:
        LOG(DEBUG) << "SSL: CONNECT ACCEPT LOOKUP";
        return 0;
      case SSL_ERROR_SYSCALL:
        if (ERR_peek_error() == 0) {
          if (os_error.code() != 0) {
            LOG(DEBUG) << "SSL_ERROR_SYSCALL";
            return std::move(os_error);
          } else {
            LOG(DEBUG) << "SSL_SYSCALL";
            return 0;
          }
        }
        /* fallthrough */
      default:
        LOG(DEBUG) << "SSL_ERROR Default";
        return create_openssl_error(1, "SSL error ");
    }
  }
};

namespace {
int strm_read(BIO *b, char *buf, int len) {
  auto *stream = static_cast<SslStreamImpl *>(BIO_get_data(b));
  CHECK(stream != nullptr);
  BIO_clear_retry_flags(b);
  CHECK(buf != nullptr);
  auto res = narrow_cast<int>(stream->flow_read(MutableSlice(buf, len)));
  if (res == 0) {
    BIO_set_retry_read(b);
    return -1;
  }
  return res;
}
int strm_write(BIO *b, const char *buf, int len) {
  auto *stream = static_cast<SslStreamImpl *>(BIO_get_data(b));
  CHECK(stream != nullptr);
  BIO_clear_retry_flags(b);
  CHECK(buf != nullptr);
  return narrow_cast<int>(stream->flow_write(Slice(buf, len)));
}
}  // namespace

}  // namespace detail

SslStream::SslStream() = default;
SslStream::SslStream(SslStream &&) noexcept = default;
SslStream &SslStream::operator=(SslStream &&) noexcept = default;
SslStream::~SslStream() = default;

Result<SslStream> SslStream::create(CSlice host, CSlice cert_file, VerifyPeer verify_peer,
                                    bool use_ip_address_as_host) {
  auto impl = make_unique<detail::SslStreamImpl>();
  TRY_STATUS(impl->init(host, cert_file, verify_peer, use_ip_address_as_host));
  return SslStream(std::move(impl));
}
SslStream::SslStream(unique_ptr<detail::SslStreamImpl> impl) : impl_(std::move(impl)) {
}
ByteFlowInterface &SslStream::read_byte_flow() {
  return impl_->read_byte_flow();
}
ByteFlowInterface &SslStream::write_byte_flow() {
  return impl_->write_byte_flow();
}
size_t SslStream::flow_read(MutableSlice slice) {
  return impl_->flow_read(slice);
}
size_t SslStream::flow_write(Slice slice) {
  return impl_->flow_write(slice);
}

}  // namespace td

#else

namespace td {

namespace detail {
class SslStreamImpl {};
}  // namespace detail

SslStream::SslStream() = default;
SslStream::SslStream(SslStream &&) = default;
SslStream &SslStream::operator=(SslStream &&) = default;
SslStream::~SslStream() = default;

Result<SslStream> SslStream::create(CSlice host, CSlice cert_file, VerifyPeer verify_peer,
                                    bool check_ip_address_as_host) {
  return Status::Error("Not supported in emscripten");
}

SslStream::SslStream(unique_ptr<detail::SslStreamImpl> impl) : impl_(std::move(impl)) {
}

ByteFlowInterface &SslStream::read_byte_flow() {
  UNREACHABLE();
}

ByteFlowInterface &SslStream::write_byte_flow() {
  UNREACHABLE();
}

size_t SslStream::flow_read(MutableSlice slice) {
  UNREACHABLE();
}

size_t SslStream::flow_write(Slice slice) {
  UNREACHABLE();
}

}  // namespace td

#endif