]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
Require modern ssl features (SSLContext, etc)
authorBen Darnell <ben@bendarnell.com>
Sun, 22 Oct 2017 16:20:18 +0000 (12:20 -0400)
committerBen Darnell <ben@bendarnell.com>
Sun, 22 Oct 2017 16:37:19 +0000 (12:37 -0400)
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
setup.py
tornado/iostream.py
tornado/netutil.py
tornado/simple_httpclient.py
tornado/test/httpserver_test.py
tornado/test/iostream_test.py
tornado/test/simple_httpclient_test.py

index 64beb5fc2d2743d31636dfdaeba5b4c431970d4e..eeb2f4d6a390727ac54b14a189955256e1f3b26a 100644 (file)
@@ -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.
 #
index 72569a1f3a4dbb6c1c4449c72cfe09f531deb718..987c0cf487217d8124659f038cf81b786f2a6641 100644 (file)
--- 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,
index 632952b524d32ea69b6d3d3764f05ee58d1ed40b..3e971bfde57fc430ba5b7217623ca8ecb96a3f49 100644 (file)
@@ -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:
index c054f625cf5e3bffa86c5b3eb45b944e4ea39638..63c80cb33148347b9268b51d28a3e374cf8f53da 100644 (file)
@@ -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)
index f394689d122103b9ff68cdb301bc6cf1387fea52..6534399117a842ee3919273e7a16d7bc556cf234 100644 (file)
@@ -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:
index 4444a39220a5812b3772f2745da78b57ae89ea22..2564e83b9320c5d2f711f99251b1e6f65f874cba 100644 (file)
@@ -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(
index 751dfe2e1f2709618f6b97c6edba06c7080a5075..4df23dd2bd5bd0a301ef541a61e3634000e8a680 100644 (file)
@@ -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.
index f7133143178ae049ac04338def1aceda35897e77..309678efdc4bcc6a4ac8dea59dfef9a0404c2c58 100644 (file)
@@ -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)