Skip to content

Commit 99300c6

Browse files
authored
Merge pull request #25 from clue-labs/custom-headers
Add support for custom HTTP request headers
2 parents bac60c2 + 031d626 commit 99300c6

File tree

4 files changed

+115
-6
lines changed

4 files changed

+115
-6
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ existing higher-level protocol implementation.
4343
* [Connection timeout](#connection-timeout)
4444
* [DNS resolution](#dns-resolution)
4545
* [Authentication](#authentication)
46+
* [Advanced HTTP headers](#advanced-http-headers)
4647
* [Advanced secure proxy connections](#advanced-secure-proxy-connections)
4748
* [Advanced Unix domain sockets](#advanced-unix-domain-sockets)
4849
* [Install](#install)
@@ -307,6 +308,22 @@ $proxy = new ProxyConnector(
307308
`407` (Proxy Authentication Required) response status code and an exception
308309
error code of `SOCKET_EACCES` (13).
309310

311+
#### Advanced HTTP headers
312+
313+
The `ProxyConnector` constructor accepts an optional array of custom request
314+
headers to send in the `CONNECT` request. This can be useful if you're using a
315+
custom proxy setup or authentication scheme if the proxy server does not support
316+
basic [authentication](#authentication) as documented above. This is rarely used
317+
in practice, but may be useful for some more advanced use cases. In this case,
318+
you may simply pass an assoc array of additional request headers like this:
319+
320+
```php
321+
$proxy = new ProxyConnector('127.0.0.1:8080', $connector, array(
322+
'Proxy-Authentication' => 'Bearer abc123',
323+
'User-Agent' => 'ReactPHP'
324+
));
325+
```
326+
310327
#### Advanced secure proxy connections
311328

312329
Note that communication between the client and the proxy is usually via an

examples/03-custom-proxy-headers.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
// A simple example which requests https://google.com/ through an HTTP CONNECT proxy.
4+
// The proxy can be given as first argument and defaults to localhost:8080 otherwise.
5+
//
6+
// For illustration purposes only. If you want to send HTTP requests in a real
7+
// world project, take a look at https://github.com/clue/reactphp-buzz#http-proxy
8+
9+
use Clue\React\HttpProxy\ProxyConnector;
10+
use React\Socket\Connector;
11+
use React\Socket\ConnectionInterface;
12+
13+
require __DIR__ . '/../vendor/autoload.php';
14+
15+
$url = isset($argv[1]) ? $argv[1] : '127.0.0.1:8080';
16+
17+
$loop = React\EventLoop\Factory::create();
18+
19+
$proxy = new ProxyConnector($url, new Connector($loop), array(
20+
'X-Custom-Header-1' => 'Value-1',
21+
'X-Custom-Header-2' => 'Value-2',
22+
));
23+
$connector = new Connector($loop, array(
24+
'tcp' => $proxy,
25+
'timeout' => 3.0,
26+
'dns' => false,
27+
));
28+
29+
$connector->connect('tls://google.com:443')->then(function (ConnectionInterface $stream) {
30+
$stream->write("GET / HTTP/1.1\r\nHost: google.com\r\nConnection: close\r\n\r\n");
31+
$stream->on('data', function ($chunk) {
32+
echo $chunk;
33+
});
34+
}, 'printf');
35+
36+
$loop->run();

src/ProxyConnector.php

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class ProxyConnector implements ConnectorInterface
4343
{
4444
private $connector;
4545
private $proxyUri;
46-
private $proxyAuth = '';
46+
private $headers = '';
4747

4848
/**
4949
* Instantiate a new ProxyConnector which uses the given $proxyUrl
@@ -54,9 +54,10 @@ class ProxyConnector implements ConnectorInterface
5454
* @param ConnectorInterface $connector In its most simple form, the given
5555
* connector will be a \React\Socket\Connector if you want to connect to
5656
* a given IP address.
57+
* @param array $httpHeaders Custom HTTP headers to be sent to the proxy.
5758
* @throws InvalidArgumentException if the proxy URL is invalid
5859
*/
59-
public function __construct($proxyUrl, ConnectorInterface $connector)
60+
public function __construct($proxyUrl, ConnectorInterface $connector, array $httpHeaders = array())
6061
{
6162
// support `http+unix://` scheme for Unix domain socket (UDS) paths
6263
if (preg_match('/^http\+unix:\/\/(.*?@)?(.+?)$/', $proxyUrl, $match)) {
@@ -90,10 +91,17 @@ public function __construct($proxyUrl, ConnectorInterface $connector)
9091

9192
// prepare Proxy-Authorization header if URI contains username/password
9293
if (isset($parts['user']) || isset($parts['pass'])) {
93-
$this->proxyAuth = 'Proxy-Authorization: Basic ' . base64_encode(
94+
$this->headers = 'Proxy-Authorization: Basic ' . base64_encode(
9495
rawurldecode($parts['user'] . ':' . (isset($parts['pass']) ? $parts['pass'] : ''))
9596
) . "\r\n";
9697
}
98+
99+
// append any additional custom request headers
100+
foreach ($httpHeaders as $name => $values) {
101+
foreach ((array)$values as $value) {
102+
$this->headers .= $name . ': ' . $value . "\r\n";
103+
}
104+
}
97105
}
98106

99107
public function connect($uri)
@@ -151,8 +159,8 @@ public function connect($uri)
151159
$connecting->cancel();
152160
});
153161

154-
$auth = $this->proxyAuth;
155-
$connecting->then(function (ConnectionInterface $stream) use ($target, $auth, $deferred) {
162+
$headers = $this->headers;
163+
$connecting->then(function (ConnectionInterface $stream) use ($target, $headers, $deferred) {
156164
// keep buffering data until headers are complete
157165
$buffer = '';
158166
$stream->on('data', $fn = function ($chunk) use (&$buffer, $deferred, $stream, &$fn) {
@@ -212,7 +220,7 @@ public function connect($uri)
212220
$deferred->reject(new RuntimeException('Connection to proxy lost while waiting for response (ECONNRESET)', defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104));
213221
});
214222

215-
$stream->write("CONNECT " . $target . " HTTP/1.1\r\nHost: " . $target . "\r\n" . $auth . "\r\n");
223+
$stream->write("CONNECT " . $target . " HTTP/1.1\r\nHost: " . $target . "\r\n" . $headers . "\r\n");
216224
}, function (Exception $e) use ($deferred) {
217225
$deferred->reject($e = new RuntimeException(
218226
'Unable to connect to proxy (ECONNREFUSED)',

tests/ProxyConnectorTest.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,54 @@ public function testWillProxyAuthorizationHeaderIfUnixProxyUriContainsAuthentica
215215
$proxy->connect('google.com:80');
216216
}
217217

218+
public function testWillSendCustomHttpHeadersToProxy()
219+
{
220+
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
221+
$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");
222+
223+
$promise = \React\Promise\resolve($stream);
224+
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
225+
226+
$proxy = new ProxyConnector('proxy.example.com', $this->connector, array(
227+
'X-Custom-Header' => 'X-Custom-Value'
228+
));
229+
230+
$proxy->connect('google.com:80');
231+
}
232+
233+
public function testWillSendMultipleCustomCookieHeadersToProxy()
234+
{
235+
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
236+
$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");
237+
238+
$promise = \React\Promise\resolve($stream);
239+
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
240+
241+
$proxy = new ProxyConnector('proxy.example.com', $this->connector, array(
242+
'Cookie' => array(
243+
'id=123',
244+
'year=2018'
245+
)
246+
));
247+
248+
$proxy->connect('google.com:80');
249+
}
250+
251+
public function testWillAppendCustomProxyAuthorizationHeaderWithCredentialsFromUri()
252+
{
253+
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
254+
$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");
255+
256+
$promise = \React\Promise\resolve($stream);
257+
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
258+
259+
$proxy = new ProxyConnector('user:[email protected]', $this->connector, array(
260+
'Proxy-Authorization' => 'foobar'
261+
));
262+
263+
$proxy->connect('google.com:80');
264+
}
265+
218266
public function testRejectsInvalidUri()
219267
{
220268
$this->connector->expects($this->never())->method('connect');

0 commit comments

Comments
 (0)