From: Ben Darnell Date: Tue, 15 Feb 2011 04:14:43 +0000 (-0800) Subject: Add ssl.match_hostname function backported from Python 3.2. X-Git-Tag: v1.2.0~24 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=372e545435371192010ad70189fd6eabc74d197d;p=thirdparty%2Ftornado.git Add ssl.match_hostname function backported from Python 3.2. https://bitbucket.org/brandon/backports.ssl_match_hostname --- diff --git a/tornado/simple_httpclient.py b/tornado/simple_httpclient.py index f550980bb..cf9db3bf3 100644 --- a/tornado/simple_httpclient.py +++ b/tornado/simple_httpclient.py @@ -304,6 +304,65 @@ class _HTTPConnection(object): self.stream.read_until("\r\n", self._on_chunk_length) +# match_hostname was added to the standard library ssl module in python 3.2. +# The following code was backported for older releases and copied from +# https://bitbucket.org/brandon/backports.ssl_match_hostname +class CertificateError(ValueError): + pass + +def _dnsname_to_pat(dn): + pats = [] + for frag in dn.split(r'.'): + if frag == '*': + # When '*' is a fragment by itself, it matches a non-empty dotless + # fragment. + pats.append('[^.]+') + else: + # Otherwise, '*' matches any dotless fragment. + frag = re.escape(frag) + pats.append(frag.replace(r'\*', '[^.]*')) + return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE) + +def match_hostname(cert, hostname): + """Verify that *cert* (in decoded format as returned by + SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules + are mostly followed, but IP addresses are not accepted for *hostname*. + + CertificateError is raised on failure. On success, the function + returns nothing. + """ + if not cert: + raise ValueError("empty or no certificate") + dnsnames = [] + san = cert.get('subjectAltName', ()) + for key, value in san: + if key == 'DNS': + if _dnsname_to_pat(value).match(hostname): + return + dnsnames.append(value) + if not san: + # The subject is only checked when subjectAltName is empty + for sub in cert.get('subject', ()): + for key, value in sub: + # XXX according to RFC 2818, the most specific Common Name + # must be used. + if key == 'commonName': + if _dnsname_to_pat(value).match(hostname): + return + dnsnames.append(value) + if len(dnsnames) > 1: + raise CertificateError("hostname %r " + "doesn't match either of %s" + % (hostname, ', '.join(map(repr, dnsnames)))) + elif len(dnsnames) == 1: + raise CertificateError("hostname %r " + "doesn't match %r" + % (hostname, dnsnames[0])) + else: + raise CertificateError("no appropriate commonName or " + "subjectAltName fields were found") + + def main(): from tornado.options import define, options, parse_command_line args = parse_command_line()