CurlClient.php 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  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. class CurlClient implements ClientInterface
  20. {
  21. private static $instance;
  22. public static function instance()
  23. {
  24. if (!self::$instance) {
  25. self::$instance = new self();
  26. }
  27. return self::$instance;
  28. }
  29. protected $defaultOptions;
  30. /**
  31. * CurlClient constructor.
  32. *
  33. * Pass in a callable to $defaultOptions that returns an array of CURLOPT_* values to start
  34. * off a request with, or an flat array with the same format used by curl_setopt_array() to
  35. * provide a static set of options. Note that many options are overridden later in the request
  36. * call, including timeouts, which can be set via setTimeout() and setConnectTimeout().
  37. *
  38. * Note that request() will silently ignore a non-callable, non-array $defaultOptions, and will
  39. * throw an exception if $defaultOptions returns a non-array value.
  40. *
  41. * @param array|callable|null $defaultOptions
  42. */
  43. public function __construct($defaultOptions = null)
  44. {
  45. $this->defaultOptions = $defaultOptions;
  46. }
  47. public function getDefaultOptions()
  48. {
  49. return $this->defaultOptions;
  50. }
  51. // USER DEFINED TIMEOUTS
  52. const DEFAULT_TIMEOUT = 80;
  53. const DEFAULT_CONNECT_TIMEOUT = 30;
  54. private $timeout = self::DEFAULT_TIMEOUT;
  55. private $connectTimeout = self::DEFAULT_CONNECT_TIMEOUT;
  56. public function setTimeout($seconds)
  57. {
  58. $this->timeout = (int) max($seconds, 0);
  59. return $this;
  60. }
  61. public function setConnectTimeout($seconds)
  62. {
  63. $this->connectTimeout = (int) max($seconds, 0);
  64. return $this;
  65. }
  66. public function getTimeout()
  67. {
  68. return $this->timeout;
  69. }
  70. public function getConnectTimeout()
  71. {
  72. return $this->connectTimeout;
  73. }
  74. // END OF USER DEFINED TIMEOUTS
  75. public function request($method, $absUrl, $headers, $params, $hasFile)
  76. {
  77. $curl = curl_init();
  78. $method = strtolower($method);
  79. $opts = array();
  80. if (is_callable($this->defaultOptions)) { // call defaultOptions callback, set options to return value
  81. $opts = call_user_func_array($this->defaultOptions, func_get_args());
  82. if (!is_array($opts)) {
  83. throw new Error\Api("Non-array value returned by defaultOptions CurlClient callback");
  84. }
  85. } elseif (is_array($this->defaultOptions)) { // set default curlopts from array
  86. $opts = $this->defaultOptions;
  87. }
  88. if ($method == 'get') {
  89. if ($hasFile) {
  90. throw new Error\Api(
  91. "Issuing a GET request with a file parameter"
  92. );
  93. }
  94. $opts[CURLOPT_HTTPGET] = 1;
  95. if (count($params) > 0) {
  96. $encoded = self::encode($params);
  97. $absUrl = "$absUrl?$encoded";
  98. }
  99. } elseif ($method == 'post') {
  100. $opts[CURLOPT_POST] = 1;
  101. $opts[CURLOPT_POSTFIELDS] = $hasFile ? $params : self::encode($params);
  102. } elseif ($method == 'delete') {
  103. $opts[CURLOPT_CUSTOMREQUEST] = 'DELETE';
  104. if (count($params) > 0) {
  105. $encoded = self::encode($params);
  106. $absUrl = "$absUrl?$encoded";
  107. }
  108. } else {
  109. throw new Error\Api("Unrecognized method $method");
  110. }
  111. // Create a callback to capture HTTP headers for the response
  112. $rheaders = array();
  113. $headerCallback = function ($curl, $header_line) use (&$rheaders) {
  114. // Ignore the HTTP request line (HTTP/1.1 200 OK)
  115. if (strpos($header_line, ":") === false) {
  116. return strlen($header_line);
  117. }
  118. list($key, $value) = explode(":", trim($header_line), 2);
  119. $rheaders[trim($key)] = trim($value);
  120. return strlen($header_line);
  121. };
  122. // By default for large request body sizes (> 1024 bytes), cURL will
  123. // send a request without a body and with a `Expect: 100-continue`
  124. // header, which gives the server a chance to respond with an error
  125. // status code in cases where one can be determined right away (say
  126. // on an authentication problem for example), and saves the "large"
  127. // request body from being ever sent.
  128. //
  129. // Unfortunately, the bindings don't currently correctly handle the
  130. // success case (in which the server sends back a 100 CONTINUE), so
  131. // we'll error under that condition. To compensate for that problem
  132. // for the time being, override cURL's behavior by simply always
  133. // sending an empty `Expect:` header.
  134. array_push($headers, 'Expect: ');
  135. $absUrl = Util\Util::utf8($absUrl);
  136. $opts[CURLOPT_URL] = $absUrl;
  137. $opts[CURLOPT_RETURNTRANSFER] = true;
  138. $opts[CURLOPT_CONNECTTIMEOUT] = $this->connectTimeout;
  139. $opts[CURLOPT_TIMEOUT] = $this->timeout;
  140. $opts[CURLOPT_HEADERFUNCTION] = $headerCallback;
  141. $opts[CURLOPT_HTTPHEADER] = $headers;
  142. if (!Stripe::$verifySslCerts) {
  143. $opts[CURLOPT_SSL_VERIFYPEER] = false;
  144. }
  145. curl_setopt_array($curl, $opts);
  146. $rbody = curl_exec($curl);
  147. if (!defined('CURLE_SSL_CACERT_BADFILE')) {
  148. define('CURLE_SSL_CACERT_BADFILE', 77); // constant not defined in PHP
  149. }
  150. $errno = curl_errno($curl);
  151. if ($errno == CURLE_SSL_CACERT ||
  152. $errno == CURLE_SSL_PEER_CERTIFICATE ||
  153. $errno == CURLE_SSL_CACERT_BADFILE
  154. ) {
  155. array_push(
  156. $headers,
  157. 'X-Stripe-Client-Info: {"ca":"using Stripe-supplied CA bundle"}'
  158. );
  159. $cert = self::caBundle();
  160. curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
  161. curl_setopt($curl, CURLOPT_CAINFO, $cert);
  162. $rbody = curl_exec($curl);
  163. }
  164. if ($rbody === false) {
  165. $errno = curl_errno($curl);
  166. $message = curl_error($curl);
  167. curl_close($curl);
  168. $this->handleCurlError($absUrl, $errno, $message);
  169. }
  170. $rcode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
  171. curl_close($curl);
  172. return array($rbody, $rcode, $rheaders);
  173. }
  174. /**
  175. * @param number $errno
  176. * @param string $message
  177. * @throws Error\ApiConnection
  178. */
  179. private function handleCurlError($url, $errno, $message)
  180. {
  181. switch ($errno) {
  182. case CURLE_COULDNT_CONNECT:
  183. case CURLE_COULDNT_RESOLVE_HOST:
  184. case CURLE_OPERATION_TIMEOUTED:
  185. $msg = "Could not connect to Stripe ($url). Please check your "
  186. . "internet connection and try again. If this problem persists, "
  187. . "you should check Stripe's service status at "
  188. . "https://twitter.com/stripestatus, or";
  189. break;
  190. case CURLE_SSL_CACERT:
  191. case CURLE_SSL_PEER_CERTIFICATE:
  192. $msg = "Could not verify Stripe's SSL certificate. Please make sure "
  193. . "that your network is not intercepting certificates. "
  194. . "(Try going to $url in your browser.) "
  195. . "If this problem persists,";
  196. break;
  197. default:
  198. $msg = "Unexpected error communicating with Stripe. "
  199. . "If this problem persists,";
  200. }
  201. $msg .= " let us know at support@stripe.com.";
  202. $msg .= "\n\n(Network error [errno $errno]: $message)";
  203. throw new Error\ApiConnection($msg);
  204. }
  205. private static function caBundle()
  206. {
  207. return dirname(__FILE__) . '/../../data/ca-certificates.crt';
  208. }
  209. /**
  210. * @param array $arr An map of param keys to values.
  211. * @param string|null $prefix
  212. *
  213. * Only public for testability, should not be called outside of CurlClient
  214. *
  215. * @return string A querystring, essentially.
  216. */
  217. public static function encode($arr, $prefix = null)
  218. {
  219. if (!is_array($arr)) {
  220. return $arr;
  221. }
  222. $r = array();
  223. foreach ($arr as $k => $v) {
  224. if (is_null($v)) {
  225. continue;
  226. }
  227. if ($prefix) {
  228. if ($k !== null && (!is_int($k) || is_array($v))) {
  229. $k = $prefix."[".$k."]";
  230. } else {
  231. $k = $prefix."[]";
  232. }
  233. }
  234. if (is_array($v)) {
  235. $enc = self::encode($v, $k);
  236. if ($enc) {
  237. $r[] = $enc;
  238. }
  239. } else {
  240. $r[] = urlencode($k)."=".urlencode($v);
  241. }
  242. }
  243. return implode("&", $r);
  244. }
  245. }