mirror of
https://github.com/moodle/moodle.git
synced 2025-01-17 21:49:15 +01:00
Merge branch 'MDL-72622-redis-tls' of https://github.com/catalyst/moodle
This commit is contained in:
commit
d757e1e1cc
8
cache/stores/redis/addinstanceform.php
vendored
8
cache/stores/redis/addinstanceform.php
vendored
@ -44,6 +44,14 @@ class cachestore_redis_addinstance_form extends cachestore_addinstance_form {
|
||||
$form->addHelpButton('server', 'server', 'cachestore_redis');
|
||||
$form->addRule('server', get_string('required'), 'required');
|
||||
|
||||
$form->addElement('advcheckbox', 'encryption', get_string('encrypt_connection', 'cachestore_redis'));
|
||||
$form->setType('encryption', PARAM_BOOL);
|
||||
$form->addHelpButton('encryption', 'encrypt_connection', 'cachestore_redis');
|
||||
|
||||
$form->addElement('text', 'cafile', get_string('ca_file', 'cachestore_redis'));
|
||||
$form->setType('cafile', PARAM_TEXT);
|
||||
$form->addHelpButton('cafile', 'ca_file', 'cachestore_redis');
|
||||
|
||||
$form->addElement('passwordunmask', 'password', get_string('password', 'cachestore_redis'));
|
||||
$form->setType('password', PARAM_RAW);
|
||||
$form->addHelpButton('password', 'password', 'cachestore_redis');
|
||||
|
@ -27,6 +27,10 @@ defined('MOODLE_INTERNAL') || die();
|
||||
$string['compressor_none'] = 'No compression.';
|
||||
$string['compressor_php_gzip'] = 'Use gzip compression.';
|
||||
$string['compressor_php_zstd'] = 'Use Zstandard compression.';
|
||||
$string['encrypt_connection'] = 'Use TLS encryption.';
|
||||
$string['encrypt_connection_help'] = 'Use TLS to connect to Redis. Do not use \'tls://\' in the hostname for Redis, use this option instead.';
|
||||
$string['ca_file'] = 'CA file path';
|
||||
$string['ca_file_help'] = 'Location of Certificate Authority file on local filesystem';
|
||||
$string['pluginname'] = 'Redis';
|
||||
$string['prefix'] = 'Key prefix';
|
||||
$string['prefix_help'] = 'This prefix is used for all key names on the Redis server.
|
||||
|
66
cache/stores/redis/lib.php
vendored
66
cache/stores/redis/lib.php
vendored
@ -189,42 +189,60 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
|
||||
if (array_key_exists('compressor', $configuration)) {
|
||||
$this->compressor = (int)$configuration['compressor'];
|
||||
}
|
||||
$password = !empty($configuration['password']) ? $configuration['password'] : '';
|
||||
$prefix = !empty($configuration['prefix']) ? $configuration['prefix'] : '';
|
||||
if (array_key_exists('lockwait', $configuration)) {
|
||||
$this->lockwait = (int)$configuration['lockwait'];
|
||||
}
|
||||
if (array_key_exists('locktimeout', $configuration)) {
|
||||
$this->locktimeout = (int)$configuration['locktimeout'];
|
||||
}
|
||||
$this->redis = $this->new_redis($configuration['server'], $prefix, $password);
|
||||
$this->redis = $this->new_redis($configuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Redis instance and
|
||||
* connect to the server.
|
||||
*
|
||||
* @param string $server The server connection string
|
||||
* @param string $prefix The key prefix
|
||||
* @param string $password The server connection password
|
||||
* @param array $configuration The server configuration
|
||||
* @return Redis
|
||||
*/
|
||||
protected function new_redis($server, $prefix = '', $password = '') {
|
||||
protected function new_redis(array $configuration): \Redis {
|
||||
global $CFG;
|
||||
|
||||
$redis = new Redis();
|
||||
// Check for Unix socket.
|
||||
|
||||
$server = $configuration['server'];
|
||||
$encrypt = (bool) ($configuration['encryption'] ?? false);
|
||||
$password = !empty($configuration['password']) ? $configuration['password'] : '';
|
||||
$prefix = !empty($configuration['prefix']) ? $configuration['prefix'] : '';
|
||||
// Check if it isn't a Unix socket to set default port.
|
||||
$port = null;
|
||||
$opts = [];
|
||||
if ($server[0] === '/') {
|
||||
$port = 0;
|
||||
} else {
|
||||
$port = 6379; // No Unix socket so set default port.
|
||||
if (strpos($server, ':')) { // Check for custom port.
|
||||
$serverconf = explode(':', $server);
|
||||
$server = $serverconf[0];
|
||||
$port = $serverconf[1];
|
||||
list($server, $port) = explode(':', $server);
|
||||
}
|
||||
|
||||
// We can encrypt if we aren't unix socket.
|
||||
if ($encrypt) {
|
||||
$server = 'tls://' . $server;
|
||||
if (empty($configuration['cafile'])) {
|
||||
$sslopts = [
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
];
|
||||
} else {
|
||||
$sslopts = ['cafile' => $configuration['cafile']];
|
||||
}
|
||||
$opts['stream'] = $sslopts;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if ($redis->connect($server, $port)) {
|
||||
if ($redis->connect($server, $port, 1, null, 100, 1, $opts)) {
|
||||
|
||||
if (!empty($password)) {
|
||||
$redis->auth($password);
|
||||
}
|
||||
@ -235,11 +253,20 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
|
||||
if (!empty($prefix)) {
|
||||
$redis->setOption(Redis::OPT_PREFIX, $prefix);
|
||||
}
|
||||
if ($encrypt && !$redis->ping()) {
|
||||
/*
|
||||
* In case of a TLS connection, if phpredis client does not
|
||||
* communicate immediately with the server the connection hangs.
|
||||
* See https://github.com/phpredis/phpredis/issues/2332 .
|
||||
*/
|
||||
throw new \RedisException("Ping failed");
|
||||
}
|
||||
$this->isready = true;
|
||||
} else {
|
||||
$this->isready = false;
|
||||
}
|
||||
} catch (\RedisException $e) {
|
||||
debugging("redis $server: $e", DEBUG_NORMAL);
|
||||
$this->isready = false;
|
||||
}
|
||||
|
||||
@ -759,6 +786,8 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
|
||||
'password' => $data->password,
|
||||
'serializer' => $data->serializer,
|
||||
'compressor' => $data->compressor,
|
||||
'encryption' => $data->encryption,
|
||||
'cafile' => $data->cafile,
|
||||
);
|
||||
}
|
||||
|
||||
@ -780,6 +809,12 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
|
||||
if (!empty($config['compressor'])) {
|
||||
$data['compressor'] = $config['compressor'];
|
||||
}
|
||||
if (!empty($config['encryption'])) {
|
||||
$data['encryption'] = $config['encryption'];
|
||||
}
|
||||
if (!empty($config['cafile'])) {
|
||||
$data['cafile'] = $config['cafile'];
|
||||
}
|
||||
$editform->set_data($data);
|
||||
}
|
||||
|
||||
@ -805,6 +840,12 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
|
||||
if (!empty($config->test_password)) {
|
||||
$configuration['password'] = $config->test_password;
|
||||
}
|
||||
if (!empty($config->test_encryption)) {
|
||||
$configuration['encryption'] = $config->test_encryption;
|
||||
}
|
||||
if (!empty($config->test_cafile)) {
|
||||
$configuration['cafile'] = $config->test_cafile;
|
||||
}
|
||||
// Make it possible to test TTL performance by hacking a copy of the cache definition.
|
||||
if (!empty($config->test_ttl)) {
|
||||
$definition = clone $definition;
|
||||
@ -832,6 +873,7 @@ class cachestore_redis extends cache_store implements cache_is_key_aware, cache_
|
||||
|
||||
return ['server' => TEST_CACHESTORE_REDIS_TESTSERVERS,
|
||||
'prefix' => $DB->get_prefix(),
|
||||
'encryption' => defined('TEST_CACHESTORE_REDIS_ENCRYPT') && TEST_CACHESTORE_REDIS_ENCRYPT,
|
||||
];
|
||||
}
|
||||
|
||||
|
15
cache/stores/redis/settings.php
vendored
15
cache/stores/redis/settings.php
vendored
@ -34,6 +34,21 @@ $settings->add(
|
||||
16
|
||||
)
|
||||
);
|
||||
$settings->add(new admin_setting_configcheckbox(
|
||||
'cachestore_redis/test_encryption',
|
||||
get_string('encrypt_connection', 'cachestore_redis'),
|
||||
get_string('encrypt_connection', 'cachestore_redis'),
|
||||
false));
|
||||
$settings->add(
|
||||
new admin_setting_configtext(
|
||||
'cachestore_redis/test_cafile',
|
||||
get_string('ca_file', 'cachestore_redis'),
|
||||
get_string('ca_file', 'cachestore_redis'),
|
||||
'',
|
||||
PARAM_TEXT,
|
||||
16
|
||||
)
|
||||
);
|
||||
$settings->add(
|
||||
new admin_setting_configpasswordunmask(
|
||||
'cachestore_redis/test_password',
|
||||
|
@ -333,6 +333,9 @@ $CFG->admin = 'admin';
|
||||
// Redis session handler (requires redis server and redis extension):
|
||||
// $CFG->session_handler_class = '\core\session\redis';
|
||||
// $CFG->session_redis_host = '127.0.0.1';
|
||||
// Use TLS to connect to Redis. An array of SSL context options. Usually:
|
||||
// $CFG->session_redis_encrypt = ['cafile' => '/path/to/ca.crt']; or...
|
||||
// $CFG->session_redis_encrypt = ['verify_peer' => false, 'verify_peer_name' => false];
|
||||
// $CFG->session_redis_port = 6379; // Optional.
|
||||
// $CFG->session_redis_database = 0; // Optional, default is db 0.
|
||||
// $CFG->session_redis_auth = ''; // Optional, default is don't set one.
|
||||
|
@ -57,6 +57,8 @@ class redis extends handler {
|
||||
protected $host = '';
|
||||
/** @var int $port The port to connect to */
|
||||
protected $port = 6379;
|
||||
/** @var array $sslopts SSL options, if applicable */
|
||||
protected $sslopts = [];
|
||||
/** @var string $auth redis password */
|
||||
protected $auth = '';
|
||||
/** @var int $database the Redis database to store sesions in */
|
||||
@ -105,6 +107,11 @@ class redis extends handler {
|
||||
$this->port = (int)$CFG->session_redis_port;
|
||||
}
|
||||
|
||||
if (isset($CFG->session_redis_encrypt) && $CFG->session_redis_encrypt) {
|
||||
$this->host = 'tls://' . $this->host;
|
||||
$this->sslopts = $CFG->session_redis_encrypt;
|
||||
}
|
||||
|
||||
if (isset($CFG->session_redis_auth)) {
|
||||
$this->auth = $CFG->session_redis_auth;
|
||||
}
|
||||
@ -210,6 +217,11 @@ class redis extends handler {
|
||||
// MDL-59866: Add retries for connections (up to 5 times) to make sure it goes through.
|
||||
$counter = 1;
|
||||
$maxnumberofretries = 5;
|
||||
$opts = [];
|
||||
if ($this->sslopts) {
|
||||
// Do not set $opts['stream'] = [], breaks connect().
|
||||
$opts['stream'] = $this->sslopts;
|
||||
}
|
||||
|
||||
while ($counter <= $maxnumberofretries) {
|
||||
|
||||
@ -218,7 +230,7 @@ class redis extends handler {
|
||||
$delay = rand(100, 500);
|
||||
|
||||
// One second timeout was chosen as it is long for connection, but short enough for a user to be patient.
|
||||
if (!$this->connection->connect($this->host, $this->port, 1, null, $delay)) {
|
||||
if (!$this->connection->connect($this->host, $this->port, 1, null, $delay, 1, $opts)) {
|
||||
throw new RedisException('Unable to connect to host.');
|
||||
}
|
||||
|
||||
@ -238,6 +250,16 @@ class redis extends handler {
|
||||
throw new RedisException('Unable to set Redis Prefix option.');
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->sslopts && !$this->connection->ping()) {
|
||||
/*
|
||||
* In case of a TLS connection, if phpredis client does not
|
||||
* communicate immediately with the server the connection hangs.
|
||||
* See https://github.com/phpredis/phpredis/issues/2332 .
|
||||
*/
|
||||
throw new \RedisException("Ping failed");
|
||||
}
|
||||
|
||||
if ($this->database !== 0) {
|
||||
if (!$this->connection->select($this->database)) {
|
||||
throw new RedisException('Unable to select Redis database '.$this->database.'.');
|
||||
|
@ -29,6 +29,7 @@ use RedisException;
|
||||
* define('TEST_SESSION_REDIS_HOST', '127.0.0.1');
|
||||
*
|
||||
* @package core
|
||||
* @covers \core\session\redis
|
||||
* @author Russell Smith <mr-russ@smith2001.net>
|
||||
* @copyright 2016 Russell Smith
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
@ -40,6 +41,8 @@ class session_redis_test extends \advanced_testcase {
|
||||
protected $keyprefix = null;
|
||||
/** @var $redis The current testing redis connection */
|
||||
protected $redis = null;
|
||||
/** @var bool $encrypted Is the current testing redis connection encrypted*/
|
||||
protected $encrypted = false;
|
||||
/** @var int $acquiretimeout how long we wait for session lock in seconds when testing Redis */
|
||||
protected $acquiretimeout = 1;
|
||||
/** @var int $lockexpire how long to wait in seconds before expiring the lock when testing Redis */
|
||||
@ -67,6 +70,19 @@ class session_redis_test extends \advanced_testcase {
|
||||
$this->keyprefix = 'phpunit'.rand(1, 100000);
|
||||
|
||||
$CFG->session_redis_host = TEST_SESSION_REDIS_HOST;
|
||||
if (strpos(TEST_SESSION_REDIS_HOST, ':')) {
|
||||
list($server, $port) = explode(':', TEST_SESSION_REDIS_HOST);
|
||||
} else {
|
||||
$server = TEST_SESSION_REDIS_HOST;
|
||||
$port = 6379;
|
||||
}
|
||||
|
||||
$opts = [];
|
||||
if (defined('TEST_SESSION_REDIS_ENCRYPT') && TEST_SESSION_REDIS_ENCRYPT) {
|
||||
$this->encrypted = true;
|
||||
$sslopts = $CFG->session_redis_encrypt = ['verify_peer' => false, 'verify_peer_name' => false];
|
||||
$opts['stream'] = $sslopts;
|
||||
}
|
||||
$CFG->session_redis_prefix = $this->keyprefix;
|
||||
|
||||
// Set a very short lock timeout to ensure tests run quickly. We are running single threaded,
|
||||
@ -75,7 +91,10 @@ class session_redis_test extends \advanced_testcase {
|
||||
$CFG->session_redis_lock_expire = $this->lockexpire;
|
||||
|
||||
$this->redis = new Redis();
|
||||
$this->redis->connect(TEST_SESSION_REDIS_HOST);
|
||||
$this->redis->connect($server, $port, 1, null, 1, 0, $opts);
|
||||
if (!$this->redis->ping()) {
|
||||
$this->markTestSkipped("Redis ping failed");
|
||||
}
|
||||
}
|
||||
|
||||
public function tearDown(): void {
|
||||
@ -325,7 +344,11 @@ class session_redis_test extends \advanced_testcase {
|
||||
$actual = $e->getMessage();
|
||||
}
|
||||
|
||||
$expected = 'Failed to connect (try 5 out of 5) to redis at ' . TEST_SESSION_REDIS_HOST . ':111111';
|
||||
$host = TEST_SESSION_REDIS_HOST;
|
||||
if ($this->encrypted) {
|
||||
$host = "tls://$host";
|
||||
}
|
||||
$expected = "Failed to connect (try 5 out of 5) to redis at $host:111111";
|
||||
$this->assertDebuggingCalledCount(5);
|
||||
$this->assertStringContainsString($expected, $actual);
|
||||
}
|
||||
@ -336,4 +359,16 @@ class session_redis_test extends \advanced_testcase {
|
||||
protected function assertSessionNoLocks() {
|
||||
$this->assertEmpty($this->redis->keys($this->keyprefix.'*.lock'));
|
||||
}
|
||||
|
||||
public function test_session_redis_encrypt() {
|
||||
global $CFG;
|
||||
|
||||
$CFG->session_redis_encrypt = ['verify_peer' => false, 'verify_peer_name' => false];
|
||||
|
||||
$sess = new \core\session\redis();
|
||||
|
||||
$prop = new \ReflectionProperty(\core\session\redis::class, 'host');
|
||||
$prop->setAccessible(true);
|
||||
$this->assertEquals('tls://' . TEST_SESSION_REDIS_HOST, $prop->getValue($sess));
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user