Permalink
Browse files

Implement TLS/SSL-encrypted connections.

This is handy for accessing remote Redis instances over a secure SSL connection
which is currently a popular option or even requirement with many cloud hosting
environments.

In order to configure the client to use an SSL-encrypted connection the scheme
in the connection parameters must be either "tsl" or "rediss" and a set of SSL
options (see http://php.net/manual/en/context.ssl.php) must be provided via the
"ssl" parameter as a named array.

The following example (which does not necessarily represent an example of good
practices!) illustrates how to set the "ssl" parameter using a named array and
the equivalent URI string:

  // Parameters as named array
  $parameters = [
    'scheme' => 'tls',
    'host'   => '127.0.0.1',
    'ssl'    => [
        'cafile'            => '/home/adaniele/redis.pem',
        'verify_peer_name'  => false,
    ],
  ];

  // Parameters as URI string
  $parameters = 'tls://127.0.0.1?ssl[cafile]=redis.pem&ssl[verify_peer_name]=1';

Support for SSL is currently limited to the Predis\Connection\StreamConnection
backend but we intend to investigate if it is possible to extend this feature
to Predis\Connection\PhpiredisStreamConnection in the future.

Be aware that using encrypted connections may lead to a performance degradation
especially in the connect() operation due to the overhead of the TLS handshake.
Unfortunately there is no real way to reuse SSL sessions from userland, aside
from enabling persistent connections, but this will work only on PHP >= 7.0.0
because previous versions of PHP do not provide enough info about a stream from
get_stream_meta_data().

NOTE: Redis does not have built-in support for SSL-encrypted connections, but if
you want to expose it to public networks you may want to rely on "stunnel".
  • Loading branch information...
nrk committed Jul 26, 2015
1 parent cb91ad1 commit ebb72377bb7c2a10b8b57dbf5ad1d3b1f45d0569
View
@@ -21,6 +21,9 @@ v1.1.0 (2015-xx-xx)
The fallback to a default value is a responsibility of connection classes, but
internally the default timeout for connect() operations is still 5 seconds.
- Implemented support for SSL-encrypted connections, the connection parameters
must use either the `tls` or `rediss` scheme.
v1.0.1 (2015-01-02)
================================================================================
View
8 FAQ.md
@@ -18,6 +18,14 @@ least to some degree).
Yes. Obviously persistent connections actually work only when using PHP configured as a persistent
process reused by the web server (see [PHP-FPM](http://php-fpm.org)).
### Does Predis support SSL-encrypted connections? ###
Yes. Encrypted connections are mostly useful when connecting to Redis instances exposed by various
cloud hosting providers without the need to configure an SSL proxy, but you should also take into
account the general performances degradation especially during the connect() operation when the TLS
handshake must be performed to secure the connection. Persistent SSL-encrypted connections may help
in that respect, but they are supported only when running on PHP >= 7.0.0.
### Does Predis support transparent (de)serialization of values? ###
No and it will not ever do that by default. The reason behind this decision is that serialization is
View
@@ -32,8 +32,8 @@ More details about this project can be found on the [frequently asked questions]
- Abstraction for Redis transactions (Redis >= 2.0) supporting CAS operations (Redis >= 2.2).
- Abstraction for Lua scripting (Redis >= 2.6) with automatic switching between `EVALSHA` or `EVAL`.
- Abstraction for `SCAN`, `SSCAN`, `ZSCAN` and `HSCAN` (Redis >= 2.8) based on PHP iterators.
- Connections to Redis are established lazily by the client upon the first command.
- Support for both TCP/IP and UNIX domain sockets and persistent connections.
- Connections are established lazily by the client upon the first command and can be persisted.
- Connections can be established via TCP/IP (optionally TLS/SSL-encrypted) or UNIX domain sockets.
- Support for [Webdis](http://webd.is) (requires both `ext-curl` and `ext-phpiredis`).
- Support for custom connection classes for providing different network or protocol backends.
- Flexible system for defining custom commands and server profiles.
@@ -95,8 +95,26 @@ $client = new Predis\Client([
$client = new Predis\Client('tcp://10.0.0.1:6379');
```
Starting with Predis v1.0.2 the client also understands the `redis` scheme in URI strings as defined
by the [provisional IANA registration](http://www.iana.org/assignments/uri-schemes/prov/redis).
The client can leverage TLS/SSL encryption to connect to secured remote Redis instances without the
the need to configure an SSL proxy like stunnel. This can be useful when connecting to nodes run by
various cloud hosting providers. Encryption can be enabled with the use the `tls` scheme along with
an array of suitable [options](http://php.net/manual/context.ssl.php) passed in the `ssl` parameter:
```php
// Named array of connection parameters:
$client = new Predis\Client([
'scheme' => 'tls',
'ssl' => ['cafile' => 'provate.pem', 'verify_peer' => true],
]
// Same set of parameters, but using an URI string:
$client = new Predis\Client('tls://127.0.0.1?ssl[cafile]=private.pem&ssl[verify_peer]=1');
```
The connection schemes [`redis`](http://www.iana.org/assignments/uri-schemes/prov/redis) (alias of
`tcp`) and [`rediss`](http://www.iana.org/assignments/uri-schemes/prov/rediss) (alias of `tls`) are
also supported, with the difference that URI strings containing these schemes are parsed following
the rules described on their respective IANA provisional registration documents.
The actual list of supported connection parameters can vary depending on each connection backend so
it is recommended to refer to their specific documentation or implementation for details.
@@ -25,7 +25,9 @@ class Factory implements FactoryInterface
protected $schemes = array(
'tcp' => 'Predis\Connection\StreamConnection',
'unix' => 'Predis\Connection\StreamConnection',
'tls' => 'Predis\Connection\StreamConnection',
'redis' => 'Predis\Connection\StreamConnection',
'rediss' => 'Predis\Connection\StreamConnection',
'http' => 'Predis\Connection\WebdisConnection',
);
@@ -87,19 +87,9 @@ private function assertExtensions()
/**
* {@inheritdoc}
*/
protected function assertParameters(ParametersInterface $parameters)
protected function assertSslSupport(ParametersInterface $parameters)
{
switch ($parameters->scheme) {
case 'tcp':
case 'redis':
case 'unix':
break;
default:
throw new \InvalidArgumentException("Invalid scheme: '$parameters->scheme'.");
}
return $parameters;
throw new \InvalidArgumentException('SSL encryption is not supported by this connection backend.');
}
/**
@@ -19,7 +19,7 @@
* Standard connection to Redis servers implemented on top of PHP's streams.
* The connection parameters supported by this class are:.
*
* - scheme: it can be either 'redis', 'tcp' or 'unix'.
* - scheme: it can be either 'redis', 'tcp', 'rediss', 'tls' or 'unix'.
* - host: hostname or IP address of the server.
* - port: TCP port of the server.
* - path: path of a UNIX domain socket when scheme is 'unix'.
@@ -28,6 +28,7 @@
* - async_connect: performs the connection asynchronously.
* - tcp_nodelay: enables or disables Nagle's algorithm for coalescing.
* - persistent: the connection is left intact after a GC collection.
* - ssl: context options array (see http://php.net/manual/en/context.ssl.php)
*
* @author Daniele Alessandri <suppakilla@gmail.com>
*/
@@ -58,13 +59,35 @@ protected function assertParameters(ParametersInterface $parameters)
case 'unix':
break;
case 'tls':
case 'rediss':
$this->assertSslSupport($parameters);
break;
default:
throw new \InvalidArgumentException("Invalid scheme: '$parameters->scheme'.");
}
return $parameters;
}
/**
* Checks needed conditions for SSL-encrypted connections.
*
* @param ParametersInterface $parameters Initialization parameters for the connection.
*
* @throws \InvalidArgumentException
*/
protected function assertSslSupport(ParametersInterface $parameters)
{
if (
filter_var($parameters->persistent, FILTER_VALIDATE_BOOLEAN) &&
version_compare(PHP_VERSION, '7.0.0beta') < 0
) {
throw new \InvalidArgumentException('Persistent SSL connections require PHP >= 7.0.0.');
}
}
/**
* {@inheritdoc}
*/
@@ -78,6 +101,10 @@ protected function createResource()
case 'unix':
return $this->unixStreamInitializer($this->parameters);
case 'tls':
case 'rediss':
return $this->tlsStreamInitializer($this->parameters);
default:
throw new \InvalidArgumentException("Invalid scheme: '{$this->parameters->scheme}'.");
}
@@ -179,6 +206,44 @@ protected function unixStreamInitializer(ParametersInterface $parameters)
return $resource;
}
/**
* Initializes a SSL-encrypted TCP stream resource.
*
* @param ParametersInterface $parameters Initialization parameters for the connection.
*
* @return resource
*/
protected function tlsStreamInitializer(ParametersInterface $parameters)
{
$resource = $this->tcpStreamInitializer($parameters);
$metadata = stream_get_meta_data($resource);
// Detect if crypto mode is already enabled for this stream (PHP >= 7.0.0).
if (isset($metadata['crypto'])) {
return $resource;
}
if (is_array($parameters->ssl)) {
$options = $parameters->ssl;
} else {
$options = array();
}
if (!isset($options['crypto_type'])) {
$options['crypto_type'] = STREAM_CRYPTO_METHOD_TLS_CLIENT;
}
if (!stream_context_set_option($resource, array('ssl' => $options))) {
$this->onConnectionError('Error while setting SSL context options');
}
if (!stream_socket_enable_crypto($resource, true, $options['crypto_type'])) {
$this->onConnectionError('Error while switching to encrypted communication');
}
return $resource;
}
/**
* {@inheritdoc}
*/
@@ -48,6 +48,26 @@ public function testSupportsSchemeRedis()
$this->assertInstanceOf('Predis\Connection\NodeConnectionInterface', $connection);
}
/**
* @group disconnected
*/
public function testSupportsSchemeTls()
{
$connection = $this->createConnectionWithParams(array('scheme' => 'tls'));
$this->assertInstanceOf('Predis\Connection\NodeConnectionInterface', $connection);
}
/**
* @group disconnected
*/
public function testSupportsSchemeRediss()
{
$connection = $this->createConnectionWithParams(array('scheme' => 'rediss'));
$this->assertInstanceOf('Predis\Connection\NodeConnectionInterface', $connection);
}
/**
* @group disconnected
*/
@@ -49,45 +49,55 @@ public function testSettingDefaultParameters()
/**
* @group disconnected
*/
public function testCreateConnection()
public function testCreateTcpConnection()
{
$factory = new Factory();
$tcp = new Parameters(array(
'scheme' => 'tcp',
'host' => 'locahost',
));
$parameters = new Parameters(array('scheme' => 'tcp'));
$connection = $factory->create($parameters);
$connection = $factory->create($tcp);
$parameters = $connection->getParameters();
$this->assertInstanceOf('Predis\Connection\StreamConnection', $connection);
$this->assertEquals($tcp->scheme, $parameters->scheme);
$this->assertEquals($tcp->host, $parameters->host);
$this->assertEquals($tcp->database, $parameters->database);
$this->assertSame($parameters, $connection->getParameters());
$tcp = new Parameters(array(
'scheme' => 'redis',
'host' => 'locahost',
));
$parameters = new Parameters(array('scheme' => 'redis'));
$connection = $factory->create($parameters);
$connection = $factory->create($tcp);
$parameters = $connection->getParameters();
$this->assertInstanceOf('Predis\Connection\StreamConnection', $connection);
$this->assertEquals($tcp->scheme, $parameters->scheme);
$this->assertEquals($tcp->host, $parameters->host);
$this->assertEquals($tcp->database, $parameters->database);
$this->assertSame($parameters, $connection->getParameters());
}
$unix = new Parameters(array(
'scheme' => 'unix',
'path' => '/tmp/redis.sock',
));
/**
* @group disconnected
*/
public function testCreateSslConnection()
{
$factory = new Factory();
$parameters = new Parameters(array('scheme' => 'tls'));
$connection = $factory->create($parameters);
$this->assertInstanceOf('Predis\Connection\StreamConnection', $connection);
$this->assertSame($parameters, $connection->getParameters());
$parameters = new Parameters(array('scheme' => 'rediss'));
$connection = $factory->create($parameters);
$this->assertInstanceOf('Predis\Connection\StreamConnection', $connection);
$this->assertSame($parameters, $connection->getParameters());
}
/**
* @group disconnected
*/
public function testCreateUnixConnection()
{
$factory = new Factory();
$parameters = new Parameters(array('scheme' => 'unix', 'path' => '/tmp/redis.sock'));
$connection = $factory->create($parameters);
$connection = $factory->create($unix);
$parameters = $connection->getParameters();
$this->assertInstanceOf('Predis\Connection\StreamConnection', $connection);
$this->assertEquals($unix->scheme, $parameters->scheme);
$this->assertEquals($unix->path, $parameters->path);
$this->assertEquals($unix->database, $parameters->database);
$this->assertSame($parameters, $connection->getParameters());
}
/**
@@ -19,6 +19,30 @@ class PhpiredisSocketConnectionTest extends PredisConnectionTestCase
{
const CONNECTION_CLASS = 'Predis\Connection\PhpiredisSocketConnection';
/**
* @group disconnected
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Invalid scheme: 'tls'.
*/
public function testSupportsSchemeTls()
{
$connection = $this->createConnectionWithParams(array('scheme' => 'tls'));
$this->assertInstanceOf('Predis\Connection\NodeConnectionInterface', $connection);
}
/**
* @group disconnected
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Invalid scheme: 'rediss'.
*/
public function testSupportsSchemeRediss()
{
$connection = $this->createConnectionWithParams(array('scheme' => 'rediss'));
$this->assertInstanceOf('Predis\Connection\NodeConnectionInterface', $connection);
}
// ******************************************************************** //
// ---- INTEGRATION TESTS --------------------------------------------- //
// ******************************************************************** //
@@ -19,6 +19,30 @@ class PhpiredisStreamConnectionTest extends PredisConnectionTestCase
{
const CONNECTION_CLASS = 'Predis\Connection\PhpiredisStreamConnection';
/**
* @group disconnected
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage SSL encryption is not supported by this connection backend.
*/
public function testSupportsSchemeTls()
{
$connection = $this->createConnectionWithParams(array('scheme' => 'tls'));
$this->assertInstanceOf('Predis\Connection\NodeConnectionInterface', $connection);
}
/**
* @group disconnected
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage SSL encryption is not supported by this connection backend.
*/
public function testSupportsSchemeRediss()
{
$connection = $this->createConnectionWithParams(array('scheme' => 'rediss'));
$this->assertInstanceOf('Predis\Connection\NodeConnectionInterface', $connection);
}
// ******************************************************************** //
// ---- INTEGRATION TESTS --------------------------------------------- //
// ******************************************************************** //

0 comments on commit ebb7237

Please sign in to comment.