CurlFactory.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  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($resource, CURLOPT_HEADERFUNCTION, null);
  65. curl_setopt($resource, CURLOPT_READFUNCTION, null);
  66. curl_setopt($resource, CURLOPT_WRITEFUNCTION, null);
  67. curl_setopt($resource, CURLOPT_PROGRESSFUNCTION, null);
  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->handle, CURLINFO_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->handle, CURLINFO_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->request, null, $ctx)
  177. : new RequestException($message, $easy->request, $easy->response, null, $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_OS, 0, 3)) !== '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. }