From 2793fe24bf73da918728adfb075bdaba73ba7c7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Sun, 7 Mar 2021 10:21:23 +0100 Subject: [PATCH] Support the cURL (http://) scheme for StreamHandler proxies (#2850) * 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() --- docs/request-options.rst | 6 ++-- src/Handler/StreamHandler.php | 48 +++++++++++++++++++++++++++-- tests/Handler/CurlFactoryTest.php | 23 ++++++++++++++ tests/Handler/StreamHandlerTest.php | 26 +++++++++++++--- 4 files changed, 93 insertions(+), 10 deletions(-) diff --git a/docs/request-options.rst b/docs/request-options.rst index c5afff16e..e631b6335 100644 --- a/docs/request-options.rst +++ b/docs/request-options.rst @@ -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 @@ -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 ] ]); diff --git a/src/Handler/StreamHandler.php b/src/Handler/StreamHandler.php index bfdeac5e8..5be0bd4c1 100644 --- a/src/Handler/StreamHandler.php +++ b/src/Handler/StreamHandler.php @@ -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, + ]; } /** diff --git a/tests/Handler/CurlFactoryTest.php b/tests/Handler/CurlFactoryTest.php index b324bbdbc..f8ad46903 100644 --- a/tests/Handler/CurlFactoryTest.php +++ b/tests/Handler/CurlFactoryTest.php @@ -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); diff --git a/tests/Handler/StreamHandlerTest.php b/tests/Handler/StreamHandlerTest.php index f8ae2e09e..5bab7f43a 100644 --- a/tests/Handler/StreamHandlerTest.php +++ b/tests/Handler/StreamHandlerTest.php @@ -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' => ['*'] @@ -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]);