diff --git a/README.md b/README.md index a1acb6d..d62883c 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ built on top of [ReactPHP](https://reactphp.org). * [DNS resolution](#dns-resolution) * [Authentication](#authentication) * [Advanced secure proxy connections](#advanced-secure-proxy-connections) + * [Advanced Unix domain sockets](#advanced-unix-domain-sockets) * [Install](#install) * [Tests](#tests) * [License](#license) @@ -287,6 +288,42 @@ $proxy = new ProxyConnector('https://127.0.0.1:443', $connector); $proxy->connect('tcp://smtp.googlemail.com:587'); ``` +#### Advanced Unix domain sockets + +HTTP CONNECT proxy servers support forwarding TCP/IP based connections and +higher level protocols. +In some advanced cases, it may be useful to let your HTTP CONNECT proxy server +listen on a Unix domain socket (UDS) path instead of a IP:port combination. +For example, this allows you to rely on file system permissions instead of +having to rely on explicit [authentication](#authentication). + +You can simply use the `http+unix://` URI scheme like this: + +```php +$proxy = new ProxyConnector('http+unix:///tmp/proxy.sock', $connector); + +$proxy->connect('tcp://google.com:80')->then(function (ConnectionInterface $stream) { + // connected… +}); +``` + +Similarly, you can also combine this with [authentication](#authentication) +like this: + +```php +$proxy = new ProxyConnector('http+unix://user:pass@/tmp/proxy.sock', $connector); +``` + +> Note that Unix domain sockets (UDS) are considered advanced usage and PHP only + has limited support for this. + In particular, enabling [secure TLS](#secure-tls-connections) may not be + supported. + +> Note that the HTTP CONNECT protocol does not support the notion of UDS paths. + The above works reasonably well because UDS is only used for the connection between + client and proxy server and the path will not actually passed over the protocol. + This implies that this does not support connecting to UDS destination paths. + ## Install The recommended way to install this library is [through Composer](https://getcomposer.org). diff --git a/composer.json b/composer.json index f95eba5..b95d850 100644 --- a/composer.json +++ b/composer.json @@ -18,8 +18,8 @@ }, "require": { "php": ">=5.3", - "react/socket": "^1.0 || ^0.8 || ^0.7.1", "react/promise": " ^2.1 || ^1.2.1", + "react/socket": "^1.0 || ^0.8.4", "ringcentral/psr7": "^1.2" }, "require-dev": { diff --git a/src/ProxyConnector.php b/src/ProxyConnector.php index dd070f3..69eb5ee 100644 --- a/src/ProxyConnector.php +++ b/src/ProxyConnector.php @@ -2,7 +2,6 @@ namespace Clue\React\HttpProxy; -use React\Socket\ConnectorInterface; use Exception; use InvalidArgumentException; use RuntimeException; @@ -10,6 +9,8 @@ use React\Promise; use React\Promise\Deferred; use React\Socket\ConnectionInterface; +use React\Socket\ConnectorInterface; +use React\Socket\FixedUriConnector; /** * A simple Connector that uses an HTTP CONNECT proxy to create plain TCP/IP connections to any destination @@ -57,13 +58,25 @@ class ProxyConnector implements ConnectorInterface */ public function __construct($proxyUrl, ConnectorInterface $connector) { + // support `http+unix://` scheme for Unix domain socket (UDS) paths + if (preg_match('/^http\+unix:\/\/(.*?@)?(.+?)$/', $proxyUrl, $match)) { + // rewrite URI to parse authentication from dummy host + $proxyUrl = 'http://' . $match[1] . 'localhost'; + + // connector uses Unix transport scheme and explicit path given + $connector = new FixedUriConnector( + 'unix://' . $match[2], + $connector + ); + } + if (strpos($proxyUrl, '://') === false) { $proxyUrl = 'http://' . $proxyUrl; } $parts = parse_url($proxyUrl); if (!$parts || !isset($parts['scheme'], $parts['host']) || ($parts['scheme'] !== 'http' && $parts['scheme'] !== 'https')) { - throw new InvalidArgumentException('Invalid proxy URL'); + throw new InvalidArgumentException('Invalid proxy URL "' . $proxyUrl . '"'); } // apply default port and TCP/TLS transport for given scheme diff --git a/tests/ProxyConnectorTest.php b/tests/ProxyConnectorTest.php index 6b11828..13d6e42 100644 --- a/tests/ProxyConnectorTest.php +++ b/tests/ProxyConnectorTest.php @@ -31,6 +31,14 @@ public function testInvalidProxyScheme() new ProxyConnector('ftp://example.com', $this->connector); } + /** + * @expectedException InvalidArgumentException + */ + public function testInvalidHttpsUnixScheme() + { + new ProxyConnector('https+unix:///tmp/proxy.sock', $this->connector); + } + public function testCreatesConnectionToHttpPort() { $promise = new Promise(function () { }); @@ -71,6 +79,16 @@ public function testCreatesConnectionToHttpsPort() $proxy->connect('google.com:80'); } + public function testCreatesConnectionToUnixPath() + { + $promise = new Promise(function () { }); + $this->connector->expects($this->once())->method('connect')->with('unix:///tmp/proxy.sock')->willReturn($promise); + + $proxy = new ProxyConnector('http+unix:///tmp/proxy.sock', $this->connector); + + $proxy->connect('google.com:80'); + } + public function testCancelPromiseWillCancelPendingConnection() { $promise = new Promise(function () { }, $this->expectCallableOnce()); @@ -140,6 +158,19 @@ public function testWillProxyAuthorizationHeaderIfProxyUriContainsAuthentication $proxy->connect('google.com:80'); } + public function testWillProxyAuthorizationHeaderIfUnixProxyUriContainsAuthentication() + { + $stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock(); + $stream->expects($this->once())->method('write')->with("CONNECT google.com:80 HTTP/1.1\r\nHost: google.com:80\r\nProxy-Authorization: Basic dXNlcjpwYXNz\r\n\r\n"); + + $promise = \React\Promise\resolve($stream); + $this->connector->expects($this->once())->method('connect')->with('unix:///tmp/proxy.sock')->willReturn($promise); + + $proxy = new ProxyConnector('http+unix://user:pass@/tmp/proxy.sock', $this->connector); + + $proxy->connect('google.com:80'); + } + public function testRejectsInvalidUri() { $this->connector->expects($this->never())->method('connect');