diff --git a/CurlHttpClient.php b/CurlHttpClient.php index 3a2fba0..478f9c0 100644 --- a/CurlHttpClient.php +++ b/CurlHttpClient.php @@ -67,7 +67,7 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface, * * @see HttpClientInterface::OPTIONS_DEFAULTS for available options */ - public function __construct(array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 50) + public function __construct(array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 0) { if (!\extension_loaded('curl')) { throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\CurlHttpClient" as the "curl" extension is not installed.'); @@ -185,10 +185,10 @@ public function request(string $method, string $url, array $options = []): Respo $multi->reset(); } - foreach ($options['resolve'] as $host => $ip) { - $resolve[] = null === $ip ? "-$host:$port" : "$host:$port:$ip"; - $multi->dnsCache->hostnames[$host] = $ip; - $multi->dnsCache->removals["-$host:$port"] = "-$host:$port"; + foreach ($options['resolve'] as $resolveHost => $ip) { + $resolve[] = null === $ip ? "-$resolveHost:$port" : "$resolveHost:$port:$ip"; + $multi->dnsCache->hostnames[$resolveHost] = $ip; + $multi->dnsCache->removals["-$resolveHost:$port"] = "-$resolveHost:$port"; } $curlopts[\CURLOPT_RESOLVE] = $resolve; diff --git a/HttpClientTrait.php b/HttpClientTrait.php index 3f44f36..d436a4c 100644 --- a/HttpClientTrait.php +++ b/HttpClientTrait.php @@ -445,6 +445,8 @@ private static function jsonEncode($value, ?int $flags = null, int $maxDepth = 5 */ private static function resolveUrl(array $url, ?array $base, array $queryDefaults = []): array { + $givenUrl = $url; + if (null !== $base && '' === ($base['scheme'] ?? '').($base['authority'] ?? '')) { throw new InvalidArgumentException(sprintf('Invalid "base_uri" option: host or scheme is missing in "%s".', implode('', $base))); } @@ -498,6 +500,10 @@ private static function resolveUrl(array $url, ?array $base, array $queryDefault $url['query'] = null; } + if (null !== $url['scheme'] && null === $url['authority']) { + throw new InvalidArgumentException(\sprintf('Invalid URL: host is missing in "%s".', implode('', $givenUrl))); + } + return $url; } diff --git a/Tests/CurlHttpClientTest.php b/Tests/CurlHttpClientTest.php index 3ff0c9a..9ea9762 100644 --- a/Tests/CurlHttpClientTest.php +++ b/Tests/CurlHttpClientTest.php @@ -22,17 +22,19 @@ class CurlHttpClientTest extends HttpClientTestCase { protected function getHttpClient(string $testCase): HttpClientInterface { - if (false !== strpos($testCase, 'Push')) { - if (\PHP_VERSION_ID >= 70300 && \PHP_VERSION_ID < 70304) { - $this->markTestSkipped('PHP 7.3.0 to 7.3.3 don\'t support HTTP/2 PUSH'); - } - - if (!\defined('CURLMOPT_PUSHFUNCTION') || 0x073D00 > ($v = curl_version())['version_number'] || !(\CURL_VERSION_HTTP2 & $v['features'])) { - $this->markTestSkipped('curl <7.61 is used or it is not compiled with support for HTTP/2 PUSH'); - } + if (!str_contains($testCase, 'Push')) { + return new CurlHttpClient(['verify_peer' => false, 'verify_host' => false]); } - return new CurlHttpClient(['verify_peer' => false, 'verify_host' => false]); + if (\PHP_VERSION_ID >= 70300 && \PHP_VERSION_ID < 70304) { + $this->markTestSkipped('PHP 7.3.0 to 7.3.3 don\'t support HTTP/2 PUSH'); + } + + if (!\defined('CURLMOPT_PUSHFUNCTION') || 0x073D00 > ($v = curl_version())['version_number'] || !(\CURL_VERSION_HTTP2 & $v['features'])) { + $this->markTestSkipped('curl <7.61 is used or it is not compiled with support for HTTP/2 PUSH'); + } + + return new CurlHttpClient(['verify_peer' => false, 'verify_host' => false], 6, 50); } public function testBindToPort() @@ -126,4 +128,20 @@ public function testOverridingInternalAttributesUsingCurlOptions() ], ]); } + + public function testKeepAuthorizationHeaderOnRedirectToSameHostWithConfiguredHostToIpAddressMapping() + { + $httpClient = $this->getHttpClient(__FUNCTION__); + $response = $httpClient->request('POST', 'http://127.0.0.1:8057/301', [ + 'headers' => [ + 'Authorization' => 'Basic Zm9vOmJhcg==', + ], + 'resolve' => [ + 'symfony.com' => '10.10.10.10', + ], + ]); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame('/302', $response->toArray()['REQUEST_URI'] ?? null); + } } diff --git a/Tests/HttpClientTestCase.php b/Tests/HttpClientTestCase.php index 9a1c177..d1213f0 100644 --- a/Tests/HttpClientTestCase.php +++ b/Tests/HttpClientTestCase.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\SkippedTestSuiteError; use Symfony\Component\HttpClient\Exception\ClientException; +use Symfony\Component\HttpClient\Exception\InvalidArgumentException; use Symfony\Component\HttpClient\Exception\TransportException; use Symfony\Component\HttpClient\Internal\ClientState; use Symfony\Component\HttpClient\Response\StreamWrapper; @@ -455,4 +456,14 @@ public function testNullBody() $this->expectNotToPerformAssertions(); } + + public function testMisspelledScheme() + { + $httpClient = $this->getHttpClient(__FUNCTION__); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid URL: host is missing in "http:/localhost:8057/".'); + + $httpClient->request('GET', 'http:/localhost:8057/'); + } } diff --git a/Tests/HttpClientTraitTest.php b/Tests/HttpClientTraitTest.php index 2f42eb8..aa03378 100644 --- a/Tests/HttpClientTraitTest.php +++ b/Tests/HttpClientTraitTest.php @@ -63,7 +63,6 @@ public function testResolveUrl(string $base, string $url, string $expected) public static function provideResolveUrl(): array { return [ - [self::RFC3986_BASE, 'http:h', 'http:h'], [self::RFC3986_BASE, 'g', 'http://a/b/c/g'], [self::RFC3986_BASE, './g', 'http://a/b/c/g'], [self::RFC3986_BASE, 'g/', 'http://a/b/c/g/'], @@ -117,7 +116,6 @@ public static function provideResolveUrl(): array ['http://u:p@a/b/c/d;p?q', '.', 'http://u:p@a/b/c/'], // path ending with slash or no slash at all ['http://a/b/c/d/', 'e', 'http://a/b/c/d/e'], - ['http:no-slash', 'e', 'http:e'], // falsey relative parts [self::RFC3986_BASE, '//0', 'http://0/'], [self::RFC3986_BASE, '0', 'http://a/b/c/0'],