Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Add support for custom HTTP request headers #25

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 25, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:[email protected]', $this->connector, array(
'Proxy-Authorization' => 'foobar'
));

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

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