From 04b1abad5dcf19ec18dc9bca9e04af2e2c618d59 Mon Sep 17 00:00:00 2001 From: demeritcowboy Date: Sun, 9 Aug 2020 13:19:32 -0400 Subject: [PATCH] allow mysql SSL connection for civicrm-setup --- Civi/Install/Requirements.php | 19 +++++- .../CheckDbWellFormed.civi-setup.php | 19 +++--- .../CoreRequirementsAdapter.civi-setup.php | 1 + .../InstallSettingsFile.civi-setup.php | 7 +++ setup/src/Setup/DbUtil.php | 51 +++++++++++++++- .../CRM/common/civicrm.settings.php.template | 2 +- tests/phpunit/Civi/Setup/DbUtilTest.php | 59 +++++++++++++++++++ 7 files changed, 147 insertions(+), 11 deletions(-) create mode 100644 tests/phpunit/Civi/Setup/DbUtilTest.php diff --git a/Civi/Install/Requirements.php b/Civi/Install/Requirements.php index da7b2cc765..7d30855076 100644 --- a/Civi/Install/Requirements.php +++ b/Civi/Install/Requirements.php @@ -132,7 +132,24 @@ class Requirements { elseif (!empty($db_config['server'])) { $host = $db_config['server']; } - $conn = @mysqli_connect($host, $db_config['username'], $db_config['password'], $db_config['database'], !empty($db_config['port']) ? $db_config['port'] : NULL); + if (empty($db_config['ssl_params'])) { + $conn = @mysqli_connect($host, $db_config['username'], $db_config['password'], $db_config['database'], !empty($db_config['port']) ? $db_config['port'] : NULL); + } + else { + $conn = NULL; + $init = mysqli_init(); + mysqli_ssl_set( + $init, + $db_config['ssl_params']['key'] ?? NULL, + $db_config['ssl_params']['cert'] ?? NULL, + $db_config['ssl_params']['ca'] ?? NULL, + $db_config['ssl_params']['capath'] ?? NULL, + $db_config['ssl_params']['cipher'] ?? NULL + ); + if (@mysqli_real_connect($init, $host, $db_config['username'], $db_config['password'], $db_config['database'], (!empty($db_config['port']) ? $db_config['port'] : NULL), NULL, MYSQLI_CLIENT_SSL)) { + $conn = $init; + } + } return $conn; } diff --git a/setup/plugins/checkRequirements/CheckDbWellFormed.civi-setup.php b/setup/plugins/checkRequirements/CheckDbWellFormed.civi-setup.php index d48925c3de..4f09d7c832 100644 --- a/setup/plugins/checkRequirements/CheckDbWellFormed.civi-setup.php +++ b/setup/plugins/checkRequirements/CheckDbWellFormed.civi-setup.php @@ -23,19 +23,24 @@ if (!defined('CIVI_SETUP')) { $expectedKeys = array('server', 'username', 'password', 'database'); sort($expectedKeys); if ($keys !== $expectedKeys) { - $e->addError('database', $dbField, sprintf("The database credentials for \"%s\" should be specified as (%s) not (%s)", - $dbField, - implode(',', $expectedKeys), - implode(',', $keys) - )); - $errors++; + // if it failed it might be because of the optional ssl parameters + $expectedKeys[] = 'ssl_params'; + sort($expectedKeys); + if ($keys !== $expectedKeys) { + $e->addError('database', $dbField, sprintf("The database credentials for \"%s\" should be specified as (%s) not (%s)", + $dbField, + implode(',', $expectedKeys), + implode(',', $keys) + )); + $errors++; + } } foreach ($db as $k => $v) { if ($k === 'password' && empty($v)) { $e->addWarning('database', "$dbField.$k", "The property \"$dbField.$k\" is blank. This may be correct in some controlled environments; it could also be a mistake or a symptom of an insecure configuration."); } - elseif (!is_scalar($v)) { + elseif ($k !== 'ssl_params' && !is_scalar($v)) { $e->addError('database', "$dbField.$k", "The property \"$dbField.$k\" is not well-formed."); $errors++; } diff --git a/setup/plugins/checkRequirements/CoreRequirementsAdapter.civi-setup.php b/setup/plugins/checkRequirements/CoreRequirementsAdapter.civi-setup.php index f0d15507c3..4e4e051f31 100644 --- a/setup/plugins/checkRequirements/CoreRequirementsAdapter.civi-setup.php +++ b/setup/plugins/checkRequirements/CoreRequirementsAdapter.civi-setup.php @@ -29,6 +29,7 @@ if (!defined('CIVI_SETUP')) { 'username' => $model->db['username'], 'password' => $model->db['password'], 'database' => $model->db['database'], + 'ssl_params' => $model->db['ssl_params'] ?? NULL, )); _corereqadapter_addMessages($e, 'database', $dbMsgs); }); diff --git a/setup/plugins/installFiles/InstallSettingsFile.civi-setup.php b/setup/plugins/installFiles/InstallSettingsFile.civi-setup.php index 3581eb3bed..29aedb2b07 100644 --- a/setup/plugins/installFiles/InstallSettingsFile.civi-setup.php +++ b/setup/plugins/installFiles/InstallSettingsFile.civi-setup.php @@ -59,6 +59,13 @@ if (!defined('CIVI_SETUP')) { $params['dbPass'] = addslashes($m->db['password']); $params['dbHost'] = addslashes($m->db['server']); $params['dbName'] = addslashes($m->db['database']); + // The '&' prefix is awkward, but we don't know what's already in the file. + // At the time of writing, it has ?new_link=true. If that is removed, + // then need to update this. + // The PHP_QUERY_RFC3986 is important because PEAR::DB will interpret plus + // signs as a reference to its old DSN format and mangle the DSN, so we + // need to use %20 for spaces. + $params['dbSSL'] = empty($m->db['ssl_params']) ? '' : addslashes('&' . http_build_query($m->db['ssl_params'], '', '&', PHP_QUERY_RFC3986)); $params['cms'] = addslashes($m->cms); $params['CMSdbUser'] = addslashes($m->cmsDb['username']); $params['CMSdbPass'] = addslashes($m->cmsDb['password']); diff --git a/setup/src/Setup/DbUtil.php b/setup/src/Setup/DbUtil.php index 054fe8d731..87849057c6 100644 --- a/setup/src/Setup/DbUtil.php +++ b/setup/src/Setup/DbUtil.php @@ -12,14 +12,16 @@ class DbUtil { public static function parseDsn($dsn) { $parsed = parse_url($dsn); return array( - 'server' => self::encodeHostPort($parsed['host'], $parsed['port']), + 'server' => self::encodeHostPort($parsed['host'], $parsed['port'] ?? NULL), 'username' => $parsed['user'] ?: NULL, 'password' => $parsed['pass'] ?: NULL, 'database' => $parsed['path'] ? ltrim($parsed['path'], '/') : NULL, + 'ssl_params' => self::parseSSL($parsed['query'] ?? NULL), ); } /** + * @todo Is this used anywhere? It doesn't support SSL as-is. * Convert an datasource from array notation to URL notation. * * @param array $db @@ -40,7 +42,25 @@ class DbUtil { */ public static function softConnect($db) { list($host, $port) = self::decodeHostPort($db['server']); - $conn = @mysqli_connect($host, $db['username'], $db['password'], $db['database'], $port); + if (empty($db['ssl_params'])) { + $conn = @mysqli_connect($host, $db['username'], $db['password'], $db['database'], $port); + } + else { + $conn = NULL; + $init = mysqli_init(); + mysqli_ssl_set( + $init, + $db['ssl_params']['key'] ?? NULL, + $db['ssl_params']['cert'] ?? NULL, + $db['ssl_params']['ca'] ?? NULL, + $db['ssl_params']['capath'] ?? NULL, + $db['ssl_params']['cipher'] ?? NULL + ); + // @todo socket parameter, but if you're using sockets do you need SSL? + if (@mysqli_real_connect($init, $host, $db['username'], $db['password'], $db['database'], $port, NULL, MYSQLI_CLIENT_SSL)) { + $conn = $init; + } + } return $conn; } @@ -94,6 +114,33 @@ class DbUtil { return $host . ($port ? (':' . $port) : ''); } + /** + * For SSL you can have client certificates, which has some required and + * optional parameters, or you can have anonymous SSL, which just requires + * some indication that you want that. + * + * @param string $query_string + * @return array + */ + public static function parseSSL($query_string) { + if (empty($query_string)) { + return []; + } + parse_str($query_string, $parsed_query); + $sensible_parameters = [ + // ssl=1 alone means no client certificate - it's not a real mysqli option + 'ssl' => NULL, + 'key' => NULL, + 'cert' => NULL, + 'ca' => NULL, + 'capath' => NULL, + 'cipher' => NULL, + ]; + // Only want to include a param if it's in our list of sensibles, e.g. + // we don't want new_link=true. + return array_intersect_key($parsed_query, $sensible_parameters); + } + /** * @param array $db * @param string $SQLcontent diff --git a/templates/CRM/common/civicrm.settings.php.template b/templates/CRM/common/civicrm.settings.php.template index e36756840f..3d0575569f 100644 --- a/templates/CRM/common/civicrm.settings.php.template +++ b/templates/CRM/common/civicrm.settings.php.template @@ -106,7 +106,7 @@ if (!defined('CIVICRM_DSN')) { define('CIVICRM_DSN', $GLOBALS['_CV']['TEST_DB_DSN']); } else { - define('CIVICRM_DSN', 'mysql://%%dbUser%%:%%dbPass%%@%%dbHost%%/%%dbName%%?new_link=true'); + define('CIVICRM_DSN', 'mysql://%%dbUser%%:%%dbPass%%@%%dbHost%%/%%dbName%%?new_link=true%%dbSSL%%'); } } diff --git a/tests/phpunit/Civi/Setup/DbUtilTest.php b/tests/phpunit/Civi/Setup/DbUtilTest.php new file mode 100644 index 0000000000..7c42ff7551 --- /dev/null +++ b/tests/phpunit/Civi/Setup/DbUtilTest.php @@ -0,0 +1,59 @@ +assertSame($expected, \Civi\Setup\DbUtil::parseSSL($input)); + } + + /** + * Data provider for testParseSSL + * @return array + */ + public function queryStringProvider():array { + return [ + ['', []], + ['new_link=true', []], + ['ssl=1', ['ssl' => '1']], + ['new_link=true&ssl=1', ['ssl' => '1']], + ['ca=%2Ftmp%2Fcacert.crt', ['ca' => '/tmp/cacert.crt']], + [ + 'ca=%2Ftmp%2Fcacert.crt&cert=%2Ftmp%2Fcert.crt&key=%2Ftmp%2Fmy.key', + [ + 'ca' => '/tmp/cacert.crt', + 'cert' => '/tmp/cert.crt', + 'key' => '/tmp/my.key', + ], + ], + [ + 'ca=%2Fpath%20with%20spaces%2Fcacert.crt', + [ + 'ca' => '/path with spaces/cacert.crt', + ], + ], + ['cipher=aes', ['cipher' => 'aes']], + ['capath=%2Ftmp', ['capath' => '/tmp']], + [ + 'cipher=aes&capath=%2Ftmp&food=banana', + [ + 'cipher' => 'aes', + 'capath' => '/tmp', + ], + ], + ['food=banana&cipher=aes', ['cipher' => 'aes']], + ]; + } + +} -- 2.25.1