Changeset View
Changeset View
Standalone View
Standalone View
externals/stripe-php/lib/Stripe/ApiRequestor.php
| <?php | <?php | ||||
| class Stripe_ApiRequestor | class Stripe_ApiRequestor | ||||
| { | { | ||||
| /** | |||||
| * @var string $apiKey The API key that's to be used to make requests. | |||||
| */ | |||||
| public $apiKey; | public $apiKey; | ||||
| private static $_preFlight; | |||||
| private static function blacklistedCerts() | |||||
| { | |||||
| return array( | |||||
| '05c0b3643694470a888c6e7feb5c9e24e823dc53', | |||||
| '5b7dc7fbc98d78bf76d4d4fa6f597a0c901fad5c', | |||||
| ); | |||||
| } | |||||
| public function __construct($apiKey=null) | public function __construct($apiKey=null) | ||||
| { | { | ||||
| $this->_apiKey = $apiKey; | $this->_apiKey = $apiKey; | ||||
| } | } | ||||
| /** | |||||
| * @param string $url The path to the API endpoint. | |||||
| * | |||||
| * @returns string The full path. | |||||
| */ | |||||
| public static function apiUrl($url='') | public static function apiUrl($url='') | ||||
| { | { | ||||
| $apiBase = Stripe::$apiBase; | $apiBase = Stripe::$apiBase; | ||||
| return "$apiBase$url"; | return "$apiBase$url"; | ||||
| } | } | ||||
| /** | |||||
| * @param string|mixed $value A string to UTF8-encode. | |||||
| * | |||||
| * @returns string|mixed The UTF8-encoded string, or the object passed in if | |||||
| * it wasn't a string. | |||||
| */ | |||||
| public static function utf8($value) | public static function utf8($value) | ||||
| { | { | ||||
| if (is_string($value)) | if (is_string($value) | ||||
| && mb_detect_encoding($value, "UTF-8", TRUE) != "UTF-8") { | |||||
| return utf8_encode($value); | return utf8_encode($value); | ||||
| else | } else { | ||||
| return $value; | return $value; | ||||
| } | } | ||||
| } | |||||
| private static function _encodeObjects($d) | private static function _encodeObjects($d) | ||||
| { | { | ||||
| if ($d instanceof Stripe_ApiRequestor) { | if ($d instanceof Stripe_ApiResource) { | ||||
| return $d->id; | return self::utf8($d->id); | ||||
| } else if ($d === true) { | } else if ($d === true) { | ||||
| return 'true'; | return 'true'; | ||||
| } else if ($d === false) { | } else if ($d === false) { | ||||
| return 'false'; | return 'false'; | ||||
| } else if (is_array($d)) { | } else if (is_array($d)) { | ||||
| $res = array(); | $res = array(); | ||||
| foreach ($d as $k => $v) | foreach ($d as $k => $v) | ||||
| $res[$k] = self::_encodeObjects($v); | $res[$k] = self::_encodeObjects($v); | ||||
| return $res; | return $res; | ||||
| } else { | } else { | ||||
| return $d; | return self::utf8($d); | ||||
| } | } | ||||
| } | } | ||||
| public static function encode($d) | /** | ||||
| * @param array $arr An map of param keys to values. | |||||
| * @param string|null $prefix (It doesn't look like we ever use $prefix...) | |||||
| * | |||||
| * @returns string A querystring, essentially. | |||||
| */ | |||||
| public static function encode($arr, $prefix=null) | |||||
| { | { | ||||
| return http_build_query($d, null, '&'); | if (!is_array($arr)) | ||||
| return $arr; | |||||
| $r = array(); | |||||
| foreach ($arr as $k => $v) { | |||||
| if (is_null($v)) | |||||
| continue; | |||||
| if ($prefix && $k && !is_int($k)) | |||||
| $k = $prefix."[".$k."]"; | |||||
| else if ($prefix) | |||||
| $k = $prefix."[]"; | |||||
| if (is_array($v)) { | |||||
| $r[] = self::encode($v, $k, true); | |||||
| } else { | |||||
| $r[] = urlencode($k)."=".urlencode($v); | |||||
| } | |||||
| } | } | ||||
| public function request($meth, $url, $params=null) | return implode("&", $r); | ||||
| } | |||||
| /** | |||||
| * @param string $method | |||||
| * @param string $url | |||||
| * @param array|null $params | |||||
| * | |||||
| * @return array An array whose first element is the response and second | |||||
| * element is the API key used to make the request. | |||||
| */ | |||||
| public function request($method, $url, $params=null) | |||||
| { | { | ||||
| if (!$params) | if (!$params) | ||||
| $params = array(); | $params = array(); | ||||
| list($rbody, $rcode, $myApiKey) = $this->_requestRaw($meth, $url, $params); | list($rbody, $rcode, $myApiKey) = | ||||
| $this->_requestRaw($method, $url, $params); | |||||
| $resp = $this->_interpretResponse($rbody, $rcode); | $resp = $this->_interpretResponse($rbody, $rcode); | ||||
| return array($resp, $myApiKey); | return array($resp, $myApiKey); | ||||
| } | } | ||||
| /** | |||||
| * @param string $rbody A JSON string. | |||||
| * @param int $rcode | |||||
| * @param array $resp | |||||
| * | |||||
| * @throws Stripe_InvalidRequestError if the error is caused by the user. | |||||
| * @throws Stripe_AuthenticationError if the error is caused by a lack of | |||||
| * permissions. | |||||
| * @throws Stripe_CardError if the error is the error code is 402 (payment | |||||
| * required) | |||||
| * @throws Stripe_ApiError otherwise. | |||||
| */ | |||||
| public function handleApiError($rbody, $rcode, $resp) | public function handleApiError($rbody, $rcode, $resp) | ||||
| { | { | ||||
| if (!is_array($resp) || !isset($resp['error'])) | if (!is_array($resp) || !isset($resp['error'])) { | ||||
| throw new Stripe_ApiError("Invalid response object from API: $rbody (HTTP response code was $rcode)", $rcode, $rbody, $resp); | $msg = "Invalid response object from API: $rbody " | ||||
| ."(HTTP response code was $rcode)"; | |||||
| throw new Stripe_ApiError($msg, $rcode, $rbody, $resp); | |||||
| } | |||||
| $error = $resp['error']; | $error = $resp['error']; | ||||
| $msg = isset($error['message']) ? $error['message'] : null; | |||||
| $param = isset($error['param']) ? $error['param'] : null; | |||||
| $code = isset($error['code']) ? $error['code'] : null; | |||||
| switch ($rcode) { | switch ($rcode) { | ||||
| case 400: | case 400: | ||||
| if ($code == 'rate_limit') { | |||||
| throw new Stripe_RateLimitError( | |||||
| $msg, $param, $rcode, $rbody, $resp | |||||
| ); | |||||
| } | |||||
| case 404: | case 404: | ||||
| throw new Stripe_InvalidRequestError(isset($error['message']) ? $error['message'] : null, | throw new Stripe_InvalidRequestError( | ||||
| isset($error['param']) ? $error['param'] : null, | $msg, $param, $rcode, $rbody, $resp | ||||
| $rcode, $rbody, $resp); | ); | ||||
| case 401: | case 401: | ||||
| throw new Stripe_AuthenticationError(isset($error['message']) ? $error['message'] : null, $rcode, $rbody, $resp); | throw new Stripe_AuthenticationError($msg, $rcode, $rbody, $resp); | ||||
| case 402: | case 402: | ||||
| throw new Stripe_CardError(isset($error['message']) ? $error['message'] : null, | throw new Stripe_CardError($msg, $param, $code, $rcode, $rbody, $resp); | ||||
| isset($error['param']) ? $error['param'] : null, | |||||
| isset($error['code']) ? $error['code'] : null, | |||||
| $rcode, $rbody, $resp); | |||||
| default: | default: | ||||
| throw new Stripe_ApiError(isset($error['message']) ? $error['message'] : null, $rcode, $rbody, $resp); | throw new Stripe_ApiError($msg, $rcode, $rbody, $resp); | ||||
| } | } | ||||
| } | } | ||||
| private function _requestRaw($meth, $url, $params) | private function _requestRaw($method, $url, $params) | ||||
| { | { | ||||
| $myApiKey = $this->_apiKey; | $myApiKey = $this->_apiKey; | ||||
| if (!$myApiKey) | if (!$myApiKey) | ||||
| $myApiKey = Stripe::$apiKey; | $myApiKey = Stripe::$apiKey; | ||||
| if (!$myApiKey) | |||||
| throw new Stripe_AuthenticationError('No API key provided. (HINT: set your API key using "Stripe::setApiKey(<API-KEY>)". You can generate API keys from the Stripe web interface. See https://stripe.com/api for details, or email support@stripe.com if you have any questions.'); | if (!$myApiKey) { | ||||
| $msg = 'No API key provided. (HINT: set your API key using ' | |||||
| . '"Stripe::setApiKey(<API-KEY>)". You can generate API keys from ' | |||||
| . 'the Stripe web interface. See https://stripe.com/api for ' | |||||
| . 'details, or email support@stripe.com if you have any questions.'; | |||||
| throw new Stripe_AuthenticationError($msg); | |||||
| } | |||||
| $absUrl = $this->apiUrl($url); | $absUrl = $this->apiUrl($url); | ||||
| $params = self::_encodeObjects($params); | $params = self::_encodeObjects($params); | ||||
| $langVersion = phpversion(); | $langVersion = phpversion(); | ||||
| $uname = php_uname(); | $uname = php_uname(); | ||||
| $ua = array('bindings_version' => Stripe::VERSION, | $ua = array('bindings_version' => Stripe::VERSION, | ||||
| 'lang' => 'php', | 'lang' => 'php', | ||||
| 'lang_version' => $langVersion, | 'lang_version' => $langVersion, | ||||
| 'publisher' => 'stripe', | 'publisher' => 'stripe', | ||||
| 'uname' => $uname); | 'uname' => $uname); | ||||
| $headers = array('X-Stripe-Client-User-Agent: ' . json_encode($ua), | $headers = array('X-Stripe-Client-User-Agent: ' . json_encode($ua), | ||||
| 'User-Agent: Stripe/v1 PhpBindings/' . Stripe::VERSION); | 'User-Agent: Stripe/v1 PhpBindings/' . Stripe::VERSION, | ||||
| list($rbody, $rcode) = $this->_curlRequest($meth, $absUrl, $headers, $params, $myApiKey); | 'Authorization: Bearer ' . $myApiKey); | ||||
| if (Stripe::$apiVersion) | |||||
| $headers[] = 'Stripe-Version: ' . Stripe::$apiVersion; | |||||
| list($rbody, $rcode) = $this->_curlRequest( | |||||
| $method, | |||||
| $absUrl, | |||||
| $headers, | |||||
| $params | |||||
| ); | |||||
| return array($rbody, $rcode, $myApiKey); | return array($rbody, $rcode, $myApiKey); | ||||
| } | } | ||||
| private function _interpretResponse($rbody, $rcode) | private function _interpretResponse($rbody, $rcode) | ||||
| { | { | ||||
| try { | try { | ||||
| $resp = json_decode($rbody, true); | $resp = json_decode($rbody, true); | ||||
| } catch (Exception $e) { | } catch (Exception $e) { | ||||
| throw new Stripe_ApiError("Invalid response body from API: $rbody (HTTP response code was $rcode)", $rcode, $rbody); | $msg = "Invalid response body from API: $rbody " | ||||
| . "(HTTP response code was $rcode)"; | |||||
| throw new Stripe_ApiError($msg, $rcode, $rbody); | |||||
| } | } | ||||
| if ($rcode < 200 || $rcode >= 300) { | if ($rcode < 200 || $rcode >= 300) { | ||||
| $this->handleApiError($rbody, $rcode, $resp); | $this->handleApiError($rbody, $rcode, $resp); | ||||
| } | } | ||||
| return $resp; | return $resp; | ||||
| } | } | ||||
| private function _curlRequest($meth, $absUrl, $headers, $params, $myApiKey) | private function _curlRequest($method, $absUrl, $headers, $params) | ||||
| { | { | ||||
| if (!self::$_preFlight) { | |||||
| self::$_preFlight = $this->checkSslCert($this->apiUrl()); | |||||
| } | |||||
| $curl = curl_init(); | $curl = curl_init(); | ||||
| $meth = strtolower($meth); | $method = strtolower($method); | ||||
| $opts = array(); | $opts = array(); | ||||
| if ($meth == 'get') { | if ($method == 'get') { | ||||
| $opts[CURLOPT_HTTPGET] = 1; | $opts[CURLOPT_HTTPGET] = 1; | ||||
| if (count($params) > 0) { | if (count($params) > 0) { | ||||
| $encoded = self::encode($params); | $encoded = self::encode($params); | ||||
| $absUrl = "$absUrl?$encoded"; | $absUrl = "$absUrl?$encoded"; | ||||
| } | } | ||||
| } else if ($meth == 'post') { | } else if ($method == 'post') { | ||||
| $opts[CURLOPT_POST] = 1; | $opts[CURLOPT_POST] = 1; | ||||
| $opts[CURLOPT_POSTFIELDS] = self::encode($params); | $opts[CURLOPT_POSTFIELDS] = self::encode($params); | ||||
| } else if ($meth == 'delete') { | } else if ($method == 'delete') { | ||||
| $opts[CURLOPT_CUSTOMREQUEST] = 'DELETE'; | $opts[CURLOPT_CUSTOMREQUEST] = 'DELETE'; | ||||
| if (count($params) > 0) { | if (count($params) > 0) { | ||||
| $encoded = self::encode($params); | $encoded = self::encode($params); | ||||
| $absUrl = "$absUrl?$encoded"; | $absUrl = "$absUrl?$encoded"; | ||||
| } | } | ||||
| } else { | } else { | ||||
| throw new Stripe_ApiError("Unrecognized method $meth"); | throw new Stripe_ApiError("Unrecognized method $method"); | ||||
| } | } | ||||
| $absUrl = self::utf8($absUrl); | $absUrl = self::utf8($absUrl); | ||||
| $opts[CURLOPT_URL] = $absUrl; | $opts[CURLOPT_URL] = $absUrl; | ||||
| $opts[CURLOPT_RETURNTRANSFER] = true; | $opts[CURLOPT_RETURNTRANSFER] = true; | ||||
| $opts[CURLOPT_CONNECTTIMEOUT] = 30; | $opts[CURLOPT_CONNECTTIMEOUT] = 30; | ||||
| $opts[CURLOPT_TIMEOUT] = 80; | $opts[CURLOPT_TIMEOUT] = 80; | ||||
| $opts[CURLOPT_RETURNTRANSFER] = true; | $opts[CURLOPT_RETURNTRANSFER] = true; | ||||
| $opts[CURLOPT_HTTPHEADER] = $headers; | $opts[CURLOPT_HTTPHEADER] = $headers; | ||||
| $opts[CURLOPT_USERPWD] = $myApiKey . ':'; | |||||
| if (!Stripe::$verifySslCerts) | if (!Stripe::$verifySslCerts) | ||||
| $opts[CURLOPT_SSL_VERIFYPEER] = false; | $opts[CURLOPT_SSL_VERIFYPEER] = false; | ||||
| curl_setopt_array($curl, $opts); | curl_setopt_array($curl, $opts); | ||||
| $rbody = curl_exec($curl); | $rbody = curl_exec($curl); | ||||
| if (!defined('CURLE_SSL_CACERT_BADFILE')) { | |||||
| define('CURLE_SSL_CACERT_BADFILE', 77); // constant not defined in PHP | |||||
| } | |||||
| $errno = curl_errno($curl); | $errno = curl_errno($curl); | ||||
| if ($errno == CURLE_SSL_CACERT || $errno == CURLE_SSL_PEER_CERTIFICATE) { | if ($errno == CURLE_SSL_CACERT || | ||||
| array_push($headers, 'X-Stripe-Client-Info: {"ca":"using Stripe-supplied CA bundle"}'); | $errno == CURLE_SSL_PEER_CERTIFICATE || | ||||
| $errno == CURLE_SSL_CACERT_BADFILE) { | |||||
| array_push( | |||||
| $headers, | |||||
| 'X-Stripe-Client-Info: {"ca":"using Stripe-supplied CA bundle"}' | |||||
| ); | |||||
| $cert = $this->caBundle(); | |||||
| curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); | curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); | ||||
| curl_setopt($curl, CURLOPT_CAINFO, | curl_setopt($curl, CURLOPT_CAINFO, $cert); | ||||
| dirname(__FILE__) . '/../data/ca-certificates.crt'); | |||||
| $rbody = curl_exec($curl); | $rbody = curl_exec($curl); | ||||
| } | } | ||||
| if ($rbody === false) { | if ($rbody === false) { | ||||
| $errno = curl_errno($curl); | $errno = curl_errno($curl); | ||||
| $message = curl_error($curl); | $message = curl_error($curl); | ||||
| curl_close($curl); | curl_close($curl); | ||||
| $this->handleCurlError($errno, $message); | $this->handleCurlError($errno, $message); | ||||
| } | } | ||||
| $rcode = curl_getinfo($curl, CURLINFO_HTTP_CODE); | $rcode = curl_getinfo($curl, CURLINFO_HTTP_CODE); | ||||
| curl_close($curl); | curl_close($curl); | ||||
| return array($rbody, $rcode); | return array($rbody, $rcode); | ||||
| } | } | ||||
| /** | |||||
| * @param number $errno | |||||
| * @param string $message | |||||
| * @throws Stripe_ApiConnectionError | |||||
| */ | |||||
| public function handleCurlError($errno, $message) | public function handleCurlError($errno, $message) | ||||
| { | { | ||||
| $apiBase = Stripe::$apiBase; | $apiBase = Stripe::$apiBase; | ||||
| switch ($errno) { | switch ($errno) { | ||||
| case CURLE_COULDNT_CONNECT: | case CURLE_COULDNT_CONNECT: | ||||
| case CURLE_COULDNT_RESOLVE_HOST: | case CURLE_COULDNT_RESOLVE_HOST: | ||||
| case CURLE_OPERATION_TIMEOUTED: | case CURLE_OPERATION_TIMEOUTED: | ||||
| $msg = "Could not connect to Stripe ($apiBase). 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 let us know at support@stripe.com."; | $msg = "Could not connect to Stripe ($apiBase). 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; | break; | ||||
| case CURLE_SSL_CACERT: | case CURLE_SSL_CACERT: | ||||
| case CURLE_SSL_PEER_CERTIFICATE: | 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 $apiBase in your browser.) If this problem persists, let us know at support@stripe.com."; | $msg = "Could not verify Stripe's SSL certificate. Please make sure " | ||||
| . "that your network is not intercepting certificates. " | |||||
| . "(Try going to $apiBase in your browser.) " | |||||
| . "If this problem persists,"; | |||||
| break; | break; | ||||
| default: | default: | ||||
| $msg = "Unexpected error communicating with Stripe. If this problem persists, let us know at support@stripe.com."; | $msg = "Unexpected error communicating with Stripe. " | ||||
| . "If this problem persists,"; | |||||
| } | } | ||||
| $msg .= " let us know at support@stripe.com."; | |||||
| $msg .= "\n\n(Network error: $message)"; | $msg .= "\n\n(Network error [errno $errno]: $message)"; | ||||
| throw new Stripe_ApiConnectionError($msg); | throw new Stripe_ApiConnectionError($msg); | ||||
| } | } | ||||
| /** | |||||
| * Preflight the SSL certificate presented by the backend. This isn't 100% | |||||
| * bulletproof, in that we're not actually validating the transport used to | |||||
| * communicate with Stripe, merely that the first attempt to does not use a | |||||
| * revoked certificate. | |||||
| * | |||||
| * Unfortunately the interface to OpenSSL doesn't make it easy to check the | |||||
| * certificate before sending potentially sensitive data on the wire. This | |||||
| * approach raises the bar for an attacker significantly. | |||||
| */ | |||||
| private function checkSslCert($url) | |||||
| { | |||||
| if (version_compare(PHP_VERSION, '5.3.0', '<')) { | |||||
| error_log( | |||||
| 'Warning: This version of PHP is too old to check SSL certificates '. | |||||
| 'correctly. Stripe cannot guarantee that the server has a '. | |||||
| 'certificate which is not blacklisted' | |||||
| ); | |||||
| return true; | |||||
| } | |||||
| if (strpos(PHP_VERSION, 'hiphop') !== false) { | |||||
| error_log( | |||||
| 'Warning: HHVM does not support Stripe\'s SSL certificate '. | |||||
| 'verification. (See http://docs.hhvm.com/manual/en/context.ssl.php) '. | |||||
| 'Stripe cannot guarantee that the server has a certificate which is '. | |||||
| 'not blacklisted' | |||||
| ); | |||||
| return true; | |||||
| } | |||||
| $url = parse_url($url); | |||||
| $port = isset($url["port"]) ? $url["port"] : 443; | |||||
| $url = "ssl://{$url["host"]}:{$port}"; | |||||
| $sslContext = stream_context_create( | |||||
| array('ssl' => array( | |||||
| 'capture_peer_cert' => true, | |||||
| 'verify_peer' => true, | |||||
| 'cafile' => $this->caBundle(), | |||||
| )) | |||||
| ); | |||||
| $result = stream_socket_client( | |||||
| $url, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $sslContext | |||||
| ); | |||||
| if ($errno !== 0) { | |||||
| $apiBase = Stripe::$apiBase; | |||||
| throw new Stripe_ApiConnectionError( | |||||
| 'Could not connect to Stripe (' . $apiBase . '). Please check your '. | |||||
| 'internet connection and try again. If this problem persists, '. | |||||
| 'you should check Stripe\'s service status at '. | |||||
| 'https://twitter.com/stripestatus. Reason was: '.$errstr | |||||
| ); | |||||
| } | |||||
| $params = stream_context_get_params($result); | |||||
| $cert = $params['options']['ssl']['peer_certificate']; | |||||
| openssl_x509_export($cert, $pemCert); | |||||
| if (self::isBlackListed($pemCert)) { | |||||
| throw new Stripe_ApiConnectionError( | |||||
| 'Invalid server certificate. You tried to connect to a server that '. | |||||
| 'has a revoked SSL certificate, which means we cannot securely send '. | |||||
| 'data to that server. Please email support@stripe.com if you need '. | |||||
| 'help connecting to the correct API server.' | |||||
| ); | |||||
| } | |||||
| return true; | |||||
| } | |||||
| /* Checks if a valid PEM encoded certificate is blacklisted | |||||
| * @return boolean | |||||
| */ | |||||
| public static function isBlackListed($certificate) | |||||
| { | |||||
| $certificate = trim($certificate); | |||||
| $lines = explode("\n", $certificate); | |||||
| // Kludgily remove the PEM padding | |||||
| array_shift($lines); array_pop($lines); | |||||
| $derCert = base64_decode(implode("", $lines)); | |||||
| $fingerprint = sha1($derCert); | |||||
| return in_array($fingerprint, self::blacklistedCerts()); | |||||
| } | |||||
| private function caBundle() | |||||
| { | |||||
| return dirname(__FILE__) . '/../data/ca-certificates.crt'; | |||||
| } | |||||
| } | } | ||||