Skip to content

Commit 372e545

Browse files
committed
Add ssl.match_hostname function backported from Python 3.2.
https://bitbucket.org/brandon/backports.ssl_match_hostname
1 parent 4191ed3 commit 372e545

File tree

1 file changed

+59
-0
lines changed

1 file changed

+59
-0
lines changed

tornado/simple_httpclient.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,65 @@ def _on_chunk_data(self, data):
304304
self.stream.read_until("\r\n", self._on_chunk_length)
305305

306306

307+
# match_hostname was added to the standard library ssl module in python 3.2.
308+
# The following code was backported for older releases and copied from
309+
# https://bitbucket.org/brandon/backports.ssl_match_hostname
310+
class CertificateError(ValueError):
311+
pass
312+
313+
def _dnsname_to_pat(dn):
314+
pats = []
315+
for frag in dn.split(r'.'):
316+
if frag == '*':
317+
# When '*' is a fragment by itself, it matches a non-empty dotless
318+
# fragment.
319+
pats.append('[^.]+')
320+
else:
321+
# Otherwise, '*' matches any dotless fragment.
322+
frag = re.escape(frag)
323+
pats.append(frag.replace(r'\*', '[^.]*'))
324+
return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
325+
326+
def match_hostname(cert, hostname):
327+
"""Verify that *cert* (in decoded format as returned by
328+
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules
329+
are mostly followed, but IP addresses are not accepted for *hostname*.
330+
331+
CertificateError is raised on failure. On success, the function
332+
returns nothing.
333+
"""
334+
if not cert:
335+
raise ValueError("empty or no certificate")
336+
dnsnames = []
337+
san = cert.get('subjectAltName', ())
338+
for key, value in san:
339+
if key == 'DNS':
340+
if _dnsname_to_pat(value).match(hostname):
341+
return
342+
dnsnames.append(value)
343+
if not san:
344+
# The subject is only checked when subjectAltName is empty
345+
for sub in cert.get('subject', ()):
346+
for key, value in sub:
347+
# XXX according to RFC 2818, the most specific Common Name
348+
# must be used.
349+
if key == 'commonName':
350+
if _dnsname_to_pat(value).match(hostname):
351+
return
352+
dnsnames.append(value)
353+
if len(dnsnames) > 1:
354+
raise CertificateError("hostname %r "
355+
"doesn't match either of %s"
356+
% (hostname, ', '.join(map(repr, dnsnames))))
357+
elif len(dnsnames) == 1:
358+
raise CertificateError("hostname %r "
359+
"doesn't match %r"
360+
% (hostname, dnsnames[0]))
361+
else:
362+
raise CertificateError("no appropriate commonName or "
363+
"subjectAltName fields were found")
364+
365+
307366
def main():
308367
from tornado.options import define, options, parse_command_line
309368
args = parse_command_line()

0 commit comments

Comments
 (0)