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:pass@127.0.0.1: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:pass@proxy.example.com', $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('user@proxy.example.com', $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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy