vendor/guzzlehttp/ringphp/src/Client/CurlFactory.php line 95

Open in your IDE?
  1. <?php
  2. namespace GuzzleHttp\Ring\Client;
  3. use GuzzleHttp\Ring\Core;
  4. use GuzzleHttp\Ring\Exception\ConnectException;
  5. use GuzzleHttp\Ring\Exception\RingException;
  6. use GuzzleHttp\Stream\LazyOpenStream;
  7. use GuzzleHttp\Stream\StreamInterface;
  8. /**
  9.  * Creates curl resources from a request
  10.  */
  11. class CurlFactory
  12. {
  13.     /**
  14.      * Creates a cURL handle, header resource, and body resource based on a
  15.      * transaction.
  16.      *
  17.      * @param array         $request Request hash
  18.      * @param null|resource $handle  Optionally provide a curl handle to modify
  19.      *
  20.      * @return array Returns an array of the curl handle, headers array, and
  21.      *               response body handle.
  22.      * @throws \RuntimeException when an option cannot be applied
  23.      */
  24.     public function __invoke(array $request$handle null)
  25.     {
  26.         $headers = [];
  27.         $options $this->getDefaultOptions($request$headers);
  28.         $this->applyMethod($request$options);
  29.         if (isset($request['client'])) {
  30.             $this->applyHandlerOptions($request$options);
  31.         }
  32.         $this->applyHeaders($request$options);
  33.         unset($options['_headers']);
  34.         // Add handler options from the request's configuration options
  35.         if (isset($request['client']['curl'])) {
  36.             $options $this->applyCustomCurlOptions(
  37.                 $request['client']['curl'],
  38.                 $options
  39.             );
  40.         }
  41.         if (!$handle) {
  42.             $handle curl_init();
  43.         }
  44.         $body $this->getOutputBody($request$options);
  45.         curl_setopt_array($handle$options);
  46.         return [$handle, &$headers$body];
  47.     }
  48.     /**
  49.      * Creates a response hash from a cURL result.
  50.      *
  51.      * @param callable $handler  Handler that was used.
  52.      * @param array    $request  Request that sent.
  53.      * @param array    $response Response hash to update.
  54.      * @param array    $headers  Headers received during transfer.
  55.      * @param resource $body     Body fopen response.
  56.      *
  57.      * @return array
  58.      */
  59.     public static function createResponse(
  60.         callable $handler,
  61.         array $request,
  62.         array $response,
  63.         array $headers,
  64.         $body
  65.     ) {
  66.         if (isset($response['transfer_stats']['url'])) {
  67.             $response['effective_url'] = $response['transfer_stats']['url'];
  68.         }
  69.         if (!empty($headers)) {
  70.             $startLine explode(' 'array_shift($headers), 3);
  71.             $headerList Core::headersFromLines($headers);
  72.             $response['headers'] = $headerList;
  73.             $response['version'] = isset($startLine[0]) ? substr($startLine[0], 5) : null;
  74.             $response['status'] = isset($startLine[1]) ? (int) $startLine[1] : null;
  75.             $response['reason'] = isset($startLine[2]) ? $startLine[2] : null;
  76.             $response['body'] = $body;
  77.             Core::rewindBody($response);
  78.         }
  79.         return !empty($response['curl']['errno']) || !isset($response['status'])
  80.             ? self::createErrorResponse($handler$request$response)
  81.             : $response;
  82.     }
  83.     private static function createErrorResponse(
  84.         callable $handler,
  85.         array $request,
  86.         array $response
  87.     ) {
  88.         static $connectionErrors = [
  89.             CURLE_OPERATION_TIMEOUTED  => true,
  90.             CURLE_COULDNT_RESOLVE_HOST => true,
  91.             CURLE_COULDNT_CONNECT      => true,
  92.             CURLE_SSL_CONNECT_ERROR    => true,
  93.             CURLE_GOT_NOTHING          => true,
  94.         ];
  95.         // Retry when nothing is present or when curl failed to rewind.
  96.         if (!isset($response['err_message'])
  97.             && (empty($response['curl']['errno'])
  98.                 || $response['curl']['errno'] == 65)
  99.         ) {
  100.             return self::retryFailedRewind($handler$request$response);
  101.         }
  102.         $message = isset($response['err_message'])
  103.             ? $response['err_message']
  104.             : sprintf('cURL error %s: %s',
  105.                 $response['curl']['errno'],
  106.                 isset($response['curl']['error'])
  107.                     ? $response['curl']['error']
  108.                     : 'See http://curl.haxx.se/libcurl/c/libcurl-errors.html');
  109.         $error = isset($response['curl']['errno'])
  110.             && isset($connectionErrors[$response['curl']['errno']])
  111.             ? new ConnectException($message)
  112.             : new RingException($message);
  113.         return $response + [
  114.             'status'  => null,
  115.             'reason'  => null,
  116.             'body'    => null,
  117.             'headers' => [],
  118.             'error'   => $error,
  119.         ];
  120.     }
  121.     private function getOutputBody(array $request, array &$options)
  122.     {
  123.         // Determine where the body of the response (if any) will be streamed.
  124.         if (isset($options[CURLOPT_WRITEFUNCTION])) {
  125.             return $request['client']['save_to'];
  126.         }
  127.         if (isset($options[CURLOPT_FILE])) {
  128.             return $options[CURLOPT_FILE];
  129.         }
  130.         if ($request['http_method'] != 'HEAD') {
  131.             // Create a default body if one was not provided
  132.             return $options[CURLOPT_FILE] = fopen('php://temp''w+');
  133.         }
  134.         return null;
  135.     }
  136.     private function getDefaultOptions(array $request, array &$headers)
  137.     {
  138.         $url Core::url($request);
  139.         $startingResponse false;
  140.         $options = [
  141.             '_headers'             => $request['headers'],
  142.             CURLOPT_CUSTOMREQUEST  => $request['http_method'],
  143.             CURLOPT_URL            => $url,
  144.             CURLOPT_RETURNTRANSFER => false,
  145.             CURLOPT_HEADER         => false,
  146.             CURLOPT_CONNECTTIMEOUT => 150,
  147.             CURLOPT_HEADERFUNCTION => function ($ch$h) use (&$headers, &$startingResponse) {
  148.                 $value trim($h);
  149.                 if ($value === '') {
  150.                     $startingResponse true;
  151.                 } elseif ($startingResponse) {
  152.                     $startingResponse false;
  153.                     $headers = [$value];
  154.                 } else {
  155.                     $headers[] = $value;
  156.                 }
  157.                 return strlen($h);
  158.             },
  159.         ];
  160.         if (isset($request['version'])) {
  161.             if ($request['version'] == 2.0) {
  162.                 $options[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0;
  163.             } else if ($request['version'] == 1.1) {
  164.                 $options[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
  165.             } else {
  166.                 $options[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
  167.             }
  168.         }
  169.         if (defined('CURLOPT_PROTOCOLS')) {
  170.             $options[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP CURLPROTO_HTTPS;
  171.         }
  172.         return $options;
  173.     }
  174.     private function applyMethod(array $request, array &$options)
  175.     {
  176.         if (isset($request['body'])) {
  177.             $this->applyBody($request$options);
  178.             return;
  179.         }
  180.         switch ($request['http_method']) {
  181.             case 'PUT':
  182.             case 'POST':
  183.                 // See http://tools.ietf.org/html/rfc7230#section-3.3.2
  184.                 if (!Core::hasHeader($request'Content-Length')) {
  185.                     $options[CURLOPT_HTTPHEADER][] = 'Content-Length: 0';
  186.                 }
  187.                 break;
  188.             case 'HEAD':
  189.                 $options[CURLOPT_NOBODY] = true;
  190.                 unset(
  191.                     $options[CURLOPT_WRITEFUNCTION],
  192.                     $options[CURLOPT_READFUNCTION],
  193.                     $options[CURLOPT_FILE],
  194.                     $options[CURLOPT_INFILE]
  195.                 );
  196.         }
  197.     }
  198.     private function applyBody(array $request, array &$options)
  199.     {
  200.         $contentLength Core::firstHeader($request'Content-Length');
  201.         $size $contentLength !== null ? (int) $contentLength null;
  202.         // Send the body as a string if the size is less than 1MB OR if the
  203.         // [client][curl][body_as_string] request value is set.
  204.         if (($size !== null && $size 1000000) ||
  205.             isset($request['client']['curl']['body_as_string']) ||
  206.             is_string($request['body'])
  207.         ) {
  208.             $options[CURLOPT_POSTFIELDS] = Core::body($request);
  209.             // Don't duplicate the Content-Length header
  210.             $this->removeHeader('Content-Length'$options);
  211.             $this->removeHeader('Transfer-Encoding'$options);
  212.         } else {
  213.             $options[CURLOPT_UPLOAD] = true;
  214.             if ($size !== null) {
  215.                 // Let cURL handle setting the Content-Length header
  216.                 $options[CURLOPT_INFILESIZE] = $size;
  217.                 $this->removeHeader('Content-Length'$options);
  218.             }
  219.             $this->addStreamingBody($request$options);
  220.         }
  221.         // If the Expect header is not present, prevent curl from adding it
  222.         if (!Core::hasHeader($request'Expect')) {
  223.             $options[CURLOPT_HTTPHEADER][] = 'Expect:';
  224.         }
  225.         // cURL sometimes adds a content-type by default. Prevent this.
  226.         if (!Core::hasHeader($request'Content-Type')) {
  227.             $options[CURLOPT_HTTPHEADER][] = 'Content-Type:';
  228.         }
  229.     }
  230.     private function addStreamingBody(array $request, array &$options)
  231.     {
  232.         $body $request['body'];
  233.         if ($body instanceof StreamInterface) {
  234.             $options[CURLOPT_READFUNCTION] = function ($ch$fd$length) use ($body) {
  235.                 return (string) $body->read($length);
  236.             };
  237.             if (!isset($options[CURLOPT_INFILESIZE])) {
  238.                 if ($size $body->getSize()) {
  239.                     $options[CURLOPT_INFILESIZE] = $size;
  240.                 }
  241.             }
  242.         } elseif (is_resource($body)) {
  243.             $options[CURLOPT_INFILE] = $body;
  244.         } elseif ($body instanceof \Iterator) {
  245.             $buf '';
  246.             $options[CURLOPT_READFUNCTION] = function ($ch$fd$length) use ($body, &$buf) {
  247.                 if ($body->valid()) {
  248.                     $buf .= $body->current();
  249.                     $body->next();
  250.                 }
  251.                 $result = (string) substr($buf0$length);
  252.                 $buf substr($buf$length);
  253.                 return $result;
  254.             };
  255.         } else {
  256.             throw new \InvalidArgumentException('Invalid request body provided');
  257.         }
  258.     }
  259.     private function applyHeaders(array $request, array &$options)
  260.     {
  261.         foreach ($options['_headers'] as $name => $values) {
  262.             foreach ($values as $value) {
  263.                 $options[CURLOPT_HTTPHEADER][] = "$name$value";
  264.             }
  265.         }
  266.         // Remove the Accept header if one was not set
  267.         if (!Core::hasHeader($request'Accept')) {
  268.             $options[CURLOPT_HTTPHEADER][] = 'Accept:';
  269.         }
  270.     }
  271.     /**
  272.      * Takes an array of curl options specified in the 'curl' option of a
  273.      * request's configuration array and maps them to CURLOPT_* options.
  274.      *
  275.      * This method is only called when a  request has a 'curl' config setting.
  276.      *
  277.      * @param array $config  Configuration array of custom curl option
  278.      * @param array $options Array of existing curl options
  279.      *
  280.      * @return array Returns a new array of curl options
  281.      */
  282.     private function applyCustomCurlOptions(array $config, array $options)
  283.     {
  284.         $curlOptions = [];
  285.         foreach ($config as $key => $value) {
  286.             if (is_int($key)) {
  287.                 $curlOptions[$key] = $value;
  288.             }
  289.         }
  290.         return $curlOptions $options;
  291.     }
  292.     /**
  293.      * Remove a header from the options array.
  294.      *
  295.      * @param string $name    Case-insensitive header to remove
  296.      * @param array  $options Array of options to modify
  297.      */
  298.     private function removeHeader($name, array &$options)
  299.     {
  300.         foreach (array_keys($options['_headers']) as $key) {
  301.             if (!strcasecmp($key$name)) {
  302.                 unset($options['_headers'][$key]);
  303.                 return;
  304.             }
  305.         }
  306.     }
  307.     /**
  308.      * Applies an array of request client options to a the options array.
  309.      *
  310.      * This method uses a large switch rather than double-dispatch to save on
  311.      * high overhead of calling functions in PHP.
  312.      */
  313.     private function applyHandlerOptions(array $request, array &$options)
  314.     {
  315.         foreach ($request['client'] as $key => $value) {
  316.             switch ($key) {
  317.             // Violating PSR-4 to provide more room.
  318.             case 'verify':
  319.                 if ($value === false) {
  320.                     unset($options[CURLOPT_CAINFO]);
  321.                     $options[CURLOPT_SSL_VERIFYHOST] = 0;
  322.                     $options[CURLOPT_SSL_VERIFYPEER] = false;
  323.                     continue 2;
  324.                 }
  325.                 $options[CURLOPT_SSL_VERIFYHOST] = 2;
  326.                 $options[CURLOPT_SSL_VERIFYPEER] = true;
  327.                 if (is_string($value)) {
  328.                     $options[CURLOPT_CAINFO] = $value;
  329.                     if (!file_exists($value)) {
  330.                         throw new \InvalidArgumentException(
  331.                             "SSL CA bundle not found: $value"
  332.                         );
  333.                     }
  334.                 }
  335.                 break;
  336.             case 'decode_content':
  337.                 if ($value === false) {
  338.                     continue 2;
  339.                 }
  340.                 $accept Core::firstHeader($request'Accept-Encoding');
  341.                 if ($accept) {
  342.                     $options[CURLOPT_ENCODING] = $accept;
  343.                 } else {
  344.                     $options[CURLOPT_ENCODING] = '';
  345.                     // Don't let curl send the header over the wire
  346.                     $options[CURLOPT_HTTPHEADER][] = 'Accept-Encoding:';
  347.                 }
  348.                 break;
  349.             case 'save_to':
  350.                 if (is_string($value)) {
  351.                     if (!is_dir(dirname($value))) {
  352.                         throw new \RuntimeException(sprintf(
  353.                             'Directory %s does not exist for save_to value of %s',
  354.                             dirname($value),
  355.                             $value
  356.                         ));
  357.                     }
  358.                     $value = new LazyOpenStream($value'w+');
  359.                 }
  360.                 if ($value instanceof StreamInterface) {
  361.                     $options[CURLOPT_WRITEFUNCTION] =
  362.                         function ($ch$write) use ($value) {
  363.                             return $value->write($write);
  364.                         };
  365.                 } elseif (is_resource($value)) {
  366.                     $options[CURLOPT_FILE] = $value;
  367.                 } else {
  368.                     throw new \InvalidArgumentException('save_to must be a '
  369.                         'GuzzleHttp\Stream\StreamInterface or resource');
  370.                 }
  371.                 break;
  372.             case 'timeout':
  373.                 if (defined('CURLOPT_TIMEOUT_MS')) {
  374.                     $options[CURLOPT_TIMEOUT_MS] = $value 1000;
  375.                 } else {
  376.                     $options[CURLOPT_TIMEOUT] = $value;
  377.                 }
  378.                 break;
  379.             case 'connect_timeout':
  380.                 if (defined('CURLOPT_CONNECTTIMEOUT_MS')) {
  381.                     $options[CURLOPT_CONNECTTIMEOUT_MS] = $value 1000;
  382.                 } else {
  383.                     $options[CURLOPT_CONNECTTIMEOUT] = $value;
  384.                 }
  385.                 break;
  386.             case 'proxy':
  387.                 if (!is_array($value)) {
  388.                     $options[CURLOPT_PROXY] = $value;
  389.                 } elseif (isset($request['scheme'])) {
  390.                     $scheme $request['scheme'];
  391.                     if (isset($value[$scheme])) {
  392.                         $options[CURLOPT_PROXY] = $value[$scheme];
  393.                     }
  394.                 }
  395.                 break;
  396.             case 'cert':
  397.                 if (is_array($value)) {
  398.                     $options[CURLOPT_SSLCERTPASSWD] = $value[1];
  399.                     $value $value[0];
  400.                 }
  401.                 if (!file_exists($value)) {
  402.                     throw new \InvalidArgumentException(
  403.                         "SSL certificate not found: {$value}"
  404.                     );
  405.                 }
  406.                 $options[CURLOPT_SSLCERT] = $value;
  407.                 break;
  408.             case 'ssl_key':
  409.                 if (is_array($value)) {
  410.                     $options[CURLOPT_SSLKEYPASSWD] = $value[1];
  411.                     $value $value[0];
  412.                 }
  413.                 if (!file_exists($value)) {
  414.                     throw new \InvalidArgumentException(
  415.                         "SSL private key not found: {$value}"
  416.                     );
  417.                 }
  418.                 $options[CURLOPT_SSLKEY] = $value;
  419.                 break;
  420.             case 'progress':
  421.                 if (!is_callable($value)) {
  422.                     throw new \InvalidArgumentException(
  423.                         'progress client option must be callable'
  424.                     );
  425.                 }
  426.                 $options[CURLOPT_NOPROGRESS] = false;
  427.                 $options[CURLOPT_PROGRESSFUNCTION] =
  428.                     function () use ($value) {
  429.                         $args func_get_args();
  430.                         // PHP 5.5 pushed the handle onto the start of the args
  431.                         if (is_resource($args[0])) {
  432.                             array_shift($args);
  433.                         }
  434.                         call_user_func_array($value$args);
  435.                     };
  436.                 break;
  437.             case 'debug':
  438.                 if ($value) {
  439.                     $options[CURLOPT_STDERR] = Core::getDebugResource($value);
  440.                     $options[CURLOPT_VERBOSE] = true;
  441.                 }
  442.                 break;
  443.             }
  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.         array $request,
  458.         array $response
  459.     ) {
  460.         // If there is no body, then there is some other kind of issue. This
  461.         // is weird and should probably never happen.
  462.         if (!isset($request['body'])) {
  463.             $response['err_message'] = 'No response was received for a request '
  464.                 'with no body. This could mean that you are saturating your '
  465.                 'network.';
  466.             return self::createErrorResponse($handler$request$response);
  467.         }
  468.         if (!Core::rewindBody($request)) {
  469.             $response['err_message'] = 'The connection unexpectedly failed '
  470.                 'without providing an error. The request would have been '
  471.                 'retried, but attempting to rewind the request body failed.';
  472.             return self::createErrorResponse($handler$request$response);
  473.         }
  474.         // Retry no more than 3 times before giving up.
  475.         if (!isset($request['curl']['retries'])) {
  476.             $request['curl']['retries'] = 1;
  477.         } elseif ($request['curl']['retries'] == 2) {
  478.             $response['err_message'] = 'The cURL request was retried 3 times '
  479.                 'and did no succeed. cURL was unable to rewind the body of '
  480.                 'the request and subsequent retries resulted in the same '
  481.                 'error. Turn on the debug option to see what went wrong. '
  482.                 'See https://bugs.php.net/bug.php?id=47204 for more information.';
  483.             return self::createErrorResponse($handler$request$response);
  484.         } else {
  485.             $request['curl']['retries']++;
  486.         }
  487.         return $handler($request);
  488.     }
  489. }