CurlClient.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. <?php
  2. namespace Stripe\HttpClient;
  3. use Stripe\Exception;
  4. use Stripe\Stripe;
  5. use Stripe\Util;
  6. // @codingStandardsIgnoreStart
  7. // PSR2 requires all constants be upper case. Sadly, the CURL_SSLVERSION
  8. // constants do not abide by those rules.
  9. // Note the values come from their position in the enums that
  10. // defines them in cURL's source code.
  11. // Available since PHP 5.5.19 and 5.6.3
  12. if (!\defined('CURL_SSLVERSION_TLSv1_2')) {
  13. \define('CURL_SSLVERSION_TLSv1_2', 6);
  14. }
  15. // @codingStandardsIgnoreEnd
  16. // Available since PHP 7.0.7 and cURL 7.47.0
  17. if (!\defined('CURL_HTTP_VERSION_2TLS')) {
  18. \define('CURL_HTTP_VERSION_2TLS', 4);
  19. }
  20. class CurlClient implements ClientInterface
  21. {
  22. private static $instance;
  23. public static function instance()
  24. {
  25. if (!self::$instance) {
  26. self::$instance = new self();
  27. }
  28. return self::$instance;
  29. }
  30. protected $defaultOptions;
  31. /** @var \Stripe\Util\RandomGenerator */
  32. protected $randomGenerator;
  33. protected $userAgentInfo;
  34. protected $enablePersistentConnections = true;
  35. protected $enableHttp2;
  36. protected $curlHandle;
  37. protected $requestStatusCallback;
  38. /**
  39. * CurlClient constructor.
  40. *
  41. * Pass in a callable to $defaultOptions that returns an array of CURLOPT_* values to start
  42. * off a request with, or an flat array with the same format used by curl_setopt_array() to
  43. * provide a static set of options. Note that many options are overridden later in the request
  44. * call, including timeouts, which can be set via setTimeout() and setConnectTimeout().
  45. *
  46. * Note that request() will silently ignore a non-callable, non-array $defaultOptions, and will
  47. * throw an exception if $defaultOptions returns a non-array value.
  48. *
  49. * @param null|array|callable $defaultOptions
  50. * @param null|\Stripe\Util\RandomGenerator $randomGenerator
  51. */
  52. public function __construct($defaultOptions = null, $randomGenerator = null)
  53. {
  54. $this->defaultOptions = $defaultOptions;
  55. $this->randomGenerator = $randomGenerator ?: new Util\RandomGenerator();
  56. $this->initUserAgentInfo();
  57. $this->enableHttp2 = $this->canSafelyUseHttp2();
  58. }
  59. public function __destruct()
  60. {
  61. $this->closeCurlHandle();
  62. }
  63. public function initUserAgentInfo()
  64. {
  65. $curlVersion = \curl_version();
  66. $this->userAgentInfo = [
  67. 'httplib' => 'curl ' . $curlVersion['version'],
  68. 'ssllib' => $curlVersion['ssl_version'],
  69. ];
  70. }
  71. public function getDefaultOptions()
  72. {
  73. return $this->defaultOptions;
  74. }
  75. public function getUserAgentInfo()
  76. {
  77. return $this->userAgentInfo;
  78. }
  79. /**
  80. * @return bool
  81. */
  82. public function getEnablePersistentConnections()
  83. {
  84. return $this->enablePersistentConnections;
  85. }
  86. /**
  87. * @param bool $enable
  88. */
  89. public function setEnablePersistentConnections($enable)
  90. {
  91. $this->enablePersistentConnections = $enable;
  92. }
  93. /**
  94. * @return bool
  95. */
  96. public function getEnableHttp2()
  97. {
  98. return $this->enableHttp2;
  99. }
  100. /**
  101. * @param bool $enable
  102. */
  103. public function setEnableHttp2($enable)
  104. {
  105. $this->enableHttp2 = $enable;
  106. }
  107. /**
  108. * @return null|callable
  109. */
  110. public function getRequestStatusCallback()
  111. {
  112. return $this->requestStatusCallback;
  113. }
  114. /**
  115. * Sets a callback that is called after each request. The callback will
  116. * receive the following parameters:
  117. * <ol>
  118. * <li>string $rbody The response body</li>
  119. * <li>integer $rcode The response status code</li>
  120. * <li>\Stripe\Util\CaseInsensitiveArray $rheaders The response headers</li>
  121. * <li>integer $errno The curl error number</li>
  122. * <li>string|null $message The curl error message</li>
  123. * <li>boolean $shouldRetry Whether the request will be retried</li>
  124. * <li>integer $numRetries The number of the retry attempt</li>
  125. * </ol>.
  126. *
  127. * @param null|callable $requestStatusCallback
  128. */
  129. public function setRequestStatusCallback($requestStatusCallback)
  130. {
  131. $this->requestStatusCallback = $requestStatusCallback;
  132. }
  133. // USER DEFINED TIMEOUTS
  134. const DEFAULT_TIMEOUT = 80;
  135. const DEFAULT_CONNECT_TIMEOUT = 30;
  136. private $timeout = self::DEFAULT_TIMEOUT;
  137. private $connectTimeout = self::DEFAULT_CONNECT_TIMEOUT;
  138. public function setTimeout($seconds)
  139. {
  140. $this->timeout = (int) \max($seconds, 0);
  141. return $this;
  142. }
  143. public function setConnectTimeout($seconds)
  144. {
  145. $this->connectTimeout = (int) \max($seconds, 0);
  146. return $this;
  147. }
  148. public function getTimeout()
  149. {
  150. return $this->timeout;
  151. }
  152. public function getConnectTimeout()
  153. {
  154. return $this->connectTimeout;
  155. }
  156. // END OF USER DEFINED TIMEOUTS
  157. public function request($method, $absUrl, $headers, $params, $hasFile)
  158. {
  159. $method = \strtolower($method);
  160. $opts = [];
  161. if (\is_callable($this->defaultOptions)) { // call defaultOptions callback, set options to return value
  162. $opts = \call_user_func_array($this->defaultOptions, \func_get_args());
  163. if (!\is_array($opts)) {
  164. throw new Exception\UnexpectedValueException('Non-array value returned by defaultOptions CurlClient callback');
  165. }
  166. } elseif (\is_array($this->defaultOptions)) { // set default curlopts from array
  167. $opts = $this->defaultOptions;
  168. }
  169. $params = Util\Util::objectsToIds($params);
  170. if ('get' === $method) {
  171. if ($hasFile) {
  172. throw new Exception\UnexpectedValueException(
  173. 'Issuing a GET request with a file parameter'
  174. );
  175. }
  176. $opts[\CURLOPT_HTTPGET] = 1;
  177. if (\count($params) > 0) {
  178. $encoded = Util\Util::encodeParameters($params);
  179. $absUrl = "{$absUrl}?{$encoded}";
  180. }
  181. } elseif ('post' === $method) {
  182. $opts[\CURLOPT_POST] = 1;
  183. $opts[\CURLOPT_POSTFIELDS] = $hasFile ? $params : Util\Util::encodeParameters($params);
  184. } elseif ('delete' === $method) {
  185. $opts[\CURLOPT_CUSTOMREQUEST] = 'DELETE';
  186. if (\count($params) > 0) {
  187. $encoded = Util\Util::encodeParameters($params);
  188. $absUrl = "{$absUrl}?{$encoded}";
  189. }
  190. } else {
  191. throw new Exception\UnexpectedValueException("Unrecognized method {$method}");
  192. }
  193. // It is only safe to retry network failures on POST requests if we
  194. // add an Idempotency-Key header
  195. if (('post' === $method) && (Stripe::$maxNetworkRetries > 0)) {
  196. if (!$this->hasHeader($headers, 'Idempotency-Key')) {
  197. $headers[] = 'Idempotency-Key: ' . $this->randomGenerator->uuid();
  198. }
  199. }
  200. // By default for large request body sizes (> 1024 bytes), cURL will
  201. // send a request without a body and with a `Expect: 100-continue`
  202. // header, which gives the server a chance to respond with an error
  203. // status code in cases where one can be determined right away (say
  204. // on an authentication problem for example), and saves the "large"
  205. // request body from being ever sent.
  206. //
  207. // Unfortunately, the bindings don't currently correctly handle the
  208. // success case (in which the server sends back a 100 CONTINUE), so
  209. // we'll error under that condition. To compensate for that problem
  210. // for the time being, override cURL's behavior by simply always
  211. // sending an empty `Expect:` header.
  212. $headers[] = 'Expect: ';
  213. $absUrl = Util\Util::utf8($absUrl);
  214. $opts[\CURLOPT_URL] = $absUrl;
  215. $opts[\CURLOPT_RETURNTRANSFER] = true;
  216. $opts[\CURLOPT_CONNECTTIMEOUT] = $this->connectTimeout;
  217. $opts[\CURLOPT_TIMEOUT] = $this->timeout;
  218. $opts[\CURLOPT_HTTPHEADER] = $headers;
  219. $opts[\CURLOPT_CAINFO] = Stripe::getCABundlePath();
  220. if (!Stripe::getVerifySslCerts()) {
  221. $opts[\CURLOPT_SSL_VERIFYPEER] = false;
  222. }
  223. if (!isset($opts[\CURLOPT_HTTP_VERSION]) && $this->getEnableHttp2()) {
  224. // For HTTPS requests, enable HTTP/2, if supported
  225. $opts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2TLS;
  226. }
  227. // Stripe's API servers are only accessible over IPv4. Force IPv4 resolving to avoid
  228. // potential issues (cf. https://github.com/stripe/stripe-php/issues/1045).
  229. $opts[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4;
  230. list($rbody, $rcode, $rheaders) = $this->executeRequestWithRetries($opts, $absUrl);
  231. return [$rbody, $rcode, $rheaders];
  232. }
  233. /**
  234. * @param array $opts cURL options
  235. * @param string $absUrl
  236. */
  237. private function executeRequestWithRetries($opts, $absUrl)
  238. {
  239. $numRetries = 0;
  240. while (true) {
  241. $rcode = 0;
  242. $errno = 0;
  243. $message = null;
  244. // Create a callback to capture HTTP headers for the response
  245. $rheaders = new Util\CaseInsensitiveArray();
  246. $headerCallback = function ($curl, $header_line) use (&$rheaders) {
  247. // Ignore the HTTP request line (HTTP/1.1 200 OK)
  248. if (false === \strpos($header_line, ':')) {
  249. return \strlen($header_line);
  250. }
  251. list($key, $value) = \explode(':', \trim($header_line), 2);
  252. $rheaders[\trim($key)] = \trim($value);
  253. return \strlen($header_line);
  254. };
  255. $opts[\CURLOPT_HEADERFUNCTION] = $headerCallback;
  256. $this->resetCurlHandle();
  257. \curl_setopt_array($this->curlHandle, $opts);
  258. $rbody = \curl_exec($this->curlHandle);
  259. if (false === $rbody) {
  260. $errno = \curl_errno($this->curlHandle);
  261. $message = \curl_error($this->curlHandle);
  262. } else {
  263. $rcode = \curl_getinfo($this->curlHandle, \CURLINFO_HTTP_CODE);
  264. }
  265. if (!$this->getEnablePersistentConnections()) {
  266. $this->closeCurlHandle();
  267. }
  268. $shouldRetry = $this->shouldRetry($errno, $rcode, $rheaders, $numRetries);
  269. if (\is_callable($this->getRequestStatusCallback())) {
  270. \call_user_func_array(
  271. $this->getRequestStatusCallback(),
  272. [$rbody, $rcode, $rheaders, $errno, $message, $shouldRetry, $numRetries]
  273. );
  274. }
  275. if ($shouldRetry) {
  276. ++$numRetries;
  277. $sleepSeconds = $this->sleepTime($numRetries, $rheaders);
  278. \usleep((int) ($sleepSeconds * 1000000));
  279. } else {
  280. break;
  281. }
  282. }
  283. if (false === $rbody) {
  284. $this->handleCurlError($absUrl, $errno, $message, $numRetries);
  285. }
  286. return [$rbody, $rcode, $rheaders];
  287. }
  288. /**
  289. * @param string $url
  290. * @param int $errno
  291. * @param string $message
  292. * @param int $numRetries
  293. *
  294. * @throws Exception\ApiConnectionException
  295. */
  296. private function handleCurlError($url, $errno, $message, $numRetries)
  297. {
  298. switch ($errno) {
  299. case \CURLE_COULDNT_CONNECT:
  300. case \CURLE_COULDNT_RESOLVE_HOST:
  301. case \CURLE_OPERATION_TIMEOUTED:
  302. $msg = "Could not connect to Stripe ({$url}). Please check your "
  303. . 'internet connection and try again. If this problem persists, '
  304. . "you should check Stripe's service status at "
  305. . 'https://twitter.com/stripestatus, or';
  306. break;
  307. case \CURLE_SSL_CACERT:
  308. case \CURLE_SSL_PEER_CERTIFICATE:
  309. $msg = "Could not verify Stripe's SSL certificate. Please make sure "
  310. . 'that your network is not intercepting certificates. '
  311. . "(Try going to {$url} in your browser.) "
  312. . 'If this problem persists,';
  313. break;
  314. default:
  315. $msg = 'Unexpected error communicating with Stripe. '
  316. . 'If this problem persists,';
  317. }
  318. $msg .= ' let us know at support@stripe.com.';
  319. $msg .= "\n\n(Network error [errno {$errno}]: {$message})";
  320. if ($numRetries > 0) {
  321. $msg .= "\n\nRequest was retried {$numRetries} times.";
  322. }
  323. throw new Exception\ApiConnectionException($msg);
  324. }
  325. /**
  326. * Checks if an error is a problem that we should retry on. This includes both
  327. * socket errors that may represent an intermittent problem and some special
  328. * HTTP statuses.
  329. *
  330. * @param int $errno
  331. * @param int $rcode
  332. * @param array|\Stripe\Util\CaseInsensitiveArray $rheaders
  333. * @param int $numRetries
  334. *
  335. * @return bool
  336. */
  337. private function shouldRetry($errno, $rcode, $rheaders, $numRetries)
  338. {
  339. if ($numRetries >= Stripe::getMaxNetworkRetries()) {
  340. return false;
  341. }
  342. // Retry on timeout-related problems (either on open or read).
  343. if (\CURLE_OPERATION_TIMEOUTED === $errno) {
  344. return true;
  345. }
  346. // Destination refused the connection, the connection was reset, or a
  347. // variety of other connection failures. This could occur from a single
  348. // saturated server, so retry in case it's intermittent.
  349. if (\CURLE_COULDNT_CONNECT === $errno) {
  350. return true;
  351. }
  352. // The API may ask us not to retry (eg; if doing so would be a no-op)
  353. // or advise us to retry (eg; in cases of lock timeouts); we defer to that.
  354. if (isset($rheaders['stripe-should-retry'])) {
  355. if ('false' === $rheaders['stripe-should-retry']) {
  356. return false;
  357. }
  358. if ('true' === $rheaders['stripe-should-retry']) {
  359. return true;
  360. }
  361. }
  362. // 409 Conflict
  363. if (409 === $rcode) {
  364. return true;
  365. }
  366. // Retry on 500, 503, and other internal errors.
  367. //
  368. // Note that we expect the stripe-should-retry header to be false
  369. // in most cases when a 500 is returned, since our idempotency framework
  370. // would typically replay it anyway.
  371. if ($rcode >= 500) {
  372. return true;
  373. }
  374. return false;
  375. }
  376. /**
  377. * Provides the number of seconds to wait before retrying a request.
  378. *
  379. * @param int $numRetries
  380. * @param array|\Stripe\Util\CaseInsensitiveArray $rheaders
  381. *
  382. * @return int
  383. */
  384. private function sleepTime($numRetries, $rheaders)
  385. {
  386. // Apply exponential backoff with $initialNetworkRetryDelay on the
  387. // number of $numRetries so far as inputs. Do not allow the number to exceed
  388. // $maxNetworkRetryDelay.
  389. $sleepSeconds = \min(
  390. Stripe::getInitialNetworkRetryDelay() * 1.0 * 2 ** ($numRetries - 1),
  391. Stripe::getMaxNetworkRetryDelay()
  392. );
  393. // Apply some jitter by randomizing the value in the range of
  394. // ($sleepSeconds / 2) to ($sleepSeconds).
  395. $sleepSeconds *= 0.5 * (1 + $this->randomGenerator->randFloat());
  396. // But never sleep less than the base sleep seconds.
  397. $sleepSeconds = \max(Stripe::getInitialNetworkRetryDelay(), $sleepSeconds);
  398. // And never sleep less than the time the API asks us to wait, assuming it's a reasonable ask.
  399. $retryAfter = isset($rheaders['retry-after']) ? (float) ($rheaders['retry-after']) : 0.0;
  400. if (\floor($retryAfter) === $retryAfter && $retryAfter <= Stripe::getMaxRetryAfter()) {
  401. $sleepSeconds = \max($sleepSeconds, $retryAfter);
  402. }
  403. return $sleepSeconds;
  404. }
  405. /**
  406. * Initializes the curl handle. If already initialized, the handle is closed first.
  407. */
  408. private function initCurlHandle()
  409. {
  410. $this->closeCurlHandle();
  411. $this->curlHandle = \curl_init();
  412. }
  413. /**
  414. * Closes the curl handle if initialized. Do nothing if already closed.
  415. */
  416. private function closeCurlHandle()
  417. {
  418. if (null !== $this->curlHandle) {
  419. \curl_close($this->curlHandle);
  420. $this->curlHandle = null;
  421. }
  422. }
  423. /**
  424. * Resets the curl handle. If the handle is not already initialized, or if persistent
  425. * connections are disabled, the handle is reinitialized instead.
  426. */
  427. private function resetCurlHandle()
  428. {
  429. if (null !== $this->curlHandle && $this->getEnablePersistentConnections()) {
  430. \curl_reset($this->curlHandle);
  431. } else {
  432. $this->initCurlHandle();
  433. }
  434. }
  435. /**
  436. * Indicates whether it is safe to use HTTP/2 or not.
  437. *
  438. * @return bool
  439. */
  440. private function canSafelyUseHttp2()
  441. {
  442. // Versions of curl older than 7.60.0 don't respect GOAWAY frames
  443. // (cf. https://github.com/curl/curl/issues/2416), which Stripe use.
  444. $curlVersion = \curl_version()['version'];
  445. return \version_compare($curlVersion, '7.60.0') >= 0;
  446. }
  447. /**
  448. * Checks if a list of headers contains a specific header name.
  449. *
  450. * @param string[] $headers
  451. * @param string $name
  452. *
  453. * @return bool
  454. */
  455. private function hasHeader($headers, $name)
  456. {
  457. foreach ($headers as $header) {
  458. if (0 === \strncasecmp($header, "{$name}: ", \strlen($name) + 2)) {
  459. return true;
  460. }
  461. }
  462. return false;
  463. }
  464. }