From 23ede166b5b0096bd7db0eb48a51aabe521166f8 Mon Sep 17 00:00:00 2001 From: emilio Date: Mon, 5 Jun 2023 12:18:14 +0200 Subject: [PATCH 1/4] Branch for redissentinel support --- classes/redis_store.php | 24 ++++- classes/sentinel.php | 204 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 224 insertions(+), 4 deletions(-) create mode 100644 classes/sentinel.php diff --git a/classes/redis_store.php b/classes/redis_store.php index a939c12d5..9bbf0f4be 100644 --- a/classes/redis_store.php +++ b/classes/redis_store.php @@ -28,6 +28,7 @@ defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/../.extlib/simplesamlphp/lib/SimpleSAML/Store.php'); +require_once(__DIR__ . '/sentinel.php'); /** * Redis store simpleSAMLphp class for auth/saml2. @@ -113,14 +114,28 @@ protected function bootstrap_redis() { if (!class_exists('Redis')) { throw new \coding_exception('Redis class not found, Redis PHP Extension is probably not installed'); - } - if (empty($CFG->auth_saml2_redis_server)) { - throw new \coding_exception('Redis connection string is not configured in $CFG->auth_saml2_redis_server'); + } + if (!empty($CFG->auth_saml2_redissentinel_servers) && !empty($CFG->auth_saml2_redissentinel_group)) { + $servers = explode(',',$CFG->auth_saml2_redissentinel_servers); + try { + $sentinel = new \sentinel($servers); + $master = $sentinel->get_master_addr($CFG->auth_saml2_redissentinel_group); + } catch(Exception $e) { + debugging('Unable to connect to Redis Sentinel servers: '.$CFG->auth_saml2_redissentinel_servers, DEBUG_ALL); + return; + } + } + + if (!empty($CFG->auth_saml2_redis_server)) { + $server = explode(':',$CFG->auth_saml2_redis_server); + $master = new \stdClass(); + $master->ip = $server[0]; + $master->port = (count($server)>1)?$server[1]:"6379"; } try { $redis = new \Redis(); - $redis->connect($CFG->auth_saml2_redis_server); + $redis->connect($master->ip, $master->port); } catch (\RedisException $e) { throw new \coding_exception("RedisException caught with message: {$e->getMessage()}"); } @@ -162,3 +177,4 @@ protected function get_set_options($expire) { return $options; } } + diff --git a/classes/sentinel.php b/classes/sentinel.php new file mode 100644 index 000000000..01587e165 --- /dev/null +++ b/classes/sentinel.php @@ -0,0 +1,204 @@ +. + +/** + * Redis Sentinel class + * + * @package cachestore_redissentinelsentinel + * @copyright 2017 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace auth_saml2; + +class sentinel { + + private $sentinels = array(); + + public $connecttimeout = 1; + public $readtimeout = 1; + public $persistent = true; + + private $flags; + + private $connected; + + private $socket; + + private $pingonconnect = false; + + public function __construct($sentinels) { + + $this->sentinels = $sentinels; + + $this->flags = STREAM_CLIENT_CONNECT; + + $this->connected = false; + + } + + public function __destruct() { + if (!$this->persistent && $this->connected) { + $this->disconnect(); + } + } + + public function connecttopool() { + if ($this->connected) { + return true; + } + + foreach ($this->sentinels as $sentinel) { + if ($this->connect($sentinel)) { + return true; + } + } + + throw new \Exception('Unable to connect to sentinel pool'); + } + + + private function connect($sentinel) { + + if ($this->persistent) { + $this->socket = @stream_socket_client($sentinel, $errorno, $errstr, $this->connecttimeout, STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT); + } else { + $this->socket = @stream_socket_client($sentinel, $errorno, $errstr, $this->connecttimeout); + } + + if (!$this->socket) { + $this->connected = false; + return false; + } + + $this->connected = true; + + stream_set_blocking($this->socket, true); + stream_set_timeout($this->socket, $this->readtimeout); + + // Test sentinel is alive + if ($this->pingonconnect) { + fwrite($this->socket, "PING\n"); + if (trim(fgets($this->socket)) != '+PONG') { + fclose($this->socket); + $this->connected = false; + return false; + } + } + return true; + } + + + + public function disconnect() { + fclose($this->socket); + $this->connected = false; + + } + + public function get_master_addr($name) { + + $cmd = "get-master-addr-by-name $name"; + + $this->command($cmd); + if (!$resp = $this->cachestore_redissentinelsentinel()) { + return false; + } + + $ret = new \stdClass(); + $ret->ip = $resp[0]; + $ret->port = $resp[1]; + + return ($ret); + } + + + private function command($command) { + if (!$this->connected) { + $this->connecttopool(); + } + + if (!$this->connected) { + return false; + } + $cmd = "SENTINEL $command\n"; + + + + $cmdlen = strlen($cmd); + $lastwrite = 0; + for ($written = 0; $written < $cmdlen; $written += $lastwrite) { + $lastwrite = fwrite($this->socket, substr($cmd, $written)); + + if ($lastwrite === false || $lastwrite == 0) { + $this->connected = false; + throw new \Exception('Failed to write command to stream'); + } + } + } + + + private function cachestore_redissentinelsentinel() { + if (!$this->connected) { + return false; + } + + $resp = fgets($this->socket); + + $type = substr($resp, 0, 1); + + switch($type) { + + // Error response + case '-': + throw new \Exception('Error response received: '.$resp); + break; + + // In-line response + case '+': + $response = substr($resp, 1); + return(substr($resp, 1)); + + // Defined size response + case '$': + $size = (int) substr($resp, 1); + $resp = stream_get_contents($this->socket, $size+2); + if ($resp === false) { + throw new \Exception('Failed to read from stream'); + } + return (trim($resp)); + + // Int response + case ':': + return ((int)substr($reply,1)); + + // Multi line response + case '*': + $multireponse = array(); + $size = (int) substr($resp, 1); + + for ($i=0;$i<$size; $i++) { + $multireponse[] = $this->cachestore_redissentinelsentinel(); + } + return($multireponse); + + + // Unknown reesponse + default: + throw new \Exception('Unknown read response from stream'); + } + } + +} From 624eb8429344a6bfb584ef496ff74577df0b6a5e Mon Sep 17 00:00:00 2001 From: emilio Date: Mon, 5 Jun 2023 12:38:35 +0200 Subject: [PATCH 2/4] Fix. Using sentinel class of plugin namespace --- classes/redis_store.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/redis_store.php b/classes/redis_store.php index 9bbf0f4be..30d885df1 100644 --- a/classes/redis_store.php +++ b/classes/redis_store.php @@ -118,7 +118,7 @@ protected function bootstrap_redis() { if (!empty($CFG->auth_saml2_redissentinel_servers) && !empty($CFG->auth_saml2_redissentinel_group)) { $servers = explode(',',$CFG->auth_saml2_redissentinel_servers); try { - $sentinel = new \sentinel($servers); + $sentinel = new sentinel($servers); $master = $sentinel->get_master_addr($CFG->auth_saml2_redissentinel_group); } catch(Exception $e) { debugging('Unable to connect to Redis Sentinel servers: '.$CFG->auth_saml2_redissentinel_servers, DEBUG_ALL); From 9a4ce646c3627009567f8a5db0b9ea74d1d64933 Mon Sep 17 00:00:00 2001 From: emilio Date: Tue, 6 Jun 2023 09:23:21 +0200 Subject: [PATCH 3/4] Added comments. Fixed some function names --- classes/sentinel.php | 54 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/classes/sentinel.php b/classes/sentinel.php index 01587e165..6a8c10bf3 100644 --- a/classes/sentinel.php +++ b/classes/sentinel.php @@ -17,12 +17,19 @@ /** * Redis Sentinel class * - * @package cachestore_redissentinelsentinel + * @package auth_saml2 * @copyright 2017 Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_saml2; +/** + * Redis Sentinel class + * + * @package auth_saml2 + * @copyright 2017 Catalyst IT + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ class sentinel { private $sentinels = array(); @@ -39,6 +46,11 @@ class sentinel { private $pingonconnect = false; + /** + * Constructs Sentinel + * @param array $sentinels + */ + public function __construct($sentinels) { $this->sentinels = $sentinels; @@ -49,12 +61,20 @@ public function __construct($sentinels) { } + /** + * Destructs Sentinel + */ public function __destruct() { if (!$this->persistent && $this->connected) { $this->disconnect(); } } + /** + * Try to connect to one of the sentinel servers defined in $this->sentinels + * @return boolean + * @throws \Exception + */ public function connecttopool() { if ($this->connected) { return true; @@ -69,7 +89,11 @@ public function connecttopool() { throw new \Exception('Unable to connect to sentinel pool'); } - + /** + * Connects to one sentinel server + * @param string $sentinel + * @return boolean + */ private function connect($sentinel) { if ($this->persistent) { @@ -101,19 +125,26 @@ private function connect($sentinel) { } - + /** + * Disconnects from sentinel socket + */ public function disconnect() { fclose($this->socket); $this->connected = false; } + /** + * Returns ip:port of the redis master of a redis sentinel group named $name + * @param string $name + * @return string + */ public function get_master_addr($name) { $cmd = "get-master-addr-by-name $name"; $this->command($cmd); - if (!$resp = $this->cachestore_redissentinelsentinel()) { + if (!$resp = $this->read_response()) { return false; } @@ -125,6 +156,12 @@ public function get_master_addr($name) { } + /** + * Send a command to redis cluster + * @param string $command + * @return boolean + * @throws \Exception + */ private function command($command) { if (!$this->connected) { $this->connecttopool(); @@ -150,7 +187,12 @@ private function command($command) { } - private function cachestore_redissentinelsentinel() { + /** + * Read the response of a command + * @return string + * @throws \Exception + */ + private function read_response() { if (!$this->connected) { return false; } @@ -190,7 +232,7 @@ private function cachestore_redissentinelsentinel() { $size = (int) substr($resp, 1); for ($i=0;$i<$size; $i++) { - $multireponse[] = $this->cachestore_redissentinelsentinel(); + $multireponse[] = $this->read_response(); } return($multireponse); From 1be0588070991969f99f2b673b4a1b3ca1c20037 Mon Sep 17 00:00:00 2001 From: emilio Date: Tue, 6 Jun 2023 09:27:37 +0200 Subject: [PATCH 4/4] Fixed return type --- classes/sentinel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/classes/sentinel.php b/classes/sentinel.php index 6a8c10bf3..0a19a5ce7 100644 --- a/classes/sentinel.php +++ b/classes/sentinel.php @@ -189,7 +189,7 @@ private function command($command) { /** * Read the response of a command - * @return string + * @return mixed * @throws \Exception */ private function read_response() {