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