vendor/guzzlehttp/guzzle/src/Handler/CurlFactory.php line 150

Open in your IDE?
  1. <?php
  2. namespace GuzzleHttp\Handler;
  3. use GuzzleHttp\Exception\ConnectException;
  4. use GuzzleHttp\Exception\RequestException;
  5. use GuzzleHttp\Promise as P;
  6. use GuzzleHttp\Promise\FulfilledPromise;
  7. use GuzzleHttp\Promise\PromiseInterface;
  8. use GuzzleHttp\Psr7\LazyOpenStream;
  9. use GuzzleHttp\TransferStats;
  10. use GuzzleHttp\Utils;
  11. use Psr\Http\Message\RequestInterface;
  12. use Psr\Http\Message\UriInterface;
  13. /**
  14.  * Creates curl resources from a request
  15.  *
  16.  * @final
  17.  */
  18. class CurlFactory implements CurlFactoryInterface
  19. {
  20.     public const CURL_VERSION_STR 'curl_version';
  21.     /**
  22.      * @deprecated
  23.      */
  24.     public const LOW_CURL_VERSION_NUMBER '7.21.2';
  25.     /**
  26.      * @var resource[]|\CurlHandle[]
  27.      */
  28.     private $handles = [];
  29.     /**
  30.      * @var int Total number of idle handles to keep in cache
  31.      */
  32.     private $maxHandles;
  33.     /**
  34.      * @param int $maxHandles Maximum number of idle handles.
  35.      */
  36.     public function __construct(int $maxHandles)
  37.     {
  38.         $this->maxHandles $maxHandles;
  39.     }
  40.     public function create(RequestInterface $request, array $options): EasyHandle
  41.     {
  42.         $protocolVersion $request->getProtocolVersion();
  43.         if ('2' === $protocolVersion || '2.0' === $protocolVersion) {
  44.             if (!self::supportsHttp2()) {
  45.                 throw new ConnectException('HTTP/2 is supported by the cURL handler, however libcurl is built without HTTP/2 support.'$request);
  46.             }
  47.         } elseif ('1.0' !== $protocolVersion && '1.1' !== $protocolVersion) {
  48.             throw new ConnectException(sprintf('HTTP/%s is not supported by the cURL handler.'$protocolVersion), $request);
  49.         }
  50.         if (isset($options['curl']['body_as_string'])) {
  51.             $options['_body_as_string'] = $options['curl']['body_as_string'];
  52.             unset($options['curl']['body_as_string']);
  53.         }
  54.         $easy = new EasyHandle();
  55.         $easy->request $request;
  56.         $easy->options $options;
  57.         $conf $this->getDefaultConf($easy);
  58.         $this->applyMethod($easy$conf);
  59.         $this->applyHandlerOptions($easy$conf);
  60.         $this->applyHeaders($easy$conf);
  61.         unset($conf['_headers']);
  62.         // Add handler options from the request configuration options
  63.         if (isset($options['curl'])) {
  64.             $conf \array_replace($conf$options['curl']);
  65.         }
  66.         $conf[\CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy);
  67.         $easy->handle $this->handles \array_pop($this->handles) : \curl_init();
  68.         curl_setopt_array($easy->handle$conf);
  69.         return $easy;
  70.     }
  71.     private static function supportsHttp2(): bool
  72.     {
  73.         static $supportsHttp2 null;
  74.         if (null === $supportsHttp2) {
  75.             $supportsHttp2 self::supportsTls12()
  76.                 && defined('CURL_VERSION_HTTP2')
  77.                 && (\CURL_VERSION_HTTP2 \curl_version()['features']);
  78.         }
  79.         return $supportsHttp2;
  80.     }
  81.     private static function supportsTls12(): bool
  82.     {
  83.         static $supportsTls12 null;
  84.         if (null === $supportsTls12) {
  85.             $supportsTls12 \CURL_SSLVERSION_TLSv1_2 \curl_version()['features'];
  86.         }
  87.         return $supportsTls12;
  88.     }
  89.     private static function supportsTls13(): bool
  90.     {
  91.         static $supportsTls13 null;
  92.         if (null === $supportsTls13) {
  93.             $supportsTls13 defined('CURL_SSLVERSION_TLSv1_3')
  94.                 && (\CURL_SSLVERSION_TLSv1_3 \curl_version()['features']);
  95.         }
  96.         return $supportsTls13;
  97.     }
  98.     public function release(EasyHandle $easy): void
  99.     {
  100.         $resource $easy->handle;
  101.         unset($easy->handle);
  102.         if (\count($this->handles) >= $this->maxHandles) {
  103.             \curl_close($resource);
  104.         } else {
  105.             // Remove all callback functions as they can hold onto references
  106.             // and are not cleaned up by curl_reset. Using curl_setopt_array
  107.             // does not work for some reason, so removing each one
  108.             // individually.
  109.             \curl_setopt($resource\CURLOPT_HEADERFUNCTIONnull);
  110.             \curl_setopt($resource\CURLOPT_READFUNCTIONnull);
  111.             \curl_setopt($resource\CURLOPT_WRITEFUNCTIONnull);
  112.             \curl_setopt($resource\CURLOPT_PROGRESSFUNCTIONnull);
  113.             \curl_reset($resource);
  114.             $this->handles[] = $resource;
  115.         }
  116.     }
  117.     /**
  118.      * Completes a cURL transaction, either returning a response promise or a
  119.      * rejected promise.
  120.      *
  121.      * @param callable(RequestInterface, array): PromiseInterface $handler
  122.      * @param CurlFactoryInterface                                $factory Dictates how the handle is released
  123.      */
  124.     public static function finish(callable $handlerEasyHandle $easyCurlFactoryInterface $factory): PromiseInterface
  125.     {
  126.         if (isset($easy->options['on_stats'])) {
  127.             self::invokeStats($easy);
  128.         }
  129.         if (!$easy->response || $easy->errno) {
  130.             return self::finishError($handler$easy$factory);
  131.         }
  132.         // Return the response if it is present and there is no error.
  133.         $factory->release($easy);
  134.         // Rewind the body of the response if possible.
  135.         $body $easy->response->getBody();
  136.         if ($body->isSeekable()) {
  137.             $body->rewind();
  138.         }
  139.         return new FulfilledPromise($easy->response);
  140.     }
  141.     private static function invokeStats(EasyHandle $easy): void
  142.     {
  143.         $curlStats \curl_getinfo($easy->handle);
  144.         $curlStats['appconnect_time'] = \curl_getinfo($easy->handle\CURLINFO_APPCONNECT_TIME);
  145.         $stats = new TransferStats(
  146.             $easy->request,
  147.             $easy->response,
  148.             $curlStats['total_time'],
  149.             $easy->errno,
  150.             $curlStats
  151.         );
  152.         ($easy->options['on_stats'])($stats);
  153.     }
  154.     /**
  155.      * @param callable(RequestInterface, array): PromiseInterface $handler
  156.      */
  157.     private static function finishError(callable $handlerEasyHandle $easyCurlFactoryInterface $factory): PromiseInterface
  158.     {
  159.         // Get error information and release the handle to the factory.
  160.         $ctx = [
  161.             'errno' => $easy->errno,
  162.             'error' => \curl_error($easy->handle),
  163.             'appconnect_time' => \curl_getinfo($easy->handle\CURLINFO_APPCONNECT_TIME),
  164.         ] + \curl_getinfo($easy->handle);
  165.         $ctx[self::CURL_VERSION_STR] = self::getCurlVersion();
  166.         $factory->release($easy);
  167.         // Retry when nothing is present or when curl failed to rewind.
  168.         if (empty($easy->options['_err_message']) && (!$easy->errno || $easy->errno == 65)) {
  169.             return self::retryFailedRewind($handler$easy$ctx);
  170.         }
  171.         return self::createRejection($easy$ctx);
  172.     }
  173.     private static function getCurlVersion(): string
  174.     {
  175.         static $curlVersion null;
  176.         if (null === $curlVersion) {
  177.             $curlVersion \curl_version()['version'];
  178.         }
  179.         return $curlVersion;
  180.     }
  181.     private static function createRejection(EasyHandle $easy, array $ctx): PromiseInterface
  182.     {
  183.         static $connectionErrors = [
  184.             \CURLE_OPERATION_TIMEOUTED => true,
  185.             \CURLE_COULDNT_RESOLVE_HOST => true,
  186.             \CURLE_COULDNT_CONNECT => true,
  187.             \CURLE_SSL_CONNECT_ERROR => true,
  188.             \CURLE_GOT_NOTHING => true,
  189.         ];
  190.         if ($easy->createResponseException) {
  191.             return P\Create::rejectionFor(
  192.                 new RequestException(
  193.                     'An error was encountered while creating the response',
  194.                     $easy->request,
  195.                     $easy->response,
  196.                     $easy->createResponseException,
  197.                     $ctx
  198.                 )
  199.             );
  200.         }
  201.         // If an exception was encountered during the onHeaders event, then
  202.         // return a rejected promise that wraps that exception.
  203.         if ($easy->onHeadersException) {
  204.             return P\Create::rejectionFor(
  205.                 new RequestException(
  206.                     'An error was encountered during the on_headers event',
  207.                     $easy->request,
  208.                     $easy->response,
  209.                     $easy->onHeadersException,
  210.                     $ctx
  211.                 )
  212.             );
  213.         }
  214.         $uri $easy->request->getUri();
  215.         $sanitizedError self::sanitizeCurlError($ctx['error'] ?? ''$uri);
  216.         $message \sprintf(
  217.             'cURL error %s: %s (%s)',
  218.             $ctx['errno'],
  219.             $sanitizedError,
  220.             'see https://curl.haxx.se/libcurl/c/libcurl-errors.html'
  221.         );
  222.         if ('' !== $sanitizedError) {
  223.             $redactedUriString \GuzzleHttp\Psr7\Utils::redactUserInfo($uri)->__toString();
  224.             if ($redactedUriString !== '' && false === \strpos($sanitizedError$redactedUriString)) {
  225.                 $message .= \sprintf(' for %s'$redactedUriString);
  226.             }
  227.         }
  228.         // Create a connection exception if it was a specific error code.
  229.         $error = isset($connectionErrors[$easy->errno])
  230.             ? new ConnectException($message$easy->requestnull$ctx)
  231.             : new RequestException($message$easy->request$easy->responsenull$ctx);
  232.         return P\Create::rejectionFor($error);
  233.     }
  234.     private static function sanitizeCurlError(string $errorUriInterface $uri): string
  235.     {
  236.         if ('' === $error) {
  237.             return $error;
  238.         }
  239.         $baseUri $uri->withQuery('')->withFragment('');
  240.         $baseUriString $baseUri->__toString();
  241.         if ('' === $baseUriString) {
  242.             return $error;
  243.         }
  244.         $redactedUriString \GuzzleHttp\Psr7\Utils::redactUserInfo($baseUri)->__toString();
  245.         return str_replace($baseUriString$redactedUriString$error);
  246.     }
  247.     /**
  248.      * @return array<int|string, mixed>
  249.      */
  250.     private function getDefaultConf(EasyHandle $easy): array
  251.     {
  252.         $conf = [
  253.             '_headers' => $easy->request->getHeaders(),
  254.             \CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(),
  255.             \CURLOPT_URL => (string) $easy->request->getUri()->withFragment(''),
  256.             \CURLOPT_RETURNTRANSFER => false,
  257.             \CURLOPT_HEADER => false,
  258.             \CURLOPT_CONNECTTIMEOUT => 300,
  259.         ];
  260.         if (\defined('CURLOPT_PROTOCOLS')) {
  261.             $conf[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTP \CURLPROTO_HTTPS;
  262.         }
  263.         $version $easy->request->getProtocolVersion();
  264.         if ('2' === $version || '2.0' === $version) {
  265.             $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0;
  266.         } elseif ('1.1' === $version) {
  267.             $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
  268.         } else {
  269.             $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0;
  270.         }
  271.         return $conf;
  272.     }
  273.     private function applyMethod(EasyHandle $easy, array &$conf): void
  274.     {
  275.         $body $easy->request->getBody();
  276.         $size $body->getSize();
  277.         if ($size === null || $size 0) {
  278.             $this->applyBody($easy->request$easy->options$conf);
  279.             return;
  280.         }
  281.         $method $easy->request->getMethod();
  282.         if ($method === 'PUT' || $method === 'POST') {
  283.             // See https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2
  284.             if (!$easy->request->hasHeader('Content-Length')) {
  285.                 $conf[\CURLOPT_HTTPHEADER][] = 'Content-Length: 0';
  286.             }
  287.         } elseif ($method === 'HEAD') {
  288.             $conf[\CURLOPT_NOBODY] = true;
  289.             unset(
  290.                 $conf[\CURLOPT_WRITEFUNCTION],
  291.                 $conf[\CURLOPT_READFUNCTION],
  292.                 $conf[\CURLOPT_FILE],
  293.                 $conf[\CURLOPT_INFILE]
  294.             );
  295.         }
  296.     }
  297.     private function applyBody(RequestInterface $request, array $options, array &$conf): void
  298.     {
  299.         $size $request->hasHeader('Content-Length')
  300.             ? (int) $request->getHeaderLine('Content-Length')
  301.             : null;
  302.         // Send the body as a string if the size is less than 1MB OR if the
  303.         // [curl][body_as_string] request value is set.
  304.         if (($size !== null && $size 1000000) || !empty($options['_body_as_string'])) {
  305.             $conf[\CURLOPT_POSTFIELDS] = (string) $request->getBody();
  306.             // Don't duplicate the Content-Length header
  307.             $this->removeHeader('Content-Length'$conf);
  308.             $this->removeHeader('Transfer-Encoding'$conf);
  309.         } else {
  310.             $conf[\CURLOPT_UPLOAD] = true;
  311.             if ($size !== null) {
  312.                 $conf[\CURLOPT_INFILESIZE] = $size;
  313.                 $this->removeHeader('Content-Length'$conf);
  314.             }
  315.             $body $request->getBody();
  316.             if ($body->isSeekable()) {
  317.                 $body->rewind();
  318.             }
  319.             $conf[\CURLOPT_READFUNCTION] = static function ($ch$fd$length) use ($body) {
  320.                 return $body->read($length);
  321.             };
  322.         }
  323.         // If the Expect header is not present, prevent curl from adding it
  324.         if (!$request->hasHeader('Expect')) {
  325.             $conf[\CURLOPT_HTTPHEADER][] = 'Expect:';
  326.         }
  327.         // cURL sometimes adds a content-type by default. Prevent this.
  328.         if (!$request->hasHeader('Content-Type')) {
  329.             $conf[\CURLOPT_HTTPHEADER][] = 'Content-Type:';
  330.         }
  331.     }
  332.     private function applyHeaders(EasyHandle $easy, array &$conf): void
  333.     {
  334.         foreach ($conf['_headers'] as $name => $values) {
  335.             foreach ($values as $value) {
  336.                 $value = (string) $value;
  337.                 if ($value === '') {
  338.                     // cURL requires a special format for empty headers.
  339.                     // See https://github.com/guzzle/guzzle/issues/1882 for more details.
  340.                     $conf[\CURLOPT_HTTPHEADER][] = "$name;";
  341.                 } else {
  342.                     $conf[\CURLOPT_HTTPHEADER][] = "$name$value";
  343.                 }
  344.             }
  345.         }
  346.         // Remove the Accept header if one was not set
  347.         if (!$easy->request->hasHeader('Accept')) {
  348.             $conf[\CURLOPT_HTTPHEADER][] = 'Accept:';
  349.         }
  350.     }
  351.     /**
  352.      * Remove a header from the options array.
  353.      *
  354.      * @param string $name    Case-insensitive header to remove
  355.      * @param array  $options Array of options to modify
  356.      */
  357.     private function removeHeader(string $name, array &$options): void
  358.     {
  359.         foreach (\array_keys($options['_headers']) as $key) {
  360.             if (!\strcasecmp($key$name)) {
  361.                 unset($options['_headers'][$key]);
  362.                 return;
  363.             }
  364.         }
  365.     }
  366.     private function applyHandlerOptions(EasyHandle $easy, array &$conf): void
  367.     {
  368.         $options $easy->options;
  369.         if (isset($options['verify'])) {
  370.             if ($options['verify'] === false) {
  371.                 unset($conf[\CURLOPT_CAINFO]);
  372.                 $conf[\CURLOPT_SSL_VERIFYHOST] = 0;
  373.                 $conf[\CURLOPT_SSL_VERIFYPEER] = false;
  374.             } else {
  375.                 $conf[\CURLOPT_SSL_VERIFYHOST] = 2;
  376.                 $conf[\CURLOPT_SSL_VERIFYPEER] = true;
  377.                 if (\is_string($options['verify'])) {
  378.                     // Throw an error if the file/folder/link path is not valid or doesn't exist.
  379.                     if (!\file_exists($options['verify'])) {
  380.                         throw new \InvalidArgumentException("SSL CA bundle not found: {$options['verify']}");
  381.                     }
  382.                     // If it's a directory or a link to a directory use CURLOPT_CAPATH.
  383.                     // If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO.
  384.                     if (
  385.                         \is_dir($options['verify'])
  386.                         || (
  387.                             \is_link($options['verify']) === true
  388.                             && ($verifyLink \readlink($options['verify'])) !== false
  389.                             && \is_dir($verifyLink)
  390.                         )
  391.                     ) {
  392.                         $conf[\CURLOPT_CAPATH] = $options['verify'];
  393.                     } else {
  394.                         $conf[\CURLOPT_CAINFO] = $options['verify'];
  395.                     }
  396.                 }
  397.             }
  398.         }
  399.         if (!isset($options['curl'][\CURLOPT_ENCODING]) && !empty($options['decode_content'])) {
  400.             $accept $easy->request->getHeaderLine('Accept-Encoding');
  401.             if ($accept) {
  402.                 $conf[\CURLOPT_ENCODING] = $accept;
  403.             } else {
  404.                 // The empty string enables all available decoders and implicitly
  405.                 // sets a matching 'Accept-Encoding' header.
  406.                 $conf[\CURLOPT_ENCODING] = '';
  407.                 // But as the user did not specify any encoding preference,
  408.                 // let's leave it up to server by preventing curl from sending
  409.                 // the header, which will be interpreted as 'Accept-Encoding: *'.
  410.                 // https://www.rfc-editor.org/rfc/rfc9110#field.accept-encoding
  411.                 $conf[\CURLOPT_HTTPHEADER][] = 'Accept-Encoding:';
  412.             }
  413.         }
  414.         if (!isset($options['sink'])) {
  415.             // Use a default temp stream if no sink was set.
  416.             $options['sink'] = \GuzzleHttp\Psr7\Utils::tryFopen('php://temp''w+');
  417.         }
  418.         $sink $options['sink'];
  419.         if (!\is_string($sink)) {
  420.             $sink \GuzzleHttp\Psr7\Utils::streamFor($sink);
  421.         } elseif (!\is_dir(\dirname($sink))) {
  422.             // Ensure that the directory exists before failing in curl.
  423.             throw new \RuntimeException(\sprintf('Directory %s does not exist for sink value of %s'\dirname($sink), $sink));
  424.         } else {
  425.             $sink = new LazyOpenStream($sink'w+');
  426.         }
  427.         $easy->sink $sink;
  428.         $conf[\CURLOPT_WRITEFUNCTION] = static function ($ch$write) use ($sink): int {
  429.             return $sink->write($write);
  430.         };
  431.         $timeoutRequiresNoSignal false;
  432.         if (isset($options['timeout'])) {
  433.             $timeoutRequiresNoSignal |= $options['timeout'] < 1;
  434.             $conf[\CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000;
  435.         }
  436.         // CURL default value is CURL_IPRESOLVE_WHATEVER
  437.         if (isset($options['force_ip_resolve'])) {
  438.             if ('v4' === $options['force_ip_resolve']) {
  439.                 $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4;
  440.             } elseif ('v6' === $options['force_ip_resolve']) {
  441.                 $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V6;
  442.             }
  443.         }
  444.         if (isset($options['connect_timeout'])) {
  445.             $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1;
  446.             $conf[\CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000;
  447.         }
  448.         if ($timeoutRequiresNoSignal && \strtoupper(\substr(\PHP_OS03)) !== 'WIN') {
  449.             $conf[\CURLOPT_NOSIGNAL] = true;
  450.         }
  451.         if (isset($options['proxy'])) {
  452.             if (!\is_array($options['proxy'])) {
  453.                 $conf[\CURLOPT_PROXY] = $options['proxy'];
  454.             } else {
  455.                 $scheme $easy->request->getUri()->getScheme();
  456.                 if (isset($options['proxy'][$scheme])) {
  457.                     $host $easy->request->getUri()->getHost();
  458.                     if (isset($options['proxy']['no']) && Utils::isHostInNoProxy($host$options['proxy']['no'])) {
  459.                         unset($conf[\CURLOPT_PROXY]);
  460.                     } else {
  461.                         $conf[\CURLOPT_PROXY] = $options['proxy'][$scheme];
  462.                     }
  463.                 }
  464.             }
  465.         }
  466.         if (isset($options['crypto_method'])) {
  467.             $protocolVersion $easy->request->getProtocolVersion();
  468.             // If HTTP/2, upgrade TLS 1.0 and 1.1 to 1.2
  469.             if ('2' === $protocolVersion || '2.0' === $protocolVersion) {
  470.                 if (
  471.                     \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method']
  472.                     || \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method']
  473.                     || \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']
  474.                 ) {
  475.                     $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2;
  476.                 } elseif (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) {
  477.                     if (!self::supportsTls13()) {
  478.                         throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL');
  479.                     }
  480.                     $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3;
  481.                 } else {
  482.                     throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided');
  483.                 }
  484.             } elseif (\STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method']) {
  485.                 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_0;
  486.             } elseif (\STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method']) {
  487.                 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_1;
  488.             } elseif (\STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']) {
  489.                 if (!self::supportsTls12()) {
  490.                     throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.2 not supported by your version of cURL');
  491.                 }
  492.                 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2;
  493.             } elseif (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) {
  494.                 if (!self::supportsTls13()) {
  495.                     throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL');
  496.                 }
  497.                 $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3;
  498.             } else {
  499.                 throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided');
  500.             }
  501.         }
  502.         if (isset($options['cert'])) {
  503.             $cert $options['cert'];
  504.             if (\is_array($cert)) {
  505.                 $conf[\CURLOPT_SSLCERTPASSWD] = $cert[1];
  506.                 $cert $cert[0];
  507.             }
  508.             if (!\file_exists($cert)) {
  509.                 throw new \InvalidArgumentException("SSL certificate not found: {$cert}");
  510.             }
  511.             // OpenSSL (versions 0.9.3 and later) also support "P12" for PKCS#12-encoded files.
  512.             // see https://curl.se/libcurl/c/CURLOPT_SSLCERTTYPE.html
  513.             $ext pathinfo($cert\PATHINFO_EXTENSION);
  514.             if (preg_match('#^(der|p12)$#i'$ext)) {
  515.                 $conf[\CURLOPT_SSLCERTTYPE] = strtoupper($ext);
  516.             }
  517.             $conf[\CURLOPT_SSLCERT] = $cert;
  518.         }
  519.         if (isset($options['ssl_key'])) {
  520.             if (\is_array($options['ssl_key'])) {
  521.                 if (\count($options['ssl_key']) === 2) {
  522.                     [$sslKey$conf[\CURLOPT_SSLKEYPASSWD]] = $options['ssl_key'];
  523.                 } else {
  524.                     [$sslKey] = $options['ssl_key'];
  525.                 }
  526.             }
  527.             $sslKey $sslKey ?? $options['ssl_key'];
  528.             if (!\file_exists($sslKey)) {
  529.                 throw new \InvalidArgumentException("SSL private key not found: {$sslKey}");
  530.             }
  531.             $conf[\CURLOPT_SSLKEY] = $sslKey;
  532.         }
  533.         if (isset($options['progress'])) {
  534.             $progress $options['progress'];
  535.             if (!\is_callable($progress)) {
  536.                 throw new \InvalidArgumentException('progress client option must be callable');
  537.             }
  538.             $conf[\CURLOPT_NOPROGRESS] = false;
  539.             $conf[\CURLOPT_PROGRESSFUNCTION] = static function ($resourceint $downloadSizeint $downloadedint $uploadSizeint $uploaded) use ($progress) {
  540.                 $progress($downloadSize$downloaded$uploadSize$uploaded);
  541.             };
  542.         }
  543.         if (!empty($options['debug'])) {
  544.             $conf[\CURLOPT_STDERR] = Utils::debugResource($options['debug']);
  545.             $conf[\CURLOPT_VERBOSE] = true;
  546.         }
  547.     }
  548.     /**
  549.      * This function ensures that a response was set on a transaction. If one
  550.      * was not set, then the request is retried if possible. This error
  551.      * typically means you are sending a payload, curl encountered a
  552.      * "Connection died, retrying a fresh connect" error, tried to rewind the
  553.      * stream, and then encountered a "necessary data rewind wasn't possible"
  554.      * error, causing the request to be sent through curl_multi_info_read()
  555.      * without an error status.
  556.      *
  557.      * @param callable(RequestInterface, array): PromiseInterface $handler
  558.      */
  559.     private static function retryFailedRewind(callable $handlerEasyHandle $easy, array $ctx): PromiseInterface
  560.     {
  561.         try {
  562.             // Only rewind if the body has been read from.
  563.             $body $easy->request->getBody();
  564.             if ($body->tell() > 0) {
  565.                 $body->rewind();
  566.             }
  567.         } catch (\RuntimeException $e) {
  568.             $ctx['error'] = 'The connection unexpectedly failed without '
  569.                 .'providing an error. The request would have been retried, '
  570.                 .'but attempting to rewind the request body failed. '
  571.                 .'Exception: '.$e;
  572.             return self::createRejection($easy$ctx);
  573.         }
  574.         // Retry no more than 3 times before giving up.
  575.         if (!isset($easy->options['_curl_retries'])) {
  576.             $easy->options['_curl_retries'] = 1;
  577.         } elseif ($easy->options['_curl_retries'] == 2) {
  578.             $ctx['error'] = 'The cURL request was retried 3 times '
  579.                 .'and did not succeed. The most likely reason for the failure '
  580.                 .'is that cURL was unable to rewind the body of the request '
  581.                 .'and subsequent retries resulted in the same error. Turn on '
  582.                 .'the debug option to see what went wrong. See '
  583.                 .'https://bugs.php.net/bug.php?id=47204 for more information.';
  584.             return self::createRejection($easy$ctx);
  585.         } else {
  586.             ++$easy->options['_curl_retries'];
  587.         }
  588.         return $handler($easy->request$easy->options);
  589.     }
  590.     private function createHeaderFn(EasyHandle $easy): callable
  591.     {
  592.         if (isset($easy->options['on_headers'])) {
  593.             $onHeaders $easy->options['on_headers'];
  594.             if (!\is_callable($onHeaders)) {
  595.                 throw new \InvalidArgumentException('on_headers must be callable');
  596.             }
  597.         } else {
  598.             $onHeaders null;
  599.         }
  600.         return static function ($ch$h) use (
  601.             $onHeaders,
  602.             $easy,
  603.             &$startingResponse
  604.         ) {
  605.             $value \trim($h);
  606.             if ($value === '') {
  607.                 $startingResponse true;
  608.                 try {
  609.                     $easy->createResponse();
  610.                 } catch (\Exception $e) {
  611.                     $easy->createResponseException $e;
  612.                     return -1;
  613.                 }
  614.                 if ($onHeaders !== null) {
  615.                     try {
  616.                         $onHeaders($easy->response);
  617.                     } catch (\Exception $e) {
  618.                         // Associate the exception with the handle and trigger
  619.                         // a curl header write error by returning 0.
  620.                         $easy->onHeadersException $e;
  621.                         return -1;
  622.                     }
  623.                 }
  624.             } elseif ($startingResponse) {
  625.                 $startingResponse false;
  626.                 $easy->headers = [$value];
  627.             } else {
  628.                 $easy->headers[] = $value;
  629.             }
  630.             return \strlen($h);
  631.         };
  632.     }
  633.     public function __destruct()
  634.     {
  635.         foreach ($this->handles as $id => $handle) {
  636.             \curl_close($handle);
  637.             unset($this->handles[$id]);
  638.         }
  639.     }
  640. }