This is necessary to support SNI and NPN.
`HTTPServer` can serve SSL traffic with Python 2.6+ and OpenSSL.
To make this server serve SSL traffic, send the ssl_options dictionary
argument with the arguments required for the `ssl.wrap_socket` method,
- including "certfile" and "keyfile"::
+ including "certfile" and "keyfile". In Python 3.2+ you can pass
+ an `ssl.SSLContext` object instead of a dict::
HTTPServer(applicaton, ssl_options={
"certfile": os.path.join(data_dir, "mydomain.crt"),
from tornado import ioloop
from tornado.log import gen_log, app_log
+from tornado.netutil import ssl_wrap_socket
from tornado import stack_context
from tornado.util import bytes_type
def __init__(self, *args, **kwargs):
"""Creates an SSLIOStream.
- If a dictionary is provided as keyword argument ssl_options,
- it will be used as additional keyword arguments to ssl.wrap_socket.
+ The ``ssl_options`` keyword argument may either be a dictionary
+ of keywords arguments for `ssl.wrap_socket`, or an `ssl.SSLContext`
+ object.
"""
self._ssl_options = kwargs.pop('ssl_options', {})
super(SSLIOStream, self).__init__(*args, **kwargs)
# user callbacks are enqueued asynchronously on the IOLoop,
# but since _handle_events calls _handle_connect immediately
# followed by _handle_write we need this to be synchronous.
- self.socket = ssl.wrap_socket(self.socket,
- do_handshake_on_connect=False,
- **self._ssl_options)
+ self.socket = ssl_wrap_socket(self.socket, self._ssl_options,
+ do_handshake_on_connect=False)
super(SSLIOStream, self)._handle_connect()
def read_from_fd(self):
import errno
import os
import socket
+import ssl
import stat
from tornado.concurrent import dummy_executor, run_on_executor
@run_on_executor
def getaddrinfo(self, *args, **kwargs):
return socket.getaddrinfo(*args, **kwargs)
+
+
+# These are the keyword arguments to ssl.wrap_socket that must be translated
+# to their SSLContext equivalents (the other arguments are still passed
+# to SSLContext.wrap_socket).
+_SSL_CONTEXT_KEYWORDS = frozenset(['ssl_version', 'certfile', 'keyfile',
+ 'cert_reqs', 'ca_certs', 'ciphers'])
+
+def ssl_options_to_context(ssl_options):
+ """Try to Convert an ssl_options dictionary to an SSLContext object.
+
+ The ``ssl_options`` dictionary contains keywords to be passed to
+ `ssl.wrap_sockets`. In Python 3.2+, `ssl.SSLContext` objects can
+ be used instead. This function converts the dict form to its
+ `SSLContext` equivalent, and may be used when a component which
+ accepts both forms needs to upgrade to the `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)):
+ return ssl_options
+ context = ssl.SSLContext(
+ ssl_options.get('ssl_version', ssl.PROTOCOL_SSLv23))
+ if 'certfile' in ssl_options:
+ context.load_cert_chain(ssl_options['certfile'], ssl_options.get('keyfile', None))
+ if 'cert_reqs' in ssl_options:
+ context.verify_mode = ssl_options['cert_reqs']
+ if 'ca_certs' in ssl_options:
+ context.load_verify_locations(ssl_options['ca_certs'])
+ if 'ciphers' in ssl_options:
+ context.set_ciphers(ssl_options['ciphers'])
+ return context
+
+
+def ssl_wrap_socket(socket, ssl_options, **kwargs):
+ """Returns an `ssl.SSLSocket` wrapping the given socket.
+
+ ``ssl_options`` may be either a dictionary (as accepted by
+ `ssl_options_to_context) or an `ssl.SSLContext` object.
+ Additional keyword arguments are passed to `wrap_socket`
+ (either the `SSLContext` method or the `ssl` module function
+ as appropriate).
+ """
+ context = ssl_options_to_context(ssl_options)
+ if hasattr(ssl, 'SSLContext') and isinstance(context, ssl.SSLContext):
+ return context.wrap_socket(socket, **kwargs)
+ else:
+ return ssl.wrap_socket(socket, **dict(context, **kwargs))
from tornado.log import app_log
from tornado.ioloop import IOLoop
from tornado.iostream import IOStream, SSLIOStream
-from tornado.netutil import bind_sockets, add_accept_handler
+from tornado.netutil import bind_sockets, add_accept_handler, ssl_wrap_socket
from tornado import process
class TCPServer(object):
# connect. This doesn't verify that the keys are legitimate, but
# the SSL module doesn't do that until there is a connected socket
# which seems like too much work
- if self.ssl_options is not None:
+ if self.ssl_options is not None and isinstance(self.ssl_options, dict):
# Only certfile is required: it can contain both keys
if 'certfile' not in self.ssl_options:
raise KeyError('missing key "certfile" in ssl_options')
if self.ssl_options is not None:
assert ssl, "Python 2.6+ and OpenSSL required for SSL"
try:
- connection = ssl.wrap_socket(connection,
+ connection = ssl_wrap_socket(connection,
+ self.ssl_options,
server_side=True,
- do_handshake_on_connect=False,
- **self.ssl_options)
+ do_handshake_on_connect=False)
except ssl.SSLError as err:
if err.args[0] == ssl.SSL_ERROR_EOF:
return connection.close()
from tornado.httputil import HTTPHeaders
from tornado.iostream import IOStream
from tornado.log import gen_log
+from tornado.netutil import ssl_options_to_context
from tornado.simple_httpclient import SimpleAsyncHTTPClient
from tornado.testing import AsyncHTTPTestCase, AsyncHTTPSTestCase, AsyncTestCase, ExpectLog
from tornado.test.util import unittest
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(
+ AsyncHTTPSTestCase.get_ssl_options(self))
+ assert isinstance(context, ssl.SSLContext)
+ return context
+
+
class BadSSLOptionsTest(unittest.TestCase):
def test_missing_arguments(self):
application = Application()
from tornado.ioloop import IOLoop
from tornado.iostream import IOStream, SSLIOStream, PipeIOStream
from tornado.log import gen_log, app_log
+from tornado.netutil import ssl_wrap_socket
from tornado.stack_context import NullContext
from tornado.testing import AsyncHTTPTestCase, AsyncHTTPSTestCase, AsyncTestCase, bind_unused_port, ExpectLog
from tornado.test.util import unittest, skipIfNonUnix
return SSLIOStream(connection, io_loop=self.io_loop, **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)
+ context.load_cert_chain(
+ os.path.join(os.path.dirname(__file__), 'test.crt'),
+ os.path.join(os.path.dirname(__file__), 'test.key'))
+ connection = ssl_wrap_socket(connection, context,
+ server_side=True,
+ do_handshake_on_connect=False)
+ return SSLIOStream(connection, io_loop=self.io_loop, **kwargs)
+
+ def _make_client_iostream(self, connection, **kwargs):
+ context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
+ return SSLIOStream(connection, io_loop=self.io_loop,
+ ssl_options=context, **kwargs)
+
+
@skipIfNonUnix
class TestPipeIOStream(AsyncTestCase):
def test_pipe_iostream(self):
method that should be used instead of reaching into its ``stream``
attribute.
* `tornado.netutil.TCPServer` has moved to its own module, `tornado.tcpserver`.
+* On python 3.2+, methods that take an ``ssl_options`` argument (on
+ `SSLIOStream`, `TCPServer`, and `HTTPServer`) now accept either a
+ dictionary of options or an `ssl.SSLContext` object.