123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341 |
- <?php
- namespace Stripe\HttpClient;
- use Stripe\Stripe;
- use Stripe\Error;
- use Stripe\Util;
- // cURL constants are not defined in PHP < 5.5
- // @codingStandardsIgnoreStart
- // PSR2 requires all constants be upper case. Sadly, the CURL_SSLVERSION
- // constants do not abide by those rules.
- // Note the values 1 and 6 come from their position in the enum that
- // defines them in cURL's source code.
- if (!defined('CURL_SSLVERSION_TLSv1')) {
- define('CURL_SSLVERSION_TLSv1', 1);
- }
- if (!defined('CURL_SSLVERSION_TLSv1_2')) {
- define('CURL_SSLVERSION_TLSv1_2', 6);
- }
- // @codingStandardsIgnoreEnd
- if (!defined('CURL_HTTP_VERSION_2TLS')) {
- define('CURL_HTTP_VERSION_2TLS', 4);
- }
- class CurlClient implements ClientInterface
- {
- private static $instance;
- public static function instance()
- {
- if (!self::$instance) {
- self::$instance = new self();
- }
- return self::$instance;
- }
- protected $defaultOptions;
- protected $userAgentInfo;
- /**
- * CurlClient constructor.
- *
- * Pass in a callable to $defaultOptions that returns an array of CURLOPT_* values to start
- * off a request with, or an flat array with the same format used by curl_setopt_array() to
- * provide a static set of options. Note that many options are overridden later in the request
- * call, including timeouts, which can be set via setTimeout() and setConnectTimeout().
- *
- * Note that request() will silently ignore a non-callable, non-array $defaultOptions, and will
- * throw an exception if $defaultOptions returns a non-array value.
- *
- * @param array|callable|null $defaultOptions
- */
- public function __construct($defaultOptions = null, $randomGenerator = null)
- {
- $this->defaultOptions = $defaultOptions;
- $this->randomGenerator = $randomGenerator ?: new Util\RandomGenerator();
- $this->initUserAgentInfo();
- }
- public function initUserAgentInfo()
- {
- $curlVersion = curl_version();
- $this->userAgentInfo = [
- 'httplib' => 'curl ' . $curlVersion['version'],
- 'ssllib' => $curlVersion['ssl_version'],
- ];
- }
- public function getDefaultOptions()
- {
- return $this->defaultOptions;
- }
- public function getUserAgentInfo()
- {
- return $this->userAgentInfo;
- }
- // USER DEFINED TIMEOUTS
- const DEFAULT_TIMEOUT = 80;
- const DEFAULT_CONNECT_TIMEOUT = 30;
- private $timeout = self::DEFAULT_TIMEOUT;
- private $connectTimeout = self::DEFAULT_CONNECT_TIMEOUT;
- public function setTimeout($seconds)
- {
- $this->timeout = (int) max($seconds, 0);
- return $this;
- }
- public function setConnectTimeout($seconds)
- {
- $this->connectTimeout = (int) max($seconds, 0);
- return $this;
- }
- public function getTimeout()
- {
- return $this->timeout;
- }
- public function getConnectTimeout()
- {
- return $this->connectTimeout;
- }
- // END OF USER DEFINED TIMEOUTS
- public function request($method, $absUrl, $headers, $params, $hasFile)
- {
- $method = strtolower($method);
- $opts = [];
- if (is_callable($this->defaultOptions)) { // call defaultOptions callback, set options to return value
- $opts = call_user_func_array($this->defaultOptions, func_get_args());
- if (!is_array($opts)) {
- throw new Error\Api("Non-array value returned by defaultOptions CurlClient callback");
- }
- } elseif (is_array($this->defaultOptions)) { // set default curlopts from array
- $opts = $this->defaultOptions;
- }
- if ($method == 'get') {
- if ($hasFile) {
- throw new Error\Api(
- "Issuing a GET request with a file parameter"
- );
- }
- $opts[CURLOPT_HTTPGET] = 1;
- if (count($params) > 0) {
- $encoded = Util\Util::urlEncode($params);
- $absUrl = "$absUrl?$encoded";
- }
- } elseif ($method == 'post') {
- $opts[CURLOPT_POST] = 1;
- $opts[CURLOPT_POSTFIELDS] = $hasFile ? $params : Util\Util::urlEncode($params);
- } elseif ($method == 'delete') {
- $opts[CURLOPT_CUSTOMREQUEST] = 'DELETE';
- if (count($params) > 0) {
- $encoded = Util\Util::urlEncode($params);
- $absUrl = "$absUrl?$encoded";
- }
- } else {
- throw new Error\Api("Unrecognized method $method");
- }
- // It is only safe to retry network failures on POST requests if we
- // add an Idempotency-Key header
- if (($method == 'post') && (Stripe::$maxNetworkRetries > 0)) {
- if (!isset($headers['Idempotency-Key'])) {
- array_push($headers, 'Idempotency-Key: ' . $this->randomGenerator->uuid());
- }
- }
- // Create a callback to capture HTTP headers for the response
- $rheaders = [];
- $headerCallback = function ($curl, $header_line) use (&$rheaders) {
- // Ignore the HTTP request line (HTTP/1.1 200 OK)
- if (strpos($header_line, ":") === false) {
- return strlen($header_line);
- }
- list($key, $value) = explode(":", trim($header_line), 2);
- $rheaders[trim($key)] = trim($value);
- return strlen($header_line);
- };
- // By default for large request body sizes (> 1024 bytes), cURL will
- // send a request without a body and with a `Expect: 100-continue`
- // header, which gives the server a chance to respond with an error
- // status code in cases where one can be determined right away (say
- // on an authentication problem for example), and saves the "large"
- // request body from being ever sent.
- //
- // Unfortunately, the bindings don't currently correctly handle the
- // success case (in which the server sends back a 100 CONTINUE), so
- // we'll error under that condition. To compensate for that problem
- // for the time being, override cURL's behavior by simply always
- // sending an empty `Expect:` header.
- array_push($headers, 'Expect: ');
- $absUrl = Util\Util::utf8($absUrl);
- $opts[CURLOPT_URL] = $absUrl;
- $opts[CURLOPT_RETURNTRANSFER] = true;
- $opts[CURLOPT_CONNECTTIMEOUT] = $this->connectTimeout;
- $opts[CURLOPT_TIMEOUT] = $this->timeout;
- $opts[CURLOPT_HEADERFUNCTION] = $headerCallback;
- $opts[CURLOPT_HTTPHEADER] = $headers;
- $opts[CURLOPT_CAINFO] = Stripe::getCABundlePath();
- if (!Stripe::getVerifySslCerts()) {
- $opts[CURLOPT_SSL_VERIFYPEER] = false;
- }
- // For HTTPS requests, enable HTTP/2, if supported
- $opts[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2TLS;
- list($rbody, $rcode) = $this->executeRequestWithRetries($opts, $absUrl);
- return [$rbody, $rcode, $rheaders];
- }
- /**
- * @param array $opts cURL options
- */
- private function executeRequestWithRetries($opts, $absUrl)
- {
- $numRetries = 0;
- while (true) {
- $rcode = 0;
- $errno = 0;
- $curl = curl_init();
- curl_setopt_array($curl, $opts);
- $rbody = curl_exec($curl);
- if ($rbody === false) {
- $errno = curl_errno($curl);
- $message = curl_error($curl);
- } else {
- $rcode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
- }
- curl_close($curl);
- if ($this->shouldRetry($errno, $rcode, $numRetries)) {
- $numRetries += 1;
- $sleepSeconds = $this->sleepTime($numRetries);
- usleep(intval($sleepSeconds * 1000000));
- } else {
- break;
- }
- }
- if ($rbody === false) {
- $this->handleCurlError($absUrl, $errno, $message, $numRetries);
- }
- return [$rbody, $rcode];
- }
- /**
- * @param string $url
- * @param int $errno
- * @param string $message
- * @param int $numRetries
- * @throws Error\ApiConnection
- */
- private function handleCurlError($url, $errno, $message, $numRetries)
- {
- switch ($errno) {
- case CURLE_COULDNT_CONNECT:
- case CURLE_COULDNT_RESOLVE_HOST:
- case CURLE_OPERATION_TIMEOUTED:
- $msg = "Could not connect to Stripe ($url). Please check your "
- . "internet connection and try again. If this problem persists, "
- . "you should check Stripe's service status at "
- . "https://twitter.com/stripestatus, or";
- break;
- case CURLE_SSL_CACERT:
- case CURLE_SSL_PEER_CERTIFICATE:
- $msg = "Could not verify Stripe's SSL certificate. Please make sure "
- . "that your network is not intercepting certificates. "
- . "(Try going to $url in your browser.) "
- . "If this problem persists,";
- break;
- default:
- $msg = "Unexpected error communicating with Stripe. "
- . "If this problem persists,";
- }
- $msg .= " let us know at support@stripe.com.";
- $msg .= "\n\n(Network error [errno $errno]: $message)";
- if ($numRetries > 0) {
- $msg .= "\n\nRequest was retried $numRetries times.";
- }
- throw new Error\ApiConnection($msg);
- }
- /**
- * Checks if an error is a problem that we should retry on. This includes both
- * socket errors that may represent an intermittent problem and some special
- * HTTP statuses.
- * @param int $errno
- * @param int $rcode
- * @param int $numRetries
- * @return bool
- */
- private function shouldRetry($errno, $rcode, $numRetries)
- {
- if ($numRetries >= Stripe::getMaxNetworkRetries()) {
- return false;
- }
- // Retry on timeout-related problems (either on open or read).
- if ($errno === CURLE_OPERATION_TIMEOUTED) {
- return true;
- }
- // Destination refused the connection, the connection was reset, or a
- // variety of other connection failures. This could occur from a single
- // saturated server, so retry in case it's intermittent.
- if ($errno === CURLE_COULDNT_CONNECT) {
- return true;
- }
- // 409 conflict
- if ($rcode === 409) {
- return true;
- }
- return false;
- }
- private function sleepTime($numRetries)
- {
- // Apply exponential backoff with $initialNetworkRetryDelay on the
- // number of $numRetries so far as inputs. Do not allow the number to exceed
- // $maxNetworkRetryDelay.
- $sleepSeconds = min(
- Stripe::getInitialNetworkRetryDelay() * 1.0 * pow(2, $numRetries - 1),
- Stripe::getMaxNetworkRetryDelay()
- );
- // Apply some jitter by randomizing the value in the range of
- // ($sleepSeconds / 2) to ($sleepSeconds).
- $sleepSeconds *= 0.5 * (1 + $this->randomGenerator->randFloat());
- // But never sleep less than the base sleep seconds.
- $sleepSeconds = max(Stripe::getInitialNetworkRetryDelay(), $sleepSeconds);
- return $sleepSeconds;
- }
- }
|