diff --git a/README.md b/README.md index 61e76297..77d0d53a 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,11 @@ This may be needed if you deploy to a system where these libraries are located somewhere different than on your build system. This overrides any rpath calculated by default or by the options above. +* `--with-openssl-dir[=/path/to/openssl]` - Specify the directory where OpenSSL +is installed. In most cases, the Ruby runtime and MySQL client libraries will +link against a system-installed OpenSSL library and this option is not needed. +Use this option when non-default library paths are needed. + * `--with-sanitize[=address,cfi,integer,memory,thread,undefined]` - Enable sanitizers for Clang / GCC. If no argument is given, try to enable all sanitizers or fail if none are available. If a command-separated list of @@ -89,13 +94,48 @@ the library file `libmysqlclient.so` but is missing the header file `mysql.h` ### Mac OS X -You may use MacPorts, Homebrew, or a native MySQL installer package. The most +You may use Homebew, MacPorts, or a native MySQL installer package. The most common paths will be automatically searched. If you want to select a specific MySQL directory, use the `--with-mysql-dir` or `--with-mysql-config` options above. If you have not done so already, you will need to install the XCode select tools by running `xcode-select --install`. +Later versions of MacOS no longer distribute a linkable OpenSSL library. It is +common to use Homebrew or MacPorts to install OpenSSL. Make sure that both the +Ruby runtime and MySQL client libraries are compiled with the same OpenSSL +family, 1.0 or 1.1 or 3.0, since only one can be loaded at runtime. + +``` sh +$ brew install openssl@1.1 +$ gem install mysql2 -- --with-openssl-dir=$(brew --prefix openssl@1.1) + +or + +$ sudo port install openssl11 +``` + +Since most Ruby projects use Bundler, you can set build options in the Bundler +config rather than manually installing a global mysql2 gem. This example shows +how to set build arguments with [Bundler config](https://bundler.io/man/bundle-config.1.html): + +``` sh +$ bundle config --local build.mysql2 -- --with-openssl-dir=$(brew --prefix openssl@1.1) +``` + +Another helpful trick is to use the same OpenSSL library that your Ruby was +built with, if it was built with an alternate OpenSSL path. This example finds +the argument `--with-openssl-dir=/some/path` from the Ruby build and adds that +to the [Bundler config](https://bundler.io/man/bundle-config.1.html): + +``` sh +$ bundle config --local build.mysql2 -- $(ruby -r rbconfig -e 'puts RbConfig::CONFIG["configure_args"]' | xargs -n1 | grep with-openssl-dir) +``` + +Note the additional double dashes (`--`) these separate command-line arguments +that `gem` or `bundler` interpret from the addiitonal arguments that are passed +to the mysql2 build process. + ### Windows Make sure that you have Ruby and the DevKit compilers installed. We recommend @@ -205,7 +245,7 @@ result = statement.execute(1, "CA", :as => :array) Session Tracking information can be accessed with -```ruby +``` ruby c = Mysql2::Client.new( host: "127.0.0.1", username: "root", @@ -261,20 +301,14 @@ type of connection to make, with special interpretation you should be aware of: * An IPv4 or IPv6 address will result in a TCP connection. * Any other value will be looked up as a hostname for a TCP connection. -### SSL options +### SSL/TLS options -Setting any of the following options will enable an SSL connection, but only if +Setting any of the following options will enable a SSL/TLS connection, but only if your MySQL client library and server have been compiled with SSL support. MySQL client library defaults will be used for any parameters that are left out or set to nil. Relative paths are allowed, and may be required by managed hosting providers such as Heroku. -For MySQL versions 5.7.11 and higher, use `:ssl_mode` to prefer or require an -SSL connection and certificate validation. For earlier versions of MySQL, use -the `:sslverify` boolean. For details on each of the `:ssl_mode` options, see -[https://dev.mysql.com/doc/refman/8.0/en/connection-options.html](https://dev.mysql.com/doc/refman/8.0/en/connection-options.html#option_general_ssl-mode). - - ``` ruby Mysql2::Client.new( # ...options as above..., @@ -284,10 +318,27 @@ Mysql2::Client.new( :sslcapath => '/path/to/cacerts', :sslcipher => 'DHE-RSA-AES256-SHA', :sslverify => true, # Removed in MySQL 8.0 - :ssl_mode = :disabled / :preferred / :required / :verify_ca / :verify_identity, + :ssl_mode => :disabled / :preferred / :required / :verify_ca / :verify_identity, ) ``` +For MySQL versions 5.7.11 and higher, use `:ssl_mode` to prefer or require an +SSL connection and certificate validation. For earlier versions of MySQL, use +the `:sslverify` boolean. For details on each of the `:ssl_mode` options, see +[https://dev.mysql.com/doc/refman/8.0/en/connection-options.html](https://dev.mysql.com/doc/refman/8.0/en/connection-options.html#option_general_ssl-mode). + +The `:ssl_mode` option will also set the appropriate MariaDB connection flags: + +| `:ssl_mode` | MariaDB option value | +| --- | --- | +| `:disabled` | MYSQL_OPT_SSL_ENFORCE = 0 | +| `:required` | MYSQL_OPT_SSL_ENFORCE = 1 | +| `:verify_identity` | MYSQL_OPT_SSL_VERIFY_SERVER_CERT = 1 | + +MariaDB does not support the `:preferred` or `:verify_ca` options. For more information SSL/TLS in MariaDB, see +[https://mariadb.com/kb/en/securing-connections-for-client-and-server/](https://mariadb.com/kb/en/securing-connections-for-client-and-server/) +and [https://mariadb.com/kb/en/mysql_optionsv/#tls-options](https://mariadb.com/kb/en/mysql_optionsv/#tls-options) + ### Secure auth Starting with MySQL 5.6.5, secure_auth is enabled by default on servers (it was disabled by default prior to this). @@ -334,7 +385,7 @@ In this example, the compression flag is negated with `-COMPRESS`. Active Record typically reads its configuration from a file named `database.yml` or an environment variable `DATABASE_URL`. Use the value `mysql2` as the protocol name. For example: -``` shell +``` sh DATABASE_URL=mysql2://sql_user:sql_pass@sql_host_name:port/sql_db_name?option1=value1&option2=value2 ``` @@ -393,7 +444,7 @@ end Yields: -```ruby +``` ruby {"1"=>1} {"2"=>2} next_result: Unknown column 'A' in 'field list' (Mysql2::Error) @@ -573,14 +624,16 @@ As for field values themselves, I'm workin on it - but expect that soon. This gem is tested with the following Ruby versions on Linux and Mac OS X: -* Ruby MRI 2.0.0, 2.1.x, 2.2.x, 2.3.x, 2.4.x, 2.5.x, 2.6.x +* Ruby MRI 2.0 through 2.7 (all versions to date) +* Ruby MRI 3.0, 3.1, 3.2 (all versions to date) * Rubinius 2.x and 3.x do work but may fail under some workloads This gem is tested with the following MySQL and MariaDB versions: * MySQL 5.5, 5.6, 5.7, 8.0 -* MySQL Connector/C 6.0 and 6.1 (primarily on Windows) -* MariaDB 5.5, 10.0, 10.1, 10.2, 10.3 +* MySQL Connector/C 6.0 and 6.1 and 8.0 (primarily on Windows) +* MariaDB 5.5 and 10.x, with a focus on 10.6 LTS and 10.11 LTS +* MariaDB Connector/C 2.x and 3.x ### Ruby on Rails / Active Record diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c index fbc73572..a49b3c34 100644 --- a/ext/mysql2/client.c +++ b/ext/mysql2/client.c @@ -120,53 +120,80 @@ struct nogvl_select_db_args { static VALUE rb_set_ssl_mode_option(VALUE self, VALUE setting) { unsigned long version = mysql_get_client_version(); + const char *version_str = mysql_get_client_info(); - if (version < 50630 || (version >= 50700 && version < 50703)) { - rb_warn( "Your mysql client library does not support setting ssl_mode; full support comes with 5.6.36+, 5.7.11+, 8.0+" ); + /* Warn about versions that are known to be incomplete; these are pretty + * ancient, we want people to upgrade if they need SSL/TLS to work + * + * MySQL 5.x before 5.6.30 -- ssl_mode introduced but not fully working until 5.6.36) + * MySQL 5.7 before 5.7.3 -- ssl_mode introduced but not fully working until 5.7.11) + */ + if ((version >= 50000 && version < 50630) || (version >= 50700 && version < 50703)) { + rb_warn("Your mysql client library version %s does not support setting ssl_mode; full support comes with 5.6.36+, 5.7.11+, 8.0+", version_str); return Qnil; } + + /* For these versions, map from the options we're exposing to Ruby to the constant available: + * ssl_mode: :verify_identity to MYSQL_OPT_SSL_VERIFY_SERVER_CERT = 1 + * ssl_mode: :required to MYSQL_OPT_SSL_ENFORCE = 1 + * ssl_mode: :disabled to MYSQL_OPT_SSL_ENFORCE = 0 + */ #if defined(HAVE_CONST_MYSQL_OPT_SSL_VERIFY_SERVER_CERT) || defined(HAVE_CONST_MYSQL_OPT_SSL_ENFORCE) GET_CLIENT(self); - int val = NUM2INT( setting ); - // Either MySQL 5.7.3 - 5.7.10, or Connector/C 6.1.3 - 6.1.x, or MariaDB 10.x and later - if ((version >= 50703 && version < 50711) || (version >= 60103 && version < 60200) || version >= 100000) { + int val = NUM2INT(setting); + + /* Expected code path for MariaDB 10.x and MariaDB Connector/C 3.x + * Workaround code path for MySQL 5.7.3 - 5.7.10 and MySQL Connector/C 6.1.3 - 6.1.x + */ + if (version >= 100000 // MariaDB (all versions numbered 10.x) + || (version >= 30000 && version < 40000) // MariaDB Connector/C (all versions numbered 3.x) + || (version >= 50703 && version < 50711) // Workaround for MySQL 5.7.3 - 5.7.10 + || (version >= 60103 && version < 60200)) { // Workaround for MySQL Connector/C 6.1.3 - 6.1.x #ifdef HAVE_CONST_MYSQL_OPT_SSL_VERIFY_SERVER_CERT if (val == SSL_MODE_VERIFY_IDENTITY) { my_bool b = 1; - int result = mysql_options( wrapper->client, MYSQL_OPT_SSL_VERIFY_SERVER_CERT, &b ); + int result = mysql_options(wrapper->client, MYSQL_OPT_SSL_VERIFY_SERVER_CERT, &b); return INT2NUM(result); } #endif #ifdef HAVE_CONST_MYSQL_OPT_SSL_ENFORCE if (val == SSL_MODE_DISABLED || val == SSL_MODE_REQUIRED) { - my_bool b = ( val == SSL_MODE_REQUIRED ); - int result = mysql_options( wrapper->client, MYSQL_OPT_SSL_ENFORCE, &b ); + my_bool b = (val == SSL_MODE_REQUIRED); + int result = mysql_options(wrapper->client, MYSQL_OPT_SSL_ENFORCE, &b); return INT2NUM(result); } #endif - rb_warn( "Your mysql client library does not support ssl_mode %d.", val ); + rb_warn("Your mysql client library version %s does not support ssl_mode %d", version_str, val); return Qnil; } else { - rb_warn( "Your mysql client library does not support ssl_mode as expected." ); + rb_warn("Your mysql client library version %s does not support ssl_mode as expected", version_str); return Qnil; } #endif + + /* For other versions -- known to be MySQL 5.6.36+, 5.7.11+, 8.0+ + * pass the value of the argument to MYSQL_OPT_SSL_MODE -- note the code + * mapping from atoms / constants is in the MySQL::Client Ruby class + */ #ifdef FULL_SSL_MODE_SUPPORT GET_CLIENT(self); - int val = NUM2INT( setting ); + int val = NUM2INT(setting); if (val != SSL_MODE_DISABLED && val != SSL_MODE_PREFERRED && val != SSL_MODE_REQUIRED && val != SSL_MODE_VERIFY_CA && val != SSL_MODE_VERIFY_IDENTITY) { rb_raise(cMysql2Error, "ssl_mode= takes DISABLED, PREFERRED, REQUIRED, VERIFY_CA, VERIFY_IDENTITY, you passed: %d", val ); } - int result = mysql_options( wrapper->client, MYSQL_OPT_SSL_MODE, &val ); + int result = mysql_options(wrapper->client, MYSQL_OPT_SSL_MODE, &val); return INT2NUM(result); #endif + + // Warn if we get this far #ifdef NO_SSL_MODE_SUPPORT - rb_warn( "Your mysql client library does not support setting ssl_mode; full support comes with 5.7.11." ); + rb_warn("Your mysql client library does not support setting ssl_mode"); return Qnil; #endif } + /* * non-blocking mysql_*() functions that we won't be wrapping since * they do not appear to hit the network nor issue any interruptible diff --git a/spec/mysql2/client_spec.rb b/spec/mysql2/client_spec.rb index ede316b7..0a298a15 100644 --- a/spec/mysql2/client_spec.rb +++ b/spec/mysql2/client_spec.rb @@ -166,16 +166,26 @@ def connect(*args) new_client(option_overrides) end - %i[disabled preferred required verify_ca verify_identity].each do |ssl_mode| + # 'preferred' or 'verify_ca' are only in MySQL 5.6.36+, 5.7.11+, 8.0+ + version = Mysql2::Client.info + ssl_modes = case version + when 50636...50700, 50711...50800, 80000...90000 + %i[disabled required verify_identity] + else + %i[disabled preferred required verifa_ca verify_identity] + end + + # MySQL and MariaDB and all versions of Connector/C + ssl_modes.each do |ssl_mode| it "should set ssl_mode option #{ssl_mode}" do options = { ssl_mode: ssl_mode, } options.merge!(option_overrides) - # Relax the matching condition by checking if an error is not raised. - # TODO: Verify warnings by checking stderr. expect do - new_client(options) + expect do + new_client(options) + end.not_to output(/does not support ssl_mode/).to_stderr end.not_to raise_error end end