From 8cf55df456561077f363ce46d9760716daa1580a Mon Sep 17 00:00:00 2001 From: Ben Darnell Date: Sun, 22 Oct 2017 12:20:18 -0400 Subject: [PATCH] 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. --- maint/requirements.in | 2 - setup.py | 17 ++++--- tornado/iostream.py | 6 +-- tornado/netutil.py | 66 ++++++++------------------ tornado/simple_httpclient.py | 17 ------- tornado/test/httpserver_test.py | 1 - tornado/test/iostream_test.py | 3 -- tornado/test/simple_httpclient_test.py | 4 -- 8 files changed, 33 insertions(+), 83 deletions(-) diff --git a/maint/requirements.in b/maint/requirements.in index 64beb5fc..eeb2f4d6 100644 --- a/maint/requirements.in +++ b/maint/requirements.in @@ -1,9 +1,7 @@ # Requirements for tools used in the development of tornado. # This list is for python 3.5; for 2.7 add: -# - backports.ssl-match-hostname # - futures # - mock -# - certifi # # Use virtualenv instead of venv; tox seems to get confused otherwise. # diff --git a/setup.py b/setup.py index 72569a1f..987c0cf4 100644 --- a/setup.py +++ b/setup.py @@ -16,6 +16,7 @@ import os import platform +import ssl import sys import warnings @@ -129,18 +130,22 @@ if setuptools is not None: if sys.version_info < (2, 7): # Only needed indirectly, for singledispatch. install_requires.append('ordereddict') - if sys.version_info < (2, 7, 9): - install_requires.append('backports.ssl_match_hostname') if sys.version_info < (3, 4): 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): install_requires.append('backports_abc>=0.4') 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( name="tornado", version=version, diff --git a/tornado/iostream.py b/tornado/iostream.py index 632952b5..3e971bfd 100644 --- a/tornado/iostream.py +++ b/tornado/iostream.py @@ -37,7 +37,7 @@ import re from tornado.concurrent import TracebackFuture from tornado import ioloop 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.util import errno_from_exception @@ -1382,8 +1382,8 @@ class SSLIOStream(IOStream): gen_log.warning("No SSL certificate given") return False try: - ssl_match_hostname(peercert, self._server_hostname) - except SSLCertificateError as e: + ssl.match_hostname(peercert, self._server_hostname) + except ssl.CertificateError as e: gen_log.warning("Invalid SSL certificate: %s" % e) return False else: diff --git a/tornado/netutil.py b/tornado/netutil.py index c054f625..63c80cb3 100644 --- a/tornado/netutil.py +++ b/tornado/netutil.py @@ -35,42 +35,16 @@ except ImportError: # ssl is not available on Google App Engine 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: xrange = range -if hasattr(ssl, 'match_hostname') and hasattr(ssl, 'CertificateError'): # python 3.2+ - ssl_match_hostname = ssl.match_hostname - SSLCertificateError = ssl.CertificateError -elif ssl is None: - ssl_match_hostname = SSLCertificateError = None # type: ignore -else: - import backports.ssl_match_hostname - 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 = {} +if ssl is not None: + # 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) else: # Google App Engine _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 to use features like SNI or NPN. """ - if isinstance(ssl_options, dict): - 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)): + if isinstance(ssl_options, ssl.SSLContext): 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( ssl_options.get('ssl_version', ssl.PROTOCOL_SSLv23)) if 'certfile' in ssl_options: @@ -519,14 +492,13 @@ def ssl_wrap_socket(socket, ssl_options, server_hostname=None, **kwargs): appropriate). """ context = ssl_options_to_context(ssl_options) - if hasattr(ssl, 'SSLContext') and isinstance(context, ssl.SSLContext): - if server_hostname is not None and getattr(ssl, 'HAS_SNI'): - # Python doesn't have server-side SNI support so we can't - # really unittest this, but it can be manually tested with - # python3.2 -m tornado.httpclient https://sni.velox.ch - return context.wrap_socket(socket, server_hostname=server_hostname, - **kwargs) - else: - return context.wrap_socket(socket, **kwargs) + if ssl.HAS_SNI: + # In python 3.4, wrap_socket only accepts the server_hostname + # argument if HAS_SNI is true. + # TODO: add a unittest (python added server-side SNI support in 3.4) + # In the meantime it can be manually tested with + # python3 -m tornado.httpclient https://sni.velox.ch + return context.wrap_socket(socket, server_hostname=server_hostname, + **kwargs) else: - return ssl.wrap_socket(socket, **dict(context, **kwargs)) # type: ignore + return context.wrap_socket(socket, **kwargs) diff --git a/tornado/simple_httpclient.py b/tornado/simple_httpclient.py index f394689d..65343991 100644 --- a/tornado/simple_httpclient.py +++ b/tornado/simple_httpclient.py @@ -35,18 +35,6 @@ except ImportError: # ssl is not available on Google App Engine. 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): """Non-blocking HTTP client with no external dependencies. @@ -261,11 +249,6 @@ class _HTTPConnection(httputil.HTTPMessageDelegate): ssl_options["cert_reqs"] = ssl.CERT_REQUIRED if self.request.ca_certs is not None: 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: ssl_options["keyfile"] = self.request.client_key if self.request.client_cert is not None: diff --git a/tornado/test/httpserver_test.py b/tornado/test/httpserver_test.py index 4444a392..2564e83b 100644 --- a/tornado/test/httpserver_test.py +++ b/tornado/test/httpserver_test.py @@ -150,7 +150,6 @@ class TLSv1Test(BaseSSLTest, SSLTestMixin): return ssl.PROTOCOL_TLSv1 -@unittest.skipIf(not hasattr(ssl, 'SSLContext'), 'ssl.SSLContext not present') class SSLContextTest(BaseSSLTest, SSLTestMixin): def get_ssl_options(self): context = ssl_options_to_context( diff --git a/tornado/test/iostream_test.py b/tornado/test/iostream_test.py index 751dfe2e..4df23dd2 100644 --- a/tornado/test/iostream_test.py +++ b/tornado/test/iostream_test.py @@ -880,7 +880,6 @@ class TestIOStreamSSL(TestIOStreamMixin, AsyncTestCase): # 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 # instead of an ssl_options dict to the SSLIOStream constructor. -@unittest.skipIf(not hasattr(ssl, 'SSLContext'), 'ssl.SSLContext not present') class TestIOStreamSSLContext(TestIOStreamMixin, AsyncTestCase): def _make_server_iostream(self, connection, **kwargs): context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) @@ -983,8 +982,6 @@ class TestIOStreamStartTLS(AsyncTestCase): with self.assertRaises((ssl.SSLError, socket.error)): yield server_future - @unittest.skipIf(not hasattr(ssl, 'create_default_context'), - 'ssl.create_default_context not present') @gen_test def test_check_hostname(self): # Test that server_hostname parameter to start_tls is being used. diff --git a/tornado/test/simple_httpclient_test.py b/tornado/test/simple_httpclient_test.py index f7133143..309678ef 100644 --- a/tornado/test/simple_httpclient_test.py +++ b/tornado/test/simple_httpclient_test.py @@ -497,8 +497,6 @@ class SimpleHTTPSClientTestCase(SimpleHTTPClientTestMixin, AsyncHTTPSTestCase): resp = self.fetch("/hello", ssl_options={}) self.assertEqual(resp.body, b"Hello world!") - @unittest.skipIf(not hasattr(ssl, 'SSLContext'), - 'ssl.SSLContext not present') def test_ssl_context(self): resp = self.fetch("/hello", ssl_options=ssl.SSLContext(ssl.PROTOCOL_SSLv23)) @@ -511,8 +509,6 @@ class SimpleHTTPSClientTestCase(SimpleHTTPClientTestMixin, AsyncHTTPSTestCase): "/hello", ssl_options=dict(cert_reqs=ssl.CERT_REQUIRED)) self.assertRaises(ssl.SSLError, resp.rethrow) - @unittest.skipIf(not hasattr(ssl, 'SSLContext'), - 'ssl.SSLContext not present') def test_ssl_context_handshake_fail(self): with ExpectLog(gen_log, "SSL Error|Uncaught exception"): ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)