Compare commits

..

10 Commits

Author SHA1 Message Date
Daniele Alessandri 8ca99f91e4 Update CHANGELOG and bump VERSION (v1.1.5) 2020-09-10 15:35:23 +02:00
Qusonann c778843b0d Fix zrevrangebyscore pipeline annotation
[ci skip]
2020-09-09 23:42:03 +02:00
Daniele Alessandri dca201291c Fix CHANGELOG for broken Markdown rendering.
[ci skip]
2020-09-05 16:50:25 +02:00
Daniele Alessandri 2e76410e65 Support password-based authentication for sentinel instances.
Password-based authentication for sentinels has been added in Redis 5.
Predis was actively ignoring any "password" parameter for sentinels when
creating connections to them to avoid issues when this parameter is set
in the default "parameters" array passed via client options, as they are
applied to **every** connection created by Predis (see #346).

We need to find a better way to specify a common password for sentinels
to be handled in a different way than the ones for Redis nodes. For now
each sentinel node protected by password must have an explicit password
set in its parameters list even if this password, by design, is the same
for all sentinels. Since we cannot use default "parameters" as explained
above but we still need to pass a common value for all sentinels an idea
could be using a dedicated client option like we did with "service", but
we will see later.

In this commit we also explicitly reset any "username" parameter as it
would trigger an `AUTH $username $password` but sentinels do not support
ACL authentication.

Fixes #594.
2020-09-05 16:25:06 +02:00
Daniele Alessandri ca468b785c Throw exception on FALSE passed to "replication" option.
While "replication" do accept values evaluating to TRUE, the same cannot
be said for values evaluating to FALSE. TRUE is used to tell the client
that we want replication handled using the default backend for unmanaged
replication setups. For using redis-sentinel the "sentinel" string value
must be passed.

Setting "replication" to FALSE led to a failure (and a PHP warning) on
client initialization because this condition was not handled properly.
Being able to do so would not make sense anyway: when the client does
not need to be set up to rely on replication, users simply have to omit
the option. Furthermore, users must always specify either "replication"
or "cluster" and not both with one of them set to FALSE.

Unfortunately options for aggregate connections in Predis v1.1 are a bit
of a mess, they did not scale well with the addition of new features and
are also quite inconsistent (e.g. "cluster" does not accept TRUE).

This has been largely fixed in Predis v2.0-dev but required implementing
a few breaking changes. It also means that this change does not need to
be ported to the main branch.

Addresses #381 using a different approach.
2020-09-05 14:18:30 +02:00
Daniele Alessandri 4a7fb55058 [tests] Add utility method to skip tests on CI environment.
We still have disabled all PUB/SUB related tests on CI for now, until we
understand why they fail at random.

Backported from main branch (ref. 5133706, f723f67, eb8a89e)
2020-09-04 18:42:38 +02:00
Daniele Alessandri fdf663ebba Fix WebdisConnection with IPv4 hosts.
Apparently something changed since last time in cURL internals and now
an IPv4 wrapped by square brackets (which are used for IPv6 addresses)
returns a malformed request error.

m
2020-09-04 18:06:36 +02:00
Daniele Alessandri 880ad09cad Restore broken change from a few years ago.
After this change there was no check to prevent the user from using SSL
with PhpiredisStreamConnection (it does not work due to the fact that
internally is uses stream_socket_recvfrom()).

Pretty obvious phpiredis was not installed on my system when I made that
change or the test suite would have caught the wrong behaviour (like it
just happened now that I build it from scratch).
2020-09-04 18:05:26 +02:00
Daniele Alessandri 5eccc65d42 Fix handling of username when using "redis://".
The username is now correctly retrieved from the userinfo fragment of
the URI when using the "redis" scheme and a "username:password" pair is
present. Values retrieved from the userinfo fragment always override the
ones specified in `username` and `password` if those fields are present
in the query string.
2020-09-01 11:40:30 +02:00
Daniele Alessandri f225c55a01 Back to development.
[ci skip]
2020-08-31 12:39:52 +02:00
16 changed files with 190 additions and 58 deletions
+29
View File
@@ -1,3 +1,32 @@
v1.1.5 (2020-09-10)
================================================================================
- __FIX__: authentication for sentinels is now supported, previously it was not
possible to specify a `password` for sentinels as its value was stripped during
initialization because sentinels did not support authentication until Redis 5.
**Please note** that with the current implementation each sentinel must have
its own `password` parameter set in the parameters list despite this password is
the same for all sentinels (read how `requirepass` works on the Redis docs). In
this case you should avoid using the global `parameters` client option used to
set default parameters for every connection created by Predis as this would end
up using the same password even when connecting to actual Redis nodes.
- __FIX__: the username is now correctly retrieved from the userinfo fragment of
the URI when using the "redis" scheme and a "username:password" pair is present.
Values retrieved from the userinfo fragment always override the ones specified
in `username` and `password` if those fields are present in the query string.
- __FIX__: `Predis\Connection\WebdisConnection` was unable to connect to Webdis
when using an IPv4 address in the URL and this is probably due to some change in
cURL internals since the last time we tested it.
- __FIX__: an exception is thrown whe passing `FALSE` or any value evaluating to
`FALSE` to the `replication` client option. This was supposed to be unsupported,
in fact it actually breaks client initialization and raises a PHP warning. Now
the user is alerted with an `InvalidArgumentException` and a proper message.
(PR #381).
v1.1.4 (2020-08-31)
================================================================================
+1 -1
View File
@@ -1 +1 @@
1.1.4
1.1.5
+1 -1
View File
@@ -10,7 +10,7 @@ name = "Predis"
desc = "Flexible and feature-complete Redis client for PHP and HHVM"
homepage = "http://github.com/nrk/predis"
license = "MIT"
version = "1.1.4"
version = "1.1.5"
stability = "stable"
channel = "pear.nrk.io"
+1 -1
View File
@@ -40,7 +40,7 @@ use Predis\Transaction\MultiExec as MultiExecTransaction;
*/
class Client implements ClientInterface, \IteratorAggregate
{
const VERSION = '1.1.4';
const VERSION = '1.1.5';
protected $connection;
protected $options;
+1 -1
View File
@@ -118,7 +118,7 @@ use Predis\Command\CommandInterface;
* @method $this zremrangebyrank($key, $start, $stop)
* @method $this zremrangebyscore($key, $min, $max)
* @method $this zrevrange($key, $start, $stop, array $options = null)
* @method $this zrevrangebyscore($key, $min, $max, array $options = null)
* @method $this zrevrangebyscore($key, $max, $min, array $options = null)
* @method $this zrevrank($key, $member)
* @method $this zunionstore($destination, array|string $keys, array $options = null)
* @method $this zscore($key, $member)
+7 -5
View File
@@ -36,10 +36,6 @@ class ReplicationOption implements OptionInterface
return $value;
}
if (is_bool($value) || $value === null) {
return $value ? $this->getDefault($options) : null;
}
if ($value === 'sentinel') {
return function ($sentinels, $options) {
return new SentinelReplication($options->service, $sentinels, $options->connections);
@@ -50,7 +46,13 @@ class ReplicationOption implements OptionInterface
!is_object($value) &&
null !== $asbool = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)
) {
return $asbool ? $this->getDefault($options) : null;
if (true === $asbool) {
return $this->getDefault($options);
} else {
throw new \InvalidArgumentException(
"Values evaluating to FALSE are not accepted for `replication`"
);
}
}
throw new \InvalidArgumentException(
@@ -239,10 +239,12 @@ class SentinelReplication implements ReplicationInterface
}
if (is_array($parameters)) {
// We explicitly set "database" and "password" to null,
// so that no AUTH and SELECT command is send to the sentinels.
// Password authentication is fine now that Redis Sentinel supports
// password-protected sentinel instances, but we must explicitly set
// "database" and "username" to NULL so that no augmented AUTH (ACL)
// and SELECT command are sent by accident to the sentinels.
$parameters['database'] = null;
$parameters['password'] = null;
$parameters['username'] = null;
if (!isset($parameters['timeout'])) {
$parameters['timeout'] = $this->sentinelTimeout;
+10 -1
View File
@@ -109,8 +109,17 @@ class Parameters implements ParametersInterface
}
if (stripos($uri, 'redis') === 0) {
if (isset($parsed['user'])) {
if (strlen($parsed['user'])) {
$parsed['username'] = $parsed['user'];
}
unset($parsed['user']);
}
if (isset($parsed['pass'])) {
$parsed['password'] = $parsed['pass'];
if (strlen($parsed['pass'])) {
$parsed['password'] = $parsed['pass'];
}
unset($parsed['pass']);
}
+16 -2
View File
@@ -87,9 +87,23 @@ class PhpiredisStreamConnection extends StreamConnection
/**
* {@inheritdoc}
*/
protected function assertSslSupport(ParametersInterface $parameters)
protected function assertParameters(ParametersInterface $parameters)
{
throw new \InvalidArgumentException('SSL encryption is not supported by this connection backend.');
switch ($parameters->scheme) {
case 'tcp':
case 'redis':
case 'unix':
break;
case 'tls':
case 'rediss':
throw new \InvalidArgumentException('SSL encryption is not supported by this connection backend.');
default:
throw new \InvalidArgumentException("Invalid scheme: '$parameters->scheme'.");
}
return $parameters;
}
/**
+1 -1
View File
@@ -119,7 +119,7 @@ class WebdisConnection implements NodeConnectionInterface
$parameters = $this->getParameters();
$timeout = (isset($parameters->timeout) ? (float) $parameters->timeout : 5.0) * 1000;
if (filter_var($host = $parameters->host, FILTER_VALIDATE_IP)) {
if (filter_var($host = $parameters->host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$host = "[$host]";
}
+12
View File
@@ -337,4 +337,16 @@ abstract class PredisTestCase extends \PHPUnit_Framework_TestCase
$this->checkRequiredRedisServerVersion();
}
/**
* Marks current test skipped when test suite is running on CI environments.
*
* @param string $message
*/
protected function markTestSkippedOnCIEnvironment($message = 'Test skipped on CI environment')
{
if (getenv('GITHUB_ACTIONS') || getenv('TRAVIS')) {
$this->markTestSkipped($message);
}
}
}
@@ -31,26 +31,45 @@ class ReplicationOptionTest extends PredisTestCase
}
/**
* @group disconnected
* @return array
*/
public function testAcceptsValuesThatCanBeInterpretedAsBooleans()
public function provideValuesEvaluatingTrue()
{
return array(array(true), array(1), array('true'), array('on'));
}
/**
* @group disconnected
* @dataProvider provideValuesEvaluatingTrue
*/
public function testAcceptsValuesThatCanBeInterpretedAsBooleanTrue($value)
{
$option = new ReplicationOption();
$options = $this->getMock('Predis\Configuration\OptionsInterface');
$this->assertNull($option->filter($options, null));
$this->assertInstanceOf('Predis\Connection\Aggregate\MasterSlaveReplication', $option->filter($options, $value));
}
$this->assertInstanceOf('Predis\Connection\Aggregate\ReplicationInterface', $option->filter($options, true));
$this->assertNull($option->filter($options, false));
/**
* @return array
*/
public function provideValuesEvaluatingFalse()
{
return array(array(false), array(0), array('false'), array('off'));
}
$this->assertInstanceOf('Predis\Connection\Aggregate\ReplicationInterface', $option->filter($options, 1));
$this->assertNull($option->filter($options, 0));
/**
* @group disconnected
* @dataProvider provideValuesEvaluatingFalse
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Values evaluating to FALSE are not accepted for `replication`
*/
public function testDoesNotAcceptValuesThatCanBeInterpretedAsBooleanFalse($value)
{
$option = new ReplicationOption();
$options = $this->getMock('Predis\Configuration\OptionsInterface');
$this->assertInstanceOf('Predis\Connection\Aggregate\ReplicationInterface', $option->filter($options, 'true'));
$this->assertNull($option->filter($options, 'false'));
$this->assertInstanceOf('Predis\Connection\Aggregate\ReplicationInterface', $option->filter($options, 'on'));
$this->assertNull($option->filter($options, 'off'));
$option->filter($options, $value);
}
/**
@@ -36,15 +36,29 @@ class SentinelReplicationTest extends PredisTestCase
/**
* @group disconnected
*/
public function testParametersForSentinelConnectionShouldNotUseDatabaseAndPassword()
public function testParametersForSentinelConnectionShouldUsePasswordForAuthentication()
{
$replication = $this->getReplicationConnection('svc', array(
'tcp://127.0.0.1:5381?alias=sentinel1&database=1&password=secret',
'tcp://127.0.0.1:5381?alias=sentinel1&password=secret',
));
$parameters = $replication->getSentinelConnection()->getParameters()->toArray();
$this->assertArraySubset(array('database' => null, 'password' => null), $parameters);
$this->assertArraySubset(array('password' => 'secret'), $parameters);
}
/**
* @group disconnected
*/
public function testParametersForSentinelConnectionShouldNotUseDatabaseAndUsername()
{
$replication = $this->getReplicationConnection('svc', array(
'tcp://127.0.0.1:5381?alias=sentinel1&database=1&username=myusername',
));
$parameters = $replication->getSentinelConnection()->getParameters()->toArray();
$this->assertArraySubset(array('database' => null, 'username' => null), $parameters);
}
/**
+34 -17
View File
@@ -156,7 +156,7 @@ class ParametersTest extends PredisTestCase
*/
public function testParsingURIWithRedisScheme()
{
$uri = 'redis://:secret@10.10.10.10:6400/5?timeout=0.5&persistent=1';
$uri = 'redis://predis:secret@10.10.10.10:6400/5?timeout=0.5&persistent=1';
$expected = array(
'scheme' => 'redis',
@@ -164,20 +164,48 @@ class ParametersTest extends PredisTestCase
'port' => 6400,
'timeout' => '0.5',
'persistent' => '1',
'username' => 'predis',
'password' => 'secret',
'database' => '5',
);
$parameters = Parameters::parse($uri);
// TODO: parse_url() in PHP >= 5.6 returns an empty "user" entry in the
// dictionary when no username has been provided in the URI string. This
// actually makes sense, but let's keep the test ugly & simple for now.
unset($parameters['user']);
$this->assertSame($expected, $parameters);
}
/**
* @group disconnected
*/
public function testRedisSchemeOverridesUsernameAndPasswordInQueryString()
{
$parameters = Parameters::parse('redis://predis:secret@10.10.10.10/5?username=ignored&password=ignored');
$this->assertSame('predis', $parameters['username']);
$this->assertSame('secret', $parameters['password']);
}
/**
* @group disconnected
*/
public function testRedisSchemeDoesNotOverridesUsernameAndPasswordInQueryStringOnEmptyAuthFragment()
{
$parameters = Parameters::parse('redis://:@10.10.10.10/5?username=predis&password=secret');
$this->assertSame('predis', $parameters['username']);
$this->assertSame('secret', $parameters['password']);
}
/**
* @group disconnected
*/
public function testRedisSchemeOverridesDatabaseInQueryString()
{
$parameters = Parameters::parse('redis://10.10.10.10/5?database=10');
$this->assertSame('5', $parameters['database']);
}
/**
* @group disconnected
*/
@@ -197,17 +225,6 @@ class ParametersTest extends PredisTestCase
$this->assertSame($expected, $parameters);
}
/**
* @group disconnected
*/
public function testRedisSchemeOverridesPasswordAndDatabaseInQueryString()
{
$parameters = Parameters::parse('redis://:secret@10.10.10.10/5?password=ignored&database=4');
$this->assertSame('secret', $parameters['password']);
$this->assertSame('5', $parameters['database']);
}
/**
* @group disconnected
*/
+6 -10
View File
@@ -312,11 +312,9 @@ class ConsumerTest extends PredisTestCase
*/
public function testPubSubAgainstRedisServer()
{
if (getenv('GITHUB_ACTIONS') || getenv('TRAVIS')) {
$this->markTestSkipped(
'Test temporarily skipped on CI environments, see note in the body of the test'
);
}
$this->markTestSkippedOnCIEnvironment(
'Test temporarily skipped on CI environments, see note in the body of the test' // TODO
);
$parameters = array(
'host' => REDIS_SERVER_HOST,
@@ -363,11 +361,9 @@ class ConsumerTest extends PredisTestCase
*/
public function testPubSubAgainstRedisServerBlocking()
{
if (getenv('GITHUB_ACTIONS') || getenv('TRAVIS')) {
$this->markTestSkipped(
'Test temporarily skipped on CI environments, see note in the body of the test'
);
}
$this->markTestSkippedOnCIEnvironment(
'Test temporarily skipped on CI environments, see note in the body of the test' // TODO
);
$parameters = array(
'host' => REDIS_SERVER_HOST,
@@ -23,11 +23,25 @@ class DispatcherLoopTest extends PredisTestCase
// ---- INTEGRATION TESTS --------------------------------------------- //
// ******************************************************************** //
// NOTE: the following 2 tests fail at random without any apparent reason
// when executed on our CI environments and these failures are not tied
// to a particular version of PHP or Redis. It is most likely some weird
// timing issue on busy systems as it is really rare to get it triggered
// locally. The chances it is a bug in the library are pretty low so for
// now we just mark this test skipped on our CI environments (but still
// enabled for local test runs) and "debug" this issue using a separate
// branch to avoid having spurious failures on main development branches
// which is utterly annoying.
/**
* @group connected
*/
public function testDispatcherLoopAgainstRedisServer()
{
$this->markTestSkippedOnCIEnvironment(
'Test temporarily skipped on CI environments, see note in the body of the test' // TODO
);
$parameters = array(
'host' => REDIS_SERVER_HOST,
'port' => REDIS_SERVER_PORT,
@@ -87,6 +101,10 @@ class DispatcherLoopTest extends PredisTestCase
*/
public function testDispatcherLoopAgainstRedisServerWithPrefix()
{
$this->markTestSkippedOnCIEnvironment(
'Test temporarily skipped on CI environments, see note in the body of the test' // TODO
);
$parameters = array(
'host' => REDIS_SERVER_HOST,
'port' => REDIS_SERVER_PORT,