Skip to content

Commit 78b51ac

Browse files
authored
Merge pull request clue#60 from clue-labs/uri
Support redis[s]:// URI scheme and deprecate legacy URIs
2 parents d586c8e + 0b5afb2 commit 78b51ac

File tree

3 files changed

+118
-17
lines changed

3 files changed

+118
-17
lines changed

README.md

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,11 @@ $factory = new Factory($loop, $connector);
107107
#### createClient()
108108

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

113113
```php
114-
$factory->createClient('localhost:6379')->then(
114+
$factory->createClient('redis://localhost:6379')->then(
115115
function (Client $client) {
116116
// client connected (and authenticated)
117117
},
@@ -121,28 +121,56 @@ $factory->createClient('localhost:6379')->then(
121121
);
122122
```
123123

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

126129
```php
127-
$factory->createClient();
130+
// both are equivalent due to defaults being applied
131+
$factory->createClient('localhost');
132+
$factory->createClient('redis://localhost:6379');
128133
```
129134

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

132139
```php
133-
$factory->createClient('localhost');
140+
// all forms are equivalent
141+
$factory->createClient('redis://:h%40llo@localhost');
142+
$factory->createClient('redis://ignored:h%40llo@localhost');
143+
$factory->createClient('redis://localhost?password=h%40llo');
134144
```
135145

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

138154
```php
139-
$factory->createClient('auth@localhost');
155+
// both forms are equivalent
156+
$factory->createClient('redis://localhost/2');
157+
$factory->createClient('redis://localhost?db=2');
140158
```
141159

142-
You can optionally include a path that will be used to select (SELECT command) the right database:
160+
You can use the [standard](https://www.iana.org/assignments/uri-schemes/prov/rediss)
161+
`rediss://` URI scheme if you're using a secure TLS proxy in front of Redis:
162+
163+
```php
164+
$factory->createClient('rediss://redis.example.com:6340');
165+
```
166+
167+
[Deprecated] You can omit the complete URI if you want to connect to the default
168+
address `redis://localhost:6379`. This legacy API will be removed in a future
169+
`v2.0.0` version, so it's highly recommended to upgrade to the above API.
143170

144171
```php
145-
$factory->createClient('localhost/2');
172+
// deprecated
173+
$factory->createClient();
146174
```
147175

148176
### Client

src/Factory.php

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ public function __construct(LoopInterface $loop, $connector = null, ProtocolFact
4444
/**
4545
* create redis client connected to address of given redis instance
4646
*
47-
* @param string|null $target
47+
* @param string|null $target Redis server URI to connect to. Not passing
48+
* this parameter is deprecated and only supported for BC reasons and
49+
* will be removed in future versions.
4850
* @return \React\Promise\PromiseInterface resolves with Client or rejects with \Exception
4951
*/
5052
public function createClient($target = null)
@@ -107,7 +109,7 @@ private function parseUrl($target)
107109
}
108110

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

@@ -120,11 +122,11 @@ private function parseUrl($target)
120122
}
121123

122124
$auth = null;
123-
if (isset($parts['user'])) {
124-
$auth = $parts['user'];
125+
if (isset($parts['user']) && $parts['scheme'] === 'tcp') {
126+
$auth = rawurldecode($parts['user']);
125127
}
126128
if (isset($parts['pass'])) {
127-
$auth .= ':' . $parts['pass'];
129+
$auth .= ($parts['scheme'] === 'tcp' ? ':' : '') . rawurldecode($parts['pass']);
128130
}
129131
if ($auth !== null) {
130132
$parts['auth'] = $auth;
@@ -135,6 +137,23 @@ private function parseUrl($target)
135137
$parts['db'] = substr($parts['path'], 1);
136138
}
137139

140+
if ($parts['scheme'] === 'rediss') {
141+
$parts['host'] = 'tls://' . $parts['host'];
142+
}
143+
144+
if (isset($parts['query'])) {
145+
$args = array();
146+
parse_str($parts['query'], $args);
147+
148+
if (isset($args['password'])) {
149+
$parts['auth'] = $args['password'];
150+
}
151+
152+
if (isset($args['db'])) {
153+
$parts['db'] = $args['db'];
154+
}
155+
}
156+
138157
unset($parts['scheme'], $parts['user'], $parts['pass'], $parts['path']);
139158

140159
return $parts;

tests/FactoryTest.php

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,64 @@ public function testWillWriteSelectCommandIfTargetContainsPath()
6565
$stream->expects($this->once())->method('write')->with("*2\r\n$6\r\nselect\r\n$4\r\ndemo\r\n");
6666

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

71-
public function testWillWriteAuthCommandIfTargetContainsUserInfo()
71+
public function testWillWriteSelectCommandIfTargetContainsDbQueryParameter()
72+
{
73+
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
74+
$stream->expects($this->once())->method('write')->with("*2\r\n$6\r\nselect\r\n$1\r\n4\r\n");
75+
76+
$this->connector->expects($this->once())->method('connect')->willReturn(Promise\resolve($stream));
77+
$this->factory->createClient('redis://127.0.0.1?db=4');
78+
}
79+
80+
public function testWillWriteAuthCommandIfRedisUriContainsUserInfo()
81+
{
82+
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
83+
$stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n");
84+
85+
$this->connector->expects($this->once())->method('connect')->with('example.com:6379')->willReturn(Promise\resolve($stream));
86+
$this->factory->createClient('redis://hello:[email protected]');
87+
}
88+
89+
public function testWillWriteAuthCommandIfRedisUriContainsEncodedUserInfo()
90+
{
91+
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
92+
$stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nh@llo\r\n");
93+
94+
$this->connector->expects($this->once())->method('connect')->with('example.com:6379')->willReturn(Promise\resolve($stream));
95+
$this->factory->createClient('redis://:h%[email protected]');
96+
}
97+
98+
public function testWillWriteAuthCommandIfTargetContainsPasswordQueryParameter()
99+
{
100+
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
101+
$stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$6\r\nsecret\r\n");
102+
103+
$this->connector->expects($this->once())->method('connect')->with('example.com:6379')->willReturn(Promise\resolve($stream));
104+
$this->factory->createClient('redis://example.com?password=secret');
105+
}
106+
107+
public function testWillWriteAuthCommandIfTargetContainsEncodedPasswordQueryParameter()
108+
{
109+
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
110+
$stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nh@llo\r\n");
111+
112+
$this->connector->expects($this->once())->method('connect')->with('example.com:6379')->willReturn(Promise\resolve($stream));
113+
$this->factory->createClient('redis://example.com?password=h%40llo');
114+
}
115+
116+
public function testWillWriteAuthCommandIfRedissUriContainsUserInfo()
117+
{
118+
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
119+
$stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$5\r\nworld\r\n");
120+
121+
$this->connector->expects($this->once())->method('connect')->with('tls://example.com:6379')->willReturn(Promise\resolve($stream));
122+
$this->factory->createClient('rediss://hello:[email protected]');
123+
}
124+
125+
public function testWillWriteAuthCommandIfTcpUriContainsUserInfo()
72126
{
73127
$stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock();
74128
$stream->expects($this->once())->method('write')->with("*2\r\n$4\r\nauth\r\n$11\r\nhello:world\r\n");

0 commit comments

Comments
 (0)