Open Monograph Press  3.3.0
CurlFactory.php
1 <?php
3 
11 
16 {
17  const CURL_VERSION_STR = 'curl_version';
18  const LOW_CURL_VERSION_NUMBER = '7.21.2';
19 
21  private $handles = [];
22 
24  private $maxHandles;
25 
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 
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 
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 
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 }
GuzzleHttp\Exception\RequestException
Definition: vendor/guzzlehttp/guzzle/src/Exception/RequestException.php:12
GuzzleHttp
Definition: paymethod/paypal/vendor/guzzlehttp/guzzle/src/Client.php:2
GuzzleHttp\Handler\CurlFactory\finish
static finish(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory)
Definition: CurlFactory.php:101
GuzzleHttp\Handler\CurlFactory\LOW_CURL_VERSION_NUMBER
const LOW_CURL_VERSION_NUMBER
Definition: CurlFactory.php:18
GuzzleHttp\Handler\CurlFactory\CURL_VERSION_STR
const CURL_VERSION_STR
Definition: CurlFactory.php:17
Psr\Http\Message\RequestInterface
Definition: vendor/psr/http-message/src/RequestInterface.php:24
GuzzleHttp\Handler\CurlFactory\release
release(EasyHandle $easy)
Definition: CurlFactory.php:70
GuzzleHttp\Handler
Definition: CurlFactory.php:2
GuzzleHttp\Handler\CurlFactory
Definition: CurlFactory.php:15
GuzzleHttp\Handler\CurlFactoryInterface\release
release(EasyHandle $easy)
GuzzleHttp\Psr7
Definition: AppendStream.php:2
GuzzleHttp\Handler\CurlFactoryInterface
Definition: CurlFactoryInterface.php:6
GuzzleHttp\TransferStats
Definition: TransferStats.php:12
GuzzleHttp\Psr7\LazyOpenStream
Definition: LazyOpenStream.php:10
GuzzleHttp\Handler\CurlFactory\create
create(RequestInterface $request, array $options)
Definition: CurlFactory.php:40
GuzzleHttp\Handler\EasyHandle
Definition: EasyHandle.php:14
GuzzleHttp\Exception\ConnectException
Definition: ConnectException.php:11
GuzzleHttp\Handler\CurlFactory\__construct
__construct($maxHandles)
Definition: CurlFactory.php:35
GuzzleHttp\Promise\FulfilledPromise
Definition: guzzlehttp/promises/src/FulfilledPromise.php:10
GuzzleHttp\Psr7\stream_for
stream_for($resource='', array $options=[])
Definition: guzzlehttp/psr7/src/functions.php:78
GuzzleHttp\is_host_in_noproxy
is_host_in_noproxy($host, array $noProxyArray)
Definition: guzzlehttp/guzzle/src/functions.php:254