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