Skip to content

Commit

Permalink
Support the cURL (http://) scheme for StreamHandler proxies (#2850)
Browse files Browse the repository at this point in the history
* Support the cURL (http://) scheme for StreamHandler proxies

* Remove 'proxy' workarounds in StreamHandlerTest

* Update documentation to use `http://` instead of `tcp://` for proxies

* Add StreamHandlerTest::testUsesProxy()

* Add CurlFactoryTest::testUsesProxy()
  • Loading branch information
TimWolla committed Mar 7, 2021
1 parent 55d46a8 commit 2793fe2
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 10 deletions.
6 changes: 3 additions & 3 deletions docs/request-options.rst
Expand Up @@ -803,7 +803,7 @@ Pass a string to specify a proxy for all protocols.

.. code-block:: php
$client->request('GET', '/', ['proxy' => 'tcp://localhost:8125']);
$client->request('GET', '/', ['proxy' => 'http://localhost:8125']);
Pass an associative array to specify HTTP proxies for specific URI schemes
(i.e., "http", "https"). Provide a ``no`` key value pair to provide a list of
Expand All @@ -821,8 +821,8 @@ host names that should not be proxied to.
$client->request('GET', '/', [
'proxy' => [
'http' => 'tcp://localhost:8125', // Use this proxy with "http"
'https' => 'tcp://localhost:9124', // Use this proxy with "https",
'http' => 'http://localhost:8125', // Use this proxy with "http"
'https' => 'http://localhost:9124', // Use this proxy with "https",
'no' => ['.mit.edu', 'foo.com'] // Don't use a proxy with these
]
]);
Expand Down
48 changes: 46 additions & 2 deletions src/Handler/StreamHandler.php
Expand Up @@ -386,16 +386,60 @@ private function getDefaultContext(RequestInterface $request): array
*/
private function add_proxy(RequestInterface $request, array &$options, $value, array &$params): void
{
$uri = null;

if (!\is_array($value)) {
$options['http']['proxy'] = $value;
$uri = $value;
} else {
$scheme = $request->getUri()->getScheme();
if (isset($value[$scheme])) {
if (!isset($value['no']) || !Utils::isHostInNoProxy($request->getUri()->getHost(), $value['no'])) {
$options['http']['proxy'] = $value[$scheme];
$uri = $value[$scheme];
}
}
}

if (!$uri) {
return;
}

$parsed = $this->parse_proxy($uri);
$options['http']['proxy'] = $parsed['proxy'];

if ($parsed['auth']) {
if (!isset($options['http']['header'])) {
$options['http']['header'] = [];
}
$options['http']['header'] .= "\r\nProxy-Authorization: {$parsed['auth']}";
}
}

/**
* Parses the given proxy URL to make it compatible with the format PHP's stream context expects.
*/
private function parse_proxy(string $url): array
{
$parsed = \parse_url($url);

if ($parsed !== false && isset($parsed['scheme']) && $parsed['scheme'] === 'http') {
if (isset($parsed['host']) && isset($parsed['port'])) {
$auth = null;
if (isset($parsed['user']) && isset($parsed['pass'])) {
$auth = \base64_encode("{$parsed['user']}:{$parsed['pass']}");
}

return [
'proxy' => "tcp://{$parsed['host']}:{$parsed['port']}",
'auth' => $auth ? "Basic {$auth}" : null,
];
}
}

// Return proxy as-is.
return [
'proxy' => $url,
'auth' => null,
];
}

/**
Expand Down
23 changes: 23 additions & 0 deletions tests/Handler/CurlFactoryTest.php
Expand Up @@ -198,6 +198,29 @@ private function checkNoProxyForHost($url, $noProxy, $assertUseProxy)
}
}

public function testUsesProxy()
{
Server::flush();
Server::enqueue([
new Psr7\Response(200, [
'Foo' => 'Bar',
'Baz' => 'bam',
'Content-Length' => 2,
], 'hi')
]);

$handler = new Handler\CurlMultiHandler();
$request = new Psr7\Request('GET', 'http://www.example.com', [], null, '1.0');
$promise = $handler($request, [
'proxy' => Server::$url
]);
$response = $promise->wait();
self::assertSame(200, $response->getStatusCode());
self::assertSame('Bar', $response->getHeaderLine('Foo'));
self::assertSame('2', $response->getHeaderLine('Content-Length'));
self::assertSame('hi', (string) $response->getBody());
}

public function testValidatesSslKey()
{
$f = new Handler\CurlFactory(3);
Expand Down
26 changes: 21 additions & 5 deletions tests/Handler/StreamHandlerTest.php
Expand Up @@ -289,17 +289,18 @@ public function testAddsProxy()

public function testAddsProxyByProtocol()
{
$url = \str_replace('http', 'tcp', Server::$url);
// Workaround until #1823 is fixed properly
$url = \rtrim($url, '/');
$url = Server::$url;
$res = $this->getSendResult(['proxy' => ['http' => $url]]);
$opts = \stream_context_get_options($res->getBody()->detach());
self::assertSame($url, $opts['http']['proxy']);

foreach ([\PHP_URL_HOST, \PHP_URL_PORT] as $part) {
self::assertSame(parse_url($url, $part), parse_url($opts['http']['proxy'], $part));
}
}

public function testAddsProxyButHonorsNoProxy()
{
$url = \str_replace('http', 'tcp', Server::$url);
$url = Server::$url;
$res = $this->getSendResult(['proxy' => [
'http' => $url,
'no' => ['*']
Expand All @@ -308,6 +309,21 @@ public function testAddsProxyButHonorsNoProxy()
self::assertArrayNotHasKey('proxy', $opts['http']);
}

public function testUsesProxy()
{
$this->queueRes();
$handler = new StreamHandler();
$request = new Request('GET', 'http://www.example.com', [], null, '1.0');
$response = $handler($request, [
'proxy' => Server::$url
])->wait();
self::assertSame(200, $response->getStatusCode());
self::assertSame('OK', $response->getReasonPhrase());
self::assertSame('Bar', $response->getHeaderLine('Foo'));
self::assertSame('8', $response->getHeaderLine('Content-Length'));
self::assertSame('hi there', (string) $response->getBody());
}

public function testAddsTimeout()
{
$res = $this->getSendResult(['stream' => true, 'timeout' => 200]);
Expand Down

0 comments on commit 2793fe2

Please sign in to comment.