[ Index ]

PHP Cross Reference of phpBB-3.3.2-deutsch

title

Body

[close]

/vendor/guzzlehttp/guzzle/src/Handler/ -> CurlFactory.php (source)

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


Generated: Wed Nov 11 20:28:18 2020 Cross-referenced by PHPXref 0.7.1