Path: blob/master/externals/stripe-php/lib/Stripe/ApiRequestor.php
12256 views
<?php12class Stripe_ApiRequestor3{4/**5* @var string $apiKey The API key that's to be used to make requests.6*/7public $apiKey;89private static $_preFlight;1011private static function blacklistedCerts()12{13return array(14'05c0b3643694470a888c6e7feb5c9e24e823dc53',15'5b7dc7fbc98d78bf76d4d4fa6f597a0c901fad5c',16);17}1819public function __construct($apiKey=null)20{21$this->_apiKey = $apiKey;22}2324/**25* @param string $url The path to the API endpoint.26*27* @returns string The full path.28*/29public static function apiUrl($url='')30{31$apiBase = Stripe::$apiBase;32return "$apiBase$url";33}3435/**36* @param string|mixed $value A string to UTF8-encode.37*38* @returns string|mixed The UTF8-encoded string, or the object passed in if39* it wasn't a string.40*/41public static function utf8($value)42{43if (is_string($value)44&& mb_detect_encoding($value, "UTF-8", TRUE) != "UTF-8") {45return utf8_encode($value);46} else {47return $value;48}49}5051private static function _encodeObjects($d)52{53if ($d instanceof Stripe_ApiResource) {54return self::utf8($d->id);55} else if ($d === true) {56return 'true';57} else if ($d === false) {58return 'false';59} else if (is_array($d)) {60$res = array();61foreach ($d as $k => $v)62$res[$k] = self::_encodeObjects($v);63return $res;64} else {65return self::utf8($d);66}67}6869/**70* @param array $arr An map of param keys to values.71* @param string|null $prefix (It doesn't look like we ever use $prefix...)72*73* @returns string A querystring, essentially.74*/75public static function encode($arr, $prefix=null)76{77if (!is_array($arr))78return $arr;7980$r = array();81foreach ($arr as $k => $v) {82if (is_null($v))83continue;8485if ($prefix && $k && !is_int($k))86$k = $prefix."[".$k."]";87else if ($prefix)88$k = $prefix."[]";8990if (is_array($v)) {91$r[] = self::encode($v, $k, true);92} else {93$r[] = urlencode($k)."=".urlencode($v);94}95}9697return implode("&", $r);98}99100/**101* @param string $method102* @param string $url103* @param array|null $params104*105* @return array An array whose first element is the response and second106* element is the API key used to make the request.107*/108public function request($method, $url, $params=null)109{110if (!$params)111$params = array();112list($rbody, $rcode, $myApiKey) =113$this->_requestRaw($method, $url, $params);114$resp = $this->_interpretResponse($rbody, $rcode);115return array($resp, $myApiKey);116}117118119/**120* @param string $rbody A JSON string.121* @param int $rcode122* @param array $resp123*124* @throws Stripe_InvalidRequestError if the error is caused by the user.125* @throws Stripe_AuthenticationError if the error is caused by a lack of126* permissions.127* @throws Stripe_CardError if the error is the error code is 402 (payment128* required)129* @throws Stripe_ApiError otherwise.130*/131public function handleApiError($rbody, $rcode, $resp)132{133if (!is_array($resp) || !isset($resp['error'])) {134$msg = "Invalid response object from API: $rbody "135."(HTTP response code was $rcode)";136throw new Stripe_ApiError($msg, $rcode, $rbody, $resp);137}138139$error = $resp['error'];140$msg = isset($error['message']) ? $error['message'] : null;141$param = isset($error['param']) ? $error['param'] : null;142$code = isset($error['code']) ? $error['code'] : null;143144switch ($rcode) {145case 400:146if ($code == 'rate_limit') {147throw new Stripe_RateLimitError(148$msg, $param, $rcode, $rbody, $resp149);150}151case 404:152throw new Stripe_InvalidRequestError(153$msg, $param, $rcode, $rbody, $resp154);155case 401:156throw new Stripe_AuthenticationError($msg, $rcode, $rbody, $resp);157case 402:158throw new Stripe_CardError($msg, $param, $code, $rcode, $rbody, $resp);159default:160throw new Stripe_ApiError($msg, $rcode, $rbody, $resp);161}162}163164private function _requestRaw($method, $url, $params)165{166$myApiKey = $this->_apiKey;167if (!$myApiKey)168$myApiKey = Stripe::$apiKey;169170if (!$myApiKey) {171$msg = 'No API key provided. (HINT: set your API key using '172. '"Stripe::setApiKey(<API-KEY>)". You can generate API keys from '173. 'the Stripe web interface. See https://stripe.com/api for '174. 'details, or email [email protected] if you have any questions.';175throw new Stripe_AuthenticationError($msg);176}177178$absUrl = $this->apiUrl($url);179$params = self::_encodeObjects($params);180$langVersion = phpversion();181$uname = php_uname();182$ua = array('bindings_version' => Stripe::VERSION,183'lang' => 'php',184'lang_version' => $langVersion,185'publisher' => 'stripe',186'uname' => $uname);187$headers = array('X-Stripe-Client-User-Agent: ' . json_encode($ua),188'User-Agent: Stripe/v1 PhpBindings/' . Stripe::VERSION,189'Authorization: Bearer ' . $myApiKey);190if (Stripe::$apiVersion)191$headers[] = 'Stripe-Version: ' . Stripe::$apiVersion;192list($rbody, $rcode) = $this->_curlRequest(193$method,194$absUrl,195$headers,196$params197);198return array($rbody, $rcode, $myApiKey);199}200201private function _interpretResponse($rbody, $rcode)202{203try {204$resp = json_decode($rbody, true);205} catch (Exception $e) {206$msg = "Invalid response body from API: $rbody "207. "(HTTP response code was $rcode)";208throw new Stripe_ApiError($msg, $rcode, $rbody);209}210211if ($rcode < 200 || $rcode >= 300) {212$this->handleApiError($rbody, $rcode, $resp);213}214return $resp;215}216217private function _curlRequest($method, $absUrl, $headers, $params)218{219220if (!self::$_preFlight) {221self::$_preFlight = $this->checkSslCert($this->apiUrl());222}223224$curl = curl_init();225$method = strtolower($method);226$opts = array();227if ($method == 'get') {228$opts[CURLOPT_HTTPGET] = 1;229if (count($params) > 0) {230$encoded = self::encode($params);231$absUrl = "$absUrl?$encoded";232}233} else if ($method == 'post') {234$opts[CURLOPT_POST] = 1;235$opts[CURLOPT_POSTFIELDS] = self::encode($params);236} else if ($method == 'delete') {237$opts[CURLOPT_CUSTOMREQUEST] = 'DELETE';238if (count($params) > 0) {239$encoded = self::encode($params);240$absUrl = "$absUrl?$encoded";241}242} else {243throw new Stripe_ApiError("Unrecognized method $method");244}245246$absUrl = self::utf8($absUrl);247$opts[CURLOPT_URL] = $absUrl;248$opts[CURLOPT_RETURNTRANSFER] = true;249$opts[CURLOPT_CONNECTTIMEOUT] = 30;250$opts[CURLOPT_TIMEOUT] = 80;251$opts[CURLOPT_RETURNTRANSFER] = true;252$opts[CURLOPT_HTTPHEADER] = $headers;253if (!Stripe::$verifySslCerts)254$opts[CURLOPT_SSL_VERIFYPEER] = false;255256curl_setopt_array($curl, $opts);257$rbody = curl_exec($curl);258259if (!defined('CURLE_SSL_CACERT_BADFILE')) {260define('CURLE_SSL_CACERT_BADFILE', 77); // constant not defined in PHP261}262263$errno = curl_errno($curl);264if ($errno == CURLE_SSL_CACERT ||265$errno == CURLE_SSL_PEER_CERTIFICATE ||266$errno == CURLE_SSL_CACERT_BADFILE) {267array_push(268$headers,269'X-Stripe-Client-Info: {"ca":"using Stripe-supplied CA bundle"}'270);271$cert = $this->caBundle();272curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);273curl_setopt($curl, CURLOPT_CAINFO, $cert);274$rbody = curl_exec($curl);275}276277if ($rbody === false) {278$errno = curl_errno($curl);279$message = curl_error($curl);280curl_close($curl);281$this->handleCurlError($errno, $message);282}283284$rcode = curl_getinfo($curl, CURLINFO_HTTP_CODE);285curl_close($curl);286return array($rbody, $rcode);287}288289/**290* @param number $errno291* @param string $message292* @throws Stripe_ApiConnectionError293*/294public function handleCurlError($errno, $message)295{296$apiBase = Stripe::$apiBase;297switch ($errno) {298case CURLE_COULDNT_CONNECT:299case CURLE_COULDNT_RESOLVE_HOST:300case CURLE_OPERATION_TIMEOUTED:301$msg = "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, or";305break;306case CURLE_SSL_CACERT:307case CURLE_SSL_PEER_CERTIFICATE:308$msg = "Could not verify Stripe's SSL certificate. Please make sure "309. "that your network is not intercepting certificates. "310. "(Try going to $apiBase in your browser.) "311. "If this problem persists,";312break;313default:314$msg = "Unexpected error communicating with Stripe. "315. "If this problem persists,";316}317$msg .= " let us know at [email protected].";318319$msg .= "\n\n(Network error [errno $errno]: $message)";320throw new Stripe_ApiConnectionError($msg);321}322323/**324* Preflight the SSL certificate presented by the backend. This isn't 100%325* bulletproof, in that we're not actually validating the transport used to326* communicate with Stripe, merely that the first attempt to does not use a327* revoked certificate.328*329* Unfortunately the interface to OpenSSL doesn't make it easy to check the330* certificate before sending potentially sensitive data on the wire. This331* approach raises the bar for an attacker significantly.332*/333private function checkSslCert($url)334{335if (version_compare(PHP_VERSION, '5.3.0', '<')) {336error_log(337'Warning: This version of PHP is too old to check SSL certificates '.338'correctly. Stripe cannot guarantee that the server has a '.339'certificate which is not blacklisted'340);341return true;342}343344if (strpos(PHP_VERSION, 'hiphop') !== false) {345error_log(346'Warning: HHVM does not support Stripe\'s SSL certificate '.347'verification. (See http://docs.hhvm.com/manual/en/context.ssl.php) '.348'Stripe cannot guarantee that the server has a certificate which is '.349'not blacklisted'350);351return true;352}353354$url = parse_url($url);355$port = isset($url["port"]) ? $url["port"] : 443;356$url = "ssl://{$url["host"]}:{$port}";357358$sslContext = stream_context_create(359array('ssl' => array(360'capture_peer_cert' => true,361'verify_peer' => true,362'cafile' => $this->caBundle(),363))364);365$result = stream_socket_client(366$url, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $sslContext367);368if ($errno !== 0) {369$apiBase = Stripe::$apiBase;370throw new Stripe_ApiConnectionError(371'Could not connect to Stripe (' . $apiBase . '). Please check your '.372'internet connection and try again. If this problem persists, '.373'you should check Stripe\'s service status at '.374'https://twitter.com/stripestatus. Reason was: '.$errstr375);376}377378$params = stream_context_get_params($result);379380$cert = $params['options']['ssl']['peer_certificate'];381382openssl_x509_export($cert, $pemCert);383384if (self::isBlackListed($pemCert)) {385throw new Stripe_ApiConnectionError(386'Invalid server certificate. You tried to connect to a server that '.387'has a revoked SSL certificate, which means we cannot securely send '.388'data to that server. Please email [email protected] if you need '.389'help connecting to the correct API server.'390);391}392393return true;394}395396/* Checks if a valid PEM encoded certificate is blacklisted397* @return boolean398*/399public static function isBlackListed($certificate)400{401$certificate = trim($certificate);402$lines = explode("\n", $certificate);403404// Kludgily remove the PEM padding405array_shift($lines); array_pop($lines);406407$derCert = base64_decode(implode("", $lines));408$fingerprint = sha1($derCert);409return in_array($fingerprint, self::blacklistedCerts());410}411412private function caBundle()413{414return dirname(__FILE__) . '/../data/ca-certificates.crt';415}416}417418419