From 479de8af18eeffd86b833b31e36cf77bd4a4002e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sat, 10 Jun 2017 20:21:51 +0200 Subject: [PATCH] Support proxy authentication if proxy URL contains username/password --- README.md | 30 ++++++++++++++++++++++++ src/ProxyConnector.php | 14 ++++++++++-- tests/ProxyConnectorTest.php | 44 +++++++++++++++++++++++++++++++++++- 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 247111c..3b0d58e 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Async HTTP CONNECT proxy connector, use any TCP/IP protocol through an HTTP prox * [Secure TLS connections](#secure-tls-connections) * [Connection timeout](#connection-timeout) * [DNS resolution](#dns-resolution) + * [Authentication](#authentication) * [Advanced secure proxy connections](#advanced-secure-proxy-connections) * [Install](#install) * [Tests](#tests) @@ -267,6 +268,35 @@ $connector = Connector($loop, array( > Also note how local DNS resolution is in fact entirely handled outside of this HTTP CONNECT client implementation. +#### Authentication + +If your HTTP proxy server requires authentication, you may pass the username and +password as part of the HTTP proxy URL like this: + +```php +$proxy = new ProxyConnector('http://user:pass@127.0.0.1:8080', $connector); +``` + +Note that both the username and password must be percent-encoded if they contain +special characters: + +```php +$user = 'he:llo'; +$pass = 'p@ss'; + +$proxy = new ProxyConnector( + rawurlencode($user) . ':' . rawurlencode($pass) . '@127.0.0.1:8080', + $connector +); +``` + +> The authentication details will be used for basic authentication and will be + transferred in the `Proxy-Authorization` HTTP request header for each + connection attempt. + If the authentication details are missing or not accepted by the remote HTTP + proxy server, it is expected to reject each connection attempt with a + `407` (Proxy Authentication Required) response status code. + #### Advanced secure proxy connections Note that communication between the client and the proxy is usually via an diff --git a/src/ProxyConnector.php b/src/ProxyConnector.php index d99e265..ed27b3d 100644 --- a/src/ProxyConnector.php +++ b/src/ProxyConnector.php @@ -42,6 +42,7 @@ class ProxyConnector implements ConnectorInterface { private $connector; private $proxyUri; + private $proxyAuth = ''; /** * Instantiate a new ProxyConnector which uses the given $proxyUrl @@ -73,6 +74,13 @@ public function __construct($proxyUrl, ConnectorInterface $connector) $this->connector = $connector; $this->proxyUri = $parts['scheme'] . '://' . $parts['host'] . ':' . $parts['port']; + + // prepare Proxy-Authorization header if URI contains username/password + if (isset($parts['user']) || isset($parts['pass'])) { + $this->proxyAuth = 'Proxy-Authorization: Basic ' . base64_encode( + rawurldecode($parts['user'] . ':' . (isset($parts['pass']) ? $parts['pass'] : '')) + ) . "\r\n"; + } } public function connect($uri) @@ -116,7 +124,9 @@ public function connect($uri) $proxyUri .= '#' . $parts['fragment']; } - return $this->connector->connect($proxyUri)->then(function (ConnectionInterface $stream) use ($host, $port) { + $auth = $this->proxyAuth; + + return $this->connector->connect($proxyUri)->then(function (ConnectionInterface $stream) use ($host, $port, $auth) { $deferred = new Deferred(function ($_, $reject) use ($stream) { $reject(new RuntimeException('Operation canceled while waiting for response from proxy')); $stream->close(); @@ -176,7 +186,7 @@ public function connect($uri) $deferred->reject(new RuntimeException('Connection to proxy lost while waiting for response')); }); - $stream->write("CONNECT " . $host . ":" . $port . " HTTP/1.1\r\nHost: " . $host . ":" . $port . "\r\n\r\n"); + $stream->write("CONNECT " . $host . ":" . $port . " HTTP/1.1\r\nHost: " . $host . ":" . $port . "\r\n" . $auth . "\r\n"); return $deferred->promise(); }); diff --git a/tests/ProxyConnectorTest.php b/tests/ProxyConnectorTest.php index 7e45d0f..6f51dea 100644 --- a/tests/ProxyConnectorTest.php +++ b/tests/ProxyConnectorTest.php @@ -88,7 +88,7 @@ public function testCancelPromiseWillCancelPendingConnection() public function testWillWriteToOpenConnection() { $stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock(); - $stream->expects($this->once())->method('write'); + $stream->expects($this->once())->method('write')->with("CONNECT google.com:80 HTTP/1.1\r\nHost: google.com:80\r\n\r\n"); $promise = \React\Promise\resolve($stream); $this->connector->expects($this->once())->method('connect')->willReturn($promise); @@ -98,6 +98,48 @@ public function testWillWriteToOpenConnection() $proxy->connect('google.com:80'); } + public function testWillProxyAuthorizationHeaderIfProxyUriContainsAuthentication() + { + $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')->willReturn($promise); + + $proxy = new ProxyConnector('user:pass@proxy.example.com', $this->connector); + + $proxy->connect('google.com:80'); + } + + public function testWillProxyAuthorizationHeaderIfProxyUriContainsOnlyUsernameWithoutPassword() + { + $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 dXNlcjo=\r\n\r\n"); + + $promise = \React\Promise\resolve($stream); + $this->connector->expects($this->once())->method('connect')->willReturn($promise); + + $proxy = new ProxyConnector('user@proxy.example.com', $this->connector); + + $proxy->connect('google.com:80'); + } + + public function testWillProxyAuthorizationHeaderIfProxyUriContainsAuthenticationWithPercentEncoding() + { + $user = 'h@llĂ–'; + $pass = '%secret?'; + + $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 " . base64_encode($user . ':' . $pass) . "\r\n\r\n"); + + $promise = \React\Promise\resolve($stream); + $this->connector->expects($this->once())->method('connect')->willReturn($promise); + + $proxy = new ProxyConnector(rawurlencode($user) . ':' . rawurlencode($pass) . '@proxy.example.com', $this->connector); + + $proxy->connect('google.com:80'); + } + public function testRejectsInvalidUri() { $this->connector->expects($this->never())->method('connect');