No Description

CurlFactory.php 18KB

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