M2_Stripe_ApiRequestor

The Membership 2 M2 Stripe ApiRequestor class.

Defined (1)

The class is defined in the following location(s).

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