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

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