vendor/symfony/http-client/CurlHttpClient.php line 322

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\HttpClient;
  11. use Psr\Log\LoggerAwareInterface;
  12. use Psr\Log\LoggerInterface;
  13. use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
  14. use Symfony\Component\HttpClient\Exception\TransportException;
  15. use Symfony\Component\HttpClient\Internal\CurlClientState;
  16. use Symfony\Component\HttpClient\Internal\PushedResponse;
  17. use Symfony\Component\HttpClient\Response\CurlResponse;
  18. use Symfony\Component\HttpClient\Response\ResponseStream;
  19. use Symfony\Contracts\HttpClient\HttpClientInterface;
  20. use Symfony\Contracts\HttpClient\ResponseInterface;
  21. use Symfony\Contracts\HttpClient\ResponseStreamInterface;
  22. use Symfony\Contracts\Service\ResetInterface;
  23. /**
  24.  * A performant implementation of the HttpClientInterface contracts based on the curl extension.
  25.  *
  26.  * This provides fully concurrent HTTP requests, with transparent
  27.  * HTTP/2 push when a curl version that supports it is installed.
  28.  *
  29.  * @author Nicolas Grekas <p@tchwork.com>
  30.  */
  31. final class CurlHttpClient implements HttpClientInterfaceLoggerAwareInterfaceResetInterface
  32. {
  33.     use HttpClientTrait;
  34.     private $defaultOptions self::OPTIONS_DEFAULTS + [
  35.         'auth_ntlm' => null// array|string - an array containing the username as first value, and optionally the
  36.                              //   password as the second one; or string like username:password - enabling NTLM auth
  37.         'extra' => [
  38.             'curl' => [],    // A list of extra curl options indexed by their corresponding CURLOPT_*
  39.         ],
  40.     ];
  41.     private static $emptyDefaults self::OPTIONS_DEFAULTS + ['auth_ntlm' => null];
  42.     /**
  43.      * @var LoggerInterface|null
  44.      */
  45.     private $logger;
  46.     private $maxHostConnections;
  47.     private $maxPendingPushes;
  48.     /**
  49.      * An internal object to share state between the client and its responses.
  50.      *
  51.      * @var CurlClientState
  52.      */
  53.     private $multi;
  54.     /**
  55.      * @param array $defaultOptions     Default request's options
  56.      * @param int   $maxHostConnections The maximum number of connections to a single host
  57.      * @param int   $maxPendingPushes   The maximum number of pushed responses to accept in the queue
  58.      *
  59.      * @see HttpClientInterface::OPTIONS_DEFAULTS for available options
  60.      */
  61.     public function __construct(array $defaultOptions = [], int $maxHostConnections 6int $maxPendingPushes 0)
  62.     {
  63.         if (!\extension_loaded('curl')) {
  64.             throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\CurlHttpClient" as the "curl" extension is not installed.');
  65.         }
  66.         $this->maxHostConnections $maxHostConnections;
  67.         $this->maxPendingPushes $maxPendingPushes;
  68.         $this->defaultOptions['buffer'] = $this->defaultOptions['buffer'] ?? \Closure::fromCallable([__CLASS__'shouldBuffer']);
  69.         if ($defaultOptions) {
  70.             [, $this->defaultOptions] = self::prepareRequest(nullnull$defaultOptions$this->defaultOptions);
  71.         }
  72.     }
  73.     public function setLogger(LoggerInterface $logger): void
  74.     {
  75.         $this->logger $logger;
  76.         if (isset($this->multi)) {
  77.             $this->multi->logger $logger;
  78.         }
  79.     }
  80.     /**
  81.      * @see HttpClientInterface::OPTIONS_DEFAULTS for available options
  82.      *
  83.      * {@inheritdoc}
  84.      */
  85.     public function request(string $methodstring $url, array $options = []): ResponseInterface
  86.     {
  87.         $multi $this->ensureState();
  88.         [$url$options] = self::prepareRequest($method$url$options$this->defaultOptions);
  89.         $scheme $url['scheme'];
  90.         $authority $url['authority'];
  91.         $host parse_url($authority\PHP_URL_HOST);
  92.         $proxy self::getProxyUrl($options['proxy'], $url);
  93.         $url implode(''$url);
  94.         if (!isset($options['normalized_headers']['user-agent'])) {
  95.             $options['headers'][] = 'User-Agent: Symfony HttpClient/Curl';
  96.         }
  97.         $curlopts = [
  98.             \CURLOPT_URL => $url,
  99.             \CURLOPT_TCP_NODELAY => true,
  100.             \CURLOPT_PROTOCOLS => \CURLPROTO_HTTP \CURLPROTO_HTTPS,
  101.             \CURLOPT_REDIR_PROTOCOLS => \CURLPROTO_HTTP \CURLPROTO_HTTPS,
  102.             \CURLOPT_FOLLOWLOCATION => true,
  103.             \CURLOPT_MAXREDIRS => $options['max_redirects'] ? $options['max_redirects'] : 0,
  104.             \CURLOPT_COOKIEFILE => ''// Keep track of cookies during redirects
  105.             \CURLOPT_TIMEOUT => 0,
  106.             \CURLOPT_PROXY => $proxy,
  107.             \CURLOPT_NOPROXY => $options['no_proxy'] ?? $_SERVER['no_proxy'] ?? $_SERVER['NO_PROXY'] ?? '',
  108.             \CURLOPT_SSL_VERIFYPEER => $options['verify_peer'],
  109.             \CURLOPT_SSL_VERIFYHOST => $options['verify_host'] ? 0,
  110.             \CURLOPT_CAINFO => $options['cafile'],
  111.             \CURLOPT_CAPATH => $options['capath'],
  112.             \CURLOPT_SSL_CIPHER_LIST => $options['ciphers'],
  113.             \CURLOPT_SSLCERT => $options['local_cert'],
  114.             \CURLOPT_SSLKEY => $options['local_pk'],
  115.             \CURLOPT_KEYPASSWD => $options['passphrase'],
  116.             \CURLOPT_CERTINFO => $options['capture_peer_cert_chain'],
  117.         ];
  118.         if (1.0 === (float) $options['http_version']) {
  119.             $curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0;
  120.         } elseif (1.1 === (float) $options['http_version']) {
  121.             $curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
  122.         } elseif (\defined('CURL_VERSION_HTTP2') && (\CURL_VERSION_HTTP2 CurlClientState::$curlVersion['features']) && ('https:' === $scheme || 2.0 === (float) $options['http_version'])) {
  123.             $curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0;
  124.         }
  125.         if (isset($options['auth_ntlm'])) {
  126.             $curlopts[\CURLOPT_HTTPAUTH] = \CURLAUTH_NTLM;
  127.             $curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
  128.             if (\is_array($options['auth_ntlm'])) {
  129.                 $count \count($options['auth_ntlm']);
  130.                 if ($count <= || $count 2) {
  131.                     throw new InvalidArgumentException(sprintf('Option "auth_ntlm" must contain 1 or 2 elements, %d given.'$count));
  132.                 }
  133.                 $options['auth_ntlm'] = implode(':'$options['auth_ntlm']);
  134.             }
  135.             if (!\is_string($options['auth_ntlm'])) {
  136.                 throw new InvalidArgumentException(sprintf('Option "auth_ntlm" must be a string or an array, "%s" given.'get_debug_type($options['auth_ntlm'])));
  137.             }
  138.             $curlopts[\CURLOPT_USERPWD] = $options['auth_ntlm'];
  139.         }
  140.         if (!\ZEND_THREAD_SAFE) {
  141.             $curlopts[\CURLOPT_DNS_USE_GLOBAL_CACHE] = false;
  142.         }
  143.         if (\defined('CURLOPT_HEADEROPT') && \defined('CURLHEADER_SEPARATE')) {
  144.             $curlopts[\CURLOPT_HEADEROPT] = \CURLHEADER_SEPARATE;
  145.         }
  146.         // curl's resolve feature varies by host:port but ours varies by host only, let's handle this with our own DNS map
  147.         if (isset($multi->dnsCache->hostnames[$host])) {
  148.             $options['resolve'] += [$host => $multi->dnsCache->hostnames[$host]];
  149.         }
  150.         if ($options['resolve'] || $multi->dnsCache->evictions) {
  151.             // First reset any old DNS cache entries then add the new ones
  152.             $resolve $multi->dnsCache->evictions;
  153.             $multi->dnsCache->evictions = [];
  154.             $port parse_url($authority\PHP_URL_PORT) ?: ('http:' === $scheme 80 443);
  155.             if ($resolve && 0x072A00 CurlClientState::$curlVersion['version_number']) {
  156.                 // DNS cache removals require curl 7.42 or higher
  157.                 $multi->reset();
  158.             }
  159.             foreach ($options['resolve'] as $resolveHost => $ip) {
  160.                 $resolve[] = null === $ip "-$resolveHost:$port"$resolveHost:$port:$ip";
  161.                 $multi->dnsCache->hostnames[$resolveHost] = $ip;
  162.                 $multi->dnsCache->removals["-$resolveHost:$port"] = "-$resolveHost:$port";
  163.             }
  164.             $curlopts[\CURLOPT_RESOLVE] = $resolve;
  165.         }
  166.         if ('POST' === $method) {
  167.             // Use CURLOPT_POST to have browser-like POST-to-GET redirects for 301, 302 and 303
  168.             $curlopts[\CURLOPT_POST] = true;
  169.         } elseif ('HEAD' === $method) {
  170.             $curlopts[\CURLOPT_NOBODY] = true;
  171.         } else {
  172.             $curlopts[\CURLOPT_CUSTOMREQUEST] = $method;
  173.         }
  174.         if ('\\' !== \DIRECTORY_SEPARATOR && $options['timeout'] < 1) {
  175.             $curlopts[\CURLOPT_NOSIGNAL] = true;
  176.         }
  177.         if (\extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) {
  178.             $options['headers'][] = 'Accept-Encoding: gzip'// Expose only one encoding, some servers mess up when more are provided
  179.         }
  180.         $body $options['body'];
  181.         foreach ($options['headers'] as $i => $header) {
  182.             if (\is_string($body) && '' !== $body && === stripos($header'Content-Length: ')) {
  183.                 // Let curl handle Content-Length headers
  184.                 unset($options['headers'][$i]);
  185.                 continue;
  186.             }
  187.             if (':' === $header[-2] && \strlen($header) - === strpos($header': ')) {
  188.                 // curl requires a special syntax to send empty headers
  189.                 $curlopts[\CURLOPT_HTTPHEADER][] = substr_replace($header';', -2);
  190.             } else {
  191.                 $curlopts[\CURLOPT_HTTPHEADER][] = $header;
  192.             }
  193.         }
  194.         // Prevent curl from sending its default Accept and Expect headers
  195.         foreach (['accept''expect'] as $header) {
  196.             if (!isset($options['normalized_headers'][$header][0])) {
  197.                 $curlopts[\CURLOPT_HTTPHEADER][] = $header.':';
  198.             }
  199.         }
  200.         if (!\is_string($body)) {
  201.             if (\is_resource($body)) {
  202.                 $curlopts[\CURLOPT_INFILE] = $body;
  203.             } else {
  204.                 $eof false;
  205.                 $buffer '';
  206.                 $curlopts[\CURLOPT_READFUNCTION] = static function ($ch$fd$length) use ($body, &$buffer, &$eof) {
  207.                     return self::readRequestBody($length$body$buffer$eof);
  208.                 };
  209.             }
  210.             if (isset($options['normalized_headers']['content-length'][0])) {
  211.                 $curlopts[\CURLOPT_INFILESIZE] = (int) substr($options['normalized_headers']['content-length'][0], \strlen('Content-Length: '));
  212.             }
  213.             if (!isset($options['normalized_headers']['transfer-encoding'])) {
  214.                 $curlopts[\CURLOPT_HTTPHEADER][] = 'Transfer-Encoding:'.(isset($curlopts[\CURLOPT_INFILESIZE]) ? '' ' chunked');
  215.             }
  216.             if ('POST' !== $method) {
  217.                 $curlopts[\CURLOPT_UPLOAD] = true;
  218.                 if (!isset($options['normalized_headers']['content-type']) && !== ($curlopts[\CURLOPT_INFILESIZE] ?? null)) {
  219.                     $curlopts[\CURLOPT_HTTPHEADER][] = 'Content-Type: application/x-www-form-urlencoded';
  220.                 }
  221.             }
  222.         } elseif ('' !== $body || 'POST' === $method) {
  223.             $curlopts[\CURLOPT_POSTFIELDS] = $body;
  224.         }
  225.         if ($options['peer_fingerprint']) {
  226.             if (!isset($options['peer_fingerprint']['pin-sha256'])) {
  227.                 throw new TransportException(__CLASS__.' supports only "pin-sha256" fingerprints.');
  228.             }
  229.             $curlopts[\CURLOPT_PINNEDPUBLICKEY] = 'sha256//'.implode(';sha256//'$options['peer_fingerprint']['pin-sha256']);
  230.         }
  231.         if ($options['bindto']) {
  232.             if (file_exists($options['bindto'])) {
  233.                 $curlopts[\CURLOPT_UNIX_SOCKET_PATH] = $options['bindto'];
  234.             } elseif (!str_starts_with($options['bindto'], 'if!') && preg_match('/^(.*):(\d+)$/'$options['bindto'], $matches)) {
  235.                 $curlopts[\CURLOPT_INTERFACE] = $matches[1];
  236.                 $curlopts[\CURLOPT_LOCALPORT] = $matches[2];
  237.             } else {
  238.                 $curlopts[\CURLOPT_INTERFACE] = $options['bindto'];
  239.             }
  240.         }
  241.         if ($options['max_duration']) {
  242.             $curlopts[\CURLOPT_TIMEOUT_MS] = 1000 $options['max_duration'];
  243.         }
  244.         if (!empty($options['extra']['curl']) && \is_array($options['extra']['curl'])) {
  245.             $this->validateExtraCurlOptions($options['extra']['curl']);
  246.             $curlopts += $options['extra']['curl'];
  247.         }
  248.         if ($pushedResponse $multi->pushedResponses[$url] ?? null) {
  249.             unset($multi->pushedResponses[$url]);
  250.             if (self::acceptPushForRequest($method$options$pushedResponse)) {
  251.                 $this->logger && $this->logger->debug(sprintf('Accepting pushed response: "%s %s"'$method$url));
  252.                 // Reinitialize the pushed response with request's options
  253.                 $ch $pushedResponse->handle;
  254.                 $pushedResponse $pushedResponse->response;
  255.                 $pushedResponse->__construct($multi$url$options$this->logger);
  256.             } else {
  257.                 $this->logger && $this->logger->debug(sprintf('Rejecting pushed response: "%s"'$url));
  258.                 $pushedResponse null;
  259.             }
  260.         }
  261.         if (!$pushedResponse) {
  262.             $ch curl_init();
  263.             $this->logger && $this->logger->info(sprintf('Request: "%s %s"'$method$url));
  264.             $curlopts += [\CURLOPT_SHARE => $multi->share];
  265.         }
  266.         foreach ($curlopts as $opt => $value) {
  267.             if (null !== $value && !curl_setopt($ch$opt$value) && \CURLOPT_CERTINFO !== $opt && (!\defined('CURLOPT_HEADEROPT') || \CURLOPT_HEADEROPT !== $opt)) {
  268.                 $constantName $this->findConstantName($opt);
  269.                 throw new TransportException(sprintf('Curl option "%s" is not supported.'$constantName ?? $opt));
  270.             }
  271.         }
  272.         return $pushedResponse ?? new CurlResponse($multi$ch$options$this->logger$methodself::createRedirectResolver($options$host), CurlClientState::$curlVersion['version_number']);
  273.     }
  274.     /**
  275.      * {@inheritdoc}
  276.      */
  277.     public function stream($responses, ?float $timeout null): ResponseStreamInterface
  278.     {
  279.         if ($responses instanceof CurlResponse) {
  280.             $responses = [$responses];
  281.         } elseif (!is_iterable($responses)) {
  282.             throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of CurlResponse objects, "%s" given.'__METHOD__get_debug_type($responses)));
  283.         }
  284.         $multi $this->ensureState();
  285.         if (\is_resource($multi->handle) || $multi->handle instanceof \CurlMultiHandle) {
  286.             $active 0;
  287.             while (\CURLM_CALL_MULTI_PERFORM === curl_multi_exec($multi->handle$active)) {
  288.             }
  289.         }
  290.         return new ResponseStream(CurlResponse::stream($responses$timeout));
  291.     }
  292.     public function reset()
  293.     {
  294.         if (isset($this->multi)) {
  295.             $this->multi->reset();
  296.         }
  297.     }
  298.     /**
  299.      * Accepts pushed responses only if their headers related to authentication match the request.
  300.      */
  301.     private static function acceptPushForRequest(string $method, array $optionsPushedResponse $pushedResponse): bool
  302.     {
  303.         if ('' !== $options['body'] || $method !== $pushedResponse->requestHeaders[':method'][0]) {
  304.             return false;
  305.         }
  306.         foreach (['proxy''no_proxy''bindto''local_cert''local_pk'] as $k) {
  307.             if ($options[$k] !== $pushedResponse->parentOptions[$k]) {
  308.                 return false;
  309.             }
  310.         }
  311.         foreach (['authorization''cookie''range''proxy-authorization'] as $k) {
  312.             $normalizedHeaders $options['normalized_headers'][$k] ?? [];
  313.             foreach ($normalizedHeaders as $i => $v) {
  314.                 $normalizedHeaders[$i] = substr($v\strlen($k) + 2);
  315.             }
  316.             if (($pushedResponse->requestHeaders[$k] ?? []) !== $normalizedHeaders) {
  317.                 return false;
  318.             }
  319.         }
  320.         return true;
  321.     }
  322.     /**
  323.      * Wraps the request's body callback to allow it to return strings longer than curl requested.
  324.      */
  325.     private static function readRequestBody(int $length\Closure $bodystring &$bufferbool &$eof): string
  326.     {
  327.         if (!$eof && \strlen($buffer) < $length) {
  328.             if (!\is_string($data $body($length))) {
  329.                 throw new TransportException(sprintf('The return value of the "body" option callback must be a string, "%s" returned.'get_debug_type($data)));
  330.             }
  331.             $buffer .= $data;
  332.             $eof '' === $data;
  333.         }
  334.         $data substr($buffer0$length);
  335.         $buffer substr($buffer$length);
  336.         return $data;
  337.     }
  338.     /**
  339.      * Resolves relative URLs on redirects and deals with authentication headers.
  340.      *
  341.      * Work around CVE-2018-1000007: Authorization and Cookie headers should not follow redirects - fixed in Curl 7.64
  342.      */
  343.     private static function createRedirectResolver(array $optionsstring $host): \Closure
  344.     {
  345.         $redirectHeaders = [];
  346.         if ($options['max_redirects']) {
  347.             $redirectHeaders['host'] = $host;
  348.             $redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
  349.                 return !== stripos($h'Host:');
  350.             });
  351.             if (isset($options['normalized_headers']['authorization'][0]) || isset($options['normalized_headers']['cookie'][0])) {
  352.                 $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
  353.                     return !== stripos($h'Authorization:') && !== stripos($h'Cookie:');
  354.                 });
  355.             }
  356.         }
  357.         return static function ($chstring $locationbool $noContent) use (&$redirectHeaders$options) {
  358.             try {
  359.                 $location self::parseUrl($location);
  360.             } catch (InvalidArgumentException $e) {
  361.                 return null;
  362.             }
  363.             if ($noContent && $redirectHeaders) {
  364.                 $filterContentHeaders = static function ($h) {
  365.                     return !== stripos($h'Content-Length:') && !== stripos($h'Content-Type:') && !== stripos($h'Transfer-Encoding:');
  366.                 };
  367.                 $redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], $filterContentHeaders);
  368.                 $redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders);
  369.             }
  370.             if ($redirectHeaders && $host parse_url('http:'.$location['authority'], \PHP_URL_HOST)) {
  371.                 $requestHeaders $redirectHeaders['host'] === $host $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
  372.                 curl_setopt($ch\CURLOPT_HTTPHEADER$requestHeaders);
  373.             } elseif ($noContent && $redirectHeaders) {
  374.                 curl_setopt($ch\CURLOPT_HTTPHEADER$redirectHeaders['with_auth']);
  375.             }
  376.             $url self::parseUrl(curl_getinfo($ch\CURLINFO_EFFECTIVE_URL));
  377.             $url self::resolveUrl($location$url);
  378.             curl_setopt($ch\CURLOPT_PROXYself::getProxyUrl($options['proxy'], $url));
  379.             return implode(''$url);
  380.         };
  381.     }
  382.     private function ensureState(): CurlClientState
  383.     {
  384.         if (!isset($this->multi)) {
  385.             $this->multi = new CurlClientState($this->maxHostConnections$this->maxPendingPushes);
  386.             $this->multi->logger $this->logger;
  387.         }
  388.         return $this->multi;
  389.     }
  390.     private function findConstantName(int $opt): ?string
  391.     {
  392.         $constants array_filter(get_defined_constants(), static function ($v$k) use ($opt) {
  393.             return $v === $opt && 'C' === $k[0] && (str_starts_with($k'CURLOPT_') || str_starts_with($k'CURLINFO_'));
  394.         }, \ARRAY_FILTER_USE_BOTH);
  395.         return key($constants);
  396.     }
  397.     /**
  398.      * Prevents overriding options that are set internally throughout the request.
  399.      */
  400.     private function validateExtraCurlOptions(array $options): void
  401.     {
  402.         $curloptsToConfig = [
  403.             // options used in CurlHttpClient
  404.             \CURLOPT_HTTPAUTH => 'auth_ntlm',
  405.             \CURLOPT_USERPWD => 'auth_ntlm',
  406.             \CURLOPT_RESOLVE => 'resolve',
  407.             \CURLOPT_NOSIGNAL => 'timeout',
  408.             \CURLOPT_HTTPHEADER => 'headers',
  409.             \CURLOPT_INFILE => 'body',
  410.             \CURLOPT_READFUNCTION => 'body',
  411.             \CURLOPT_INFILESIZE => 'body',
  412.             \CURLOPT_POSTFIELDS => 'body',
  413.             \CURLOPT_UPLOAD => 'body',
  414.             \CURLOPT_INTERFACE => 'bindto',
  415.             \CURLOPT_TIMEOUT_MS => 'max_duration',
  416.             \CURLOPT_TIMEOUT => 'max_duration',
  417.             \CURLOPT_MAXREDIRS => 'max_redirects',
  418.             \CURLOPT_POSTREDIR => 'max_redirects',
  419.             \CURLOPT_PROXY => 'proxy',
  420.             \CURLOPT_NOPROXY => 'no_proxy',
  421.             \CURLOPT_SSL_VERIFYPEER => 'verify_peer',
  422.             \CURLOPT_SSL_VERIFYHOST => 'verify_host',
  423.             \CURLOPT_CAINFO => 'cafile',
  424.             \CURLOPT_CAPATH => 'capath',
  425.             \CURLOPT_SSL_CIPHER_LIST => 'ciphers',
  426.             \CURLOPT_SSLCERT => 'local_cert',
  427.             \CURLOPT_SSLKEY => 'local_pk',
  428.             \CURLOPT_KEYPASSWD => 'passphrase',
  429.             \CURLOPT_CERTINFO => 'capture_peer_cert_chain',
  430.             \CURLOPT_USERAGENT => 'normalized_headers',
  431.             \CURLOPT_REFERER => 'headers',
  432.             // options used in CurlResponse
  433.             \CURLOPT_NOPROGRESS => 'on_progress',
  434.             \CURLOPT_PROGRESSFUNCTION => 'on_progress',
  435.         ];
  436.         if (\defined('CURLOPT_UNIX_SOCKET_PATH')) {
  437.             $curloptsToConfig[\CURLOPT_UNIX_SOCKET_PATH] = 'bindto';
  438.         }
  439.         if (\defined('CURLOPT_PINNEDPUBLICKEY')) {
  440.             $curloptsToConfig[\CURLOPT_PINNEDPUBLICKEY] = 'peer_fingerprint';
  441.         }
  442.         $curloptsToCheck = [
  443.             \CURLOPT_PRIVATE,
  444.             \CURLOPT_HEADERFUNCTION,
  445.             \CURLOPT_WRITEFUNCTION,
  446.             \CURLOPT_VERBOSE,
  447.             \CURLOPT_STDERR,
  448.             \CURLOPT_RETURNTRANSFER,
  449.             \CURLOPT_URL,
  450.             \CURLOPT_FOLLOWLOCATION,
  451.             \CURLOPT_HEADER,
  452.             \CURLOPT_CONNECTTIMEOUT,
  453.             \CURLOPT_CONNECTTIMEOUT_MS,
  454.             \CURLOPT_HTTP_VERSION,
  455.             \CURLOPT_PORT,
  456.             \CURLOPT_DNS_USE_GLOBAL_CACHE,
  457.             \CURLOPT_PROTOCOLS,
  458.             \CURLOPT_REDIR_PROTOCOLS,
  459.             \CURLOPT_COOKIEFILE,
  460.             \CURLINFO_REDIRECT_COUNT,
  461.         ];
  462.         if (\defined('CURLOPT_HTTP09_ALLOWED')) {
  463.             $curloptsToCheck[] = \CURLOPT_HTTP09_ALLOWED;
  464.         }
  465.         if (\defined('CURLOPT_HEADEROPT')) {
  466.             $curloptsToCheck[] = \CURLOPT_HEADEROPT;
  467.         }
  468.         $methodOpts = [
  469.             \CURLOPT_POST,
  470.             \CURLOPT_PUT,
  471.             \CURLOPT_CUSTOMREQUEST,
  472.             \CURLOPT_HTTPGET,
  473.             \CURLOPT_NOBODY,
  474.         ];
  475.         foreach ($options as $opt => $optValue) {
  476.             if (isset($curloptsToConfig[$opt])) {
  477.                 $constName $this->findConstantName($opt) ?? $opt;
  478.                 throw new InvalidArgumentException(sprintf('Cannot set "%s" with "extra.curl", use option "%s" instead.'$constName$curloptsToConfig[$opt]));
  479.             }
  480.             if (\in_array($opt$methodOpts)) {
  481.                 throw new InvalidArgumentException('The HTTP method cannot be overridden using "extra.curl".');
  482.             }
  483.             if (\in_array($opt$curloptsToCheck)) {
  484.                 $constName $this->findConstantName($opt) ?? $opt;
  485.                 throw new InvalidArgumentException(sprintf('Cannot set "%s" with "extra.curl".'$constName));
  486.             }
  487.         }
  488.     }
  489. }