master
py 374 lines 12.7 KB
Raw
1 # SPDX-License-Identifier: MIT
2 from __future__ import absolute_import
3 import datetime
4 import logging
5 import os
6 import sys
7 import socket
8 from socket import error as SocketError, timeout as SocketTimeout
9 import warnings
10 from .packages import six
11 from .packages.six.moves.http_client import HTTPConnection as _HTTPConnection
12 from .packages.six.moves.http_client import HTTPException # noqa: F401
13
14 try: # Compiled with SSL?
15 import ssl
16 BaseSSLError = ssl.SSLError
17 except (ImportError, AttributeError): # Platform-specific: No SSL.
18 ssl = None
19
20 class BaseSSLError(BaseException):
21 pass
22
23
24 try: # Python 3:
25 # Not a no-op, we're adding this to the namespace so it can be imported.
26 ConnectionError = ConnectionError
27 except NameError: # Python 2:
28 class ConnectionError(Exception):
29 pass
30
31
32 from .exceptions import (
33 NewConnectionError,
34 ConnectTimeoutError,
35 SubjectAltNameWarning,
36 SystemTimeWarning,
37 )
38 from .packages.ssl_match_hostname import match_hostname, CertificateError
39
40 from .util.ssl_ import (
41 resolve_cert_reqs,
42 resolve_ssl_version,
43 assert_fingerprint,
44 create_urllib3_context,
45 ssl_wrap_socket
46 )
47
48
49 from .util import connection
50
51 from ._collections import HTTPHeaderDict
52
53 log = logging.getLogger(__name__)
54
55 port_by_scheme = {
56 'http': 80,
57 'https': 443,
58 }
59
60 # When updating RECENT_DATE, move it to
61 # within two years of the current date, and no
62 # earlier than 6 months ago.
63 RECENT_DATE = datetime.date(2016, 1, 1)
64
65
66 class DummyConnection(object):
67 """Used to detect a failed ConnectionCls import."""
68 pass
69
70
71 class HTTPConnection(_HTTPConnection, object):
72 """
73 Based on httplib.HTTPConnection but provides an extra constructor
74 backwards-compatibility layer between older and newer Pythons.
75
76 Additional keyword parameters are used to configure attributes of the connection.
77 Accepted parameters include:
78
79 - ``strict``: See the documentation on :class:`urllib3.connectionpool.HTTPConnectionPool`
80 - ``source_address``: Set the source address for the current connection.
81
82 .. note:: This is ignored for Python 2.6. It is only applied for 2.7 and 3.x
83
84 - ``socket_options``: Set specific options on the underlying socket. If not specified, then
85 defaults are loaded from ``HTTPConnection.default_socket_options`` which includes disabling
86 Nagle's algorithm (sets TCP_NODELAY to 1) unless the connection is behind a proxy.
87
88 For example, if you wish to enable TCP Keep Alive in addition to the defaults,
89 you might pass::
90
91 HTTPConnection.default_socket_options + [
92 (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1),
93 ]
94
95 Or you may want to disable the defaults by passing an empty list (e.g., ``[]``).
96 """
97
98 default_port = port_by_scheme['http']
99
100 #: Disable Nagle's algorithm by default.
101 #: ``[(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]``
102 default_socket_options = [(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]
103
104 #: Whether this connection verifies the host's certificate.
105 is_verified = False
106
107 def __init__(self, *args, **kw):
108 if six.PY3: # Python 3
109 kw.pop('strict', None)
110
111 # Pre-set source_address in case we have an older Python like 2.6.
112 self.source_address = kw.get('source_address')
113
114 if sys.version_info < (2, 7): # Python 2.6
115 # _HTTPConnection on Python 2.6 will balk at this keyword arg, but
116 # not newer versions. We can still use it when creating a
117 # connection though, so we pop it *after* we have saved it as
118 # self.source_address.
119 kw.pop('source_address', None)
120
121 #: The socket options provided by the user. If no options are
122 #: provided, we use the default options.
123 self.socket_options = kw.pop('socket_options', self.default_socket_options)
124
125 # Superclass also sets self.source_address in Python 2.7+.
126 _HTTPConnection.__init__(self, *args, **kw)
127
128 def _new_conn(self):
129 """ Establish a socket connection and set nodelay settings on it.
130
131 :return: New socket connection.
132 """
133 extra_kw = {}
134 if self.source_address:
135 extra_kw['source_address'] = self.source_address
136
137 if self.socket_options:
138 extra_kw['socket_options'] = self.socket_options
139
140 try:
141 conn = connection.create_connection(
142 (self.host, self.port), self.timeout, **extra_kw)
143
144 except SocketTimeout as e:
145 raise ConnectTimeoutError(
146 self, "Connection to %s timed out. (connect timeout=%s)" %
147 (self.host, self.timeout))
148
149 except SocketError as e:
150 raise NewConnectionError(
151 self, "Failed to establish a new connection: %s" % e)
152
153 return conn
154
155 def _prepare_conn(self, conn):
156 self.sock = conn
157 # the _tunnel_host attribute was added in python 2.6.3 (via
158 # http://hg.python.org/cpython/rev/0f57b30a152f) so pythons 2.6(0-2) do
159 # not have them.
160 if getattr(self, '_tunnel_host', None):
161 # TODO: Fix tunnel so it doesn't depend on self.sock state.
162 self._tunnel()
163 # Mark this connection as not reusable
164 self.auto_open = 0
165
166 def connect(self):
167 conn = self._new_conn()
168 self._prepare_conn(conn)
169
170 def request_chunked(self, method, url, body=None, headers=None):
171 """
172 Alternative to the common request method, which sends the
173 body with chunked encoding and not as one block
174 """
175 headers = HTTPHeaderDict(headers if headers is not None else {})
176 skip_accept_encoding = 'accept-encoding' in headers
177 skip_host = 'host' in headers
178 self.putrequest(
179 method,
180 url,
181 skip_accept_encoding=skip_accept_encoding,
182 skip_host=skip_host
183 )
184 for header, value in headers.items():
185 self.putheader(header, value)
186 if 'transfer-encoding' not in headers:
187 self.putheader('Transfer-Encoding', 'chunked')
188 self.endheaders()
189
190 if body is not None:
191 stringish_types = six.string_types + (six.binary_type,)
192 if isinstance(body, stringish_types):
193 body = (body,)
194 for chunk in body:
195 if not chunk:
196 continue
197 if not isinstance(chunk, six.binary_type):
198 chunk = chunk.encode('utf8')
199 len_str = hex(len(chunk))[2:]
200 self.send(len_str.encode('utf-8'))
201 self.send(b'\r\n')
202 self.send(chunk)
203 self.send(b'\r\n')
204
205 # After the if clause, to always have a closed body
206 self.send(b'0\r\n\r\n')
207
208
209 class HTTPSConnection(HTTPConnection):
210 default_port = port_by_scheme['https']
211
212 ssl_version = None
213
214 def __init__(self, host, port=None, key_file=None, cert_file=None,
215 strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
216 ssl_context=None, **kw):
217
218 HTTPConnection.__init__(self, host, port, strict=strict,
219 timeout=timeout, **kw)
220
221 self.key_file = key_file
222 self.cert_file = cert_file
223 self.ssl_context = ssl_context
224
225 # Required property for Google AppEngine 1.9.0 which otherwise causes
226 # HTTPS requests to go out as HTTP. (See Issue #356)
227 self._protocol = 'https'
228
229 def connect(self):
230 conn = self._new_conn()
231 self._prepare_conn(conn)
232
233 if self.ssl_context is None:
234 self.ssl_context = create_urllib3_context(
235 ssl_version=resolve_ssl_version(None),
236 cert_reqs=resolve_cert_reqs(None),
237 )
238
239 self.sock = ssl_wrap_socket(
240 sock=conn,
241 keyfile=self.key_file,
242 certfile=self.cert_file,
243 ssl_context=self.ssl_context,
244 )
245
246
247 class VerifiedHTTPSConnection(HTTPSConnection):
248 """
249 Based on httplib.HTTPSConnection but wraps the socket with
250 SSL certification.
251 """
252 cert_reqs = None
253 ca_certs = None
254 ca_cert_dir = None
255 ssl_version = None
256 assert_fingerprint = None
257
258 def set_cert(self, key_file=None, cert_file=None,
259 cert_reqs=None, ca_certs=None,
260 assert_hostname=None, assert_fingerprint=None,
261 ca_cert_dir=None):
262 """
263 This method should only be called once, before the connection is used.
264 """
265 # If cert_reqs is not provided, we can try to guess. If the user gave
266 # us a cert database, we assume they want to use it: otherwise, if
267 # they gave us an SSL Context object we should use whatever is set for
268 # it.
269 if cert_reqs is None:
270 if ca_certs or ca_cert_dir:
271 cert_reqs = 'CERT_REQUIRED'
272 elif self.ssl_context is not None:
273 cert_reqs = self.ssl_context.verify_mode
274
275 self.key_file = key_file
276 self.cert_file = cert_file
277 self.cert_reqs = cert_reqs
278 self.assert_hostname = assert_hostname
279 self.assert_fingerprint = assert_fingerprint
280 self.ca_certs = ca_certs and os.path.expanduser(ca_certs)
281 self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir)
282
283 def connect(self):
284 # Add certificate verification
285 conn = self._new_conn()
286
287 hostname = self.host
288 if getattr(self, '_tunnel_host', None):
289 # _tunnel_host was added in Python 2.6.3
290 # (See: http://hg.python.org/cpython/rev/0f57b30a152f)
291
292 self.sock = conn
293 # Calls self._set_hostport(), so self.host is
294 # self._tunnel_host below.
295 self._tunnel()
296 # Mark this connection as not reusable
297 self.auto_open = 0
298
299 # Override the host with the one we're requesting data from.
300 hostname = self._tunnel_host
301
302 is_time_off = datetime.date.today() < RECENT_DATE
303 if is_time_off:
304 warnings.warn((
305 'System time is way off (before {0}). This will probably '
306 'lead to SSL verification errors').format(RECENT_DATE),
307 SystemTimeWarning
308 )
309
310 # Wrap socket using verification with the root certs in
311 # trusted_root_certs
312 if self.ssl_context is None:
313 self.ssl_context = create_urllib3_context(
314 ssl_version=resolve_ssl_version(self.ssl_version),
315 cert_reqs=resolve_cert_reqs(self.cert_reqs),
316 )
317
318 context = self.ssl_context
319 context.verify_mode = resolve_cert_reqs(self.cert_reqs)
320 self.sock = ssl_wrap_socket(
321 sock=conn,
322 keyfile=self.key_file,
323 certfile=self.cert_file,
324 ca_certs=self.ca_certs,
325 ca_cert_dir=self.ca_cert_dir,
326 server_hostname=hostname,
327 ssl_context=context)
328
329 if self.assert_fingerprint:
330 assert_fingerprint(self.sock.getpeercert(binary_form=True),
331 self.assert_fingerprint)
332 elif context.verify_mode != ssl.CERT_NONE \
333 and not getattr(context, 'check_hostname', False) \
334 and self.assert_hostname is not False:
335 # While urllib3 attempts to always turn off hostname matching from
336 # the TLS library, this cannot always be done. So we check whether
337 # the TLS Library still thinks it's matching hostnames.
338 cert = self.sock.getpeercert()
339 if not cert.get('subjectAltName', ()):
340 warnings.warn((
341 'Certificate for {0} has no `subjectAltName`, falling back to check for a '
342 '`commonName` for now. This feature is being removed by major browsers and '
343 'deprecated by RFC 2818. (See https://github.com/shazow/urllib3/issues/497 '
344 'for details.)'.format(hostname)),
345 SubjectAltNameWarning
346 )
347 _match_hostname(cert, self.assert_hostname or hostname)
348
349 self.is_verified = (
350 context.verify_mode == ssl.CERT_REQUIRED or
351 self.assert_fingerprint is not None
352 )
353
354
355 def _match_hostname(cert, asserted_hostname):
356 try:
357 match_hostname(cert, asserted_hostname)
358 except CertificateError as e:
359 log.error(
360 'Certificate did not match expected hostname: %s. '
361 'Certificate: %s', asserted_hostname, cert
362 )
363 # Add cert to exception and reraise so client code can inspect
364 # the cert when catching the exception, if they want to
365 e._peer_cert = cert
366 raise
367
368
369 if ssl:
370 # Make a copy for testing.
371 UnverifiedHTTPSConnection = HTTPSConnection
372 HTTPSConnection = VerifiedHTTPSConnection
373 else:
374 HTTPSConnection = DummyConnection