diff --git a/README.md b/README.md index 368be49..d2331d5 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,20 @@ You can use the [standard](https://www.iana.org/assignments/uri-schemes/prov/red $factory->createClient('rediss://redis.example.com:6340'); ``` +You can use the `redis+unix://` URI scheme if your Redis instance is listening +on a Unix domain socket (UDS) path: + +```php +$factory->createClient('redis+unix:///tmp/redis.sock'); + +// the URI MAY contain `password` and `db` query parameters as seen above +$factory->createClient('redis+unix:///tmp/redis.sock?password=secret&db=2'); + +// the URI MAY contain authentication details as userinfo as seen above +// should be used with care, also note that database can not be passed as path +$factory->createClient('redis+unix://:secret@/tmp/redis.sock'); +``` + ### Client The `Client` is responsible for exchanging messages with Redis diff --git a/src/Factory.php b/src/Factory.php index 8508f35..ac80a61 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -52,7 +52,7 @@ public function createClient($target) $protocol = $this->protocol; - $promise = $this->connector->connect($parts['host'] . ':' . $parts['port'])->then(function (ConnectionInterface $stream) use ($protocol) { + $promise = $this->connector->connect($parts['authority'])->then(function (ConnectionInterface $stream) use ($protocol) { return new StreamingClient($stream, $protocol->createResponseParser(), $protocol->createSerializer()); }); @@ -89,11 +89,18 @@ function ($error) use ($client) { /** * @param string $target - * @return array with keys host, port, auth and db + * @return array with keys authority, auth and db * @throws InvalidArgumentException */ private function parseUrl($target) { + $ret = array(); + // support `redis+unix://` scheme for Unix domain socket (UDS) paths + if (preg_match('/^redis\+unix:\/\/([^:]*:[^@]*@)?(.+?)(\?.*)?$/', $target, $match)) { + $ret['authority'] = 'unix://' . $match[2]; + $target = 'redis://' . (isset($match[1]) ? $match[1] : '') . 'localhost' . (isset($match[3]) ? $match[3] : ''); + } + if (strpos($target, '://') === false) { $target = 'redis://' . $target; } @@ -103,21 +110,20 @@ private function parseUrl($target) throw new InvalidArgumentException('Given URL can not be parsed'); } - if (!isset($parts['port'])) { - $parts['port'] = 6379; - } - if (isset($parts['pass'])) { - $parts['auth'] = rawurldecode($parts['pass']); + $ret['auth'] = rawurldecode($parts['pass']); } if (isset($parts['path']) && $parts['path'] !== '') { // skip first slash - $parts['db'] = substr($parts['path'], 1); + $ret['db'] = substr($parts['path'], 1); } - if ($parts['scheme'] === 'rediss') { - $parts['host'] = 'tls://' . $parts['host']; + if (!isset($ret['authority'])) { + $ret['authority'] = + ($parts['scheme'] === 'rediss' ? 'tls://' : '') . + $parts['host'] . ':' . + (isset($parts['port']) ? $parts['port'] : 6379); } if (isset($parts['query'])) { @@ -125,16 +131,14 @@ private function parseUrl($target) parse_str($parts['query'], $args); if (isset($args['password'])) { - $parts['auth'] = $args['password']; + $ret['auth'] = $args['password']; } if (isset($args['db'])) { - $parts['db'] = $args['db']; + $ret['db'] = $args['db']; } } - unset($parts['scheme'], $parts['user'], $parts['pass'], $parts['path']); - - return $parts; + return $ret; } } diff --git a/tests/FactoryTest.php b/tests/FactoryTest.php index 5924c3a..5d89786 100644 --- a/tests/FactoryTest.php +++ b/tests/FactoryTest.php @@ -107,6 +107,33 @@ public function testWillWriteAuthCommandIfRedissUriContainsUserInfo() $this->factory->createClient('rediss://hello:world@example.com'); } + public function testWillWriteAuthCommandIfRedisUnixUriContainsPasswordQueryParameter() + { + $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('unix:///tmp/redis.sock')->willReturn(Promise\resolve($stream)); + $this->factory->createClient('redis+unix:///tmp/redis.sock?password=world'); + } + + public function testWillWriteAuthCommandIfRedisUnixUriContainsUserInfo() + { + $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('unix:///tmp/redis.sock')->willReturn(Promise\resolve($stream)); + $this->factory->createClient('redis+unix://hello:world@/tmp/redis.sock'); + } + + public function testWillWriteSelectCommandIfRedisUnixUriContainsDbQueryParameter() + { + $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); + $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')->with('unix:///tmp/redis.sock')->willReturn(Promise\resolve($stream)); + $this->factory->createClient('redis+unix:///tmp/redis.sock?db=demo'); + } + public function testWillRejectIfConnectorRejects() { $this->connector->expects($this->once())->method('connect')->with('127.0.0.1:2')->willReturn(Promise\reject(new \RuntimeException()));