master
Raw Download raw file
  1import os
  2import sys
  3import textwrap
  4
  5# From this package
  6import lib.error as error
  7import lib.util as util
  8
  9# This is a port of http_client from [Stripe-Python](https://github.com/stripe/stripe-python)
 10
 11# - Requests is the preferred HTTP library
 12# - Use Pycurl if it's there (at least it verifies SSL certs)
 13# - Fall back to urllib2 with a warning if needed
 14
 15try:
 16    if sys.version_info < (3,0):
 17        import urllib2 as urllib_request
 18    else:
 19        import urllib.request as urllib_request
 20except ImportError:
 21    pass
 22
 23try:
 24    import pycurl
 25except ImportError:
 26    pycurl = None
 27
 28try:
 29    import requests
 30except ImportError:
 31    requests = None
 32else:
 33    try:
 34        # Require version 0.8.8, but don't want to depend on distutils
 35        version = requests.__version__
 36        major, minor, patch = [int(i) for i in version.split('.')]
 37    except Exception:
 38        # Probably some new-fangled version, so it should support verify
 39        pass
 40    else:
 41        if (major, minor, patch) < (0, 8, 8):
 42            util.logger.warn(
 43                'Warning: the test harness will only use your Python "requests"'
 44                'library if it is version 0.8.8 or newer, but your '
 45                '"requests" library is version %s. We will fall back to '
 46                'an alternate HTTP library so everything should work. We '
 47                'recommend upgrading your "requests" library. (HINT: running '
 48                '"pip install -U requests" should upgrade your requests '
 49                'library to the latest version.)' % (version,))
 50            requests = None
 51
 52def certs_path():
 53    return os.path.join(os.path.dirname(__file__), 'ca-certificates.crt')
 54
 55
 56def new_default_http_client(*args, **kwargs):
 57    if requests:
 58        impl = RequestsClient
 59    elif pycurl and sys.version_info < (3,0):
 60        # Officially supports in 3.1-3.3 but not 3.0. The idea is that for >=2.6
 61        # you should use requests
 62        impl = PycurlClient
 63    else:
 64        impl = Urllib2Client
 65        if sys.version_info < (2,6):
 66            reccomendation = "pycurl"
 67        else:
 68            reccomendation = "requests"
 69        util.logger.info(
 70            "Warning: The test harness is falling back to *urllib2*. "
 71            "Its SSL implementation doesn't verify server "
 72            "certificates (how's that for a distributed systems problem?). "
 73            "We recommend instead installing %(rec)s via `pip install %(rec)s`.",
 74            {'rec': reccomendation})
 75
 76    return impl(*args, **kwargs)
 77
 78
 79class HTTPClient(object):
 80
 81    def __init__(self, headers={}, verify_ssl_certs=True):
 82        self._verify_ssl_certs = verify_ssl_certs
 83        self.headers = headers
 84
 85    def request(self, method, url, post_data=None):
 86        raise NotImplementedError(
 87            'HTTPClient subclasses must implement `request`')
 88
 89
 90class RequestsClient(HTTPClient):
 91    name = 'requests'
 92
 93    def request(self, method, url, post_data=None):
 94        kwargs = {}
 95
 96        if self._verify_ssl_certs:
 97            kwargs['verify'] = certs_path()
 98        else:
 99            kwargs['verify'] = False
