Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
Kenneth Reitz committed Mar 31, 2013
2 parents d06908d + 5b5ffc9 commit 59b69d1
Show file tree
Hide file tree
Showing 4 changed files with 248 additions and 16 deletions.
38 changes: 27 additions & 11 deletions requests/packages/urllib3/connectionpool.py
Expand Up @@ -9,7 +9,7 @@
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
Expand Down Expand Up @@ -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
Expand All @@ -104,8 +107,12 @@ def connect(self):
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

Expand Down Expand Up @@ -502,18 +509,23 @@ 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'

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,
Expand All @@ -523,6 +535,8 @@ def __init__(self, host, port=None,
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):
"""
Expand All @@ -532,7 +546,7 @@ def _new_conn(self):
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.")
Expand All @@ -545,7 +559,9 @@ def _new_conn(self):
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

Expand Down
167 changes: 167 additions & 0 deletions requests/packages/urllib3/contrib/pyopenssl.py
@@ -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)
11 changes: 10 additions & 1 deletion requests/packages/urllib3/poolmanager.py
Expand Up @@ -23,6 +23,9 @@

log = logging.getLogger(__name__)

SSL_KEYWORDS = ('key_file', 'cert_file', 'cert_reqs', 'ca_certs',
'ssl_version')


class PoolManager(RequestMethods):
"""
Expand Down Expand Up @@ -67,7 +70,13 @@ def _new_pool(self, scheme, host, port):
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):
"""
Expand Down
48 changes: 44 additions & 4 deletions requests/packages/urllib3/util.py
Expand Up @@ -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
Expand All @@ -23,15 +25,15 @@
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:
pass


from .packages import six
from .exceptions import LocationParseError
from .exceptions import LocationParseError, SSLError


class Url(namedtuple('Url', ['scheme', 'auth', 'host', 'port', 'path', 'query', 'fragment'])):
Expand Down Expand Up @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 59b69d1

Please sign in to comment.