1<?php 2namespace GuzzleHttp\Handler; 3 4use GuzzleHttp\Exception\ConnectException; 5use GuzzleHttp\Exception\RequestException; 6use GuzzleHttp\Promise\FulfilledPromise; 7use GuzzleHttp\Psr7; 8use GuzzleHttp\Psr7\LazyOpenStream; 9use GuzzleHttp\TransferStats; 10use Psr\Http\Message\RequestInterface; 11 12/** 13 * Creates curl resources from a request 14 */ 15class 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} 586