100
101        try:
102            try:
103                result = requests.request(method,
104                                          url,
105                                          headers=self.headers,
106                                          data=post_data,
107                                          timeout=80,
108                                          **kwargs)
109            except TypeError:
110                e = util.exception_as()
111                raise TypeError(
112                    'Warning: It looks like your installed version of the '
113                    '"requests" library is not compatible with Stripe\'s '
114                    'usage thereof. (HINT: The most likely cause is that '
115                    'your "requests" library is out of date. You can fix '
116                    'that by running "pip install -U requests".) The '
117                    'underlying error was: %s' % (e,))
118
119            # This causes the content to actually be read, which could cause
120            # e.g. a socket timeout. TODO: The other fetch methods probably
121            # are succeptible to the same and should be updated.
122            content = result.content
123            status_code = result.status_code
124        except Exception:
125            # Would catch just requests.exceptions.RequestException, but can
126            # also raise ValueError, RuntimeError, etc.
127            e = util.exception_as()
128            self._handle_request_error(e)
129        if sys.version_info >= (3, 0):
130            content = content.decode('utf-8')
131        return content, status_code
132
133    def _handle_request_error(self, e):
134        if isinstance(e, requests.exceptions.RequestException):
135            err = "%s: %s" % (type(e).__name__, str(e))
136        else:
137            err = "A %s was raised" % (type(e).__name__,)
138            if str(e):
139                err += " with error message %s" % (str(e),)
140            else:
141                err += " with no error message"
142        msg = "Network error: %s" % (err,)
143        raise error.HTTPConnectionError(msg)
144
145class PycurlClient(HTTPClient):
146    name = 'pycurl'
147
148    def request(self, method, url, post_data=None):
149        s = util.StringIO.StringIO()
150        curl = pycurl.Curl()
151
152        if method == 'get':
153            curl.setopt(pycurl.HTTPGET, 1)
154        elif method == 'post':
155            curl.setopt(pycurl.POST, 1)
156            curl.setopt(pycurl.POSTFIELDS, post_data)
157        else:
158            curl.setopt(pycurl.CUSTOMREQUEST, method.upper())
159
160        # pycurl doesn't like unicode URLs
161        curl.setopt(pycurl.URL, util.utf8(url))
162
163        curl.setopt(pycurl.WRITEFUNCTION, s.write)
164        curl.setopt(pycurl.NOSIGNAL, 1)
165        curl.setopt(pycurl.CONNECTTIMEOUT, 30)
166        curl.setopt(pycurl.TIMEOUT, 80)
167        curl.setopt(pycurl.HTTPHEADER, ['%s: %s' % (k, v)
168                    for k, v in self.headers.iteritems()])
169        if self._verify_ssl_certs:
170            curl.setopt(pycurl.CAINFO, certs_path())
171        else:
172            curl.setopt(pycurl.SSL_VERIFYHOST, False)
173
174        try:
175            curl.perform()
176        except pycurl.error:
177            e = util.exception_as()
178            self._handle_request_error(e)
179        rbody = s.getvalue()
180        rcode = curl.getinfo(pycurl.RESPONSE_CODE)
181        return rbody, rcode
182
183    def _handle_request_error(self, e):
184        error_code = e[0]
185        if error_code in [pycurl.E_COULDNT_CONNECT,
186                          pycurl.E_COULDNT_RESOLVE_HOST,
187                          pycurl.E_OPERATION_TIMEOUTED]:
188            msg = ("Test harness could not connect to Stripe. Please check "
189                   "your internet connection and try again.")
190        elif (error_code in [pycurl.E_SSL_CACERT,
191                             pycurl.E_SSL_PEER_CERTIFICATE]):
192            msg = "Could not verify host's SSL certificate."
193        else:
194            msg = ""
195        msg = textwrap.fill(msg) + "\n\nNetwork error: %s" % e[1]
196        raise error.HTTPConnectionError(msg)
197
198
199class Urllib2Client(HTTPClient):
200    if sys.version_info >= (3, 0):
201        name = 'urllib.request'
202    else:
203        name = 'urllib2'
204
205    def request(self, method, url, post_data=None):
206        if sys.version_info >= (3, 0) and isinstance(post_data, str):
207            post_data = post_data.encode('utf-8')
208
209        req = urllib_request.Request(url, post_data, self.headers)
210
211        if method not in ('get', 'post'):
212            req.get_method = lambda: method.upper()
213
214        try:
215            response = urllib_request.urlopen(req)
216            rbody = response.read()
217            rcode = response.code
218        except urllib_request.HTTPError:
219            e = util.exception_as()
220            rcode = e.code
221            rbody = e.read()
222        except (urllib_request.URLError, ValueError):
223            e = util.exception_as()
224            self._handle_request_error(e)
225        if sys.version_info >= (3, 0):
226            rbody = rbody.decode('utf-8')
227        return rbody, rcode
228
229    def _handle_request_error(self, e):
230        msg = "Network error: %s" % str(e)
231        raise error.HTTPConnectionError(msg)