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(&copy)) { 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(&copy); 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