modules/mod_utils.cc (502 lines of code) (raw):
/*
* Copyright (c) 2017, 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 "modules/mod_utils.h"
#include <map>
#include <string>
#include <utility>
#include "mysqlshdk/include/scripting/obj_date.h"
#include "mysqlshdk/include/scripting/type_info/custom.h"
#include "mysqlshdk/include/scripting/types.h"
#include "mysqlshdk/include/shellcore/base_shell.h"
#include "mysqlshdk/include/shellcore/console.h"
#include "mysqlshdk/include/shellcore/interrupt_handler.h"
#include "mysqlshdk/include/shellcore/shell_options.h"
#include "mysqlshdk/libs/db/mysql/session.h"
#include "mysqlshdk/libs/db/mysqlx/session.h"
#include "mysqlshdk/libs/utils/fault_injection.h"
#include "mysqlshdk/libs/utils/utils_general.h"
#include "mysqlshdk/libs/utils/utils_path.h"
#include "mysqlshdk/libs/utils/utils_string.h"
#include "mysqlshdk/shellcore/credential_manager.h"
namespace mysqlsh {
namespace {
/**
* Retrieves the connection data from a String
*
* @param String containing the connection data
*
* @return a Connection_options object with the connection data
*/
Connection_options get_connection_options(const std::string &instance_def) {
if (instance_def.empty()) throw std::invalid_argument("Invalid URI: empty.");
auto ret_val = shcore::get_connection_options(instance_def, false);
for (const auto &warning : ret_val.get_warnings())
mysqlsh::current_console()->print_warning(warning);
ret_val.clear_warnings();
return ret_val;
}
/**
* Retrieves the connection data from a Dictionary
*
* @param dictionary containing the connection data
*
* @return a Connection_options object with the connection data
*/
Connection_options get_connection_options(
const shcore::Dictionary_t &instance_def) {
if (instance_def == nullptr || instance_def->size() == 0) {
throw std::invalid_argument(
"Invalid connection options, no options provided.");
}
shcore::Argument_map connection_map(*instance_def);
std::set<std::string> mandatory;
// host defaults to localhost, so it's not mandatory
// if (!connection_map.has_key(mysqlshdk::db::kSocket)) {
// mandatory.insert(mysqlshdk::db::kHost);
// }
Connection_options ret_val;
const auto case_sensitive =
ret_val.get_mode() == mysqlshdk::db::Comparison_mode::CASE_SENSITIVE;
connection_map.ensure_keys(mandatory, mysqlshdk::db::connection_attributes(),
"connection options", case_sensitive);
// First we load uri if possible, so we can override it later with explicit
// options.
for (auto &option : *instance_def) {
if (ret_val.compare(option.first, mysqlshdk::db::kUri) == 0) {
ret_val = Connection_options(connection_map.string_at(option.first));
break;
}
}
for (auto &option : *instance_def) {
// SSH URI options are all lower-case
if (mysqlshdk::db::ssh_uri_connection_attributes.find(
case_sensitive ? option.first : shcore::str_lower(option.first)) !=
mysqlshdk::db::ssh_uri_connection_attributes.end()) {
continue; // those will be handled in different place
} else if (ret_val.compare(option.first, mysqlshdk::db::kUri) == 0) {
continue; // those were already handled
} else if (ret_val.compare(option.first, mysqlshdk::db::kHost) == 0) {
const auto &host = connection_map.string_at(option.first);
if (host.empty()) {
throw std::invalid_argument("Host value cannot be an empty string.");
}
ret_val.set(option.first, host);
} else if (ret_val.compare(option.first, mysqlshdk::db::kPort) == 0) {
ret_val.set_port(connection_map.int_at(option.first));
} else if (ret_val.compare(option.first, mysqlshdk::db::kSocket) == 0) {
const auto &sock = connection_map.string_at(option.first);
#ifdef _WIN32
ret_val.set_pipe(sock);
#else // !_WIN32
ret_val.set_socket(sock);
#endif // !_WIN32
} else if (ret_val.compare(option.first,
mysqlshdk::db::kCompressionLevel) == 0) {
ret_val.set_compression_level(connection_map.int_at(option.first));
} else if (ret_val.compare(option.first, mysqlshdk::db::kConnectTimeout) ==
0) {
// Additional connection options are internally stored as strings.
// Even so, when given in a dictionary, the connect-timeout option
// must be given as an integer value
if (connection_map.at(option.first).type != shcore::Integer) {
mysqlshdk::db::Connection_options::throw_invalid_connect_timeout(
connection_map.at(option.first).descr());
} else {
ret_val.set(option.first, connection_map.at(option.first).descr());
}
} else if (ret_val.compare(option.first,
mysqlshdk::db::kConnectionAttributes) == 0) {
if (option.second.type == shcore::Map) {
// Supports connection-attributes: {key:val, key:val, ...}
auto map = option.second.as_map();
for (const auto &entry : *map) {
ret_val.set_connection_attribute(entry.first, entry.second.descr());
}
} else if (option.second.type == shcore::Array) {
// Supports connection-attributes: ["key=val", "key=val", ...]
auto array = option.second.as_array();
std::vector<std::string> values;
for (const auto &entry : *array) {
values.push_back(entry.descr());
}
ret_val.set_connection_attributes(values);
} else {
// Supports connection-attributes=false|true|1|0
ret_val.set(mysqlshdk::db::kConnectionAttributes,
option.second.descr());
}
} else {
ret_val.set(option.first, connection_map.string_at(option.first));
}
}
// The ssh has to be set as the last step cause we need the host and port to
// be set, otherwise we can't use the tunneling.
auto ssh_data = get_ssh_options(instance_def);
// don't use has_data here as there's no remote_host yet in the
// ssh_connection_options, those will be set inside of the set_ssh_options.
if (ssh_data.has_host()) ret_val.set_ssh_options(ssh_data);
for (const auto &warning : ret_val.get_warnings())
mysqlsh::current_console()->print_warning(warning);
ret_val.clear_warnings();
return ret_val;
}
} // namespace
Connection_options get_connection_options(const shcore::Value &v) {
if (shcore::String == v.type) {
return get_connection_options(v.get_string());
} else if (shcore::Map == v.type) {
return get_connection_options(v.as_map());
} else {
throw shcore::Exception::type_error(
"Invalid connection options, expected either a URI or a Connection "
"Options Dictionary");
}
}
mysqlshdk::ssh::Ssh_connection_options get_ssh_options(
const shcore::Dictionary_t &instance_def) {
if (instance_def == nullptr || instance_def->size() == 0) {
throw std::invalid_argument("Invalid SSH options, no options provided.");
}
shcore::Argument_map connection_map(*instance_def);
mysqlshdk::ssh::Ssh_connection_options ssh_config;
for (auto &option : *instance_def) {
if (ssh_config.compare(option.first, mysqlshdk::db::kSsh) == 0) {
ssh_config = mysqlshdk::ssh::Ssh_connection_options(
connection_map.string_at(option.first));
} else if (ssh_config.compare(option.first,
mysqlshdk::db::kSshConfigFile) == 0) {
ssh_config.set_config_file(connection_map.string_at(option.first));
} else if (ssh_config.compare(option.first,
mysqlshdk::db::kSshIdentityFile) == 0) {
ssh_config.set_key_file(connection_map.string_at(option.first));
} else if (ssh_config.compare(option.first,
mysqlshdk::db::kSshIdentityFilePassword) ==
0) {
ssh_config.set_key_file_password(connection_map.string_at(option.first));
} else if (ssh_config.compare(option.first, mysqlshdk::db::kSshPassword) ==
0) {
ssh_config.set_password(connection_map.string_at(option.first));
}
}
return ssh_config;
}
void set_password_from_string(Connection_options *options,
const char *password) {
if (options && password) {
options->set_password(password);
}
}
shcore::Value::Map_type_ref get_connection_map(
const mysqlshdk::db::Connection_options &connection_options) {
shcore::Value::Map_type_ref map(new shcore::Value::Map_type());
if (connection_options.has_scheme())
(*map)[mysqlshdk::db::kScheme] =
shcore::Value(connection_options.get_scheme());
if (connection_options.has_user())
(*map)[mysqlshdk::db::kUser] = shcore::Value(connection_options.get_user());
if (connection_options.has_password())
(*map)[mysqlshdk::db::kPassword] =
shcore::Value(connection_options.get_password());
if (connection_options.has_host())
(*map)[mysqlshdk::db::kHost] = shcore::Value(connection_options.get_host());
if (connection_options.has_port())
(*map)[mysqlshdk::db::kPort] = shcore::Value(connection_options.get_port());
if (connection_options.has_schema())
(*map)[mysqlshdk::db::kSchema] =
shcore::Value(connection_options.get_schema());
if (connection_options.has_socket())
(*map)[mysqlshdk::db::kSocket] =
shcore::Value(connection_options.get_socket());
// Yes: on socket, is not a typo
if (connection_options.has_pipe())
(*map)[mysqlshdk::db::kSocket] =
shcore::Value(connection_options.get_pipe());
auto ssl = connection_options.get_ssl_options();
if (ssl.has_data()) {
if (ssl.has_ca())
(*map)[mysqlshdk::db::kSslCa] = shcore::Value(ssl.get_ca());
if (ssl.has_capath())
(*map)[mysqlshdk::db::kSslCaPath] = shcore::Value(ssl.get_capath());
if (ssl.has_cert())
(*map)[mysqlshdk::db::kSslCert] = shcore::Value(ssl.get_cert());
if (ssl.has_cipher())
(*map)[mysqlshdk::db::kSslCipher] = shcore::Value(ssl.get_cipher());
if (ssl.has_crl())
(*map)[mysqlshdk::db::kSslCrl] = shcore::Value(ssl.get_crl());
if (ssl.has_crlpath())
(*map)[mysqlshdk::db::kSslCrlPath] = shcore::Value(ssl.get_crlpath());
if (ssl.has_key())
(*map)[mysqlshdk::db::kSslKey] = shcore::Value(ssl.get_key());
if (ssl.has_mode())
(*map)[mysqlshdk::db::kSslMode] =
shcore::Value(ssl.get_value(mysqlshdk::db::kSslMode));
if (ssl.has_tls_version())
(*map)[mysqlshdk::db::kSslTlsVersion] =
shcore::Value(ssl.get_tls_version());
if (ssl.has_tls_ciphersuites())
(*map)[mysqlshdk::db::kSslTlsCiphersuites] =
shcore::Value(ssl.get_tls_ciphersuites());
}
for (auto &option : connection_options.get_extra_options()) {
if (!option.second.has_value())
(*map)[option.first] = shcore::Value();
else {
if (shcore::str_caseeq(option.first, mysqlshdk::db::kConnectTimeout)) {
(*map)[option.first] =
shcore::Value(std::atoi((*option.second).c_str()));
} else {
(*map)[option.first] = shcore::Value(*option.second);
}
}
}
if (connection_options.is_connection_attributes_enabled()) {
auto conn_atts = connection_options.get_connection_attributes();
if (conn_atts.size()) {
auto attributes = shcore::make_dict();
for (auto &option : conn_atts) {
(*attributes)[option.first] = shcore::Value(*option.second);
}
(*map)[mysqlshdk::db::kConnectionAttributes] = shcore::Value(attributes);
}
} else {
(*map)[mysqlshdk::db::kConnectionAttributes] = shcore::Value::False();
}
return map;
}
namespace {
std::shared_ptr<mysqlshdk::db::mysqlx::Session> create_x_session() {
auto xsession = mysqlshdk::db::mysqlx::Session::create();
if (current_shell_options()->get().trace_protocol)
xsession->enable_protocol_trace(true);
return xsession;
}
std::shared_ptr<mysqlshdk::db::ISession> create_and_connect(
const Connection_options &connection_options) {
std::shared_ptr<mysqlshdk::db::ISession> session;
std::string connection_error;
Connection_options copy = connection_options;
SessionType type = copy.get_session_type();
// Automatic protocol detection is ON
// Attempts X Protocol first, then Classic
if (type == mysqlsh::SessionType::Auto) {
session = create_x_session();
try {
copy.set_scheme("mysqlx");
session->connect(copy);
return session;
} catch (const mysqlshdk::db::Error &e) {
// Unknown message received from server indicates an attempt to create
// And X Protocol session through the MySQL protocol
int code = e.code();
if (code == CR_MALFORMED_PACKET || // Unknown message received from
// server 10
code == CR_CONNECTION_ERROR || // No connection could be made because
// the target machine actively refused
// it connecting to host:port
code == CR_SERVER_GONE_ERROR || // MySQL server has gone away
// (randomly sent by libmysqlx)
(CR_X_UNSUPPORTED_OPTION_VALUE == code && // auth-method was not
strstr(e.what(), "authentication method"))) { // valid for X proto
type = mysqlsh::SessionType::Classic;
copy.clear_scheme();
copy.set_scheme("mysql");
// Since this is an unexpected error, we store the message to be
// logged in case the classic session connection fails too
if (code == CR_SERVER_GONE_ERROR ||
CR_X_UNSUPPORTED_OPTION_VALUE == code) {
connection_error.append("X protocol error: ").append(e.what());
}
} else {
throw;
}
}
}
switch (type) {
case mysqlsh::SessionType::X:
session = create_x_session();
break;
case mysqlsh::SessionType::Classic:
session = mysqlshdk::db::mysql::Session::create();
break;
default:
throw shcore::Exception::argument_error(
"Invalid session type specified for MySQL connection.");
break;
}
try {
session->connect(copy);
} catch (const mysqlshdk::db::Error &e) {
if (connection_error.empty()) {
throw;
} else {
// If an error was cached for the X protocol connection
// it is included on a new exception
connection_error.append("\nClassic protocol error: ");
connection_error.append(e.format());
throw shcore::Exception::argument_error(connection_error);
}
}
return session;
}
std::shared_ptr<mysqlshdk::db::ISession> create_session(
const Connection_options &connection_options) {
// allow SIGINT to interrupt the connect()
bool cancelled = false;
shcore::Interrupt_handler intr([&cancelled]() {
cancelled = true;
return true;
});
log_info("Connecting to MySQL at: %s", connection_options.as_uri().c_str());
auto session = create_and_connect(connection_options);
if (cancelled) throw shcore::cancelled("Cancelled");
return session;
}
void password_prompt(Connection_options *options) {
const std::string uri =
options->as_uri(mysqlshdk::db::uri::formats::user_transport());
const std::string prompt = "Please provide the password for '" + uri + "': ";
std::string answer;
const auto result = current_console()->prompt_password(prompt, &answer);
if (result == shcore::Prompt_result::Ok) {
options->set_password(answer);
} else {
throw shcore::cancelled("Cancelled");
}
}
} // namespace
/**
* Password Retrieval Logic
*
* When attempting to create a connection the connection data will be taken from
* the following sources in order of precedence:
*
* 1) Options File
* 2) Login Path
* 3) Command line options
* 4) Stored Password
* 5) Interactive Password Prompt
*
* Connection Option Resolution
* ============================
* Fron 1 to 3 the last connection option found overrides earlier occurrences.
*
* In 3, the right-most option overrides any previous definition.
*
* 4 and 5 are specific to passwords and are used only if the password is not
* defined by any of the previous means.
*
*
* Disabling Elements
* ==================
* Usage of options file is disabled through --no-defaults
* Usage of Login Path is disabled through --no-login-paths
* Usage of Stored Passwords is disabled through -p
*/
std::shared_ptr<mysqlshdk::db::ISession> establish_session(
const Connection_options &options, bool prompt_for_password,
bool prompt_in_loop, bool enable_stored_passwords) {
FI_SUPPRESS(mysql);
FI_SUPPRESS(mysqlx);
try {
Connection_options copy = options;
copy.set_default_data();
auto &ssh = copy.get_ssh_options_handle();
if (ssh.has_data()) {
mysqlshdk::ssh::current_ssh_manager()->create_tunnel(&ssh);
}
if (!copy.has_password() && copy.has_user() && enable_stored_passwords) {
if (shcore::Credential_manager::get().get_password(©)) {
try {
return create_session(copy);
} catch (const mysqlshdk::db::Error &e) {
if (e.code() != ER_ACCESS_DENIED_ERROR) {
throw;
} else {
copy.clear_password();
shcore::Credential_manager::get().remove_password(copy);
log_info(
"Connection to \"%s\" could not be established using the "
"stored password: %s. Invalid password has been erased.",
copy.as_uri(mysqlshdk::db::uri::formats::user_transport())
.c_str(),
e.format().c_str());
}
}
}
}
if (prompt_for_password) {
do {
bool prompted_for_password = false;
if (!copy.has_password() && copy.has_user() &&
!copy.is_auth_method(mysqlshdk::db::kAuthMethodKerberos) &&
!copy.is_auth_method(mysqlshdk::db::kAuthMethodOci)) {
password_prompt(©);
prompted_for_password = true;
}
try {
auto session = create_session(copy);
if (prompted_for_password &&
(!mysqlsh::current_shell_options()->get().passwords_from_stdin ||
mysqlsh::current_shell_options()->get().gui_mode)) {
// save password using the same connection options as the ones used
// to fetch the password from the secret storage
shcore::Credential_manager::get().save_password(copy);
}
return session;
} catch (const mysqlshdk::db::Error &e) {
if (!prompt_in_loop || e.code() != ER_ACCESS_DENIED_ERROR) {
throw;
} else {
copy.clear_password();
current_console()->print_error(e.format());
}
}
} while (prompt_in_loop);
}
return create_session(copy);
} catch (const mysqlshdk::db::Error &e) {
throw shcore::Exception::mysql_error_with_code_and_state(e.what(), e.code(),
e.sqlstate());
}
}
std::shared_ptr<mysqlshdk::db::ISession> establish_mysql_session(
const Connection_options &options, bool prompt_for_password,
bool prompt_in_loop) {
Connection_options copy = options;
if (copy.has_scheme()) {
copy.clear_scheme();
}
copy.set_scheme("mysql");
return establish_session(copy, prompt_for_password, prompt_in_loop);
}
Connection_options get_classic_connection_options(
const std::shared_ptr<mysqlshdk::db::ISession> &session) {
// copy the connection options
auto co = session->get_connection_options();
// switch from X protocol to classic
if (SessionType::Classic != co.get_session_type()) {
co.clear_scheme();
co.set_scheme("mysql");
if (co.has_port()) {
const auto result = session->query("SELECT @@GLOBAL.port");
const auto row = result->fetch_one();
if (!row) {
throw std::logic_error(
"Unable to determine classic MySQL port from the given shell "
"connection");
}
co.clear_port();
co.set_port(row->get_int(0));
// if we're here, we clear up local port, so establish session will find
// the correct ssh tunnel or make a new one
co.get_ssh_options_handle().clear_local_port();
} else {
// if we're here then socket was used
const auto result =
session->query("SELECT @@GLOBAL.socket, @@GLOBAL.datadir");
const auto row = result->fetch_one();
if (!row) {
throw std::logic_error(
"Unable to determine classic MySQL socket from the given shell "
"connection");
}
const auto socket = row->get_string(0);
co.set_socket(shcore::path::is_absolute(socket)
? socket
: shcore::path::join_path(row->get_string(1), socket));
}
}
return co;
}
std::vector<shcore::Value> get_row_values(const mysqlshdk::db::IRow &row) {
using mysqlshdk::db::Type;
using shcore::Date;
using shcore::Value;
std::vector<Value> value_array;
for (uint32_t i = 0, c = row.num_fields(); i < c; i++) {
Value v;
if (row.is_null(i)) {
v = Value::Null();
} else {
switch (row.get_type(i)) {
case Type::Null:
v = Value::Null();
break;
case Type::String:
v = Value(row.get_string(i));
break;
case Type::Integer:
v = Value(row.get_int(i));
break;
case Type::UInteger:
v = Value(row.get_uint(i));
break;
case Type::Float:
v = Value(row.get_float(i));
break;
case Type::Double:
v = Value(row.get_double(i));
break;
case Type::Decimal:
v = Value(row.get_as_string(i));
break;
case Type::Date:
case Type::DateTime:
v = Value::wrap(
std::make_shared<Date>(Date::unrepr(row.get_string(i))));
break;
case Type::Time:
v = Value::wrap(
std::make_shared<Date>(Date::unrepr(row.get_string(i))));
break;
case Type::Bit:
v = Value(std::get<0>(row.get_bit(i)));
break;
case Type::Bytes:
v = Value(row.get_string(i), true);
break;
case Type::Geometry:
case Type::Json:
case Type::Enum:
case Type::Set:
v = Value(row.get_string(i));
break;
}
}
value_array.emplace_back(std::move(v));
}
return value_array;
}
} // namespace mysqlsh
// We need to hide these from doxygen to avoid warnings
#if !defined DOXYGEN_JS && !defined DOXYGEN_PY
namespace shcore {
namespace detail {
mysqlshdk::db::Connection_options Type_info<
mysqlshdk::db::Connection_options>::to_native(const shcore::Value &in) {
return mysqlsh::get_connection_options(in);
}
mysqlshdk::db::Connection_options
Type_info<mysqlshdk::db::Connection_options>::default_value() {
return mysqlshdk::db::Connection_options();
}
} // namespace detail
} // namespace shcore
#endif