update vendored urllib3
authorThomas Weißschuh <thomas@t-8ch.de>
Thu, 28 Mar 2013 12:48:50 +0000 (12:48 +0000)
committerThomas Weißschuh <thomas@t-8ch.de>
Thu, 28 Mar 2013 12:49:04 +0000 (12:49 +0000)
requests/packages/urllib3/connectionpool.py
requests/packages/urllib3/contrib/pyopenssl.py [new file with mode: 0644]
requests/packages/urllib3/poolmanager.py
requests/packages/urllib3/util.py

index 51c87f58add91492cfcbe99323af13123dad9aa8..f93e2dfba1cdb63e8a370ee356aeb35c230d168b 100644 (file)
@@ -9,7 +9,7 @@ import socket
 import errno
 
 from socket import error as SocketError, timeout as SocketTimeout
-from .util import resolve_cert_reqs, resolve_ssl_version
+from .util import resolve_cert_reqs, resolve_ssl_version, assert_fingerprint
 
 try: # Python 3
     from http.client import HTTPConnection, HTTPException
@@ -81,12 +81,15 @@ class VerifiedHTTPSConnection(HTTPSConnection):
     ssl_version = None
 
     def set_cert(self, key_file=None, cert_file=None,
-                 cert_reqs=None, ca_certs=None):
+                 cert_reqs=None, ca_certs=None,
+                 assert_hostname=None, assert_fingerprint=None):
 
         self.key_file = key_file
         self.cert_file = cert_file
         self.cert_reqs = cert_reqs
         self.ca_certs = ca_certs
+        self.assert_hostname = assert_hostname
+        self.assert_fingerprint = assert_fingerprint
 
     def connect(self):
         # Add certificate verification
@@ -104,8 +107,12 @@ class VerifiedHTTPSConnection(HTTPSConnection):
                                     ssl_version=resolved_ssl_version)
 
         if resolved_cert_reqs != ssl.CERT_NONE:
-            match_hostname(self.sock.getpeercert(), self.host)
-
+            if self.assert_fingerprint:
+                assert_fingerprint(self.sock.getpeercert(binary_form=True),
+                                   self.assert_fingerprint)
+            else:
+                match_hostname(self.sock.getpeercert(),
+                               self.assert_hostname or self.host)
 
 ## Pool objects
 
@@ -502,9 +509,13 @@ class HTTPSConnectionPool(HTTPConnectionPool):
     :class:`.VerifiedHTTPSConnection` is used, which *can* verify certificates,
     instead of :class:`httplib.HTTPSConnection`.
 
-    The ``key_file``, ``cert_file``, ``cert_reqs``, ``ca_certs``, and ``ssl_version``
-    are only used if :mod:`ssl` is available and are fed into
-    :meth:`urllib3.util.ssl_wrap_socket` to upgrade the connection socket into an SSL socket.
+    :class:`.VerifiedHTTPSConnection` uses one of ``assert_fingerprint``,
+    ``assert_hostname`` and ``host`` in this order to verify connections.
+
+    The ``key_file``, ``cert_file``, ``cert_reqs``, ``ca_certs`` and
+    ``ssl_version`` are only used if :mod:`ssl` is available and are fed into
+    :meth:`urllib3.util.ssl_wrap_socket` to upgrade the connection socket
+    into an SSL socket.
     """
 
     scheme = 'https'
@@ -512,8 +523,9 @@ class HTTPSConnectionPool(HTTPConnectionPool):
     def __init__(self, host, port=None,
                  strict=False, timeout=None, maxsize=1,
                  block=False, headers=None,
-                 key_file=None, cert_file=None,
-                 cert_reqs=None, ca_certs=None, ssl_version=None):
+                 key_file=None, cert_file=None, cert_reqs=None,
+                 ca_certs=None, ssl_version=None,
+                 assert_hostname=None, assert_fingerprint=None):
 
         HTTPConnectionPool.__init__(self, host, port,
                                     strict, timeout, maxsize,
@@ -523,6 +535,8 @@ class HTTPSConnectionPool(HTTPConnectionPool):
         self.cert_reqs = cert_reqs
         self.ca_certs = ca_certs
         self.ssl_version = ssl_version
+        self.assert_hostname = assert_hostname
+        self.assert_fingerprint = assert_fingerprint
 
     def _new_conn(self):
         """
