From 954fc1ca2d8302bc29b126aa27865d1541b5de78 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 18 Nov 2024 18:21:26 +0100 Subject: [PATCH] [HttpClient] Fix option "resolve" with IPv6 addresses --- .../Component/HttpClient/HttpClientTrait.php | 18 +++++++++++++++-- .../HttpClient/Internal/AmpListener.php | 2 +- .../HttpClient/Internal/AmpResolver.php | 20 +++++++++++++++---- .../Component/HttpClient/NativeHttpClient.php | 19 ++++++++++++------ .../HttpClient/Test/HttpClientTestCase.php | 19 ++++++++++++++++-- .../HttpClient/Test/TestHttpServer.php | 9 ++++++++- 6 files changed, 71 insertions(+), 16 deletions(-) diff --git a/src/Symfony/Component/HttpClient/HttpClientTrait.php b/src/Symfony/Component/HttpClient/HttpClientTrait.php index 7bc037e7bd7f0..2d2fa7dd9f06f 100644 --- a/src/Symfony/Component/HttpClient/HttpClientTrait.php +++ b/src/Symfony/Component/HttpClient/HttpClientTrait.php @@ -197,7 +197,14 @@ private static function mergeDefaultOptions(array $options, array $defaultOption if ($resolve = $options['resolve'] ?? false) { $options['resolve'] = []; foreach ($resolve as $k => $v) { - $options['resolve'][substr(self::parseUrl('http://'.$k)['authority'], 2)] = (string) $v; + if ('' === $v = (string) $v) { + throw new InvalidArgumentException(sprintf('Option "resolve" for host "%s" cannot be empty.', $k)); + } + if ('[' === $v[0] && ']' === substr($v, -1) && str_contains($v, ':')) { + $v = substr($v, 1, -1); + } + + $options['resolve'][substr(self::parseUrl('http://'.$k)['authority'], 2)] = $v; } } @@ -220,7 +227,14 @@ private static function mergeDefaultOptions(array $options, array $defaultOption if ($resolve = $defaultOptions['resolve'] ?? false) { foreach ($resolve as $k => $v) { - $options['resolve'] += [substr(self::parseUrl('http://'.$k)['authority'], 2) => (string) $v]; + if ('' === $v = (string) $v) { + throw new InvalidArgumentException(sprintf('Option "resolve" for host "%s" cannot be empty.', $k)); + } + if ('[' === $v[0] && ']' === substr($v, -1) && str_contains($v, ':')) { + $v = substr($v, 1, -1); + } + + $options['resolve'] += [substr(self::parseUrl('http://'.$k)['authority'], 2) => $v]; } } diff --git a/src/Symfony/Component/HttpClient/Internal/AmpListener.php b/src/Symfony/Component/HttpClient/Internal/AmpListener.php index cb3235bca3ff6..a25dd27bec9f1 100644 --- a/src/Symfony/Component/HttpClient/Internal/AmpListener.php +++ b/src/Symfony/Component/HttpClient/Internal/AmpListener.php @@ -80,12 +80,12 @@ public function startTlsNegotiation(Request $request): Promise public function startSendingRequest(Request $request, Stream $stream): Promise { $host = $stream->getRemoteAddress()->getHost(); + $this->info['primary_ip'] = $host; if (false !== strpos($host, ':')) { $host = '['.$host.']'; } - $this->info['primary_ip'] = $host; $this->info['primary_port'] = $stream->getRemoteAddress()->getPort(); $this->info['pretransfer_time'] = microtime(true) - $this->info['start_time']; $this->info['debug'] .= sprintf("* Connected to %s (%s) port %d\n", $request->getUri()->getHost(), $host, $this->info['primary_port']); diff --git a/src/Symfony/Component/HttpClient/Internal/AmpResolver.php b/src/Symfony/Component/HttpClient/Internal/AmpResolver.php index 402f71d80d294..bb6d347c9fe64 100644 --- a/src/Symfony/Component/HttpClient/Internal/AmpResolver.php +++ b/src/Symfony/Component/HttpClient/Internal/AmpResolver.php @@ -34,19 +34,31 @@ public function __construct(array &$dnsMap) public function resolve(string $name, ?int $typeRestriction = null): Promise { - if (!isset($this->dnsMap[$name]) || !\in_array($typeRestriction, [Record::A, null], true)) { + $recordType = Record::A; + $ip = $this->dnsMap[$name] ?? null; + + if (null !== $ip && str_contains($ip, ':')) { + $recordType = Record::AAAA; + } + if (null === $ip || $recordType !== ($typeRestriction ?? $recordType)) { return Dns\resolver()->resolve($name, $typeRestriction); } - return new Success([new Record($this->dnsMap[$name], Record::A, null)]); + return new Success([new Record($ip, $recordType, null)]); } public function query(string $name, int $type): Promise { - if (!isset($this->dnsMap[$name]) || Record::A !== $type) { + $recordType = Record::A; + $ip = $this->dnsMap[$name] ?? null; + + if (null !== $ip && str_contains($ip, ':')) { + $recordType = Record::AAAA; + } + if (null === $ip || $recordType !== $type) { return Dns\resolver()->query($name, $type); } - return new Success([new Record($this->dnsMap[$name], Record::A, null)]); + return new Success([new Record($ip, $recordType, null)]); } } diff --git a/src/Symfony/Component/HttpClient/NativeHttpClient.php b/src/Symfony/Component/HttpClient/NativeHttpClient.php index 42c8be1bd8f27..71879db0352ed 100644 --- a/src/Symfony/Component/HttpClient/NativeHttpClient.php +++ b/src/Symfony/Component/HttpClient/NativeHttpClient.php @@ -79,6 +79,9 @@ public function request(string $method, string $url, array $options = []): Respo if (str_starts_with($options['bindto'], 'host!')) { $options['bindto'] = substr($options['bindto'], 5); } + if ((\PHP_VERSION_ID < 80223 || 80300 <= \PHP_VERSION_ID && 80311 < \PHP_VERSION_ID) && '\\' === \DIRECTORY_SEPARATOR && '[' === $options['bindto'][0]) { + $options['bindto'] = preg_replace('{^\[[^\]]++\]}', '[$0]', $options['bindto']); + } } $hasContentLength = isset($options['normalized_headers']['content-length']); @@ -330,23 +333,27 @@ private static function parseHostPort(array $url, array &$info): array */ private static function dnsResolve($host, NativeClientState $multi, array &$info, ?\Closure $onProgress): string { - if (null === $ip = $multi->dnsCache[$host] ?? null) { + $flag = '' !== $host && '[' === $host[0] && ']' === $host[-1] && str_contains($host, ':') ? \FILTER_FLAG_IPV6 : \FILTER_FLAG_IPV4; + $ip = \FILTER_FLAG_IPV6 === $flag ? substr($host, 1, -1) : $host; + + if (filter_var($ip, \FILTER_VALIDATE_IP, $flag)) { + // The host is already an IP address + } elseif (null === $ip = $multi->dnsCache[$host] ?? null) { $info['debug'] .= "* Hostname was NOT found in DNS cache\n"; $now = microtime(true); - if ('[' === $host[0] && ']' === $host[-1] && filter_var(substr($host, 1, -1), \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { - $ip = [$host]; - } elseif (!$ip = gethostbynamel($host)) { + if (!$ip = gethostbynamel($host)) { throw new TransportException(sprintf('Could not resolve host "%s".', $host)); } - $info['namelookup_time'] = microtime(true) - ($info['start_time'] ?: $now); $multi->dnsCache[$host] = $ip = $ip[0]; $info['debug'] .= "* Added {$host}:0:{$ip} to DNS cache\n"; } else { $info['debug'] .= "* Hostname was found in DNS cache\n"; + $host = str_contains($ip, ':') ? "[$ip]" : $ip; } + $info['namelookup_time'] = microtime(true) - ($info['start_time'] ?: $now); $info['primary_ip'] = $ip; if ($onProgress) { @@ -354,7 +361,7 @@ private static function dnsResolve($host, NativeClientState $multi, array &$info $onProgress(); } - return $ip; + return $host; } /** diff --git a/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php b/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php index eb10dbedbdbaf..3ec785444b2f5 100644 --- a/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php +++ b/src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php @@ -735,6 +735,18 @@ public function testIdnResolve() $this->assertSame(200, $response->getStatusCode()); } + public function testIPv6Resolve() + { + TestHttpServer::start(-8087, '[::1]'); + + $client = $this->getHttpClient(__FUNCTION__); + $response = $client->request('GET', 'http://symfony.com:8087/', [ + 'resolve' => ['symfony.com' => '::1'], + ]); + + $this->assertSame(200, $response->getStatusCode()); + } + public function testNotATimeout() { $client = $this->getHttpClient(__FUNCTION__); @@ -1168,7 +1180,7 @@ public function testBindToPort() public function testBindToPortV6() { - TestHttpServer::start(8087, '[::1]'); + TestHttpServer::start(-8087); $client = $this->getHttpClient(__FUNCTION__); $response = $client->request('GET', 'http://[::1]:8087', ['bindto' => '[::1]:9876']); @@ -1177,6 +1189,9 @@ public function testBindToPortV6() $vars = $response->toArray(); self::assertSame('::1', $vars['REMOTE_ADDR']); - self::assertSame('9876', $vars['REMOTE_PORT']); + + if ('\\' !== \DIRECTORY_SEPARATOR) { + self::assertSame('9876', $vars['REMOTE_PORT']); + } } } diff --git a/src/Symfony/Contracts/HttpClient/Test/TestHttpServer.php b/src/Symfony/Contracts/HttpClient/Test/TestHttpServer.php index d8b828c932484..0bea6de0ecc85 100644 --- a/src/Symfony/Contracts/HttpClient/Test/TestHttpServer.php +++ b/src/Symfony/Contracts/HttpClient/Test/TestHttpServer.php @@ -21,8 +21,15 @@ class TestHttpServer /** * @return Process */ - public static function start(int $port = 8057, $ip = '127.0.0.1') + public static function start(int $port = 8057) { + if (0 > $port) { + $port = -$port; + $ip = '[::1]'; + } else { + $ip = '127.0.0.1'; + } + if (isset(self::$process[$port])) { self::$process[$port]->stop(); } else { 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