GuzzleHttpHandlerCurlFactory

Creates curl resources from a request.

Defined (1)

The class is defined in the following location(s).

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