diff --git a/docs/index.rst b/docs/index.rst index a5f166a3e6..ed24eb6232 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -66,9 +66,10 @@ installed in this way, so you may wish to download a copy of the source tarball or clone the `git repository `_ as well. -**Prerequisites**: Tornado runs on Python 2.7, and 3.4+ -For Python 2, version 2.7.9 or newer is *strongly* -recommended for the improved SSL support. In addition to the requirements +**Prerequisites**: Tornado runs on Python 2.7, and 3.4+. +The updates to the `ssl` module in Python 2.7.9 are required +(in some distributions, these updates may be available in +older python versions). In addition to the requirements which will be installed automatically by ``pip`` or ``setup.py install``, the following optional packages may be useful: diff --git a/maint/requirements.in b/maint/requirements.in index 64beb5fc2d..eeb2f4d6a3 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 72569a1f3a..26bb716d52 100644 --- a/setup.py +++ b/setup.py @@ -16,6 +16,7 @@ import os import platform +import ssl import sys import warnings @@ -126,21 +127,22 @@ def build_extension(self, ext): if setuptools is not None: # If setuptools is not available, you're on your own for dependencies. install_requires = [] - 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 632952b524..3e971bfde5 100644 --- a/tornado/iostream.py +++ b/tornado/iostream.py @@ -37,7 +37,7 @@ 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 @@ def _verify_cert(self, peercert): 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 c054f625cf..1469b90c0b 100644 --- a/tornado/netutil.py +++ b/tornado/netutil.py @@ -35,42 +35,20 @@ # 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) + if hasattr(ssl, 'OP_NO_COMPRESSION'): + # See netutil.ssl_options_to_context + _client_ssl_defaults.options |= ssl.OP_NO_COMPRESSION + _server_ssl_defaults.options |= ssl.OP_NO_COMPRESSION else: # Google App Engine _client_ssl_defaults = dict(cert_reqs=None, @@ -487,11 +465,12 @@ 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 + # Can't use create_default_context since this interface doesn't + # tell us client vs server. context = ssl.SSLContext( ssl_options.get('ssl_version', ssl.PROTOCOL_SSLv23)) if 'certfile' in ssl_options: @@ -504,7 +483,9 @@ def ssl_options_to_context(ssl_options): context.set_ciphers(ssl_options['ciphers']) if hasattr(ssl, 'OP_NO_COMPRESSION'): # Disable TLS compression to avoid CRIME and related attacks. - # This constant wasn't added until python 3.3. + # This constant depends on openssl version 1.0. + # TODO: Do we need to do this ourselves or can we trust + # the defaults? context.options |= ssl.OP_NO_COMPRESSION return context @@ -519,14 +500,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 f394689d12..6d90352f1a 100644 --- a/tornado/simple_httpclient.py +++ b/tornado/simple_httpclient.py @@ -35,18 +35,6 @@ # 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. @@ -256,42 +244,19 @@ def _get_ssl_options(self, scheme): self.request.client_cert is None and self.request.client_key is None): return _client_ssl_defaults - ssl_options = {} - if self.request.validate_cert: - 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 + ssl_ctx = ssl.create_default_context( + ssl.Purpose.SERVER_AUTH, + cafile=self.request.ca_certs) + if not self.request.validate_cert: + ssl_ctx.check_hostname = False + ssl_ctx.verify_mode = ssl.CERT_NONE if self.request.client_cert is not None: - ssl_options["certfile"] = self.request.client_cert - - # SSL interoperability is tricky. We want to disable - # SSLv2 for security reasons; it wasn't disabled by default - # until openssl 1.0. The best way to do this is to use - # the SSL_OP_NO_SSLv2, but that wasn't exposed to python - # until 3.2. Python 2.7 adds the ciphers argument, which - # can also be used to disable SSLv2. As a last resort - # on python 2.6, we set ssl_version to TLSv1. This is - # more narrow than we'd like since it also breaks - # compatibility with servers configured for SSLv3 only, - # but nearly all servers support both SSLv3 and TLSv1: - # http://blog.ivanristic.com/2011/09/ssl-survey-protocol-support.html - if sys.version_info >= (2, 7): - # In addition to disabling SSLv2, we also exclude certain - # classes of insecure ciphers. - ssl_options["ciphers"] = "DEFAULT:!SSLv2:!EXPORT:!DES" - else: - # This is really only necessary for pre-1.0 versions - # of openssl, but python 2.6 doesn't expose version - # information. - ssl_options["ssl_version"] = ssl.PROTOCOL_TLSv1 - return ssl_options + ssl_ctx.load_cert_chain(self.request.client_cert, + self.request.client_key) + if hasattr(ssl, 'OP_NO_COMPRESSION'): + # See netutil.ssl_options_to_context + ssl_ctx.options |= ssl.OP_NO_COMPRESSION + return ssl_ctx return None def _on_timeout(self, info=None): diff --git a/tornado/test/httpserver_test.py b/tornado/test/httpserver_test.py index 4444a39220..2564e83b93 100644 --- a/tornado/test/httpserver_test.py +++ b/tornado/test/httpserver_test.py @@ -150,7 +150,6 @@ def get_ssl_version(self): 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 751dfe2e1f..4df23dd2bd 100644 --- a/tornado/test/iostream_test.py +++ b/tornado/test/iostream_test.py @@ -880,7 +880,6 @@ def _make_client_iostream(self, connection, **kwargs): # 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 @@ def test_handshake_fail(self): 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/runtests.py b/tornado/test/runtests.py index 7cabbaf133..2548058da2 100644 --- a/tornado/test/runtests.py +++ b/tornado/test/runtests.py @@ -118,10 +118,6 @@ def main(): # 2.7 and 3.2 warnings.filterwarnings("ignore", category=DeprecationWarning, message="Please use assert.* instead") - # unittest2 0.6 on py26 reports these as PendingDeprecationWarnings - # instead of DeprecationWarnings. - warnings.filterwarnings("ignore", category=PendingDeprecationWarning, - message="Please use assert.* instead") # Twisted 15.0.0 triggers some warnings on py3 with -bb. warnings.filterwarnings("ignore", category=BytesWarning, module=r"twisted\..*") diff --git a/tornado/test/simple_httpclient_test.py b/tornado/test/simple_httpclient_test.py index f713314317..309678efdc 100644 --- a/tornado/test/simple_httpclient_test.py +++ b/tornado/test/simple_httpclient_test.py @@ -497,8 +497,6 @@ def test_ssl_options(self): 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 @@ def test_ssl_options_handshake_fail(self): "/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) diff --git a/tornado/test/twisted_test.py b/tornado/test/twisted_test.py index 4b88eca862..c0a243a9bb 100644 --- a/tornado/test/twisted_test.py +++ b/tornado/test/twisted_test.py @@ -72,9 +72,6 @@ skipIfNoTwisted = unittest.skipUnless(have_twisted, "twisted module not present") -skipIfPy26 = unittest.skipIf(sys.version_info < (2, 7), - "twisted incompatible with singledispatch in py26") - def save_signal_handlers(): saved = {} @@ -494,7 +491,6 @@ def testTornadoServerTwistedClientReactor(self): 'http://127.0.0.1:%d' % self.tornado_port, self.run_reactor) self.assertEqual(response, 'Hello from tornado!') - @skipIfPy26 def testTornadoServerTwistedCoroutineClientIOLoop(self): self.start_tornado_server() response = self.twisted_coroutine_fetch( @@ -503,7 +499,6 @@ def testTornadoServerTwistedCoroutineClientIOLoop(self): @skipIfNoTwisted -@skipIfPy26 class ConvertDeferredTest(unittest.TestCase): def test_success(self): @inlineCallbacks diff --git a/tornado/test/util.py b/tornado/test/util.py index 19887c109b..0a9438babe 100644 --- a/tornado/test/util.py +++ b/tornado/test/util.py @@ -9,15 +9,8 @@ from tornado.testing import bind_unused_port -# Encapsulate the choice of unittest or unittest2 here. -# To be used as 'from tornado.test.util import unittest'. -if sys.version_info < (2, 7): - # In py26, we must always use unittest2. - import unittest2 as unittest # type: ignore -else: - # Otherwise, use whichever version of unittest was imported in - # tornado.testing. - from tornado.testing import unittest +# Delegate the choice of unittest or unittest2 to tornado.testing. +from tornado.testing import unittest skipIfNonUnix = unittest.skipIf(os.name != 'posix' or sys.platform == 'cygwin', "non-unix platform")