master
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)