From 8dc21a3807b55cf7d109d6f1ae8460cae1b8e580 Mon Sep 17 00:00:00 2001 From: Abyr Valg Date: Mon, 12 Jun 2017 13:26:57 +0300 Subject: [PATCH 1/2] Allow to supply custom HTTP headers --- examples/03-custom-proxy-headers.php | 34 ++++++++++++++++++++++++++++ src/ProxyConnector.php | 22 ++++++++++++++---- tests/ProxyConnectorTest.php | 30 ++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 examples/03-custom-proxy-headers.php diff --git a/examples/03-custom-proxy-headers.php b/examples/03-custom-proxy-headers.php new file mode 100644 index 0000000..f123320 --- /dev/null +++ b/examples/03-custom-proxy-headers.php @@ -0,0 +1,34 @@ + 'Value-1', + 'X-Custom-Header-2' => 'Value-2', +)); +$connector = new Connector($loop, array( + 'tcp' => $proxy, + 'timeout' => 3.0, + 'dns' => false, +)); + +$connector->connect('tls://google.com:443')->then(function (ConnectionInterface $stream) { + $stream->write("GET / HTTP/1.1\r\nHost: google.com\r\nConnection: close\r\n\r\n"); + $stream->on('data', function ($chunk) { + echo $chunk; + }); +}, 'printf'); + +$loop->run(); diff --git a/src/ProxyConnector.php b/src/ProxyConnector.php index 6b84f21..5d09928 100644 --- a/src/ProxyConnector.php +++ b/src/ProxyConnector.php @@ -44,6 +44,8 @@ class ProxyConnector implements ConnectorInterface private $connector; private $proxyUri; private $proxyAuth = ''; + /** @var array */ + private $proxyHeaders; /** * Instantiate a new ProxyConnector which uses the given $proxyUrl @@ -54,9 +56,10 @@ class ProxyConnector implements ConnectorInterface * @param ConnectorInterface $connector In its most simple form, the given * connector will be a \React\Socket\Connector if you want to connect to * a given IP address. + * @param array $httpHeaders Custom HTTP headers to be sent to the proxy. * @throws InvalidArgumentException if the proxy URL is invalid */ - public function __construct($proxyUrl, ConnectorInterface $connector) + public function __construct($proxyUrl, ConnectorInterface $connector, array $httpHeaders = array()) { // support `http+unix://` scheme for Unix domain socket (UDS) paths if (preg_match('/^http\+unix:\/\/(.*?@)?(.+?)$/', $proxyUrl, $match)) { @@ -90,10 +93,12 @@ public function __construct($proxyUrl, ConnectorInterface $connector) // prepare Proxy-Authorization header if URI contains username/password if (isset($parts['user']) || isset($parts['pass'])) { - $this->proxyAuth = 'Proxy-Authorization: Basic ' . base64_encode( + $this->proxyAuth = 'Basic ' . base64_encode( rawurldecode($parts['user'] . ':' . (isset($parts['pass']) ? $parts['pass'] : '')) - ) . "\r\n"; + ); } + + $this->proxyHeaders = $httpHeaders; } public function connect($uri) @@ -152,7 +157,8 @@ public function connect($uri) }); $auth = $this->proxyAuth; - $connecting->then(function (ConnectionInterface $stream) use ($target, $auth, $deferred) { + $headers = $this->proxyHeaders; + $connecting->then(function (ConnectionInterface $stream) use ($target, $auth, $headers, $deferred) { // keep buffering data until headers are complete $buffer = ''; $stream->on('data', $fn = function ($chunk) use (&$buffer, $deferred, $stream, &$fn) { @@ -212,7 +218,13 @@ public function connect($uri) $deferred->reject(new RuntimeException('Connection to proxy lost while waiting for response (ECONNRESET)', defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104)); }); - $stream->write("CONNECT " . $target . " HTTP/1.1\r\nHost: " . $target . "\r\n" . $auth . "\r\n"); + $headers['Host'] = $target; + if ($auth !== '') { + $headers['Proxy-Authorization'] = $auth; + } + $request = new Psr7\Request('CONNECT', $target, $headers); + $request = $request->withRequestTarget($target); + $stream->write(Psr7\str($request)); }, function (Exception $e) use ($deferred) { $deferred->reject($e = new RuntimeException( 'Unable to connect to proxy (ECONNREFUSED)', diff --git a/tests/ProxyConnectorTest.php b/tests/ProxyConnectorTest.php index 3147d92..20fefe0 100644 --- a/tests/ProxyConnectorTest.php +++ b/tests/ProxyConnectorTest.php @@ -215,6 +215,36 @@ public function testWillProxyAuthorizationHeaderIfUnixProxyUriContainsAuthentica $proxy->connect('google.com:80'); } + public function testWillSendCustomHttpHeadersToProxy() + { + $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\nX-Custom-Header: X-Custom-Value\r\nHost: google.com:80\r\n\r\n"); + + $promise = \React\Promise\resolve($stream); + $this->connector->expects($this->once())->method('connect')->willReturn($promise); + + $proxy = new ProxyConnector('proxy.example.com', $this->connector, array( + 'X-Custom-Header' => 'X-Custom-Value', + )); + + $proxy->connect('google.com:80'); + } + + public function testWillOverrideProxyAuthorizationHeaderWithCredentialsFromUri() + { + $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\nProxy-Authorization: Basic dXNlcjpwYXNz\r\nHost: google.com:80\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, array( + 'Proxy-Authorization' => 'foobar', + )); + + $proxy->connect('google.com:80'); + } + public function testRejectsInvalidUri() { $this->connector->expects($this->never())->method('connect'); From 031d626ef09003d35f081c74a0a75780bc39c207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Thu, 25 Oct 2018 10:45:14 +0200 Subject: [PATCH 2/2] Documentation for custom HTTP headers and simplify logic --- README.md | 17 +++++++++++++++++ examples/03-custom-proxy-headers.php | 4 +++- src/ProxyConnector.php | 28 ++++++++++++---------------- tests/ProxyConnectorTest.php | 28 +++++++++++++++++++++++----- 4 files changed, 55 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 91e998e..ee6b4c3 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ existing higher-level protocol implementation. * [Connection timeout](#connection-timeout) * [DNS resolution](#dns-resolution) * [Authentication](#authentication) + * [Advanced HTTP headers](#advanced-http-headers) * [Advanced secure proxy connections](#advanced-secure-proxy-connections) * [Advanced Unix domain sockets](#advanced-unix-domain-sockets) * [Install](#install) @@ -307,6 +308,22 @@ $proxy = new ProxyConnector( `407` (Proxy Authentication Required) response status code and an exception error code of `SOCKET_EACCES` (13). +#### Advanced HTTP headers + +The `ProxyConnector` constructor accepts an optional array of custom request +headers to send in the `CONNECT` request. This can be useful if you're using a +custom proxy setup or authentication scheme if the proxy server does not support +basic [authentication](#authentication) as documented above. This is rarely used +in practice, but may be useful for some more advanced use cases. In this case, +you may simply pass an assoc array of additional request headers like this: + +```php +$proxy = new ProxyConnector('127.0.0.1:8080', $connector, array( + 'Proxy-Authentication' => 'Bearer abc123', + 'User-Agent' => 'ReactPHP' +)); +``` + #### Advanced secure proxy connections Note that communication between the client and the proxy is usually via an diff --git a/examples/03-custom-proxy-headers.php b/examples/03-custom-proxy-headers.php index f123320..13f049b 100644 --- a/examples/03-custom-proxy-headers.php +++ b/examples/03-custom-proxy-headers.php @@ -2,11 +2,13 @@ // A simple example which requests https://google.com/ through an HTTP CONNECT proxy. // The proxy can be given as first argument and defaults to localhost:8080 otherwise. +// +// For illustration purposes only. If you want to send HTTP requests in a real +// world project, take a look at https://github.com/clue/reactphp-buzz#http-proxy use Clue\React\HttpProxy\ProxyConnector; use React\Socket\Connector; use React\Socket\ConnectionInterface; -use RingCentral\Psr7; require __DIR__ . '/../vendor/autoload.php'; diff --git a/src/ProxyConnector.php b/src/ProxyConnector.php index 5d09928..1c16e33 100644 --- a/src/ProxyConnector.php +++ b/src/ProxyConnector.php @@ -43,9 +43,7 @@ class ProxyConnector implements ConnectorInterface { private $connector; private $proxyUri; - private $proxyAuth = ''; - /** @var array */ - private $proxyHeaders; + private $headers = ''; /** * Instantiate a new ProxyConnector which uses the given $proxyUrl @@ -93,12 +91,17 @@ public function __construct($proxyUrl, ConnectorInterface $connector, array $htt // prepare Proxy-Authorization header if URI contains username/password if (isset($parts['user']) || isset($parts['pass'])) { - $this->proxyAuth = 'Basic ' . base64_encode( + $this->headers = 'Proxy-Authorization: Basic ' . base64_encode( rawurldecode($parts['user'] . ':' . (isset($parts['pass']) ? $parts['pass'] : '')) - ); + ) . "\r\n"; } - $this->proxyHeaders = $httpHeaders; + // append any additional custom request headers + foreach ($httpHeaders as $name => $values) { + foreach ((array)$values as $value) { + $this->headers .= $name . ': ' . $value . "\r\n"; + } + } } public function connect($uri) @@ -156,9 +159,8 @@ public function connect($uri) $connecting->cancel(); }); - $auth = $this->proxyAuth; - $headers = $this->proxyHeaders; - $connecting->then(function (ConnectionInterface $stream) use ($target, $auth, $headers, $deferred) { + $headers = $this->headers; + $connecting->then(function (ConnectionInterface $stream) use ($target, $headers, $deferred) { // keep buffering data until headers are complete $buffer = ''; $stream->on('data', $fn = function ($chunk) use (&$buffer, $deferred, $stream, &$fn) { @@ -218,13 +220,7 @@ public function connect($uri) $deferred->reject(new RuntimeException('Connection to proxy lost while waiting for response (ECONNRESET)', defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104)); }); - $headers['Host'] = $target; - if ($auth !== '') { - $headers['Proxy-Authorization'] = $auth; - } - $request = new Psr7\Request('CONNECT', $target, $headers); - $request = $request->withRequestTarget($target); - $stream->write(Psr7\str($request)); + $stream->write("CONNECT " . $target . " HTTP/1.1\r\nHost: " . $target . "\r\n" . $headers . "\r\n"); }, function (Exception $e) use ($deferred) { $deferred->reject($e = new RuntimeException( 'Unable to connect to proxy (ECONNREFUSED)', diff --git a/tests/ProxyConnectorTest.php b/tests/ProxyConnectorTest.php index 20fefe0..029885d 100644 --- a/tests/ProxyConnectorTest.php +++ b/tests/ProxyConnectorTest.php @@ -218,28 +218,46 @@ public function testWillProxyAuthorizationHeaderIfUnixProxyUriContainsAuthentica public function testWillSendCustomHttpHeadersToProxy() { $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\nX-Custom-Header: X-Custom-Value\r\nHost: google.com:80\r\n\r\n"); + $stream->expects($this->once())->method('write')->with("CONNECT google.com:80 HTTP/1.1\r\nHost: google.com:80\r\nX-Custom-Header: X-Custom-Value\r\n\r\n"); $promise = \React\Promise\resolve($stream); $this->connector->expects($this->once())->method('connect')->willReturn($promise); $proxy = new ProxyConnector('proxy.example.com', $this->connector, array( - 'X-Custom-Header' => 'X-Custom-Value', + 'X-Custom-Header' => 'X-Custom-Value' )); $proxy->connect('google.com:80'); } - public function testWillOverrideProxyAuthorizationHeaderWithCredentialsFromUri() + public function testWillSendMultipleCustomCookieHeadersToProxy() { $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\nProxy-Authorization: Basic dXNlcjpwYXNz\r\nHost: google.com:80\r\n\r\n"); + $stream->expects($this->once())->method('write')->with("CONNECT google.com:80 HTTP/1.1\r\nHost: google.com:80\r\nCookie: id=123\r\nCookie: year=2018\r\n\r\n"); + + $promise = \React\Promise\resolve($stream); + $this->connector->expects($this->once())->method('connect')->willReturn($promise); + + $proxy = new ProxyConnector('proxy.example.com', $this->connector, array( + 'Cookie' => array( + 'id=123', + 'year=2018' + ) + )); + + $proxy->connect('google.com:80'); + } + + public function testWillAppendCustomProxyAuthorizationHeaderWithCredentialsFromUri() + { + $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\nProxy-Authorization: foobar\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, array( - 'Proxy-Authorization' => 'foobar', + 'Proxy-Authorization' => 'foobar' )); $proxy->connect('google.com:80');