Skip to content

Commit 5c757be

Browse files
[HttpClient] Fix option "resolve" with IPv6 addresses
1 parent b815547 commit 5c757be

File tree

6 files changed

+63
-14
lines changed

6 files changed

+63
-14
lines changed

src/Symfony/Component/HttpClient/CurlHttpClient.php

+1
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,7 @@ private function validateExtraCurlOptions(array $options): void
490490
\CURLOPT_INFILESIZE => 'body',
491491
\CURLOPT_POSTFIELDS => 'body',
492492
\CURLOPT_UPLOAD => 'body',
493+
\CURLOPT_IPRESOLVE => 'bindto',
493494
\CURLOPT_INTERFACE => 'bindto',
494495
\CURLOPT_TIMEOUT_MS => 'max_duration',
495496
\CURLOPT_TIMEOUT => 'max_duration',

src/Symfony/Component/HttpClient/HttpClientTrait.php

+16-2
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,14 @@ private static function mergeDefaultOptions(array $options, array $defaultOption
197197
if ($resolve = $options['resolve'] ?? false) {
198198
$options['resolve'] = [];
199199
foreach ($resolve as $k => $v) {
200-
$options['resolve'][substr(self::parseUrl('http://'.$k)['authority'], 2)] = (string) $v;
200+
if ('' === $v = (string) $v) {
201+
throw new InvalidArgumentException(sprintf('Option "resolve" for host "%s" cannot be empty.', $k));
202+
}
203+
if ('[' === $v[0] && ']' === substr($v, -1) && str_contains($v, ':')) {
204+
$v = substr($v, 1, -1);
205+
}
206+
207+
$options['resolve'][substr(self::parseUrl('http://'.$k)['authority'], 2)] = $v;
201208
}
202209
}
203210

@@ -220,7 +227,14 @@ private static function mergeDefaultOptions(array $options, array $defaultOption
220227

221228
if ($resolve = $defaultOptions['resolve'] ?? false) {
222229
foreach ($resolve as $k => $v) {
223-
$options['resolve'] += [substr(self::parseUrl('http://'.$k)['authority'], 2) => (string) $v];
230+
if ('' === $v = (string) $v) {
231+
throw new InvalidArgumentException(sprintf('Option "resolve" for host "%s" cannot be empty.', $k));
232+
}
233+
if ('[' === $v[0] && ']' === substr($v, -1) && str_contains($v, ':')) {
234+
$v = substr($v, 1, -1);
235+
}
236+
237+
$options['resolve'] += [substr(self::parseUrl('http://'.$k)['authority'], 2) => $v];
224238
}
225239
}
226240

src/Symfony/Component/HttpClient/Internal/AmpListener.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,12 @@ public function startTlsNegotiation(Request $request): Promise
8080
public function startSendingRequest(Request $request, Stream $stream): Promise
8181
{
8282
$host = $stream->getRemoteAddress()->getHost();
83+
$this->info['primary_ip'] = $host;
8384

8485
if (false !== strpos($host, ':')) {
8586
$host = '['.$host.']';
8687
}
8788

88-
$this->info['primary_ip'] = $host;
8989
$this->info['primary_port'] = $stream->getRemoteAddress()->getPort();
9090
$this->info['pretransfer_time'] = microtime(true) - $this->info['start_time'];
9191
$this->info['debug'] .= sprintf("* Connected to %s (%s) port %d\n", $request->getUri()->getHost(), $host, $this->info['primary_port']);

src/Symfony/Component/HttpClient/Internal/AmpResolver.php

+16-4
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,31 @@ public function __construct(array &$dnsMap)
3434

3535
public function resolve(string $name, ?int $typeRestriction = null): Promise
3636
{
37-
if (!isset($this->dnsMap[$name]) || !\in_array($typeRestriction, [Record::A, null], true)) {
37+
$recordType = Record::A;
38+
$ip = $this->dnsMap[$name] ?? null;
39+
40+
if (null !== $ip && str_contains($ip, ':')) {
41+
$recordType = Record::AAAA;
42+
}
43+
if (null === $ip || $recordType !== ($typeRestriction ?? $recordType)) {
3844
return Dns\resolver()->resolve($name, $typeRestriction);
3945
}
4046

41-
return new Success([new Record($this->dnsMap[$name], Record::A, null)]);
47+
return new Success([new Record($ip, $recordType, null)]);
4248
}
4349

4450
public function query(string $name, int $type): Promise
4551
{
46-
if (!isset($this->dnsMap[$name]) || Record::A !== $type) {
52+
$recordType = Record::A;
53+
$ip = $this->dnsMap[$name] ?? null;
54+
55+
if (null !== $ip && str_contains($ip, ':')) {
56+
$recordType = Record::AAAA;
57+
}
58+
if (null === $ip || $recordType !== $type) {
4759
return Dns\resolver()->query($name, $type);
4860
}
4961

50-
return new Success([new Record($this->dnsMap[$name], Record::A, null)]);
62+
return new Success([new Record($ip, $recordType, null)]);
5163
}
5264
}

src/Symfony/Component/HttpClient/NativeHttpClient.php

+13-6
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ public function request(string $method, string $url, array $options = []): Respo
7979
if (str_starts_with($options['bindto'], 'host!')) {
8080
$options['bindto'] = substr($options['bindto'], 5);
8181
}
82+
if ((\PHP_VERSION_ID < 80223 || 80300 <= \PHP_VERSION_ID && 80311 < \PHP_VERSION_ID) && '\\' === \DIRECTORY_SEPARATOR && '[' === $options['bindto'][0]) {
83+
$options['bindto'] = preg_replace('{^\[[^\]]++\]}', '[$0]', $options['bindto']);
84+
}
8285
}
8386

8487
$hasContentLength = isset($options['normalized_headers']['content-length']);
@@ -330,31 +333,35 @@ private static function parseHostPort(array $url, array &$info): array
330333
*/
331334
private static function dnsResolve($host, NativeClientState $multi, array &$info, ?\Closure $onProgress): string
332335
{
333-
if (null === $ip = $multi->dnsCache[$host] ?? null) {
336+
$flag = '' !== $host && '[' === $host[0] && ']' === $host[-1] && str_contains($host, ':') ? \FILTER_FLAG_IPV6 : \FILTER_FLAG_IPV4;
337+
$ip = \FILTER_FLAG_IPV6 === $flag ? substr($host, 1, -1) : $host;
338+
339+
if (filter_var($ip, \FILTER_VALIDATE_IP, $flag)) {
340+
// The host is already an IP address
341+
} elseif (null === $ip = $multi->dnsCache[$host] ?? null) {
334342
$info['debug'] .= "* Hostname was NOT found in DNS cache\n";
335343
$now = microtime(true);
336344

337-
if ('[' === $host[0] && ']' === $host[-1] && filter_var(substr($host, 1, -1), \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
338-
$ip = [$host];
339-
} elseif (!$ip = gethostbynamel($host)) {
345+
if (!$ip = gethostbynamel($host)) {
340346
throw new TransportException(sprintf('Could not resolve host "%s".', $host));
341347
}
342348

343-
$info['namelookup_time'] = microtime(true) - ($info['start_time'] ?: $now);
344349
$multi->dnsCache[$host] = $ip = $ip[0];
345350
$info['debug'] .= "* Added {$host}:0:{$ip} to DNS cache\n";
346351
} else {
347352
$info['debug'] .= "* Hostname was found in DNS cache\n";
353+
$host = str_contains($ip, ':') ? "[$ip]" : $ip;
348354
}
349355

356+
$info['namelookup_time'] = microtime(true) - ($info['start_time'] ?: $now);
350357
$info['primary_ip'] = $ip;
351358

352359
if ($onProgress) {
353360
// Notify DNS resolution
354361
$onProgress();
355362
}
356363

357-
return $ip;
364+
return $host;
358365
}
359366

360367
/**

src/Symfony/Contracts/HttpClient/Test/HttpClientTestCase.php

+16-1
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,18 @@ public function testIdnResolve()
735735
$this->assertSame(200, $response->getStatusCode());
736736
}
737737

738+
public function testIPv6Resolve()
739+
{
740+
TestHttpServer::start(8087, '[::1]');
741+
742+
$client = $this->getHttpClient(__FUNCTION__);
743+
$response = $client->request('GET', 'http://symfony.com:8087/', [
744+
'resolve' => ['symfony.com' => '::1'],
745+
]);
746+
747+
$this->assertSame(200, $response->getStatusCode());
748+
}
749+
738750
public function testNotATimeout()
739751
{
740752
$client = $this->getHttpClient(__FUNCTION__);
@@ -1163,7 +1175,10 @@ public function testBindToPort()
11631175
$vars = $response->toArray();
11641176

11651177
self::assertSame('127.0.0.1', $vars['REMOTE_ADDR']);
1166-
self::assertSame('9876', $vars['REMOTE_PORT']);
1178+
1179+
if ('\\' !== \DIRECTORY_SEPARATOR) {
1180+
self::assertSame('9876', $vars['REMOTE_PORT']);
1181+
}
11671182
}
11681183

11691184
public function testBindToPortV6()

0 commit comments

Comments
 (0)