Без опису

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