[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/externals/stripe-php/lib/Stripe/ -> ApiRequestor.php (source)

   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  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1