Skip to content

Commit

Permalink
Update README and improve handling of ssl_mode settings for MySQL and…
Browse files Browse the repository at this point in the history
… MariaDB versions

Print the client version number in SSL warning.
Add comments to explain the ssl mode setting function.
Add MariaDB Connector/C 3.x to the relevant SSL mode code path.

With thanks to Jun Aruga for identifying this issue.
  • Loading branch information
sodabrew committed Jan 22, 2023
1 parent 6528137 commit 978e16e
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 33 deletions.
85 changes: 69 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 [email protected]
$ gem install mysql2 -- --with-openssl-dir=$(brew --prefix [email protected])

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 [email protected])
```

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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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...,
Expand All @@ -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).
Expand Down Expand Up @@ -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
```

Expand Down Expand Up @@ -393,7 +444,7 @@ end

Yields:

```ruby
``` ruby
{"1"=>1}
{"2"=>2}
next_result: Unknown column 'A' in 'field list' (Mysql2::Error)
Expand Down Expand Up @@ -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

Expand Down
53 changes: 40 additions & 13 deletions ext/mysql2/client.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 14 additions & 4 deletions spec/mysql2/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 978e16e

Please sign in to comment.