Skip to content

Commit 2b0488f

Browse files
authored
Merge pull request #14 from clue-labs/auth
Support proxy authentication if proxy URL contains username/password
2 parents 2622bcd + 479de8a commit 2b0488f

File tree

3 files changed

+85
-3
lines changed

3 files changed

+85
-3
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Async HTTP CONNECT proxy connector, use any TCP/IP protocol through an HTTP prox
1313
* [Secure TLS connections](#secure-tls-connections)
1414
* [Connection timeout](#connection-timeout)
1515
* [DNS resolution](#dns-resolution)
16+
* [Authentication](#authentication)
1617
* [Advanced secure proxy connections](#advanced-secure-proxy-connections)
1718
* [Install](#install)
1819
* [Tests](#tests)
@@ -267,6 +268,35 @@ $connector = Connector($loop, array(
267268
> Also note how local DNS resolution is in fact entirely handled outside of this
268269
HTTP CONNECT client implementation.
269270

271+
#### Authentication
272+
273+
If your HTTP proxy server requires authentication, you may pass the username and
274+
password as part of the HTTP proxy URL like this:
275+
276+
```php
277+
$proxy = new ProxyConnector('http://user:[email protected]:8080', $connector);
278+
```
279+
280+
Note that both the username and password must be percent-encoded if they contain
281+
special characters:
282+
283+
```php
284+
$user = 'he:llo';
285+
$pass = 'p@ss';
286+
287+
$proxy = new ProxyConnector(
288+
rawurlencode($user) . ':' . rawurlencode($pass) . '@127.0.0.1:8080',
289+
$connector
290+
);
291+
```
292+
293+
> The authentication details will be used for basic authentication and will be
294+
transferred in the `Proxy-Authorization` HTTP request header for each
295+
connection attempt.
296+
If the authentication details are missing or not accepted by the remote HTTP
297+
proxy server, it is expected to reject each connection attempt with a
298+
`407` (Proxy Authentication Required) response status code.
299+
270300
#### Advanced secure proxy connections
271301

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

src/ProxyConnector.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class ProxyConnector implements ConnectorInterface
4242
{
4343
private $connector;
4444
private $proxyUri;
45+
private $proxyAuth = '';
4546

4647
/**
4748
* Instantiate a new ProxyConnector which uses the given $proxyUrl
@@ -73,6 +74,13 @@ public function __construct($proxyUrl, ConnectorInterface $connector)
7374

7475
$this->connector = $connector;
7576
$this->proxyUri = $parts['scheme'] . '://' . $parts['host'] . ':' . $parts['port'];
77+
78+
// prepare Proxy-Authorization header if URI contains username/password
79+
if (isset($parts['user']) || isset($parts['pass'])) {
80+
$this->proxyAuth = 'Proxy-Authorization: Basic ' . base64_encode(
81+
rawurldecode($parts['user'] . ':' . (isset($parts['pass']) ? $parts['pass'] : ''))
82+
) . "\r\n";
83+
}
7684
}
7785

7886
public function connect($uri)
@@ -116,7 +124,9 @@ public function connect($uri)
116124
$proxyUri .= '#' . $parts['fragment'];
117125
}
118126

119-
return $this->connector->connect($proxyUri)->then(function (ConnectionInterface $stream) use ($host, $port) {
127+
$auth = $this->proxyAuth;
128+
129+
return $this->connector->connect($proxyUri)->then(function (ConnectionInterface $stream) use ($host, $port, $auth) {
120130
$deferred = new Deferred(function ($_, $reject) use ($stream) {
121131
$reject(new RuntimeException('Operation canceled while waiting for response from proxy'));
122132
$stream->close();
@@ -176,7 +186,7 @@ public function connect($uri)
176186
$deferred->reject(new RuntimeException('Connection to proxy lost while waiting for response'));
177187
});
178188

179-
$stream->write("CONNECT " . $host . ":" . $port . " HTTP/1.1\r\nHost: " . $host . ":" . $port . "\r\n\r\n");
189+
$stream->write("CONNECT " . $host . ":" . $port . " HTTP/1.1\r\nHost: " . $host . ":" . $port . "\r\n" . $auth . "\r\n");
180190

181191
return $deferred->promise();
182192
});

tests/ProxyConnectorTest.php

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public function testCancelPromiseWillCancelPendingConnection()
8888
public function testWillWriteToOpenConnection()
8989
{
9090
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
91-
$stream->expects($this->once())->method('write');
91+
$stream->expects($this->once())->method('write')->with("CONNECT google.com:80 HTTP/1.1\r\nHost: google.com:80\r\n\r\n");
9292

9393
$promise = \React\Promise\resolve($stream);
9494
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
@@ -98,6 +98,48 @@ public function testWillWriteToOpenConnection()
9898
$proxy->connect('google.com:80');
9999
}
100100

101+
public function testWillProxyAuthorizationHeaderIfProxyUriContainsAuthentication()
102+
{
103+
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
104+
$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\n\r\n");
105+
106+
$promise = \React\Promise\resolve($stream);
107+
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
108+
109+
$proxy = new ProxyConnector('user:[email protected]', $this->connector);
110+
111+
$proxy->connect('google.com:80');
112+
}
113+
114+
public function testWillProxyAuthorizationHeaderIfProxyUriContainsOnlyUsernameWithoutPassword()
115+
{
116+
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
117+
$stream->expects($this->once())->method('write')->with("CONNECT google.com:80 HTTP/1.1\r\nHost: google.com:80\r\nProxy-Authorization: Basic dXNlcjo=\r\n\r\n");
118+
119+
$promise = \React\Promise\resolve($stream);
120+
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
121+
122+
$proxy = new ProxyConnector('[email protected]', $this->connector);
123+
124+
$proxy->connect('google.com:80');
125+
}
126+
127+
public function testWillProxyAuthorizationHeaderIfProxyUriContainsAuthenticationWithPercentEncoding()
128+
{
129+
$user = 'h@llÖ';
130+
$pass = '%secret?';
131+
132+
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
133+
$stream->expects($this->once())->method('write')->with("CONNECT google.com:80 HTTP/1.1\r\nHost: google.com:80\r\nProxy-Authorization: Basic " . base64_encode($user . ':' . $pass) . "\r\n\r\n");
134+
135+
$promise = \React\Promise\resolve($stream);
136+
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
137+
138+
$proxy = new ProxyConnector(rawurlencode($user) . ':' . rawurlencode($pass) . '@proxy.example.com', $this->connector);
139+
140+
$proxy->connect('google.com:80');
141+
}
142+
101143
public function testRejectsInvalidUri()
102144
{
103145
$this->connector->expects($this->never())->method('connect');

0 commit comments

Comments
 (0)