allow mysql SSL connection for civicrm-setup
authordemeritcowboy <demeritcowboy@hotmail.com>
Sun, 9 Aug 2020 17:19:32 +0000 (13:19 -0400)
committerdemeritcowboy <demeritcowboy@hotmail.com>
Wed, 19 Aug 2020 03:38:48 +0000 (23:38 -0400)
Civi/Install/Requirements.php
setup/plugins/checkRequirements/CheckDbWellFormed.civi-setup.php
setup/plugins/checkRequirements/CoreRequirementsAdapter.civi-setup.php
setup/plugins/installFiles/InstallSettingsFile.civi-setup.php
setup/src/Setup/DbUtil.php
templates/CRM/common/civicrm.settings.php.template
tests/phpunit/Civi/Setup/DbUtilTest.php [new file with mode: 0644]

index da7b2cc76597194238bff22058288b01ed11c268..7d30855076b79c0380a1d6746f7deece5ce0e2b0 100644 (file)
@@ -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;
   }
 
index d48925c3de924d84e839e9bf02bc1550a5b037f2..4f09d7c8328a5b2755b042340ac1628b0c1d9d0e 100644 (file)
@@ -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++;
         }
index f0d15507c3fe5585fcd12aa16d62f7fdd53b2f03..4e4e051f31a759dafe229fea743fba5dedf557de 100644 (file)
@@ -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);
   });
index 3581eb3bed8e5778c37f927fb3f7122f4e731fc1..29aedb2b073b2e66a765f4f6116555b5738c7591 100644 (file)
@@ -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']);
index 054fe8d731b08754f5c27be202fd7ac7ce61b62d..87849057c6d896932e818dc87d7a01a38f104f90 100644 (file)
@@ -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
index e36756840f303d7376316605949676bce6223e1f..3d0575569fb8dbccc87ac797befb0fcc48ca85e1 100644 (file)
@@ -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 (file)
index 0000000..7c42ff7
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+namespace Civi\Setup;
+
+/**
+ * Class DbUtilTest
+ * @package Civi\Setup
+ * @group headless
+ */
+class DbUtilTest extends \CiviUnitTestCase {
+
+  /**
+   * Test parseSSL
+   * @dataProvider queryStringProvider
+   * @param string $input
+   * @param array $expected
+   */
+  public function testParseSSL(string $input, array $expected) {
+    $this->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']],
+    ];
+  }
+
+}