feature #38026 [HttpClient] Allow to provide additional curl options to CurlHttpClient (pizzaminded)

This PR was squashed before being merged into the 5.2-dev branch.

Discussion
----------

[HttpClient] Allow to provide additional curl options  to CurlHttpClient

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | Fix #37798
| License       | MIT
| Doc PR        | symfony/symfony-docs#14195

~~**Tagging as a draft because:**~~
- ~~there is no Doc PR Ready yet~~
- probably there are better test cases required here.

This PR introduces an option to override default curl options defined in `CurlHttpClient`.  To override them, there is a special place in `$options` provided:

````php
$response = $httpClient->request('GET', 'http://your.url.here', [
            'extra' => [
                'curl' => [
                    CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
                ]
            ]
        ]);
````

This feature is available only in `CurlHttpClient` and would be ignored in another clients.

Commits
-------

89329bd979 [HttpClient] Allow to provide additional curl options  to CurlHttpClient
This commit is contained in:
Nicolas Grekas 2020-09-10 10:40:05 +02:00
commit 0ceafbcbea
3 changed files with 154 additions and 5 deletions

View File

@ -9,6 +9,7 @@ CHANGELOG
* added `StreamableInterface` to ease turning responses into PHP streams
* added `MockResponse::getRequestMethod()` and `getRequestUrl()` to allow inspecting which request has been sent
* added `EventSourceHttpClient` a Server-Sent events stream implementing the [EventSource specification](https://www.w3.org/TR/eventsource/#eventsource)
* added option "extra.curl" to allow setting additional curl options in `CurlHttpClient`
5.1.0
-----

View File

@ -40,6 +40,9 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
private $defaultOptions = self::OPTIONS_DEFAULTS + [
'auth_ntlm' => null, // array|string - an array containing the username as first value, and optionally the
// password as the second one; or string like username:password - enabling NTLM auth
'extra' => [
'curl' => [], // A list of extra curl options indexed by their corresponding CURLOPT_*
],
];
/**
@ -274,6 +277,11 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
$curlopts[\CURLOPT_TIMEOUT_MS] = 1000 * $options['max_duration'];
}
if (!empty($options['extra']['curl']) && \is_array($options['extra']['curl'])) {
$this->validateExtraCurlOptions($options['extra']['curl']);
$curlopts += $options['extra']['curl'];
}
if ($pushedResponse = $this->multi->pushedResponses[$url] ?? null) {
unset($this->multi->pushedResponses[$url]);
@ -297,11 +305,8 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
foreach ($curlopts as $opt => $value) {
if (null !== $value && !curl_setopt($ch, $opt, $value) && \CURLOPT_CERTINFO !== $opt) {
$constants = array_filter(get_defined_constants(), static function ($v, $k) use ($opt) {
return $v === $opt && 'C' === $k[0] && (0 === strpos($k, 'CURLOPT_') || 0 === strpos($k, 'CURLINFO_'));
}, \ARRAY_FILTER_USE_BOTH);
throw new TransportException(sprintf('Curl option "%s" is not supported.', key($constants) ?? $opt));
$constantName = $this->findConstantName($opt);
throw new TransportException(sprintf('Curl option "%s" is not supported.', $constantName ?? $opt));
}
}
@ -487,4 +492,101 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
return implode('', self::resolveUrl($location, $url));
};
}
private function findConstantName($opt): ?string
{
$constants = array_filter(get_defined_constants(), static function ($v, $k) use ($opt) {
return $v === $opt && 'C' === $k[0] && (0 === strpos($k, 'CURLOPT_') || 0 === strpos($k, 'CURLINFO_'));
}, \ARRAY_FILTER_USE_BOTH);
return key($constants);
}
/**
* Prevents overriding options that are set internally throughout the request.
*/
private function validateExtraCurlOptions(array $options): void
{
$curloptsToConfig = [
//options used in CurlHttpClient
\CURLOPT_HTTPAUTH => 'auth_ntlm',
\CURLOPT_USERPWD => 'auth_ntlm',
\CURLOPT_RESOLVE => 'resolve',
\CURLOPT_NOSIGNAL => 'timeout',
\CURLOPT_HTTPHEADER => 'headers',
\CURLOPT_INFILE => 'body',
\CURLOPT_READFUNCTION => 'body',
\CURLOPT_INFILESIZE => 'body',
\CURLOPT_POSTFIELDS => 'body',
\CURLOPT_UPLOAD => 'body',
\CURLOPT_PINNEDPUBLICKEY => 'peer_fingerprint',
\CURLOPT_UNIX_SOCKET_PATH => 'bindto',
\CURLOPT_INTERFACE => 'bindto',
\CURLOPT_TIMEOUT_MS => 'max_duration',
\CURLOPT_TIMEOUT => 'max_duration',
\CURLOPT_MAXREDIRS => 'max_redirects',
\CURLOPT_PROXY => 'proxy',
\CURLOPT_NOPROXY => 'no_proxy',
\CURLOPT_SSL_VERIFYPEER => 'verify_peer',
\CURLOPT_SSL_VERIFYHOST => 'verify_host',
\CURLOPT_CAINFO => 'cafile',
\CURLOPT_CAPATH => 'capath',
\CURLOPT_SSL_CIPHER_LIST => 'ciphers',
\CURLOPT_SSLCERT => 'local_cert',
\CURLOPT_SSLKEY => 'local_pk',
\CURLOPT_KEYPASSWD => 'passphrase',
\CURLOPT_CERTINFO => 'capture_peer_cert_chain',
\CURLOPT_USERAGENT => 'normalized_headers',
\CURLOPT_REFERER => 'headers',
//options used in CurlResponse
\CURLOPT_NOPROGRESS => 'on_progress',
\CURLOPT_PROGRESSFUNCTION => 'on_progress',
];
$curloptsToCheck = [
\CURLOPT_PRIVATE,
\CURLOPT_HEADERFUNCTION,
\CURLOPT_WRITEFUNCTION,
\CURLOPT_VERBOSE,
\CURLOPT_STDERR,
\CURLOPT_RETURNTRANSFER,
\CURLOPT_URL,
\CURLOPT_FOLLOWLOCATION,
\CURLOPT_HEADER,
\CURLOPT_CONNECTTIMEOUT,
\CURLOPT_CONNECTTIMEOUT_MS,
\CURLOPT_HEADEROPT,
\CURLOPT_HTTP_VERSION,
\CURLOPT_PORT,
\CURLOPT_DNS_USE_GLOBAL_CACHE,
\CURLOPT_PROTOCOLS,
\CURLOPT_REDIR_PROTOCOLS,
\CURLOPT_COOKIEFILE,
\CURLINFO_REDIRECT_COUNT,
];
$methodOpts = [
\CURLOPT_POST,
\CURLOPT_PUT,
\CURLOPT_CUSTOMREQUEST,
\CURLOPT_HTTPGET,
\CURLOPT_NOBODY,
];
foreach ($options as $opt => $optValue) {
if (isset($curloptsToConfig[$opt])) {
$constName = $this->findConstantName($opt) ?? $opt;
throw new InvalidArgumentException(sprintf('Cannot set "%s" with "extra.curl", use option "%s" instead.', $constName, $curloptsToConfig[$opt]));
}
if (\in_array($opt, $methodOpts)) {
throw new InvalidArgumentException('The HTTP method cannot be overridden using "extra.curl".');
}
if (\in_array($opt, $curloptsToCheck)) {
$constName = $this->findConstantName($opt) ?? $opt;
throw new InvalidArgumentException(sprintf('Cannot set "%s" with "extra.curl".', $constName));
}
}
}
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\HttpClient\Tests;
use Symfony\Component\HttpClient\CurlHttpClient;
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
use Symfony\Contracts\HttpClient\HttpClientInterface;
/**
@ -42,4 +43,49 @@ class CurlHttpClientTest extends HttpClientTestCase
parent::testTimeoutIsNotAFatalError();
}
public function testOverridingRefererUsingCurlOptions()
{
$httpClient = $this->getHttpClient(__FUNCTION__);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Cannot set "CURLOPT_REFERER" with "extra.curl", use option "headers" instead.');
$httpClient->request('GET', 'http://localhost:8057/', [
'extra' => [
'curl' => [
\CURLOPT_REFERER => 'Banana',
],
],
]);
}
public function testOverridingHttpMethodUsingCurlOptions()
{
$httpClient = $this->getHttpClient(__FUNCTION__);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The HTTP method cannot be overridden using "extra.curl".');
$httpClient->request('POST', 'http://localhost:8057/', [
'extra' => [
'curl' => [
\CURLOPT_HTTPGET => true,
],
],
]);
}
public function testOverridingInternalAttributesUsingCurlOptions()
{
$httpClient = $this->getHttpClient(__FUNCTION__);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Cannot set "CURLOPT_PRIVATE" with "extra.curl".');
$httpClient->request('POST', 'http://localhost:8057/', [
'extra' => [
'curl' => [
\CURLOPT_PRIVATE => 'overriden private',
],
],
]);
}
}