@@ -532,7 +546,7 @@ class HTTPSConnectionPool(HTTPConnectionPool):
         log.info("Starting new HTTPS connection (%d): %s"
                  % (self.num_connections, self.host))
 
-        if not ssl: # Platform-specific: Python compiled without +ssl
+        if not ssl:  # Platform-specific: Python compiled without +ssl
             if not HTTPSConnection or HTTPSConnection is object:
                 raise SSLError("Can't connect to HTTPS URL because the SSL "
                                "module is not available.")
@@ -545,7 +559,9 @@ class HTTPSConnectionPool(HTTPConnectionPool):
                                              port=self.port,
                                              strict=self.strict)
         connection.set_cert(key_file=self.key_file, cert_file=self.cert_file,
-                            cert_reqs=self.cert_reqs, ca_certs=self.ca_certs)
+                            cert_reqs=self.cert_reqs, ca_certs=self.ca_certs,
+                            assert_hostname=self.assert_hostname,
+                            assert_fingerprint=self.assert_fingerprint)
 
         connection.ssl_version = self.ssl_version
 
diff --git a/requests/packages/urllib3/contrib/pyopenssl.py b/requests/packages/urllib3/contrib/pyopenssl.py
new file mode 100644 (file)
index 0000000..5c4c6d8
--- /dev/null
@@ -0,0 +1,167 @@
+'''SSL with SNI-support for Python 2.
+
+This needs the following packages installed:
+
+* pyOpenSSL (tested with 0.13)
+* ndg-httpsclient (tested with 0.3.2)
+* pyasn1 (tested with 0.1.6)
+
+To activate it call :func:`~urllib3.contrib.pyopenssl.inject_into_urllib3`.
+This can be done in a ``sitecustomize`` module, or at any other time before
+your application begins using ``urllib3``, like this::
+
+    try:
+        import urllib3.contrib.pyopenssl
+        urllib3.contrib.pyopenssl.inject_into_urllib3()
+    except ImportError:
+        pass
+
+Now you can use :mod:`urllib3` as you normally would, and it will support SNI
+when the required modules are installed.
+'''
+
+from ndg.httpsclient.ssl_peer_verification import (ServerSSLCertVerification,
+                                                   SUBJ_ALT_NAME_SUPPORT)
+from ndg.httpsclient.subj_alt_name import SubjectAltName
+import OpenSSL.SSL
+from pyasn1.codec.der import decoder as der_decoder
+from socket import _fileobject
+import ssl
+
+from .. import connectionpool
+from .. import util
+
+__all__ = ['inject_into_urllib3', 'extract_from_urllib3']
+
+# SNI only *really* works if we can read the subjectAltName of certificates.
+HAS_SNI = SUBJ_ALT_NAME_SUPPORT
+
+# Map from urllib3 to PyOpenSSL compatible parameter-values.
+_openssl_versions = {
+    ssl.PROTOCOL_SSLv23: OpenSSL.SSL.SSLv23_METHOD,
+    ssl.PROTOCOL_SSLv3: OpenSSL.SSL.SSLv3_METHOD,
+    ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD,
+}
+_openssl_verify = {
+    ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE,
+    ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER,
+    ssl.CERT_REQUIRED: OpenSSL.SSL.VERIFY_PEER
+                       + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT,
+}
+
+
+orig_util_HAS_SNI = util.HAS_SNI
+orig_connectionpool_ssl_wrap_socket = connectionpool.ssl_wrap_socket
+
+
+def inject_into_urllib3():
+    'Monkey-patch urllib3 with PyOpenSSL-backed SSL-support.'
+
+    connectionpool.ssl_wrap_socket = ssl_wrap_socket
+    util.HAS_SNI = HAS_SNI
+
+
+def extract_from_urllib3():
+    'Undo monkey-patching by :func:`inject_into_urllib3`.'
+
+    connectionpool.ssl_wrap_socket = orig_connectionpool_ssl_wrap_socket
+    util.HAS_SNI = orig_util_HAS_SNI
+
+
+### Note: This is a slightly bug-fixed version of same from ndg-httpsclient.
+def get_subj_alt_name(peer_cert):
+    # Search through extensions
+    dns_name = []
+    if not SUBJ_ALT_NAME_SUPPORT:
+        return dns_name
+
+    general_names = SubjectAltName()
+    for i in range(peer_cert.get_extension_count()):
+        ext = peer_cert.get_extension(i)
+        ext_name = ext.get_short_name()
+        if ext_name != 'subjectAltName':
+            continue
+
+        # PyOpenSSL returns extension data in ASN.1 encoded form
+        ext_dat = ext.get_data()
+        decoded_dat = der_decoder.decode(ext_dat,
+                                         asn1Spec=general_names)
+
+        for name in decoded_dat:
+            if not isinstance(name, SubjectAltName):
+                continue
+            for entry in range(len(name)):
+                component = name.getComponentByPosition(entry)
+                if component.getName() != 'dNSName':
+                    continue
+                dns_name.append(str(component.getComponent()))
+
+    return dns_name
+
+
+class WrappedSocket(object):
+    '''API-compatibility wrapper for Python OpenSSL's Connection-class.'''
+
+    def __init__(self, connection, socket):
+        self.connection = connection
+        self.socket = socket
+
+    def makefile(self, mode, bufsize=-1):
+        return _fileobject(self.connection, mode, bufsize)
+
+    def settimeout(self, timeout):
+        return self.socket.settimeout(timeout)
+
+    def sendall(self, data):
+        return self.connection.sendall(data)
+
+    def getpeercert(self, binary_form=False):
+        x509 = self.connection.get_peer_certificate()
+        if not x509:
+            raise ssl.SSLError('')
+
+        if binary_form:
+            return OpenSSL.crypto.dump_certificate(
+                OpenSSL.crypto.FILETYPE_ASN1,
+                x509)
+
+        return {
+            'subject': (
+                (('commonName', x509.get_subject().CN),),
+            ),
+            'subjectAltName': [
+                ('DNS', value)
+                for value in get_subj_alt_name(x509)
+            ]
+        }
+
+
+def _verify_callback(cnx, x509, err_no, err_depth, return_code):
+    return err_no == 0
+
+
+def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
+                    ca_certs=None, server_hostname=None,
+                    ssl_version=None):
+    ctx = OpenSSL.SSL.Context(_openssl_versions[ssl_version])
+    if certfile:
+        ctx.use_certificate_file(certfile)
+    if keyfile:
+        ctx.use_privatekey_file(keyfile)
+    if cert_reqs != ssl.CERT_NONE:
+        ctx.set_verify(_openssl_verify[cert_reqs], _verify_callback)
+    if ca_certs:
+        try:
+            ctx.load_verify_locations(ca_certs, None)
+        except OpenSSL.SSL.Error as e:
+            raise ssl.SSLError('bad ca_certs: %r' % ca_certs, e)
+
+    cnx = OpenSSL.SSL.Connection(ctx, sock)
+    cnx.set_tlsext_host_name(server_hostname)
+    cnx.set_connect_state()
+    try:
+        cnx.do_handshake()
+    except OpenSSL.SSL.Error as e:
+        raise ssl.SSLError('bad handshake', e)
+
+    return WrappedSocket(cnx, sock)
index 6e7377cb6240890a139b136c99b1681873f77b17..64a7b5d7558a06a3bae8461438bc031730dacf2e 100644 (file)
@@ -23,6 +23,9 @@ pool_classes_by_scheme = {
 
 log = logging.getLogger(__name__)
 
+SSL_KEYWORDS = ('key_file', 'cert_file', 'cert_reqs', 'ca_certs',
+                'ssl_version')
+
 
 class PoolManager(RequestMethods):
     """
@@ -67,7 +70,13 @@ class PoolManager(RequestMethods):
         to be overridden for customization.
         """
         pool_cls = pool_classes_by_scheme[scheme]
-        return pool_cls(host, port, **self.connection_pool_kw)
+        kwargs = self.connection_pool_kw
+        if scheme == 'http':
+            kwargs = self.connection_pool_kw.copy()
+            for kw in SSL_KEYWORDS:
+                kwargs.pop(kw, None)
+
+        return pool_cls(host, port, **kwargs)
 
     def clear(self):
         """
index b827bc4f5ec509e9e7a94c5b80dd01d8715573c2..681cb6c99154654d9405bbe97fad5b6032064b12 100644 (file)
@@ -8,6 +8,8 @@
 from base64 import b64encode
 from collections import namedtuple
 from socket import error as SocketError
+from hashlib import md5, sha1
+from binascii import hexlify, unhexlify
 
 try:
     from select import poll, POLLIN
@@ -23,7 +25,7 @@ try:  # Test for SSL features
     HAS_SNI = False
 
     import ssl
-    from ssl import wrap_socket, CERT_NONE, SSLError, PROTOCOL_SSLv23
+    from ssl import wrap_socket, CERT_NONE, PROTOCOL_SSLv23
     from ssl import SSLContext  # Modern SSL?
     from ssl import HAS_SNI  # Has SNI?
 except ImportError:
@@ -31,7 +33,7 @@ except ImportError:
 
 
 from .packages import six
-from .exceptions import LocationParseError
+from .exceptions import LocationParseError, SSLError
 
 
 class Url(namedtuple('Url', ['scheme', 'auth', 'host', 'port', 'path', 'query', 'fragment'])):
@@ -232,7 +234,7 @@ def make_headers(keep_alive=None, accept_encoding=None, user_agent=None,
     return headers
 
 
-def is_connection_dropped(conn):
+def is_connection_dropped(conn):  # Platform-specific
     """
     Returns True if the connection is dropped and should be closed.
 
@@ -246,7 +248,7 @@ def is_connection_dropped(conn):
     if not sock: # Platform-specific: AppEngine
         return False
 
-    if not poll: # Platform-specific
+    if not poll:
         if not select: # Platform-specific: AppEngine
             return False
 
@@ -302,6 +304,44 @@ def resolve_ssl_version(candidate):
 
     return candidate
 
+
+def assert_fingerprint(cert, fingerprint):
+    """
+    Checks if given fingerprint matches the supplied certificate.
+
+    :param cert:
+        Certificate as bytes object.
+    :param fingerprint:
+        Fingerprint as string of hexdigits, can be interspersed by colons.
+    """
+
+    # Maps the length of a digest to a possible hash function producing
+    # this digest.
+    hashfunc_map = {
+        16: md5,
+        20: sha1
+    }
+
+    fingerprint = fingerprint.replace(':', '').lower()
+
+    digest_length, rest = divmod(len(fingerprint), 2)
+
+    if rest or digest_length not in hashfunc_map:
+        raise SSLError('Fingerprint is of invalid length.')
+
+    # We need encode() here for py32; works on py2 and p33.
+    fingerprint_bytes = unhexlify(fingerprint.encode())
+
+    hashfunc = hashfunc_map[digest_length]
+
+    cert_digest = hashfunc(cert).digest()
+
+    if not cert_digest == fingerprint_bytes:
+        raise SSLError('Fingerprints did not match. Expected "{0}", got "{1}".'
+                       .format(hexlify(fingerprint_bytes),
+                               hexlify(cert_digest)))
+
+
 if SSLContext is not None:  # Python 3.2+
     def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None,
                         ca_certs=None, server_hostname=None,