Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support redis[s]:// URI scheme and deprecate legacy URIs #60

Merged
merged 5 commits into from
Sep 19, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 38 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,11 @@ $factory = new Factory($loop, $connector);
#### createClient()

The `createClient($redisUri = null)` method can be used to create a new [`Client`](#client).
It helps with establishing a plain TCP/IP connection to Redis
It helps with establishing a plain TCP/IP or secure TLS connection to Redis
and optionally authenticating (AUTH) and selecting the right database (SELECT).

```php
$factory->createClient('localhost:6379')->then(
$factory->createClient('redis://localhost:6379')->then(
function (Client $client) {
// client connected (and authenticated)
},
Expand All @@ -121,28 +121,56 @@ $factory->createClient('localhost:6379')->then(
);
```

You can omit the complete URI if you want to connect to the default address `localhost:6379`:
The `$redisUri` can be given in the
[standard](https://www.iana.org/assignments/uri-schemes/prov/redis) form
`[redis[s]://][:auth@]host[:port][/db]`.
You can omit the URI scheme and port if you're connecting to the default port 6379:

```php
$factory->createClient();
// both are equivalent due to defaults being applied
$factory->createClient('localhost');
$factory->createClient('redis://localhost:6379');
```

You can omit the port if you're connecting to the default port 6379:
Redis supports password-based authentication (`AUTH` command). Note that Redis'
authentication mechanism does not employ a username, so you can pass the
password `h@llo` URL-encoded (percent-encoded) as part of the URI like this:

```php
$factory->createClient('localhost');
// all forms are equivalent
$factory->createClient('redis://:h%40llo@localhost');
$factory->createClient('redis://ignored:h%40llo@localhost');
$factory->createClient('redis://localhost?password=h%40llo');
```

You can optionally include a password that will be used to authenticate (AUTH command) the client:
> Legacy notice: The `redis://` scheme is defined and preferred as of `v1.2.0`.
For BC reasons, the `Factory` defaults to the `tcp://` scheme in which case
the authentication details would include the otherwise unused username.
This legacy API will be removed in a future `v2.0.0` version, so it's highly
recommended to upgrade to the above API.

You can optionally include a path that will be used to select (SELECT command) the right database:

```php
$factory->createClient('auth@localhost');
// both forms are equivalent
$factory->createClient('redis://localhost/2');
$factory->createClient('redis://localhost?db=2');
```

You can optionally include a path that will be used to select (SELECT command) the right database:
You can use the [standard](https://www.iana.org/assignments/uri-schemes/prov/rediss)
`rediss://` URI scheme if you're using a secure TLS proxy in front of Redis:

```php
$factory->createClient('rediss://redis.example.com:6340');
```

[Deprecated] You can omit the complete URI if you want to connect to the default
address `redis://localhost:6379`. This legacy API will be removed in a future
`v2.0.0` version, so it's highly recommended to upgrade to the above API.

```php
$factory->createClient('localhost/2');
// deprecated
$factory->createClient();
```

### Client
Expand Down
29 changes: 24 additions & 5 deletions src/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ public function __construct(LoopInterface $loop, $connector = null, ProtocolFact
/**
* create redis client connected to address of given redis instance
*
* @param string|null $target
* @param string|null $target Redis server URI to connect to. Not passing
* this parameter is deprecated and only supported for BC reasons and
* will be removed in future versions.
* @return \React\Promise\PromiseInterface resolves with Client or rejects with \Exception
*/
public function createClient($target = null)
Expand Down Expand Up @@ -107,7 +109,7 @@ private function parseUrl($target)
}

$parts = parse_url($target);
if ($parts === false || !isset($parts['host']) || $parts['scheme'] !== 'tcp') {
if ($parts === false || !isset($parts['scheme'], $parts['host']) || !in_array($parts['scheme'], array('tcp', 'redis', 'rediss'))) {
throw new InvalidArgumentException('Given URL can not be parsed');
}

Expand All @@ -120,11 +122,11 @@ private function parseUrl($target)
}

$auth = null;
if (isset($parts['user'])) {
$auth = $parts['user'];
if (isset($parts['user']) && $parts['scheme'] === 'tcp') {
$auth = rawurldecode($parts['user']);
}
if (isset($parts['pass'])) {
$auth .= ':' . $parts['pass'];
$auth .= ($parts['scheme'] === 'tcp' ? ':' : '') . rawurldecode($parts['pass']);
}
if ($auth !== null) {
$parts['auth'] = $auth;
Expand All @@ -135,6 +137,23 @@ private function parseUrl($target)
$parts['db'] = substr($parts['path'], 1);
}

if ($parts['scheme'] === 'rediss') {
$parts['host'] = 'tls://' . $parts['host'];
}

if (isset($parts['query'])) {
$args = array();
parse_str($parts['query'], $args);

if (isset($args['password'])) {
$parts['auth'] = $args['password'];
}

if (isset($args['db'])) {
$parts['db'] = $args['db'];
}
}

unset($parts['scheme'], $parts['user'], $parts['pass'], $parts['path']);

return $parts;
Expand Down
58 changes: 56 additions & 2 deletions tests/FactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,64 @@ public function testWillWriteSelectCommandIfTargetContainsPath()
$stream->expects($this->once())->method('write')->with("*2\r\n$6\r\nselect\r\n$4\r\ndemo\r\n");

$this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream));
$this->factory->createClient('tcp://127.0.0.1/demo');
$this->factory->createClient('redis://127.0.0.1/demo');
}

public function testWillWriteAuthCommandIfTargetContainsUserInfo()
public function testWillWriteSelectCommandIfTargetContainsDbQueryParameter()
{
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
$stream->expects($this->once())->method('write')->with("*2\r\n$6\r\nselect\r\n$1\r\n4\r\n");

$this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream));
$this->factory->createClient('redis://127.0.0.1?db=4');
}

public function testWillWriteAuthCommandIfRedisUriContainsUserInfo()
{
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
$stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n");

$this->connector->expects($this->once())->method('connect')->with('example.com:6379')->willReturn(Promise\resolve($stream));
$this->factory->createClient('redis://hello:[email protected]');
}

public function testWillWriteAuthCommandIfRedisUriContainsEncodedUserInfo()
{
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
$stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nh@llo\r\n");

$this->connector->expects($this->once())->method('connect')->with('example.com:6379')->willReturn(Promise\resolve($stream));
$this->factory->createClient('redis://:h%[email protected]');
}

public function testWillWriteAuthCommandIfTargetContainsPasswordQueryParameter()
{
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
$stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$6\r\nsecret\r\n");

$this->connector->expects($this->once())->method('connect')->with('example.com:6379')->willReturn(Promise\resolve($stream));
$this->factory->createClient('redis://example.com?password=secret');
}

public function testWillWriteAuthCommandIfTargetContainsEncodedPasswordQueryParameter()
{
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
$stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nh@llo\r\n");

$this->connector->expects($this->once())->method('connect')->with('example.com:6379')->willReturn(Promise\resolve($stream));
$this->factory->createClient('redis://example.com?password=h%40llo');
}

public function testWillWriteAuthCommandIfRedissUriContainsUserInfo()
{
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
$stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n");

$this->connector->expects($this->once())->method('connect')->with('tls://example.com:6379')->willReturn(Promise\resolve($stream));
$this->factory->createClient('rediss://hello:[email protected]');
}

public function testWillWriteAuthCommandIfTcpUriContainsUserInfo()
{
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
$stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$11\r\nhello:world\r\n");
Expand Down