PageRenderTime 51ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/app/code/local/Netgains/Stripe/Model/stripe/lib/Stripe/ApiRequestor.php

https://gitlab.com/vincent.perdereau/picandparts
PHP | 399 lines | 308 code | 37 blank | 54 comment | 54 complexity | f44a40817d411f8b9cbfdedfd27f0fa5 MD5 | raw file
  1. <?php
  2. class Stripe_ApiRequestor
  3. {
  4. /**
  5. * @var string $apiKey The API key that's to be used to make requests.
  6. */
  7. public $apiKey;
  8. private static $preFlight;
  9. private static function blacklistedCerts()
  10. {
  11. return array(
  12. '05c0b3643694470a888c6e7feb5c9e24e823dc53',
  13. '5b7dc7fbc98d78bf76d4d4fa6f597a0c901fad5c',
  14. );
  15. }
  16. public function __construct($apiKey=null)
  17. {
  18. $this->_apiKey = $apiKey;
  19. }
  20. /**
  21. * @param string $url The path to the API endpoint.
  22. *
  23. * @returns string The full path.
  24. */
  25. public static function apiUrl($url='')
  26. {
  27. $apiBase = Stripe::$apiBase;
  28. return "$apiBase$url";
  29. }
  30. /**
  31. * @param string|mixed $value A string to UTF8-encode.
  32. *
  33. * @returns string|mixed The UTF8-encoded string, or the object passed in if
  34. * it wasn't a string.
  35. */
  36. public static function utf8($value)
  37. {
  38. if (is_string($value)
  39. && mb_detect_encoding($value, "UTF-8", TRUE) != "UTF-8") {
  40. return utf8_encode($value);
  41. } else {
  42. return $value;
  43. }
  44. }
  45. private static function _encodeObjects($d)
  46. {
  47. if ($d instanceof Stripe_ApiResource) {
  48. return self::utf8($d->id);
  49. } else if ($d === true) {
  50. return 'true';
  51. } else if ($d === false) {
  52. return 'false';
  53. } else if (is_array($d)) {
  54. $res = array();
  55. foreach ($d as $k => $v)
  56. $res[$k] = self::_encodeObjects($v);
  57. return $res;
  58. } else {
  59. return self::utf8($d);
  60. }
  61. }
  62. /**
  63. * @param array $arr An map of param keys to values.
  64. * @param string|null $prefix (It doesn't look like we ever use $prefix...)
  65. *
  66. * @returns string A querystring, essentially.
  67. */
  68. public static function encode($arr, $prefix=null)
  69. {
  70. if (!is_array($arr))
  71. return $arr;
  72. $r = array();
  73. foreach ($arr as $k => $v) {
  74. if (is_null($v))
  75. continue;
  76. if ($prefix && $k && !is_int($k))
  77. $k = $prefix."[".$k."]";
  78. else if ($prefix)
  79. $k = $prefix."[]";
  80. if (is_array($v)) {
  81. $r[] = self::encode($v, $k, true);
  82. } else {
  83. $r[] = urlencode($k)."=".urlencode($v);
  84. }
  85. }
  86. return implode("&", $r);
  87. }
  88. /**
  89. * @param string $method
  90. * @param string $url
  91. * @param array|null $params
  92. *
  93. * @return array An array whose first element is the response and second
  94. * element is the API key used to make the request.
  95. */
  96. public function request($method, $url, $params=null)
  97. {
  98. if (!$params)
  99. $params = array();
  100. list($rbody, $rcode, $myApiKey) = $this->_requestRaw($method, $url, $params);
  101. $resp = $this->_interpretResponse($rbody, $rcode);
  102. return array($resp, $myApiKey);
  103. }
  104. /**
  105. * @param string $rbody A JSON string.
  106. * @param int $rcode
  107. * @param array $resp
  108. *
  109. * @throws Stripe_InvalidRequestError if the error is caused by the user.
  110. * @throws Stripe_AuthenticationError if the error is caused by a lack of
  111. * permissions.
  112. * @throws Stripe_CardError if the error is the error code is 402 (payment
  113. * required)
  114. * @throws Stripe_ApiError otherwise.
  115. */
  116. public function handleApiError($rbody, $rcode, $resp)
  117. {
  118. if (!is_array($resp) || !isset($resp['error'])) {
  119. $msg = "Invalid response object from API: $rbody "
  120. ."(HTTP response code was $rcode)";
  121. throw new Stripe_ApiError($msg, $rcode, $rbody, $resp);
  122. }
  123. $error = $resp['error'];
  124. $msg = isset($error['message']) ? $error['message'] : null;
  125. $param = isset($error['param']) ? $error['param'] : null;
  126. $code = isset($error['code']) ? $error['code'] : null;
  127. switch ($rcode) {
  128. case 400:
  129. if ($code == 'rate_limit') {
  130. throw new Stripe_RateLimitError(
  131. $msg, $param, $rcode, $rbody, $resp
  132. );
  133. }
  134. case 404:
  135. throw new Stripe_InvalidRequestError(
  136. $msg, $param, $rcode, $rbody, $resp
  137. );
  138. case 401:
  139. throw new Stripe_AuthenticationError($msg, $rcode, $rbody, $resp);
  140. case 402:
  141. throw new Stripe_CardError($msg, $param, $code, $rcode, $rbody, $resp);
  142. default:
  143. throw new Stripe_ApiError($msg, $rcode, $rbody, $resp);
  144. }
  145. }
  146. private function _requestRaw($method, $url, $params)
  147. {
  148. $myApiKey = $this->_apiKey;
  149. if (!$myApiKey)
  150. $myApiKey = Stripe::$apiKey;
  151. if (!$myApiKey) {
  152. $msg = 'No API key provided. (HINT: set your API key using '
  153. . '"Stripe::setApiKey(<API-KEY>)". You can generate API keys from '
  154. . 'the Stripe web interface. See https://stripe.com/api for '
  155. . 'details, or email support@stripe.com if you have any questions.';
  156. throw new Stripe_AuthenticationError($msg);
  157. }
  158. $absUrl = $this->apiUrl($url);
  159. $params = self::_encodeObjects($params);
  160. $langVersion = phpversion();
  161. $uname = php_uname();
  162. $ua = array('bindings_version' => Stripe::VERSION,
  163. 'lang' => 'php',
  164. 'lang_version' => $langVersion,
  165. 'publisher' => 'stripe',
  166. 'uname' => $uname);
  167. $headers = array('X-Stripe-Client-User-Agent: ' . json_encode($ua),
  168. 'User-Agent: Stripe/v1 PhpBindings/' . Stripe::VERSION,
  169. 'Authorization: Bearer ' . $myApiKey);
  170. if (Stripe::$apiVersion)
  171. $headers[] = 'Stripe-Version: ' . Stripe::$apiVersion;
  172. list($rbody, $rcode) = $this->_curlRequest(
  173. $method,
  174. $absUrl,
  175. $headers,
  176. $params
  177. );
  178. return array($rbody, $rcode, $myApiKey);
  179. }
  180. private function _interpretResponse($rbody, $rcode)
  181. {
  182. try {
  183. $resp = json_decode($rbody, true);
  184. } catch (Exception $e) {
  185. $msg = "Invalid response body from API: $rbody "
  186. . "(HTTP response code was $rcode)";
  187. throw new Stripe_ApiError($msg, $rcode, $rbody);
  188. }
  189. if ($rcode < 200 || $rcode >= 300) {
  190. $this->handleApiError($rbody, $rcode, $resp);
  191. }
  192. return $resp;
  193. }
  194. private function _curlRequest($method, $absUrl, $headers, $params)
  195. {
  196. if (!self::$preFlight) {
  197. self::$preFlight = $this->checkSslCert($this->apiUrl());
  198. }
  199. $curl = curl_init();
  200. $method = strtolower($method);
  201. $opts = array();
  202. if ($method == 'get') {
  203. $opts[CURLOPT_HTTPGET] = 1;
  204. if (count($params) > 0) {
  205. $encoded = self::encode($params);
  206. $absUrl = "$absUrl?$encoded";
  207. }
  208. } else if ($method == 'post') {
  209. $opts[CURLOPT_POST] = 1;
  210. $opts[CURLOPT_POSTFIELDS] = self::encode($params);
  211. } else if ($method == 'delete') {
  212. $opts[CURLOPT_CUSTOMREQUEST] = 'DELETE';
  213. if (count($params) > 0) {
  214. $encoded = self::encode($params);
  215. $absUrl = "$absUrl?$encoded";
  216. }
  217. } else {
  218. throw new Stripe_ApiError("Unrecognized method $method");
  219. }
  220. $absUrl = self::utf8($absUrl);
  221. $opts[CURLOPT_URL] = $absUrl;
  222. $opts[CURLOPT_RETURNTRANSFER] = true;
  223. $opts[CURLOPT_CONNECTTIMEOUT] = 30;
  224. $opts[CURLOPT_TIMEOUT] = 80;
  225. $opts[CURLOPT_RETURNTRANSFER] = true;
  226. $opts[CURLOPT_HTTPHEADER] = $headers;
  227. if (!Stripe::$verifySslCerts)
  228. $opts[CURLOPT_SSL_VERIFYPEER] = false;
  229. curl_setopt_array($curl, $opts);
  230. $rbody = curl_exec($curl);
  231. if (!defined('CURLE_SSL_CACERT_BADFILE')) {
  232. define('CURLE_SSL_CACERT_BADFILE', 77); // constant not defined in PHP
  233. }
  234. $errno = curl_errno($curl);
  235. if ($errno == CURLE_SSL_CACERT ||
  236. $errno == CURLE_SSL_PEER_CERTIFICATE ||
  237. $errno == CURLE_SSL_CACERT_BADFILE) {
  238. array_push(
  239. $headers,
  240. 'X-Stripe-Client-Info: {"ca":"using Stripe-supplied CA bundle"}'
  241. );
  242. $cert = $this->caBundle();
  243. curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
  244. curl_setopt($curl, CURLOPT_CAINFO, $cert);
  245. $rbody = curl_exec($curl);
  246. }
  247. if ($rbody === false) {
  248. $errno = curl_errno($curl);
  249. $message = curl_error($curl);
  250. curl_close($curl);
  251. $this->handleCurlError($errno, $message);
  252. }
  253. $rcode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
  254. curl_close($curl);
  255. return array($rbody, $rcode);
  256. }
  257. /**
  258. * @param number $errno
  259. * @param string $message
  260. * @throws Stripe_ApiConnectionError
  261. */
  262. public function handleCurlError($errno, $message)
  263. {
  264. $apiBase = Stripe::$apiBase;
  265. switch ($errno) {
  266. case CURLE_COULDNT_CONNECT:
  267. case CURLE_COULDNT_RESOLVE_HOST:
  268. case CURLE_OPERATION_TIMEOUTED:
  269. $msg = "Could not connect to Stripe ($apiBase). Please check your "
  270. . "internet connection and try again. If this problem persists, "
  271. . "you should check Stripe's service status at "
  272. . "https://twitter.com/stripestatus, or";
  273. break;
  274. case CURLE_SSL_CACERT:
  275. case CURLE_SSL_PEER_CERTIFICATE:
  276. $msg = "Could not verify Stripe's SSL certificate. Please make sure "
  277. . "that your network is not intercepting certificates. "
  278. . "(Try going to $apiBase in your browser.) "
  279. . "If this problem persists,";
  280. break;
  281. default:
  282. $msg = "Unexpected error communicating with Stripe. "
  283. . "If this problem persists,";
  284. }
  285. $msg .= " let us know at support@stripe.com.";
  286. $msg .= "\n\n(Network error [errno $errno]: $message)";
  287. throw new Stripe_ApiConnectionError($msg);
  288. }
  289. private function checkSslCert($url)
  290. {
  291. /* Preflight the SSL certificate presented by the backend. This isn't 100%
  292. * bulletproof, in that we're not actually validating the transport used to
  293. * communicate with Stripe, merely that the first attempt to does not use a
  294. * revoked certificate.
  295. * Unfortunately the interface to OpenSSL doesn't make it easy to check the
  296. * certificate before sending potentially sensitive data on the wire. This
  297. * approach raises the bar for an attacker significantly.
  298. */
  299. if (version_compare(PHP_VERSION, '5.3.0', '<')) {
  300. error_log("Warning: This version of PHP is too old to check SSL certificates correctly. " .
  301. "Stripe cannot guarantee that the server has a certificate which is not blacklisted");
  302. return true;
  303. }
  304. $url = parse_url($url);
  305. $port = isset($url["port"]) ? $url["port"] : 443;
  306. $url = "ssl://{$url["host"]}:{$port}";
  307. $sslContext = stream_context_create(array( 'ssl' => array(
  308. 'capture_peer_cert' => true,
  309. 'verify_peer' => true,
  310. 'cafile' => $this->caBundle(),
  311. )));
  312. $result = stream_socket_client($url, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $sslContext);
  313. if ($errno !== 0) {
  314. throw new Stripe_ApiConnectionError(
  315. "Could not connect to Stripe ($apiBase). Please check your "
  316. . "internet connection and try again. If this problem persists, "
  317. . "you should check Stripe's service status at "
  318. . "https://twitter.com/stripestatus. Reason was: $errstr"
  319. );
  320. }
  321. $params = stream_context_get_params($result);
  322. $cert = $params['options']['ssl']['peer_certificate'];
  323. $cert_data = openssl_x509_parse( $cert );
  324. openssl_x509_export($cert, $pem_cert);
  325. if (self::isBlackListed($pem_cert)) {
  326. throw new Stripe_ApiConnectionError(
  327. "Invalid server certificate. You tried to connect to a server that has a " .
  328. "revoked SSL certificate, which means we cannot securely send data to " .
  329. "that server. Please email support@stripe.com if you need help " .
  330. "connecting to the correct API server."
  331. );
  332. }
  333. return true;
  334. }
  335. /* Checks if a valid PEM encoded certificate is blacklisted
  336. * @return boolean
  337. */
  338. public static function isBlackListed($certificate)
  339. {
  340. $certificate = trim($certificate);
  341. $lines = explode("\n", $certificate);
  342. // Kludgily remove the PEM padding
  343. array_shift($lines); array_pop($lines);
  344. $der_cert = base64_decode(implode("", $lines));
  345. $fingerprint = sha1($der_cert);
  346. return in_array($fingerprint, self::blacklistedCerts());
  347. }
  348. private function caBundle()
  349. {
  350. return dirname(__FILE__) . '/../data/ca-certificates.crt';
  351. }
  352. }