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

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