Skip to content

Commit

Permalink
Backport Python 3.5 match_hostname function.
Browse files Browse the repository at this point in the history
This backport actually comes from the repository maintained by Brandon
Rhodes: https://bitbucket.org/brandon/backports.ssl_match_hostname. The
reason for this is that the version in the Python 3.5 repository assumes
the presence of the ipaddress module: this version can function without
it. That makes this a good first step for handling IP address SANs, as
we can simply require that a backported ipaddress module be installed to
get the support required.
  • Loading branch information
Lukasa committed Jul 11, 2016
1 parent 1fbfb4f commit f620d99
Showing 1 changed file with 55 additions and 3 deletions.
58 changes: 55 additions & 3 deletions urllib3/packages/ssl_match_hostname/_implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,20 @@
# stdlib. http://docs.python.org/3/license.html

import re
import sys

# ipaddress has been backported to 2.6+ in pypi. If it is installed on the
# system, use it to handle IPAddress ServerAltnames (this was added in
# python-3.5) otherwise only do DNS matching. This allows
# backports.ssl_match_hostname to continue to be used all the way back to
# python-2.4.
try:
import ipaddress
except ImportError:
ipaddress = None

__version__ = '3.5.0.1'

__version__ = '3.4.0.2'

class CertificateError(ValueError):
pass
Expand Down Expand Up @@ -64,6 +76,23 @@ def _dnsname_match(dn, hostname, max_wildcards=1):
return pat.match(hostname)


def _to_unicode(obj):
if isinstance(obj, str) and sys.version_info < (3,):
obj = unicode(obj, encoding='ascii', errors='strict')
return obj

def _ipaddress_match(ipname, host_ip):
"""Exact matching of IP addresses.
RFC 6125 explicitly doesn't define an algorithm for this
(section 1.7.2 - "Out of Scope").
"""
# OpenSSL may add a trailing newline to a subjectAltName's IP address
# Divergence from upstream: ipaddress can't handle byte str
ip = ipaddress.ip_address(_to_unicode(ipname).rstrip())
return ip == host_ip


def match_hostname(cert, hostname):
"""Verify that *cert* (in decoded format as returned by
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125
Expand All @@ -73,12 +102,35 @@ def match_hostname(cert, hostname):
returns nothing.
"""
if not cert:
raise ValueError("empty or no certificate")
raise ValueError("empty or no certificate, match_hostname needs a "
"SSL socket or SSL context with either "
"CERT_OPTIONAL or CERT_REQUIRED")
try:
# Divergence from upstream: ipaddress can't handle byte str
host_ip = ipaddress.ip_address(_to_unicode(hostname))
except ValueError:
# Not an IP address (common case)
host_ip = None
except UnicodeError:
# Divergence from upstream: Have to deal with ipaddress not taking
# byte strings. addresses should be all ascii, so we consider it not
# an ipaddress in this case
host_ip = None
except AttributeError:
# Divergence from upstream: Make ipaddress library optional
if ipaddress is None:
host_ip = None
else:
raise
dnsnames = []
san = cert.get('subjectAltName', ())
for key, value in san:
if key == 'DNS':
if _dnsname_match(value, hostname):
if host_ip is None and _dnsname_match(value, hostname):
return
dnsnames.append(value)
elif key == 'IP Address':
if host_ip is not None and _ipaddress_match(value, host_ip):
return
dnsnames.append(value)
if not dnsnames:
Expand Down

0 comments on commit f620d99

Please sign in to comment.