Skip to content

Commit

Permalink
Add backports.ssl_match_hostname dependency in place of our copy.
Browse files Browse the repository at this point in the history
This function has changed recently and it makes more sense to stop
maintaining a separate copy, even though it does introduce our first
required dependency.
  • Loading branch information
bdarnell committed Nov 5, 2013
1 parent 4f276ca commit 4a5fdb1
Show file tree
Hide file tree
Showing 4 changed files with 37 additions and 85 deletions.
17 changes: 14 additions & 3 deletions docs/index.rst
Expand Up @@ -17,6 +17,13 @@ can scale to tens of thousands of open connections, making it ideal for
`WebSockets <http://en.wikipedia.org/wiki/WebSocket>`_, and other `WebSockets <http://en.wikipedia.org/wiki/WebSocket>`_, and other
applications that require a long-lived connection to each user. applications that require a long-lived connection to each user.


Upgrade notes
-------------

As of Tornado 3.2, the `backports.ssl_match_hostname
<https://pypi.python.org/pypi/backports.ssl_match_hostname>`_ package
must be installed when running Tornado on Python 2. This will be
installed automatically when using ``pip`` or ``easy_install``.


Quick links Quick links
----------- -----------
Expand Down Expand Up @@ -78,9 +85,13 @@ copy of the source tarball as well.
The Tornado source code is `hosted on GitHub The Tornado source code is `hosted on GitHub
<https://github.com/facebook/tornado>`_. <https://github.com/facebook/tornado>`_.


**Prerequisites**: Tornado runs on Python 2.6, 2.7, 3.2, and 3.3. It has **Prerequisites**: Tornado runs on Python 2.6, 2.7, 3.2, and 3.3. On
no strict dependencies outside the Python standard library, although some Python 2, the `backports.ssl_match_hostname
features may require one of the following libraries: <https://pypi.python.org/pypi/backports.ssl_match_hostname>`_ package
must be installed (This will be installed automatically when using
``pip`` or ``easy_install``); on Python 3 there are no strict
dependencies outside the standard library. Some Tornado features may
require one of the following optional libraries:


* `unittest2 <https://pypi.python.org/pypi/unittest2>`_ is needed to run * `unittest2 <https://pypi.python.org/pypi/unittest2>`_ is needed to run
Tornado's test suite on Python 2.6 (it is unnecessary on more recent Tornado's test suite on Python 2.6 (it is unnecessary on more recent
Expand Down
3 changes: 3 additions & 0 deletions maint/requirements.txt
@@ -1,5 +1,8 @@
# Frozen pip requirements for tools used in the development of tornado # Frozen pip requirements for tools used in the development of tornado


# Tornado's required dependencies
backports.ssl-match-hostname==3.4.0.2

# Tornado's optional dependencies # Tornado's optional dependencies
Twisted==13.0.0 Twisted==13.0.0
futures==2.1.3 futures==2.1.3
Expand Down
24 changes: 13 additions & 11 deletions setup.py
Expand Up @@ -14,14 +14,15 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.


import distutils.core
import sys import sys
# Importing setuptools adds some features like "setup.py develop", but
# it's optional so swallow the error if it's not there.
try: try:
# Use setuptools if available, for install_requires (among other things).
import setuptools import setuptools
from setuptools import setup
except ImportError: except ImportError:
pass setuptools = None
from distutils.core import setup


try: try:
from Cython.Build import cythonize from Cython.Build import cythonize
Expand All @@ -33,18 +34,20 @@
version = "3.2.dev2" version = "3.2.dev2"


with open('README.rst') as f: with open('README.rst') as f:
long_description = f.read() kwargs['long_description'] = f.read()


if cythonize is not None: if cythonize is not None:
extensions = cythonize('tornado/speedups.pyx') kwargs['ext_modules'] = cythonize('tornado/speedups.pyx')
else:
extensions = [] if setuptools is not None:
# If setuptools is not available, you're on your own for dependencies.
if sys.version_info < (3, 2):
kwargs['install_requires'] = ['backports.ssl_match_hostname']


distutils.core.setup( setuptools.setup(
name="tornado", name="tornado",
version=version, version=version,
packages = ["tornado", "tornado.test", "tornado.platform"], packages = ["tornado", "tornado.test", "tornado.platform"],
ext_modules = extensions,
package_data = { package_data = {
"tornado": ["ca-certificates.crt"], "tornado": ["ca-certificates.crt"],
# data files need to be listed both here (which determines what gets # data files need to be listed both here (which determines what gets
Expand Down Expand Up @@ -79,6 +82,5 @@
'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy', 'Programming Language :: Python :: Implementation :: PyPy',
], ],
long_description=long_description,
**kwargs **kwargs
) )
78 changes: 7 additions & 71 deletions tornado/netutil.py
Expand Up @@ -20,7 +20,6 @@


import errno import errno
import os import os
import re
import socket import socket
import ssl import ssl
import stat import stat
Expand All @@ -30,6 +29,13 @@
from tornado.platform.auto import set_close_exec from tornado.platform.auto import set_close_exec
from tornado.util import Configurable from tornado.util import Configurable


if hasattr(ssl, 'match_hostname') and hasattr(ssl, 'CertificateError'): # python 3.2+
ssl_match_hostname = ssl.match_hostname
SSLCertificateError = ssl.CertificateError
else:
import backports.ssl_match_hostname
ssl_match_hostname = backports.ssl_match_hostname.match_hostname
SSLCertificateError = backports.ssl_match_hostname.CertificateError


def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128, flags=None): def bind_sockets(port, address=None, family=socket.AF_UNSPEC, backlog=128, flags=None):
"""Creates listening sockets bound to the given port and address. """Creates listening sockets bound to the given port and address.
Expand Down Expand Up @@ -391,73 +397,3 @@ def ssl_wrap_socket(socket, ssl_options, server_hostname=None, **kwargs):
return context.wrap_socket(socket, **kwargs) return context.wrap_socket(socket, **kwargs)
else: else:
return ssl.wrap_socket(socket, **dict(context, **kwargs)) return ssl.wrap_socket(socket, **dict(context, **kwargs))

if hasattr(ssl, 'match_hostname') and hasattr(ssl, 'CertificateError'): # python 3.2+
ssl_match_hostname = ssl.match_hostname
SSLCertificateError = ssl.CertificateError
else:
# 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 SSLCertificateError(ValueError):
pass

def _dnsname_to_pat(dn, max_wildcards=1):
pats = []
for frag in dn.split(r'.'):
if frag.count('*') > max_wildcards:
# Issue #17980: avoid denials of service by refusing more
# than one wildcard per fragment. A survery of established
# policy among SSL implementations showed it to be a
# reasonable choice.
raise SSLCertificateError(
"too many wildcards in certificate DNS name: " + repr(dn))
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 ssl_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 dnsnames:
# The subject is only checked when there is no dNSName entry
# in subjectAltName
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 SSLCertificateError("hostname %r "
"doesn't match either of %s"
% (hostname, ', '.join(map(repr, dnsnames))))
elif len(dnsnames) == 1:
raise SSLCertificateError("hostname %r "
"doesn't match %r"
% (hostname, dnsnames[0]))
else:
raise SSLCertificateError("no appropriate commonName or "
"subjectAltName fields were found")

0 comments on commit 4a5fdb1

Please sign in to comment.