Require modern ssl features (SSLContext, etc)
Now that we've dropped python 3.3 (so create_default_context is present on all supported versions), we can drop all ssl backwards-compatibility and require the modern feature set.
This commit is contained in:
parent
648b3e9f7c
commit
8cf55df456
|
@ -1,9 +1,7 @@
|
||||||
# Requirements for tools used in the development of tornado.
|
# Requirements for tools used in the development of tornado.
|
||||||
# This list is for python 3.5; for 2.7 add:
|
# This list is for python 3.5; for 2.7 add:
|
||||||
# - backports.ssl-match-hostname
|
|
||||||
# - futures
|
# - futures
|
||||||
# - mock
|
# - mock
|
||||||
# - certifi
|
|
||||||
#
|
#
|
||||||
# Use virtualenv instead of venv; tox seems to get confused otherwise.
|
# Use virtualenv instead of venv; tox seems to get confused otherwise.
|
||||||
#
|
#
|
||||||
|
|
17
setup.py
17
setup.py
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
import ssl
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
@ -129,18 +130,22 @@ if setuptools is not None:
|
||||||
if sys.version_info < (2, 7):
|
if sys.version_info < (2, 7):
|
||||||
# Only needed indirectly, for singledispatch.
|
# Only needed indirectly, for singledispatch.
|
||||||
install_requires.append('ordereddict')
|
install_requires.append('ordereddict')
|
||||||
if sys.version_info < (2, 7, 9):
|
|
||||||
install_requires.append('backports.ssl_match_hostname')
|
|
||||||
if sys.version_info < (3, 4):
|
if sys.version_info < (3, 4):
|
||||||
install_requires.append('singledispatch')
|
install_requires.append('singledispatch')
|
||||||
# Certifi is also optional on 2.7.9+, although making our dependencies
|
|
||||||
# conditional on micro version numbers seems like a bad idea
|
|
||||||
# until we have more declarative metadata.
|
|
||||||
install_requires.append('certifi')
|
|
||||||
if sys.version_info < (3, 5):
|
if sys.version_info < (3, 5):
|
||||||
install_requires.append('backports_abc>=0.4')
|
install_requires.append('backports_abc>=0.4')
|
||||||
kwargs['install_requires'] = install_requires
|
kwargs['install_requires'] = install_requires
|
||||||
|
|
||||||
|
# Verify that the SSL module has all the modern upgrades. Check for several
|
||||||
|
# names individually since they were introduced at different versions,
|
||||||
|
# although they should all be present by Python 3.4 or 2.7.9.
|
||||||
|
if (not hasattr(ssl, 'SSLContext') or
|
||||||
|
not hasattr(ssl, 'create_default_context') or
|
||||||
|
not hasattr(ssl, 'match_hostname')):
|
||||||
|
raise ImportError("Tornado requires an up-to-date SSL module. This means "
|
||||||
|
"Python 2.7.9+ or 3.4+ (although some distributions have "
|
||||||
|
"backported the necessary changes to older versions).")
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="tornado",
|
name="tornado",
|
||||||
version=version,
|
version=version,
|
||||||
|
|
|
@ -37,7 +37,7 @@ import re
|
||||||
from tornado.concurrent import TracebackFuture
|
from tornado.concurrent import TracebackFuture
|
||||||
from tornado import ioloop
|
from tornado import ioloop
|
||||||
from tornado.log import gen_log, app_log
|
from tornado.log import gen_log, app_log
|
||||||
from tornado.netutil import ssl_wrap_socket, ssl_match_hostname, SSLCertificateError, _client_ssl_defaults, _server_ssl_defaults
|
from tornado.netutil import ssl_wrap_socket, _client_ssl_defaults, _server_ssl_defaults
|
||||||
from tornado import stack_context
|
from tornado import stack_context
|
||||||
from tornado.util import errno_from_exception
|
from tornado.util import errno_from_exception
|
||||||
|
|
||||||
|
@ -1382,8 +1382,8 @@ class SSLIOStream(IOStream):
|
||||||
gen_log.warning("No SSL certificate given")
|
gen_log.warning("No SSL certificate given")
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
ssl_match_hostname(peercert, self._server_hostname)
|
ssl.match_hostname(peercert, self._server_hostname)
|
||||||
except SSLCertificateError as e:
|
except ssl.CertificateError as e:
|
||||||
gen_log.warning("Invalid SSL certificate: %s" % e)
|
gen_log.warning("Invalid SSL certificate: %s" % e)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -35,42 +35,16 @@ except ImportError:
|
||||||
# ssl is not available on Google App Engine
|
# ssl is not available on Google App Engine
|
||||||
ssl = None
|
ssl = None
|
||||||
|
|
||||||
try:
|
|
||||||
import certifi
|
|
||||||
except ImportError:
|
|
||||||
# certifi is optional as long as we have ssl.create_default_context.
|
|
||||||
if ssl is None or hasattr(ssl, 'create_default_context'):
|
|
||||||
certifi = None
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
if PY3:
|
if PY3:
|
||||||
xrange = range
|
xrange = range
|
||||||
|
|
||||||
if hasattr(ssl, 'match_hostname') and hasattr(ssl, 'CertificateError'): # python 3.2+
|
if ssl is not None:
|
||||||
ssl_match_hostname = ssl.match_hostname
|
# Note that the naming of ssl.Purpose is confusing; the purpose
|
||||||
SSLCertificateError = ssl.CertificateError
|
# of a context is to authentiate the opposite side of the connection.
|
||||||
elif ssl is None:
|
_client_ssl_defaults = ssl.create_default_context(
|
||||||
ssl_match_hostname = SSLCertificateError = None # type: ignore
|
ssl.Purpose.SERVER_AUTH)
|
||||||
else:
|
_server_ssl_defaults = ssl.create_default_context(
|
||||||
import backports.ssl_match_hostname
|
ssl.Purpose.CLIENT_AUTH)
|
||||||
ssl_match_hostname = backports.ssl_match_hostname.match_hostname
|
|
||||||
SSLCertificateError = backports.ssl_match_hostname.CertificateError # type: ignore
|
|
||||||
|
|
||||||
if hasattr(ssl, 'SSLContext'):
|
|
||||||
if hasattr(ssl, 'create_default_context'):
|
|
||||||
# Python 2.7.9+, 3.4+
|
|
||||||
# Note that the naming of ssl.Purpose is confusing; the purpose
|
|
||||||
# of a context is to authentiate the opposite side of the connection.
|
|
||||||
_client_ssl_defaults = ssl.create_default_context(
|
|
||||||
ssl.Purpose.SERVER_AUTH)
|
|
||||||
_server_ssl_defaults = ssl.create_default_context(
|
|
||||||
ssl.Purpose.CLIENT_AUTH)
|
|
||||||
elif ssl:
|
|
||||||
# Python 2.6-2.7.8
|
|
||||||
_client_ssl_defaults = dict(cert_reqs=ssl.CERT_REQUIRED,
|
|
||||||
ca_certs=certifi.where())
|
|
||||||
_server_ssl_defaults = {}
|
|
||||||
else:
|
else:
|
||||||
# Google App Engine
|
# Google App Engine
|
||||||
_client_ssl_defaults = dict(cert_reqs=None,
|
_client_ssl_defaults = dict(cert_reqs=None,
|
||||||
|
@ -487,11 +461,10 @@ def ssl_options_to_context(ssl_options):
|
||||||
accepts both forms needs to upgrade to the `~ssl.SSLContext` version
|
accepts both forms needs to upgrade to the `~ssl.SSLContext` version
|
||||||
to use features like SNI or NPN.
|
to use features like SNI or NPN.
|
||||||
"""
|
"""
|
||||||
if isinstance(ssl_options, dict):
|
if isinstance(ssl_options, ssl.SSLContext):
|
||||||
assert all(k in _SSL_CONTEXT_KEYWORDS for k in ssl_options), ssl_options
|
|
||||||
if (not hasattr(ssl, 'SSLContext') or
|
|
||||||
isinstance(ssl_options, ssl.SSLContext)):
|
|
||||||
return ssl_options
|
return ssl_options
|
||||||
|
assert isinstance(ssl_options, dict)
|
||||||
|
assert all(k in _SSL_CONTEXT_KEYWORDS for k in ssl_options), ssl_options
|
||||||
context = ssl.SSLContext(
|
context = ssl.SSLContext(
|
||||||
ssl_options.get('ssl_version', ssl.PROTOCOL_SSLv23))
|
ssl_options.get('ssl_version', ssl.PROTOCOL_SSLv23))
|
||||||
if 'certfile' in ssl_options:
|
if 'certfile' in ssl_options:
|
||||||
|
@ -519,14 +492,13 @@ def ssl_wrap_socket(socket, ssl_options, server_hostname=None, **kwargs):
|
||||||
appropriate).
|
appropriate).
|
||||||
"""
|
"""
|
||||||
context = ssl_options_to_context(ssl_options)
|
context = ssl_options_to_context(ssl_options)
|
||||||
if hasattr(ssl, 'SSLContext') and isinstance(context, ssl.SSLContext):
|
if ssl.HAS_SNI:
|
||||||
if server_hostname is not None and getattr(ssl, 'HAS_SNI'):
|
# In python 3.4, wrap_socket only accepts the server_hostname
|
||||||
# Python doesn't have server-side SNI support so we can't
|
# argument if HAS_SNI is true.
|
||||||
# really unittest this, but it can be manually tested with
|
# TODO: add a unittest (python added server-side SNI support in 3.4)
|
||||||
# python3.2 -m tornado.httpclient https://sni.velox.ch
|
# In the meantime it can be manually tested with
|
||||||
return context.wrap_socket(socket, server_hostname=server_hostname,
|
# python3 -m tornado.httpclient https://sni.velox.ch
|
||||||
**kwargs)
|
return context.wrap_socket(socket, server_hostname=server_hostname,
|
||||||
else:
|
**kwargs)
|
||||||
return context.wrap_socket(socket, **kwargs)
|
|
||||||
else:
|
else:
|
||||||
return ssl.wrap_socket(socket, **dict(context, **kwargs)) # type: ignore
|
return context.wrap_socket(socket, **kwargs)
|
||||||
|
|
|
@ -35,18 +35,6 @@ except ImportError:
|
||||||
# ssl is not available on Google App Engine.
|
# ssl is not available on Google App Engine.
|
||||||
ssl = None
|
ssl = None
|
||||||
|
|
||||||
try:
|
|
||||||
import certifi
|
|
||||||
except ImportError:
|
|
||||||
certifi = None
|
|
||||||
|
|
||||||
|
|
||||||
def _default_ca_certs():
|
|
||||||
if certifi is None:
|
|
||||||
raise Exception("The 'certifi' package is required to use https "
|
|
||||||
"in simple_httpclient")
|
|
||||||
return certifi.where()
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleAsyncHTTPClient(AsyncHTTPClient):
|
class SimpleAsyncHTTPClient(AsyncHTTPClient):
|
||||||
"""Non-blocking HTTP client with no external dependencies.
|
"""Non-blocking HTTP client with no external dependencies.
|
||||||
|
@ -261,11 +249,6 @@ class _HTTPConnection(httputil.HTTPMessageDelegate):
|
||||||
ssl_options["cert_reqs"] = ssl.CERT_REQUIRED
|
ssl_options["cert_reqs"] = ssl.CERT_REQUIRED
|
||||||
if self.request.ca_certs is not None:
|
if self.request.ca_certs is not None:
|
||||||
ssl_options["ca_certs"] = self.request.ca_certs
|
ssl_options["ca_certs"] = self.request.ca_certs
|
||||||
elif not hasattr(ssl, 'create_default_context'):
|
|
||||||
# When create_default_context is present,
|
|
||||||
# we can omit the "ca_certs" parameter entirely,
|
|
||||||
# which avoids the dependency on "certifi" for py34.
|
|
||||||
ssl_options["ca_certs"] = _default_ca_certs()
|
|
||||||
if self.request.client_key is not None:
|
if self.request.client_key is not None:
|
||||||
ssl_options["keyfile"] = self.request.client_key
|
ssl_options["keyfile"] = self.request.client_key
|
||||||
if self.request.client_cert is not None:
|
if self.request.client_cert is not None:
|
||||||
|
|
|
@ -150,7 +150,6 @@ class TLSv1Test(BaseSSLTest, SSLTestMixin):
|
||||||
return ssl.PROTOCOL_TLSv1
|
return ssl.PROTOCOL_TLSv1
|
||||||
|
|
||||||
|
|
||||||
@unittest.skipIf(not hasattr(ssl, 'SSLContext'), 'ssl.SSLContext not present')
|
|
||||||
class SSLContextTest(BaseSSLTest, SSLTestMixin):
|
class SSLContextTest(BaseSSLTest, SSLTestMixin):
|
||||||
def get_ssl_options(self):
|
def get_ssl_options(self):
|
||||||
context = ssl_options_to_context(
|
context = ssl_options_to_context(
|
||||||
|
|
|
@ -880,7 +880,6 @@ class TestIOStreamSSL(TestIOStreamMixin, AsyncTestCase):
|
||||||
# This will run some tests that are basically redundant but it's the
|
# This will run some tests that are basically redundant but it's the
|
||||||
# simplest way to make sure that it works to pass an SSLContext
|
# simplest way to make sure that it works to pass an SSLContext
|
||||||
# instead of an ssl_options dict to the SSLIOStream constructor.
|
# instead of an ssl_options dict to the SSLIOStream constructor.
|
||||||
@unittest.skipIf(not hasattr(ssl, 'SSLContext'), 'ssl.SSLContext not present')
|
|
||||||
class TestIOStreamSSLContext(TestIOStreamMixin, AsyncTestCase):
|
class TestIOStreamSSLContext(TestIOStreamMixin, AsyncTestCase):
|
||||||
def _make_server_iostream(self, connection, **kwargs):
|
def _make_server_iostream(self, connection, **kwargs):
|
||||||
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
|
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
|
||||||
|
@ -983,8 +982,6 @@ class TestIOStreamStartTLS(AsyncTestCase):
|
||||||
with self.assertRaises((ssl.SSLError, socket.error)):
|
with self.assertRaises((ssl.SSLError, socket.error)):
|
||||||
yield server_future
|
yield server_future
|
||||||
|
|
||||||
@unittest.skipIf(not hasattr(ssl, 'create_default_context'),
|
|
||||||
'ssl.create_default_context not present')
|
|
||||||
@gen_test
|
@gen_test
|
||||||
def test_check_hostname(self):
|
def test_check_hostname(self):
|
||||||
# Test that server_hostname parameter to start_tls is being used.
|
# Test that server_hostname parameter to start_tls is being used.
|
||||||
|
|
|
@ -497,8 +497,6 @@ class SimpleHTTPSClientTestCase(SimpleHTTPClientTestMixin, AsyncHTTPSTestCase):
|
||||||
resp = self.fetch("/hello", ssl_options={})
|
resp = self.fetch("/hello", ssl_options={})
|
||||||
self.assertEqual(resp.body, b"Hello world!")
|
self.assertEqual(resp.body, b"Hello world!")
|
||||||
|
|
||||||
@unittest.skipIf(not hasattr(ssl, 'SSLContext'),
|
|
||||||
'ssl.SSLContext not present')
|
|
||||||
def test_ssl_context(self):
|
def test_ssl_context(self):
|
||||||
resp = self.fetch("/hello",
|
resp = self.fetch("/hello",
|
||||||
ssl_options=ssl.SSLContext(ssl.PROTOCOL_SSLv23))
|
ssl_options=ssl.SSLContext(ssl.PROTOCOL_SSLv23))
|
||||||
|
@ -511,8 +509,6 @@ class SimpleHTTPSClientTestCase(SimpleHTTPClientTestMixin, AsyncHTTPSTestCase):
|
||||||
"/hello", ssl_options=dict(cert_reqs=ssl.CERT_REQUIRED))
|
"/hello", ssl_options=dict(cert_reqs=ssl.CERT_REQUIRED))
|
||||||
self.assertRaises(ssl.SSLError, resp.rethrow)
|
self.assertRaises(ssl.SSLError, resp.rethrow)
|
||||||
|
|
||||||
@unittest.skipIf(not hasattr(ssl, 'SSLContext'),
|
|
||||||
'ssl.SSLContext not present')
|
|
||||||
def test_ssl_context_handshake_fail(self):
|
def test_ssl_context_handshake_fail(self):
|
||||||
with ExpectLog(gen_log, "SSL Error|Uncaught exception"):
|
with ExpectLog(gen_log, "SSL Error|Uncaught exception"):
|
||||||
ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
|
ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
|
||||||
|
|
Loading…
Reference in New Issue