Skip to content
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

Support the cURL (http://) scheme for StreamHandler proxies #2850

Merged
merged 5 commits into from Mar 7, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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']);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the previous code still work too?

Copy link
Contributor Author

@TimWolla TimWolla Feb 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Short answer: Yes.

Long answer:

tcp:// never worked with cURL, only with the StreamHandler.

For the StreamHandler the tcp:// version still works, because in parse_proxy I specifically check for the http scheme and return the URL as-is if it is not using the http scheme (https://github.com/guzzle/guzzle/pull/2850/files#diff-0cdc13d47562373b13de07ef6ea57235f3c5dc23a7cdf33fa47cd96c55ad8bacR424).

However it is not recommended to use tcp:// variant any longer. It is worse in every regard, because it does not support authentication and it is incompatible with cURL. That's why I adjusted the Docs.


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