Skip to content

Commit

Permalink
Documentation for custom HTTP headers and simplify logic
Browse files Browse the repository at this point in the history
  • Loading branch information
clue committed Oct 25, 2018
1 parent 8dc21a3 commit 031d626
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 22 deletions.
17 changes: 17 additions & 0 deletions README.md
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion examples/03-custom-proxy-headers.php
Expand Up @@ -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';

Expand Down
28 changes: 12 additions & 16 deletions src/ProxyConnector.php
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)',
Expand Down
28 changes: 23 additions & 5 deletions tests/ProxyConnectorTest.php
Expand Up @@ -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');
Expand Down

0 comments on commit 031d626

Please sign in to comment.