CurlClient.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. <?php
  2. namespace Stripe\HttpClient;
  3. use Stripe\Stripe;
  4. use Stripe\Error;
  5. use Stripe\Util;
  6. // cURL constants are not defined in PHP < 5.5
  7. // @codingStandardsIgnoreStart
  8. // PSR2 requires all constants be upper case. Sadly, the CURL_SSLVERSION
  9. // constants do not abide by those rules.
  10. // Note the values 1 and 6 come from their position in the enum that
  11. // defines them in cURL's source code.
  12. if (!defined('CURL_SSLVERSION_TLSv1')) {
  13. define('CURL_SSLVERSION_TLSv1', 1);
  14. }
  15. if (!defined('CURL_SSLVERSION_TLSv1_2')) {
  16. define('CURL_SSLVERSION_TLSv1_2', 6);
  17. }
  18. // @codingStandardsIgnoreEnd
  19. if (!defined('CURL_HTTP_VERSION_2TLS')) {
  20. define('CURL_HTTP_VERSION_2TLS', 4);
  21. }
  22. class CurlClient implements ClientInterface
  23. {
  24. private static $instance;
  25. public static function instance()
  26. {
  27. if (!self::$instance) {
  28. self::$instance = new self();
  29. }
  30. return self::$instance;
  31. }
  32. protected $defaultOptions;
  33. protected $userAgentInfo;
  34. /**
  35. * CurlClient constructor.
  36. *
  37. * Pass in a callable to $defaultOptions that returns an array of CURLOPT_* values to start
  38. * off a request with, or an flat array with the same format used by curl_setopt_array() to
  39. * provide a static set of options. Note that many options are overridden later in the request
  40. * call, including timeouts, which can be set via setTimeout() and setConnectTimeout().
  41. *
  42. * Note that request() will silently ignore a non-callable, non-array $defaultOptions, and will
  43. * throw an exception if $defaultOptions returns a non-array value.
  44. *
  45. * @param array|callable|null $defaultOptions
  46. */
  47. public function __construct($defaultOptions = null, $randomGenerator = null)
  48. {
  49. $this->defaultOptions = $defaultOptions;
  50. $this->randomGenerator = $randomGenerator ?: new Util\RandomGenerator();
  51. $this->initUserAgentInfo();
  52. }
  53. public function initUserAgentInfo()
  54. {
  55. $curlVersion = curl_version();
  56. $this->userAgentInfo = [
  57. 'httplib' => 'curl ' . $curlVersion['version'],
  58. 'ssllib' => $curlVersion['ssl_version'],
  59. ];
  60. }
  61. public function getDefaultOptions()
  62. {
  63. return $this->defaultOptions;
  64. }
  65. public function getUserAgentInfo()
  66. {
  67. return $this->userAgentInfo;
  68. }
  69. // USER DEFINED TIMEOUTS
  70. const DEFAULT_TIMEOUT = 80;
  71. const DEFAULT_CONNECT_TIMEOUT = 30;
  72. private $timeout = self::DEFAULT_TIMEOUT;
  73. private $connectTimeout = self::DEFAULT_CONNECT_TIMEOUT;
  74. public function setTimeout($seconds)
  75. {
  76. $this->timeout = (int) max($seconds, 0);
  77. return $this;
  78. }
  79. public function setConnectTimeout($seconds)
  80. {
  81. $this->connectTimeout = (int) max($seconds, 0);
  82. return $this;
  83. }
  84. public function getTimeout()
  85. {
  86. return $this->timeout;
  87. }
  88. public function getConnectTimeout()
  89. {
  90. return $this->connectTimeout;
  91. }
  92. // END OF USER DEFINED TIMEOUTS
  93. public function request($method, $absUrl, $headers, $params, $hasFile)
  94. {
  95. $method = strtolower($method);
  96. $opts = [];
  97. if (is_callable($this->defaultOptions)) { // call defaultOptions callback, set options to return value
  98. $opts = call_user_func_array($this->defaultOptions, func_get_args());
  99. if (!is_array($opts)) {
  100. throw new Error\Api("Non-array value returned by defaultOptions CurlClient callback");
  101. }
  102. } elseif (is_array($this->defaultOptions)) { // set default curlopts from array
  103. $opts = $this->defaultOptions;
  104. }
  105. if ($method == 'get') {
  106. if ($hasFile) {
  107. throw new Error\Api(
  108. "Issuing a GET request with a file parameter"
  109. );
  110. }
  111. $opts[CURLOPT_HTTPGET] = 1;
  112. if (count($params) > 0) {
  113. $encoded = Util\Util::urlEncode($params);
  114. $absUrl = "$absUrl?$encoded";
  115. }
  116. } elseif ($method == 'post') {
  117. $opts[CURLOPT_POST] = 1;
  118. $opts[CURLOPT_POSTFIELDS] = $hasFile ? $params : Util\Util::urlEncode($params);
  119. } elseif ($method == 'delete') {
  120. $opts[CURLOPT_CUSTOMREQUEST] = 'DELETE';
  121. if (count($params) > 0) {
  122. $encoded = Util\Util::urlEncode($params);
  123. $absUrl = "$absUrl?$encoded";
  124. }
  125. } else {
  126. throw new Error\Api("Unrecognized method $method");
  127. }
  128. // It is only safe to retry network failures on POST requests if we
  129. // add an Idempotency-Key header
  130. if (($method == 'post') && (Stripe::$maxNetworkRetries > 0)) {
  131. if (!isset($headers['Idempotency-Key'])) {
  132. array_push($headers, 'Idempotency-Key: ' . $this->randomGenerator->uuid());
  133. }
  134. }
  135. // Create a callback to capture HTTP headers for the response
  136. $rheaders = [];
  137. $headerCallback = function ($curl, $header_line) use (&$rheaders) {
  138. // Ignore the HTTP request line (HTTP/1.1 200 OK)
  139. if (strpos($header_line, ":") === false) {
  140. return strlen($header_line);
  141. }
  142. list($key, $value) = explode(":", trim($header_line), 2);
  143. $rheaders[trim($key)] = trim($value);
  144. return strlen($header_line);
  145. };
  146. // By default for large request body sizes (> 1024 bytes), cURL will
  147. // send a request without a body and with a `Expect: 100-continue`
  148. // header, which gives the server a chance to respond with an error
  149. // status code in cases where one can be determined right away (say
  150. // on an authentication problem for example), and saves the "large"
  151. // request body from being ever sent.
  152. //
  153. // Unfortunately, the bindings don't currently correctly handle the
  154. // success case (in which the server sends back a 100 CONTINUE), so
  155. // we'll error under that condition. To compensate for that problem
  156. // for the time being, override cURL's behavior by simply always
  157. // sending an empty `Expect:` header.
  158. array_push($headers, 'Expect: ');
  159. $absUrl = Util\Util::utf8($absUrl);
  160. $opts[CURLOPT_URL] = $absUrl;
  161. $opts[CURLOPT_RETURNTRANSFER] = true;
  162. $opts[CURLOPT_CONNECTTIMEOUT] = $this->connectTimeout;
  163. $opts[CURLOPT_TIMEOUT] = $this->timeout;
  164. $opts[CURLOPT_HEADERFUNCTION] = $headerCallback;
  165. $opts[CURLOPT_HTTPHEADER] = $headers;
  166. $opts[CURLOPT_CAINFO] = Stripe::getCABundlePath();
  167. if (!Stripe::getVerifySslCerts()) {
  168. $opts[CURLOPT_SSL_VERIFYPEER] = false;
  169. }
  170. // For HTTPS requests, enable HTTP/2, if supported
  171. $opts[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2TLS;
  172. list($rbody, $rcode) = $this->executeRequestWithRetries($opts, $absUrl);
  173. return [$rbody, $rcode, $rheaders];
  174. }
  175. /**
  176. * @param array $opts cURL options
  177. */
  178. private function executeRequestWithRetries($opts, $absUrl)
  179. {
  180. $numRetries = 0;
  181. while (true) {
  182. $rcode = 0;
  183. $errno = 0;
  184. $curl = curl_init();
  185. curl_setopt_array($curl, $opts);
  186. $rbody = curl_exec($curl);
  187. if ($rbody === false) {
  188. $errno = curl_errno($curl);
  189. $message = curl_error($curl);
  190. } else {
  191. $rcode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
  192. }
  193. curl_close($curl);
  194. if ($this->shouldRetry($errno, $rcode, $numRetries)) {
  195. $numRetries += 1;
  196. $sleepSeconds = $this->sleepTime($numRetries);
  197. usleep(intval($sleepSeconds * 1000000));
  198. } else {
  199. break;
  200. }
  201. }
  202. if ($rbody === false) {
  203. $this->handleCurlError($absUrl, $errno, $message, $numRetries);
  204. }
  205. return [$rbody, $rcode];
  206. }
  207. /**
  208. * @param string $url
  209. * @param int $errno
  210. * @param string $message
  211. * @param int $numRetries
  212. * @throws Error\ApiConnection
  213. */
  214. private function handleCurlError($url, $errno, $message, $numRetries)
  215. {
  216. switch ($errno) {
  217. case CURLE_COULDNT_CONNECT:
  218. case CURLE_COULDNT_RESOLVE_HOST:
  219. case CURLE_OPERATION_TIMEOUTED:
  220. $msg = "Could not connect to Stripe ($url). Please check your "
  221. . "internet connection and try again. If this problem persists, "
  222. . "you should check Stripe's service status at "
  223. . "https://twitter.com/stripestatus, or";
  224. break;
  225. case CURLE_SSL_CACERT:
  226. case CURLE_SSL_PEER_CERTIFICATE:
  227. $msg = "Could not verify Stripe's SSL certificate. Please make sure "
  228. . "that your network is not intercepting certificates. "
  229. . "(Try going to $url in your browser.) "
  230. . "If this problem persists,";
  231. break;
  232. default:
  233. $msg = "Unexpected error communicating with Stripe. "
  234. . "If this problem persists,";
  235. }
  236. $msg .= " let us know at support@stripe.com.";
  237. $msg .= "\n\n(Network error [errno $errno]: $message)";
  238. if ($numRetries > 0) {
  239. $msg .= "\n\nRequest was retried $numRetries times.";
  240. }
  241. throw new Error\ApiConnection($msg);
  242. }
  243. /**
  244. * Checks if an error is a problem that we should retry on. This includes both
  245. * socket errors that may represent an intermittent problem and some special
  246. * HTTP statuses.
  247. * @param int $errno
  248. * @param int $rcode
  249. * @param int $numRetries
  250. * @return bool
  251. */
  252. private function shouldRetry($errno, $rcode, $numRetries)
  253. {
  254. if ($numRetries >= Stripe::getMaxNetworkRetries()) {
  255. return false;
  256. }
  257. // Retry on timeout-related problems (either on open or read).
  258. if ($errno === CURLE_OPERATION_TIMEOUTED) {
  259. return true;
  260. }
  261. // Destination refused the connection, the connection was reset, or a
  262. // variety of other connection failures. This could occur from a single
  263. // saturated server, so retry in case it's intermittent.
  264. if ($errno === CURLE_COULDNT_CONNECT) {
  265. return true;
  266. }
  267. // 409 conflict
  268. if ($rcode === 409) {
  269. return true;
  270. }
  271. return false;
  272. }
  273. private function sleepTime($numRetries)
  274. {
  275. // Apply exponential backoff with $initialNetworkRetryDelay on the
  276. // number of $numRetries so far as inputs. Do not allow the number to exceed
  277. // $maxNetworkRetryDelay.
  278. $sleepSeconds = min(
  279. Stripe::getInitialNetworkRetryDelay() * 1.0 * pow(2, $numRetries - 1),
  280. Stripe::getMaxNetworkRetryDelay()
  281. );
  282. // Apply some jitter by randomizing the value in the range of
  283. // ($sleepSeconds / 2) to ($sleepSeconds).
  284. $sleepSeconds *= 0.5 * (1 + $this->randomGenerator->randFloat());
  285. // But never sleep less than the base sleep seconds.
  286. $sleepSeconds = max(Stripe::getInitialNetworkRetryDelay(), $sleepSeconds);
  287. return $sleepSeconds;
  288. }
  289. }