From 3b643b83f87e1765d2e9b1e946bb56ee0b4b7bde Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 8 Nov 2024 09:23:38 +0100 Subject: [PATCH] [HttpClient] Resolve hostnames in NoPrivateNetworkHttpClient --- HttpOptions.php | 2 ++ NativeHttpClient.php | 12 ++++++++++-- NoPrivateNetworkHttpClient.php | 17 +++++++++++++++-- Response/AmpResponse.php | 11 +++++++++-- Response/AsyncContext.php | 4 ++-- Response/AsyncResponse.php | 4 ++-- Response/CurlResponse.php | 11 +++++++++-- Tests/HttpClientTestCase.php | 23 +++++++++++++++++++++++ Tests/MockHttpClientTest.php | 5 ++++- TraceableHttpClient.php | 4 ++-- 10 files changed, 78 insertions(+), 15 deletions(-) diff --git a/HttpOptions.php b/HttpOptions.php index da55f99..5a178dd 100644 --- a/HttpOptions.php +++ b/HttpOptions.php @@ -148,6 +148,8 @@ public function buffer(bool $buffer) } /** + * @param callable(int, int, array, \Closure|null=):void $callback + * * @return $this */ public function setOnProgress(callable $callback) diff --git a/NativeHttpClient.php b/NativeHttpClient.php index e5bc61c..8819848 100644 --- a/NativeHttpClient.php +++ b/NativeHttpClient.php @@ -138,7 +138,15 @@ public function request(string $method, string $url, array $options = []): Respo // Memoize the last progress to ease calling the callback periodically when no network transfer happens $lastProgress = [0, 0]; $maxDuration = 0 < $options['max_duration'] ? $options['max_duration'] : \INF; - $onProgress = static function (...$progress) use ($onProgress, &$lastProgress, &$info, $maxDuration) { + $multi = $this->multi; + $resolve = static function (string $host, ?string $ip = null) use ($multi): ?string { + if (null !== $ip) { + $multi->dnsCache[$host] = $ip; + } + + return $multi->dnsCache[$host] ?? null; + }; + $onProgress = static function (...$progress) use ($onProgress, &$lastProgress, &$info, $maxDuration, $resolve) { if ($info['total_time'] >= $maxDuration) { throw new TransportException(sprintf('Max duration was reached for "%s".', implode('', $info['url']))); } @@ -154,7 +162,7 @@ public function request(string $method, string $url, array $options = []): Respo $lastProgress = $progress ?: $lastProgress; } - $onProgress($lastProgress[0], $lastProgress[1], $progressInfo); + $onProgress($lastProgress[0], $lastProgress[1], $progressInfo, $resolve); }; } elseif (0 < $options['max_duration']) { $maxDuration = $options['max_duration']; diff --git a/NoPrivateNetworkHttpClient.php b/NoPrivateNetworkHttpClient.php index c252fce..ed282e3 100644 --- a/NoPrivateNetworkHttpClient.php +++ b/NoPrivateNetworkHttpClient.php @@ -80,11 +80,24 @@ public function request(string $method, string $url, array $options = []): Respo $lastUrl = ''; $lastPrimaryIp = ''; - $options['on_progress'] = function (int $dlNow, int $dlSize, array $info) use ($onProgress, $subnets, &$lastUrl, &$lastPrimaryIp): void { + $options['on_progress'] = function (int $dlNow, int $dlSize, array $info, ?\Closure $resolve = null) use ($onProgress, $subnets, &$lastUrl, &$lastPrimaryIp): void { if ($info['url'] !== $lastUrl) { $host = trim(parse_url($info['url'], PHP_URL_HOST) ?: '', '[]'); + $resolve ??= static fn () => null; + + if (($ip = $host) + && !filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6) + && !filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4) + && !$ip = $resolve($host) + ) { + if ($ip = @(dns_get_record($host, \DNS_A)[0]['ip'] ?? null)) { + $resolve($host, $ip); + } elseif ($ip = @(dns_get_record($host, \DNS_AAAA)[0]['ipv6'] ?? null)) { + $resolve($host, '['.$ip.']'); + } + } - if ($host && IpUtils::checkIp($host, $subnets ?? self::PRIVATE_SUBNETS)) { + if ($ip && IpUtils::checkIp($ip, $subnets ?? self::PRIVATE_SUBNETS)) { throw new TransportException(sprintf('Host "%s" is blocked for "%s".', $host, $info['url'])); } diff --git a/Response/AmpResponse.php b/Response/AmpResponse.php index e4999b7..a9cc4d6 100644 --- a/Response/AmpResponse.php +++ b/Response/AmpResponse.php @@ -89,10 +89,17 @@ public function __construct(AmpClientState $multi, Request $request, array $opti $info['max_duration'] = $options['max_duration']; $info['debug'] = ''; + $resolve = static function (string $host, ?string $ip = null) use ($multi): ?string { + if (null !== $ip) { + $multi->dnsCache[$host] = $ip; + } + + return $multi->dnsCache[$host] ?? null; + }; $onProgress = $options['on_progress'] ?? static function () {}; - $onProgress = $this->onProgress = static function () use (&$info, $onProgress) { + $onProgress = $this->onProgress = static function () use (&$info, $onProgress, $resolve) { $info['total_time'] = microtime(true) - $info['start_time']; - $onProgress((int) $info['size_download'], ((int) (1 + $info['download_content_length']) ?: 1) - 1, (array) $info); + $onProgress((int) $info['size_download'], ((int) (1 + $info['download_content_length']) ?: 1) - 1, (array) $info, $resolve); }; $pauseDeferred = new Deferred(); diff --git a/Response/AsyncContext.php b/Response/AsyncContext.php index 3c5397c..de1562d 100644 --- a/Response/AsyncContext.php +++ b/Response/AsyncContext.php @@ -156,8 +156,8 @@ public function replaceRequest(string $method, string $url, array $options = []) $this->info['previous_info'][] = $info = $this->response->getInfo(); if (null !== $onProgress = $options['on_progress'] ?? null) { $thisInfo = &$this->info; - $options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use (&$thisInfo, $onProgress) { - $onProgress($dlNow, $dlSize, $thisInfo + $info); + $options['on_progress'] = static function (int $dlNow, int $dlSize, array $info, ?\Closure $resolve = null) use (&$thisInfo, $onProgress) { + $onProgress($dlNow, $dlSize, $thisInfo + $info, $resolve); }; } if (0 < ($info['max_duration'] ?? 0) && 0 < ($info['total_time'] ?? 0)) { diff --git a/Response/AsyncResponse.php b/Response/AsyncResponse.php index 890e2e9..de52ce0 100644 --- a/Response/AsyncResponse.php +++ b/Response/AsyncResponse.php @@ -51,8 +51,8 @@ public function __construct(HttpClientInterface $client, string $method, string if (null !== $onProgress = $options['on_progress'] ?? null) { $thisInfo = &$this->info; - $options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use (&$thisInfo, $onProgress) { - $onProgress($dlNow, $dlSize, $thisInfo + $info); + $options['on_progress'] = static function (int $dlNow, int $dlSize, array $info, ?\Closure $resolve = null) use (&$thisInfo, $onProgress) { + $onProgress($dlNow, $dlSize, $thisInfo + $info, $resolve); }; } $this->response = $client->request($method, $url, ['buffer' => false] + $options); diff --git a/Response/CurlResponse.php b/Response/CurlResponse.php index 633b74a..1db51da 100644 --- a/Response/CurlResponse.php +++ b/Response/CurlResponse.php @@ -115,13 +115,20 @@ public function __construct(CurlClientState $multi, $ch, ?array $options = null, curl_pause($ch, \CURLPAUSE_CONT); if ($onProgress = $options['on_progress']) { + $resolve = static function (string $host, ?string $ip = null) use ($multi): ?string { + if (null !== $ip) { + $multi->dnsCache->hostnames[$host] = $ip; + } + + return $multi->dnsCache->hostnames[$host] ?? null; + }; $url = isset($info['url']) ? ['url' => $info['url']] : []; curl_setopt($ch, \CURLOPT_NOPROGRESS, false); - curl_setopt($ch, \CURLOPT_PROGRESSFUNCTION, static function ($ch, $dlSize, $dlNow) use ($onProgress, &$info, $url, $multi, $debugBuffer) { + curl_setopt($ch, \CURLOPT_PROGRESSFUNCTION, static function ($ch, $dlSize, $dlNow) use ($onProgress, &$info, $url, $multi, $debugBuffer, $resolve) { try { rewind($debugBuffer); $debug = ['debug' => stream_get_contents($debugBuffer)]; - $onProgress($dlNow, $dlSize, $url + curl_getinfo($ch) + $info + $debug); + $onProgress($dlNow, $dlSize, $url + curl_getinfo($ch) + $info + $debug, $resolve); } catch (\Throwable $e) { $multi->handlesActivity[(int) $ch][] = null; $multi->handlesActivity[(int) $ch][] = $e; diff --git a/Tests/HttpClientTestCase.php b/Tests/HttpClientTestCase.php index d1213f0..251a8f4 100644 --- a/Tests/HttpClientTestCase.php +++ b/Tests/HttpClientTestCase.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpClient\Exception\InvalidArgumentException; use Symfony\Component\HttpClient\Exception\TransportException; use Symfony\Component\HttpClient\Internal\ClientState; +use Symfony\Component\HttpClient\NoPrivateNetworkHttpClient; use Symfony\Component\HttpClient\Response\StreamWrapper; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Process; @@ -466,4 +467,26 @@ public function testMisspelledScheme() $httpClient->request('GET', 'http:/localhost:8057/'); } + + public function testNoPrivateNetwork() + { + $client = $this->getHttpClient(__FUNCTION__); + $client = new NoPrivateNetworkHttpClient($client); + + $this->expectException(TransportException::class); + $this->expectExceptionMessage('Host "localhost" is blocked'); + + $client->request('GET', 'http://localhost:8888'); + } + + public function testNoPrivateNetworkWithResolve() + { + $client = $this->getHttpClient(__FUNCTION__); + $client = new NoPrivateNetworkHttpClient($client); + + $this->expectException(TransportException::class); + $this->expectExceptionMessage('Host "symfony.com" is blocked'); + + $client->request('GET', 'http://symfony.com', ['resolve' => ['symfony.com' => '127.0.0.1']]); + } } diff --git a/Tests/MockHttpClientTest.php b/Tests/MockHttpClientTest.php index e244c32..9f38940 100644 --- a/Tests/MockHttpClientTest.php +++ b/Tests/MockHttpClientTest.php @@ -304,7 +304,7 @@ protected function getHttpClient(string $testCase): HttpClientInterface switch ($testCase) { default: - return new MockHttpClient(function (string $method, string $url, array $options) use ($client) { + return new MockHttpClient(function (string $method, string $url, array $options) use ($client, $testCase) { try { // force the request to be completed so that we don't test side effects of the transport $response = $client->request($method, $url, ['buffer' => false] + $options); @@ -312,6 +312,9 @@ protected function getHttpClient(string $testCase): HttpClientInterface return new MockResponse($content, $response->getInfo()); } catch (\Throwable $e) { + if (str_starts_with($testCase, 'testNoPrivateNetwork')) { + throw $e; + } $this->fail($e->getMessage()); } }); diff --git a/TraceableHttpClient.php b/TraceableHttpClient.php index 0c1f05a..f83a5ca 100644 --- a/TraceableHttpClient.php +++ b/TraceableHttpClient.php @@ -58,11 +58,11 @@ public function request(string $method, string $url, array $options = []): Respo $content = false; } - $options['on_progress'] = function (int $dlNow, int $dlSize, array $info) use (&$traceInfo, $onProgress) { + $options['on_progress'] = function (int $dlNow, int $dlSize, array $info, ?\Closure $resolve = null) use (&$traceInfo, $onProgress) { $traceInfo = $info; if (null !== $onProgress) { - $onProgress($dlNow, $dlSize, $info); + $onProgress($dlNow, $dlSize, $info, $resolve); } };