RedisFactory.php 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Alejandro Varela <epma01@gmail.com>
  6. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  7. * @author Jörn Friedrich Dreyer <jfd@butonic.de>
  8. * @author Morris Jobke <hey@morrisjobke.de>
  9. * @author Robin Appelman <robin@icewind.nl>
  10. * @author Robin McCorkell <robin@mccorkell.me.uk>
  11. *
  12. * @license AGPL-3.0
  13. *
  14. * This code is free software: you can redistribute it and/or modify
  15. * it under the terms of the GNU Affero General Public License, version 3,
  16. * as published by the Free Software Foundation.
  17. *
  18. * This program is distributed in the hope that it will be useful,
  19. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. * GNU Affero General Public License for more details.
  22. *
  23. * You should have received a copy of the GNU Affero General Public License, version 3,
  24. * along with this program. If not, see <http://www.gnu.org/licenses/>
  25. *
  26. */
  27. namespace OC;
  28. use OCP\Diagnostics\IEventLogger;
  29. class RedisFactory {
  30. public const REDIS_MINIMAL_VERSION = '3.1.3';
  31. public const REDIS_EXTRA_PARAMETERS_MINIMAL_VERSION = '5.3.0';
  32. /** @var \Redis|\RedisCluster */
  33. private $instance;
  34. private SystemConfig $config;
  35. private IEventLogger $eventLogger;
  36. /**
  37. * RedisFactory constructor.
  38. *
  39. * @param SystemConfig $config
  40. */
  41. public function __construct(SystemConfig $config, IEventLogger $eventLogger) {
  42. $this->config = $config;
  43. $this->eventLogger = $eventLogger;
  44. }
  45. private function create() {
  46. $isCluster = in_array('redis.cluster', $this->config->getKeys(), true);
  47. $config = $isCluster
  48. ? $this->config->getValue('redis.cluster', [])
  49. : $this->config->getValue('redis', []);
  50. if ($isCluster && !class_exists('RedisCluster')) {
  51. throw new \Exception('Redis Cluster support is not available');
  52. }
  53. if (isset($config['timeout'])) {
  54. $timeout = $config['timeout'];
  55. } else {
  56. $timeout = 0.0;
  57. }
  58. if (isset($config['read_timeout'])) {
  59. $readTimeout = $config['read_timeout'];
  60. } else {
  61. $readTimeout = 0.0;
  62. }
  63. $auth = null;
  64. if (isset($config['password']) && (string)$config['password'] !== '') {
  65. if (isset($config['user']) && (string)$config['user'] !== '') {
  66. $auth = [$config['user'], $config['password']];
  67. } else {
  68. $auth = $config['password'];
  69. }
  70. }
  71. // # TLS support
  72. // # https://github.com/phpredis/phpredis/issues/1600
  73. $connectionParameters = $this->getSslContext($config);
  74. // cluster config
  75. if ($isCluster) {
  76. if (!isset($config['seeds'])) {
  77. throw new \Exception('Redis cluster config is missing the "seeds" attribute');
  78. }
  79. // Support for older phpredis versions not supporting connectionParameters
  80. if ($connectionParameters !== null) {
  81. $this->instance = new \RedisCluster(null, $config['seeds'], $timeout, $readTimeout, true, $auth, $connectionParameters);
  82. } else {
  83. $this->instance = new \RedisCluster(null, $config['seeds'], $timeout, $readTimeout, true, $auth);
  84. }
  85. if (isset($config['failover_mode'])) {
  86. $this->instance->setOption(\RedisCluster::OPT_SLAVE_FAILOVER, $config['failover_mode']);
  87. }
  88. } else {
  89. $this->instance = new \Redis();
  90. if (isset($config['host'])) {
  91. $host = $config['host'];
  92. } else {
  93. $host = '127.0.0.1';
  94. }
  95. if (isset($config['port'])) {
  96. $port = $config['port'];
  97. } elseif ($host[0] !== '/') {
  98. $port = 6379;
  99. } else {
  100. $port = null;
  101. }
  102. $this->eventLogger->start('connect:redis', 'Connect to redis and send AUTH, SELECT');
  103. // Support for older phpredis versions not supporting connectionParameters
  104. if ($connectionParameters !== null) {
  105. // Non-clustered redis requires connection parameters to be wrapped inside `stream`
  106. $connectionParameters = [
  107. 'stream' => $this->getSslContext($config)
  108. ];
  109. /**
  110. * even though the stubs and documentation don't want you to know this,
  111. * pconnect does have the same $connectionParameters argument connect has
  112. *
  113. * https://github.com/phpredis/phpredis/blob/0264de1824b03fb2d0ad515b4d4ec019cd2dae70/redis.c#L710-L730
  114. *
  115. * @psalm-suppress TooManyArguments
  116. */
  117. $this->instance->pconnect($host, $port, $timeout, null, 0, $readTimeout, $connectionParameters);
  118. } else {
  119. $this->instance->pconnect($host, $port, $timeout, null, 0, $readTimeout);
  120. }
  121. // Auth if configured
  122. if ($auth !== null) {
  123. $this->instance->auth($auth);
  124. }
  125. if (isset($config['dbindex'])) {
  126. $this->instance->select($config['dbindex']);
  127. }
  128. $this->eventLogger->end('connect:redis');
  129. }
  130. }
  131. /**
  132. * Get the ssl context config
  133. *
  134. * @param array $config the current config
  135. * @return array|null
  136. * @throws \UnexpectedValueException
  137. */
  138. private function getSslContext($config) {
  139. if (isset($config['ssl_context'])) {
  140. if (!$this->isConnectionParametersSupported()) {
  141. throw new \UnexpectedValueException(\sprintf(
  142. 'php-redis extension must be version %s or higher to support ssl context',
  143. self::REDIS_EXTRA_PARAMETERS_MINIMAL_VERSION
  144. ));
  145. }
  146. return $config['ssl_context'];
  147. }
  148. return null;
  149. }
  150. public function getInstance() {
  151. if (!$this->isAvailable()) {
  152. throw new \Exception('Redis support is not available');
  153. }
  154. if (!$this->instance instanceof \Redis) {
  155. $this->create();
  156. }
  157. return $this->instance;
  158. }
  159. public function isAvailable(): bool {
  160. return \extension_loaded('redis') &&
  161. \version_compare(\phpversion('redis'), self::REDIS_MINIMAL_VERSION, '>=');
  162. }
  163. /**
  164. * Php redis does support configurable extra parameters since version 5.3.0, see: https://github.com/phpredis/phpredis#connect-open.
  165. * We need to check if the current version supports extra connection parameters, otherwise the connect method will throw an exception
  166. *
  167. * @return boolean
  168. */
  169. private function isConnectionParametersSupported(): bool {
  170. return \extension_loaded('redis') &&
  171. \version_compare(\phpversion('redis'), self::REDIS_EXTRA_PARAMETERS_MINIMAL_VERSION, '>=');
  172. }
  173. }