Skip to content

Commit

Permalink
Merge pull request #25 from clue-labs/custom-headers
Browse files Browse the repository at this point in the history
Add support for custom HTTP request headers
  • Loading branch information
clue authored Oct 25, 2018
2 parents bac60c2 + 031d626 commit 99300c6
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 6 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
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
36 changes: 36 additions & 0 deletions examples/03-custom-proxy-headers.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

// 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;

require __DIR__ . '/../vendor/autoload.php';

$url = isset($argv[1]) ? $argv[1] : '127.0.0.1:8080';

$loop = React\EventLoop\Factory::create();

$proxy = new ProxyConnector($url, new Connector($loop), array(
'X-Custom-Header-1' => '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();
20 changes: 14 additions & 6 deletions src/ProxyConnector.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class ProxyConnector implements ConnectorInterface
{
private $connector;
private $proxyUri;
private $proxyAuth = '';
private $headers = '';

/**
* Instantiate a new ProxyConnector which uses the given $proxyUrl
Expand All @@ -54,9 +54,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)) {
Expand Down Expand Up @@ -90,10 +91,17 @@ 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->headers = 'Proxy-Authorization: Basic ' . base64_encode(
rawurldecode($parts['user'] . ':' . (isset($parts['pass']) ? $parts['pass'] : ''))
) . "\r\n";
}

// 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 @@ -151,8 +159,8 @@ public function connect($uri)
$connecting->cancel();
});

$auth = $this->proxyAuth;
$connecting->then(function (ConnectionInterface $stream) use ($target, $auth, $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 @@ -212,7 +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));
});

$stream->write("CONNECT " . $target . " HTTP/1.1\r\nHost: " . $target . "\r\n" . $auth . "\r\n");
$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
48 changes: 48 additions & 0 deletions tests/ProxyConnectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,54 @@ 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\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'
));

$proxy->connect('google.com:80');
}

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\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->connect('google.com:80');
}

public function testRejectsInvalidUri()
{
$this->connector->expects($this->never())->method('connect');
Expand Down

0 comments on commit 99300c6

Please sign in to comment.