/*
 * Copyright (c) 2021, 2024, Oracle and/or its affiliates.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2.0,
 * as published by the Free Software Foundation.
 *
 * This program is designed to work with certain software (including
 * but not limited to OpenSSL) that is licensed under separate terms,
 * as designated in a particular file or component or in included license
 * documentation.  The authors of MySQL hereby grant you an additional
 * permission to link the program and your derivative works with the
 * separately licensed software that they have either included with
 * the program or referenced in the documentation.
 *
 * This program is distributed in the hope that it will be useful,  but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
 * the GNU General Public License, version 2.0, for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include "mysqlshdk/libs/ssh/ssh_session.h"
#include <fcntl.h>
#include <functional>
#include <sstream>
#include <vector>
#include "mysqlshdk/include/scripting/shexcept.h"
#include "mysqlshdk/include/shellcore/console.h"
#include "mysqlshdk/libs/utils/logger.h"
#include "mysqlshdk/libs/utils/utils_file.h"
#include "mysqlshdk/libs/utils/utils_general.h"
#include "mysqlshdk/libs/utils/utils_string.h"
#include "mysqlshdk/shellcore/credential_manager.h"
#ifdef _MSC_VER
#include "Shlobj.h"
#endif  // _MSC_VER

namespace mysqlshdk {
namespace ssh {
namespace {

// lists of allowed SSH related algorithms (unaccetable ones explicitly omitted)
// according to policy

static const char k_kex_algorithm_allowed[] = {
    "curve25519-sha256,"   // recommended
    "ecdh-sha2-nistp384,"  // recommended
    "rsa2048-sha256,"      // recommended
    "ecdh-sha2-nistp256,"
    "ecdh-sha2-nistp521,"
    "curve448-sha512,"
    "diffie-hellman-group16-sha512,"
    "diffie-hellman-group18-sha512,"
    "diffie-hellman-group14-sha256,"
    "diffie-hellman-group15-sha512,"
    "diffie-hellman-group17-sha512,"
    "diffie-hellman-group-exchange-sha256,"
    "gss-group14-sha256-,"
    "gss-group16-sha512-,"
    "gss-nistp256-sha256-,"
    "gss-nistp384-sha384-,"
    "gss-curve25519-sha256-"};

static const char k_mac_allowed[] = {
    "hmac-sha2-384,"                  // recommended
    "hmac-sha384@ssh.com,"            // recommended (alt name)
    "hmac-sha2-512-etm@openssh.com,"  // recommended
    "hmac-sha2-256-etm@openssh.com,"  // recommended
    "umac-128-etm@openssh.com,"
    "hmac-sha2-512,"
    "hmac-sha2-256,"
    "umac-128@openssh.com"};

static const char k_host_key_allowed[] = {
    "ssh-ed25519,"                               // recommended
    "ssh-ed25519-cert-v01@openssh.com,"          // recommended (alt name)
    "ecdsa-sha2-nistp384,"                       // recommended
    "ecdsa-sha2-nistp384-cert-v01@openssh.com,"  // recommended (alt name)
    "rsa-sha2-512,"                              // recommended
    "rsa-sha2-512-cert-v01@openssh.com,"         // recommended (alt name)
    "ssh-ed448,"
    "rsa-sha2-256,"
    "rsa-sha2-256-cert-v01@openssh.com,"  // (alt name)
    "ecdsa-sha2-nistp256,"
    "ecdsa-sha2-nistp256-cert-v01@openssh.com,"  // (alt name)
    "ecdsa-sha2-nistp521,"
    "ecdsa-sha2-nistp521-cert-v01@openssh.com,"  // (alt name)
    "x509v3-rsa2048-sha256,"
    "x509v3-ecdsa-sha2-nistp256,"
    "x509v3-ecdsa-sha2-nistp384,"
    "x509v3-ecdsa-sha2-nistp521,"
    "ssh-rsa"  // (ssh2 version of ssh-rsa, not the one from ssh1)
};

static const char k_cipher_allowed[] = {
    "chacha20-poly1305@openssh.com,"  // recommended
    "aes256-gcm,"                     // recommended
    "AES256-Gcm@openssh.com,"         // recommended (alt name)
    "AEAD_AES_256_GCM,"               // recommended (alt name)
    "aes128-gcm,"                     // recommended
    "AES128-Gcm@openssh.com,"         // recommended (alt name)
    "AEAD_AES_128_GCM,"               // recommended (alt name)
    "aes256-ctr,"
    "aes192-ctr,"
    "aes128-ctr"};

static const char k_public_key_allowed[] = {
    "ssh-ed25519,"
    "ssh-ed25519-cert-v01@openssh.com,"
    "ecdsa-sha2-nistp384,"
    "ecdsa-sha2-nistp384-cert-v01@openssh.com,"
    "rsa-sha2-512,"
    "rsa-sha2-512-cert-v01@openssh.com,"
    "ssh-ed448,"
    "rsa-sha2-256,"
    "rsa-sha2-256-cert-v01@openssh.com,"
    "ecdsa-sha2-nistp256,"
    "ecdsa-sha2-nistp256-cert-v01@openssh.com,"
    "ecdsa-sha2-nistp521,"
    "ecdsa-sha2-nistp521-cert-v01@openssh.com,"
    "x509v3-rsa2048-sha256,"
    "x509v3-ecdsa-sha2-nistp256,"
    "x509v3-ecdsa-sha2-nistp384,"
    "x509v3-ecdsa-sha2-nistp521,"
    "ssh-rsa"  // (ssh2 version of ssh-rsa, not the one from ssh1)
};

int libssh_auth_callback(const char *prompt, char *buf, size_t len, int echo,
                         int UNUSED(verify), void *userdata) {
  std::string return_value;
  if (echo == 1) {
    if (mysqlsh::current_console()->prompt(prompt, &return_value) !=
        shcore::Prompt_result::Ok)
      return -1;
  } else {
    auto session = static_cast<Ssh_session *>(userdata);
    auto &options = session->get_options();

    if (strcmp(prompt, "Passphrase for private key:") == 0 &&
        options.has_keyfile_password()) {
      return_value = options.get_key_file_password();
    } else if (mysqlsh::current_console()->prompt_password(
                   prompt, &return_value) != shcore::Prompt_result::Ok) {
      return -1;
    }
  }
  strncpy(buf, return_value.data(),
          return_value.size() > len ? len : return_value.size());

  shcore::clear_buffer(return_value);
  return 0;
}
}  // namespace

Ssh_session::Ssh_session()
    : m_session{std::make_unique<::ssh::Session>()},
      m_is_connected(false),
      m_interactive(true) {
  init_libssh();

  memset(&m_ssh_callbacks, 0, sizeof(m_ssh_callbacks));
  m_ssh_callbacks.userdata = this;
  m_ssh_callbacks.auth_function = libssh_auth_callback;
  ssh_callbacks_init(&m_ssh_callbacks);
  auto rc = ssh_set_callbacks(m_session->getCSession(), &m_ssh_callbacks);

  if (rc == SSH_ERROR) throw std::runtime_error(m_session->getError());
}

Ssh_session::~Ssh_session() {}

std::tuple<Ssh_return_type, std::string> Ssh_session::connect(
    const Ssh_connection_options &config) {
  if (is_connected()) {
    throw std::logic_error(
        "Unable to connect already connected SSHSession, please disconnect "
        "first.");
  }

  // auto lock = lock_session();
  m_options = config;
  m_interactive = m_options.interactive();
  // We need to set the host before reading the config, otherwise we will get
  // error. This will be of course overridden by optionsParseconfig
  try {
    m_session->setOption(SSH_OPTIONS_HOST, m_options.get_host().c_str());
  } catch (::ssh::SshException &exc) {
    log_error(
        "SSH: session: Error setting remote host in ssh session option: %s",
        exc.getError().c_str());
    return std::make_tuple(Ssh_return_type::INVALID_AUTH_DATA, exc.getError());
  }

  // The default timeout of 10 seconds is set here, if different value is
  // needed, it should be set in the config file
  try {
    m_session->setOption(SSH_OPTIONS_TIMEOUT,
                         static_cast<long>(m_options.get_connection_timeout()));
  } catch (::ssh::SshException &exc) {
    log_error(
        "SSH: session: Error setting connection timeout in ssh session option: "
        "%s",
        exc.getError().c_str());
    return std::make_tuple(Ssh_return_type::INVALID_AUTH_DATA, exc.getError());
  }

  // We've already loaded the config  earlier
  // but it wasn't full data, here we do this again, overwriting what we
  // already have from the user and leave the rest like ciphers etc

  try {
    m_session->optionsParseConfig(m_options.has_config_file()
                                      ? m_options.get_config_file().c_str()
                                      : nullptr);

    Ssh_config_reader reader;

    reader.read(&m_config, m_options.get_host(),
                m_options.has_config_file() ? m_options.get_config_file() : "");
  } catch (::ssh::SshException &exc) {
    if (m_options.has_config_file()) {
      log_error("SSH: session: Unable to parse config file: %s\nError was: %s ",
                m_options.get_config_file().c_str(), exc.getError().c_str());
    } else {
      log_error(
          "SSH: session: Unable to parse default config file\nError was: %s ",
          exc.getError().c_str());
    }
  }

  // The option setter does't indicate what option failed to set, so we have to
  // wrap each call individually to be able to report exactly what failed.
  if (m_options.has_user()) {
    try {
      m_session->setOption(SSH_OPTIONS_USER, m_options.get_user().c_str());
    } catch (::ssh::SshException &exc) {
      log_error(
          "SSH: session: Error setting user name in ssh session option: %s",
          exc.getError().c_str());
      return std::make_tuple(Ssh_return_type::INVALID_AUTH_DATA,
                             exc.getError());
    }
  }

  if (m_options.has_key_file()) {
    try {
      m_session->setOption(SSH_OPTIONS_IDENTITY,
                           m_options.get_key_file().c_str());
    } catch (::ssh::SshException &exc) {
      log_error(
          "SSH: session: Error setting identity file in ssh session option: %s",
          exc.getError().c_str());
      return std::make_tuple(Ssh_return_type::INVALID_AUTH_DATA,
                             exc.getError());
    }
  }

  if (m_options.has_port()) {
    try {
      m_session->setOption(SSH_OPTIONS_PORT,
                           static_cast<long>(m_options.get_port()));
    } catch (::ssh::SshException &exc) {
      log_error(
          "SSH: session: Error setting remote port in ssh session option: %s",
          exc.getError().c_str());
      return std::make_tuple(Ssh_return_type::INVALID_AUTH_DATA,
                             exc.getError());
    }
  }

  set_algorithm_configs();

  m_options.dump_config();

  try {
    m_session->connect();
  } catch (::ssh::SshException &exc) {
    log_error(
        "SSH: session: Unable to establish SSH connection: %s:%d\nError was: "
        "%s",
        m_options.get_host().c_str(), m_options.get_port(),
        exc.getError().c_str());
    return std::make_tuple(Ssh_return_type::CONNECTION_FAILURE, exc.getError());
  }

  std::string fingerprint;
  int ret_val = verify_known_host(m_options, &fingerprint);
  switch (ret_val) {
    case SSH_SERVER_FILE_NOT_FOUND:
    case SSH_SERVER_NOT_KNOWN:
      return std::make_tuple(
          ret_val == SSH_SERVER_FILE_NOT_FOUND
              ? Ssh_return_type::FINGERPRINT_UNKNOWN_AUTH_FILE_MISSING
              : Ssh_return_type::FINGERPRINT_UNKNOWN,
          fingerprint);
    case SSH_SERVER_KNOWN_CHANGED:
      return std::make_tuple(Ssh_return_type::FINGERPRINT_CHANGED, fingerprint);
    case SSH_SERVER_FOUND_OTHER:
      return std::make_tuple(Ssh_return_type::FINGERPRINT_MISMATCH,
                             fingerprint);
    default:
      break;
  }

  try {
    authenticate_user(m_options);
  } catch (Ssh_auth_exception &sxc) {
    log_error("SSH: session: User authentication failed.");
    return std::make_tuple(Ssh_return_type::INVALID_AUTH_DATA,
                           std::string(sxc.what()));
  }

  m_is_connected = true;

  m_time_created = std::chrono::system_clock::now();

  return std::make_tuple(Ssh_return_type::CONNECTED, "");
}

void Ssh_session::disconnect() {
  log_debug2("SSH: session: disconnect");

  if (m_session != nullptr) {
    if (m_is_connected) m_session->disconnect();
    m_session = std::make_unique<::ssh::Session>();
  }
  // Now we should release the lock.
  m_is_connected = false;
}

bool Ssh_session::is_connected() const {
  return m_is_connected && ssh_is_connected(m_session->getCSession());
}

void Ssh_session::set_algorithm_configs() {
  auto set_option = [this](ssh_options_e option, const char *what,
                           const char *value) {
    try {
      m_session->setOption(option, value);
    } catch (::ssh::SshException &exc) {
      // not expected to happen
      log_error(
          "SSH: session: Error setting allowed %s list for ssh session: %s",
          what, exc.getError().c_str());
      throw;
    }
  };

  set_option(SSH_OPTIONS_CIPHERS_C_S, "ciphers", k_cipher_allowed);
  set_option(SSH_OPTIONS_CIPHERS_S_C, "ciphers", k_cipher_allowed);

  set_option(SSH_OPTIONS_KEY_EXCHANGE, "key exchange algorithm",
             k_kex_algorithm_allowed);

  set_option(SSH_OPTIONS_HMAC_C_S, "mac algorithm", k_mac_allowed);
  set_option(SSH_OPTIONS_HMAC_S_C, "mac algorithm", k_mac_allowed);

  set_option(SSH_OPTIONS_HOSTKEYS, "hostkey types", k_host_key_allowed);

  set_option(SSH_OPTIONS_PUBLICKEY_ACCEPTED_TYPES, "pubkey types",
             k_public_key_allowed);
}

const Ssh_connection_options &Ssh_session::config() const { return m_options; }

void Ssh_session::update_local_port(int port) {
  m_options.set_local_port(port);
}

bool Ssh_session::open_channel(::ssh::Channel *chann) {
  int rc = SSH_ERROR;
  std::size_t i = 0;
  while (i < m_options.get_connection_timeout()) {
    rc = ssh_channel_open_session(chann->getCChannel());
    // We can't rely on rc == 0 as there's possibility it will return 0 even
    // that the channel is closed. Because of this, we have to use isOpen()
    // and try few times
    if (rc == SSH_AGAIN || !chann->isOpen()) {
      log_debug3(
          "SSH: session: Unable to open channel, wait a moment and retry.");
      i++;
      std::this_thread::sleep_for(std::chrono::seconds(1));
    } else if (rc == SSH_ERROR) {
      log_error("SSH: session: Unable to open channel: %s",
                ssh_get_error(chann->getCSession()));
      return false;
    } else {
      log_debug("SSH: session: Channel successfully opened");
      return true;
    }
  }
  return false;
}

void Ssh_session::clean_connect() {
  if (!ssh_is_connected(m_session->getCSession())) {
    disconnect();
    connect(m_options);
  }
}

const char *Ssh_session::get_ssh_error() {
  if (m_session) {
    return m_session->getError();
  }
  return nullptr;
}

ssh_session Ssh_session::get_csession() {
  if (m_session) {
    return m_session->getCSession();
  }
  throw std::logic_error(
      "Trying to access SSH session which should be available but it's not");
}

std::unique_ptr<::ssh::Channel> Ssh_session::create_channel() {
  return std::make_unique<::ssh::Channel>(*(m_session));
}

int Ssh_session::verify_known_host(const ssh::Ssh_connection_options &config,
                                   std::string *fingerprint) {
  std::unique_ptr<unsigned char, void (*)(unsigned char *)> hash(
      nullptr, [](unsigned char *v) {
        if (v != nullptr) ssh_clean_pubkey_hash(&v);
      });
  ssh_key srv_pub_key;
  int rc = 0;
  std::size_t hlen = 0;
  errno = 0;
  rc = ssh_get_server_publickey(m_session->getCSession(), &srv_pub_key);
  if (rc < 0)
    throw Ssh_tunnel_exception("Can't get server pubkey " + get_error());

  unsigned char *hash_ptr = nullptr;
  errno = 0;
  rc = ssh_get_publickey_hash(srv_pub_key, SSH_PUBLICKEY_HASH_SHA256, &hash_ptr,
                              &hlen);
  ssh_key_free(srv_pub_key);
  if (rc < 0)
    throw Ssh_tunnel_exception("Can't calculate pubkey hash " + get_error());

  hash.reset(hash_ptr);

  std::unique_ptr<char, void (*)(char *)> hexa(ssh_get_hexa(hash.get(), hlen),
                                               [](char *ptr) { free(ptr); });
  *fingerprint = hexa.get();
  int ret_val = m_session->isServerKnown();
  switch (ret_val) {
    case SSH_SERVER_FILE_NOT_FOUND:
    case SSH_SERVER_NOT_KNOWN: {
      if (config.get_fingerprint().empty()) {
        return ret_val;
      } else {
        if (config.get_fingerprint() == hexa.get()) {
          m_session->writeKnownhost();
          return SSH_SERVER_KNOWN_OK;
        } else {
          return ret_val;
        }
      }
    }
    case SSH_SERVER_KNOWN_OK:
    case SSH_SERVER_KNOWN_CHANGED:
    case SSH_SERVER_FOUND_OTHER:
      return ret_val;
    case SSH_SERVER_ERROR:
      throw Ssh_tunnel_exception(m_session->getError());
  }

  return SSH_SERVER_KNOWN_OK;
}

namespace {
Ssh_auth_return try_auth(const std::function<Ssh_auth_return()> &fn) {
  Ssh_auth_return ret;
  int attempt = 0;
  do {
    ret = fn();
    if (ret != Ssh_auth_return::AUTH_DENIED) break;
    attempt++;
  } while (attempt < 3);
  return ret;
}
}  // namespace

void Ssh_session::authenticate_user(const ssh::Ssh_connection_options &config) {
  try {
    if (m_session->userauthNone() == SSH_AUTH_SUCCESS) {
      log_debug("SSH: session: Server accepts \"none\" auth method");
      return;
    }
    log_warning(
        "SSH: session: Failed authenticating with the \"none\" authentication "
        "method");
  } catch (::ssh::SshException &) {
    throw Ssh_tunnel_exception(ssh_get_error(m_session->getCSession()));
  }

  auto config_copy = config;
  int auth_list = m_session->getAuthList();

  ssh_set_blocking(m_session->getCSession(), 1);
  shcore::Scoped_callback scope(
      [this] { ssh_set_blocking(m_session->getCSession(), 0); });

  Ssh_auth_return ret_val = Ssh_auth_return::AUTH_NONE;

  if (auth_list == 0)
    throw std::runtime_error(
        "Unable to authenticate, no known authentication methods.");

  if (ret_val != Ssh_auth_return::AUTH_SUCCESS &&
      (auth_list & SSH_AUTH_METHOD_PUBLICKEY) &&
      m_config.auth_methods.is_set(Ssh_auth_methods::PUBKEY_AUTH)) {
    if (m_options.has_key_file()) {
      if (config_copy.get_key_encrypted()) {
        ret_val =
            try_auth([this, &config_copy]() { return auth_key(&config_copy); });
      } else {
        ret_val = auth_key(&config_copy);
      }
      if (ret_val != Ssh_auth_return::AUTH_SUCCESS) {
        log_warning(
            "SSH: session: Failed authenticating using the provided identity "
            "file");
      }

    } else {
      ret_val = auth_auto_pubkey();
      if (ret_val != Ssh_auth_return::AUTH_SUCCESS) {
        log_warning(
            "SSH: session: Failed authenticating using the default identity "
            "files");
      }
    }
  }

  if (ret_val != Ssh_auth_return::AUTH_SUCCESS &&
      (auth_list & SSH_AUTH_METHOD_PASSWORD) &&
      m_config.auth_methods.is_set(Ssh_auth_methods::PASSWORD_AUTH)) {
    ret_val = try_auth(
        [this, &config_copy]() { return auth_password(&config_copy); });
    if (ret_val != Ssh_auth_return::AUTH_SUCCESS) {
      log_warning("SSH: session: Failed authenticating using user/password");
    }
  }

  if (ret_val != Ssh_auth_return::AUTH_SUCCESS &&
      (auth_list & SSH_AUTH_METHOD_INTERACTIVE) &&
      m_config.auth_methods.is_set(Ssh_auth_methods::KBDINT_AUTH)) {
    if (!m_interactive)
      throw std::runtime_error(
          "Interactive auth mode is disabled when in disabled wizards mode.");
    ret_val = auth_interactive();
    if (ret_val != Ssh_auth_return::AUTH_SUCCESS) {
      log_warning(
          "SSH: session: Failed authenticating using interactive method");
    }
  }

  if (ret_val != Ssh_auth_return::AUTH_SUCCESS) {
    if (ret_val == Ssh_auth_return::AUTH_DENIED) {
      throw Ssh_auth_exception("Access denied");
    } else if (ret_val != Ssh_auth_return::AUTH_NONE) {
      throw std::runtime_error(
          "Unable to authenticate, all known authentication methods failed.");
    } else {
      throw std::runtime_error(
          "Unable to authenticate, no compatible authentication methods "
          "found.");
    }
  }
}

namespace {
int call_auth_in_a_loop(::ssh::Session *session,
                        const std::function<int()> &c) {
  try {
    int ret = 0;
    do {
      ret = c();

      if (ret == SSH_AUTH_AGAIN) {
        // There's a bug in libssh that it returns SSH_AUTH_AGAIN
        // in blocking mode even while it should return SSH_DENIED;
        // This has to be removed, once libssh bug will be fixed.
        if (!ssh_is_connected(session->getCSession()))
          throw std::runtime_error("Unable to authenticate, " +
                                   std::string(session->getError()));
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
      }
    } while (ret == SSH_AUTH_AGAIN);
    return ret;
  } catch (::ssh::SshException &sxc) {
    throw Ssh_auth_exception("Authentication failed: " + sxc.getError());
  }
}
}  // namespace

Ssh_auth_return Ssh_session::auth_agent(
    const ssh::Ssh_connection_options &config) {
  log_debug3("SSH trying auth_agent");
#ifdef _WIN32
  throw std::logic_error(
      "Ssh-agent functionality is not supported on this OS.");
#else
  return handle_auth_return(
      call_auth_in_a_loop(m_session.get(), [this, config]() {
        return ssh_userauth_agent(m_session->getCSession(), nullptr);
      }));
#endif
}

Ssh_auth_return Ssh_session::auth_password(
    ssh::Ssh_connection_options *config) {
  Ssh_auth_return ret_val = ssh::Ssh_auth_return::AUTH_NONE;
  log_debug3("SSH trying auth_pass");
  // here we need to take a pw from the user
  bool from_credential_manager = false;
  if (!config->has_password()) {
    bool ret = shcore::Credential_manager::get().get_password(config);
    if (ret && config->has_password()) from_credential_manager = true;

    if (!config->has_password()) {
      std::string passphrase;
      if (m_interactive) {
        if (mysqlsh::current_console()->prompt_password(
                "Please provide the password for " + config->as_uri() + ": ",
                &passphrase) == shcore::Prompt_result::Cancel)
          throw shcore::cancelled("User cancelled");
      }
      config->set_password(passphrase);
      shcore::clear_buffer(passphrase);
    }
  }
  if (config->has_password()) {
    ret_val = handle_auth_return(
        call_auth_in_a_loop(m_session.get(), [this, config]() {
          return m_session->userauthPassword(config->get_password().c_str());
        }));

    if (ret_val == Ssh_auth_return::AUTH_DENIED) {
      bool had_password = config->has_password();
      config->clear_password();
      std::string error_msg =
          "SSH Access denied, connection to \"" +
          config->as_uri(mysqlshdk::db::uri::formats::user_transport()) +
          "\" could not be established.";

      if (from_credential_manager) {
        shcore::Credential_manager::get().remove_password(*config);
        error_msg.append(" Invalid password has been erased.");
      }
      mysqlsh::current_console()->print_error(error_msg);

      // We throw exception in case it was given as a part of uri
      // so the outer loop won't try to Auth using credential store.
      if (had_password) throw Ssh_auth_exception("Access denied");
    } else {
      if (!from_credential_manager)
        shcore::Credential_manager::get().save_password(*config);
    }
  } else {
    throw Ssh_auth_exception("Access denied");
  }
  return ret_val;
}

Ssh_auth_return Ssh_session::auth_auto_pubkey() {
  log_debug3("SSH trying auth_auto");

  return handle_auth_return(call_auth_in_a_loop(m_session.get(), [this]() {
    return m_session->userauthPublickeyAuto();
  }));
}

Ssh_auth_return Ssh_session::auth_key(ssh::Ssh_connection_options *config) {
  log_debug3("SSH trying auth_key");
  ssh_key priv_key;
  if (!shcore::is_file(config->get_key_file()))
    throw std::runtime_error("The key file does not exist.");

  std::string keypass;
  bool got_password = false;
  bool from_manager = false;
  if (config->get_key_encrypted()) {
    if (config->has_keyfile_password()) {
      keypass = config->get_key_file_password();
    } else {
      try {
        got_password = shcore::Credential_manager::get().get_credential(
            config->key_file_uri(), &keypass);
        from_manager = true;
      } catch (shcore::Exception &re) {
        mysqlsh::current_console()->print_error(
            "Unable to get credentials for " + config->key_file_uri() +
            " error was " + re.what());
      }

      if (!got_password && m_interactive &&
          (mysqlsh::current_console()->prompt_password(
               "Please provide key passphrase for " + config->get_key_file() +
                   ": ",
               &keypass) == shcore::Prompt_result::Cancel)) {
        throw shcore::cancelled("User cancelled");
      }
    }
  }

  auto ret = ssh_pki_import_privkey_file(config->get_key_file().c_str(),
                                         keypass.c_str(), libssh_auth_callback,
                                         nullptr, &priv_key);

  if (ret == SSH_OK) {
    shcore::Scoped_callback scope([&priv_key] { ssh_key_free(priv_key); });
    auto auth_info = call_auth_in_a_loop(m_session.get(), [this, priv_key]() {
      return m_session->userauthPublickey(priv_key);
    });

    if ((auth_info == SSH_AUTH_SUCCESS || auth_info == SSH_AUTH_PARTIAL) &&
        !keypass.empty()) {
      config->set_key_file_password(keypass);
      try {
        if (!got_password &&
            shcore::Credential_manager::get().should_save_password(
                config->key_file_uri()))
          shcore::Credential_manager::get().store_credential(
              config->key_file_uri(), keypass);
      } catch (shcore::Exception &re) {
        mysqlsh::current_console()->print_error(
            "An error occurred while storing the passphrase for the SSH "
            "identity file: " +
            std::string(re.what()));
      }
    }

    return handle_auth_return(auth_info);
  } else {
    log_error("SSH: session: %s", m_session->getError());
    // else is SSH_EOF according to docs it means, missing keyfile or
    // permission denied either way we move on

    if (shcore::is_file(config->get_key_file())) {
      if (from_manager && got_password) {
        shcore::Credential_manager::get().delete_credential(
            config->key_file_uri());
        mysqlsh::current_console()->print_error(
            "SSH Access denied, connection to \"" +
            config->as_uri(mysqlshdk::db::uri::formats::user_transport()) +
            "\" could not be established. Invalid password has been erased.");
      }
      return Ssh_auth_return::AUTH_DENIED;
    } else {
      throw std::invalid_argument("The file " + config->get_key_file() +
                                  " doesn't exist.");
    }
  }
}
namespace {
shcore::Prompt_result prompt_user(const std::string &prompt, std::string *reply,
                                  bool echo) {
  return echo ? mysqlsh::current_console()->prompt(prompt, reply)
              : mysqlsh::current_console()->prompt_password(prompt, reply);
}
}  // namespace

Ssh_auth_return Ssh_session::auth_interactive() {
  log_debug3("SSH trying auth_interactive");
  Ssh_auth_return ret_val = Ssh_auth_return::AUTH_NONE;
  while (m_session->userauthKbdint(nullptr, nullptr) == SSH_AUTH_INFO) {
    for (auto i = 0; i < m_session->userauthKbdintGetNPrompts(); i++) {
      char echo;
      auto prompt =
          ssh_userauth_kbdint_getprompt(m_session->getCSession(), i, &echo);
      if (prompt == nullptr) {
        throw Ssh_auth_exception("Unknown authentication error: " +
                                 std::string(m_session->getError()));
      }

      std::string reply;
      if (prompt_user(prompt, &reply, echo) == shcore::Prompt_result::Cancel) {
        throw shcore::cancelled("User cancelled");
      }

      shcore::on_leave_scope cleanup([&reply]() {
        if (!reply.empty()) shcore::clear_buffer(reply);
      });
      try {
        if (m_session->userauthKbdintSetAnswer(i, reply.c_str()) == 0) {
          ret_val = Ssh_auth_return::AUTH_SUCCESS;
        } else {
          throw Ssh_auth_exception("An error occurred: " +
                                   std::string(m_session->getError()));
        }
      } catch (::ssh::SshException &e) {
        throw Ssh_auth_exception(e.getError());
      }
    }
  }
  return ret_val;
}

Ssh_auth_return Ssh_session::handle_auth_return(int auth) {
  switch (auth) {
    case SSH_AUTH_DENIED:
      return Ssh_auth_return::AUTH_DENIED;
    case SSH_AUTH_SUCCESS:
      return Ssh_auth_return::AUTH_SUCCESS;
    case SSH_AUTH_PARTIAL:
      return Ssh_auth_return::AUTH_PARTIAL;
    case SSH_AUTH_INFO:
      return Ssh_auth_return::AUTH_INFO;
    default:
      if (!ssh_is_connected(m_session->getCSession()))
        throw std::runtime_error("Unable to authenticate, " +
                                 std::string(m_session->getError()));

      throw Ssh_auth_exception("Unknown authentication error (" +
                               std::to_string(auth) + ").");
  }
}
}  // namespace ssh
}  // namespace mysqlshdk
