diff --git a/docs/source/settings.rst b/docs/source/settings.rst index 4c8e9212..bf2ad737 100644 --- a/docs/source/settings.rst +++ b/docs/source/settings.rst @@ -916,6 +916,26 @@ Called just before exiting Gunicorn. The callable needs to accept a single instance variable for the Arbiter. +.. _ssl-context: + +``ssl_context`` +~~~~~~~~~~~~~~~ + +**Default:** + +.. code-block:: python + + def ssl_context(config): + return None + +Called when SSLContext is needed. + +Allows fully customized SSL context to be used in place of the default +context. + +The callable needs to accept a single instance variable for the Config. +The callable needs to return SSLContext object. + Server Mechanics ---------------- @@ -1506,4 +1526,3 @@ set this to a higher value. .. note:: ``sync`` worker does not support persistent connections and will ignore this option. - diff --git a/examples/example_config.py b/examples/example_config.py index f8f3c1df..83c3ffbc 100644 --- a/examples/example_config.py +++ b/examples/example_config.py @@ -214,3 +214,33 @@ def worker_int(worker): def worker_abort(worker): worker.log.info("worker received SIGABRT signal") + +def ssl_context(conf): + import ssl + + def set_defaults(context): + context.verify_mode = conf.cert_reqs + context.minimum_version = ssl.TLSVersion.TLSv1_3 + if conf.ciphers: + context.set_ciphers(conf.ciphers) + if conf.ca_certs: + context.load_verify_locations(cafile=conf.ca_certs) + + # Return different server certificate depending which hostname the client + # uses. Requires Python 3.7 or later. + def sni_callback(socket, server_hostname, context): + if server_hostname == "foo.127.0.0.1.nip.io": + new_context = ssl.SSLContext() + new_context.load_cert_chain(certfile="foo.pem", keyfile="foo-key.pem") + set_defaults(new_context) + socket.context = new_context + + context = ssl.SSLContext(conf.ssl_version) + context.sni_callback = sni_callback + set_defaults(context) + + # Load fallback certificate that will be returned when there is no match + # or client did not set TLS SNI (server_hostname == None) + context.load_cert_chain(certfile=conf.certfile, keyfile=conf.keyfile) + + return context diff --git a/gunicorn/config.py b/gunicorn/config.py index bc24b700..3f397d12 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -1967,6 +1967,25 @@ class OnExit(Setting): The callable needs to accept a single instance variable for the Arbiter. """ +class NewSSLContext(Setting): + name = "ssl_context" + section = "Server Hooks" + validator = validate_callable(1) + type = callable + + def ssl_context(config): + return None + + default = staticmethod(ssl_context) + desc = """\ + Called when SSLContext is needed. + + Allows fully customized SSL context to be used in place of the default + context. + + The callable needs to accept a single instance variable for the Config. + The callable needs to return SSLContext object. + """ class ProxyProtocol(Setting): name = "proxy_protocol" diff --git a/gunicorn/sock.py b/gunicorn/sock.py index d4586770..1481c23b 100644 --- a/gunicorn/sock.py +++ b/gunicorn/sock.py @@ -6,6 +6,7 @@ import errno import os import socket +import ssl import stat import sys import time @@ -203,10 +204,27 @@ def create_sockets(conf, log, fds=None): return listeners - def close_sockets(listeners, unlink=True): for sock in listeners: sock_name = sock.getsockname() sock.close() if unlink and _sock_type(sock_name) is UnixSocket: os.unlink(sock_name) + +def ssl_context(conf): + context = conf.ssl_context(conf) + if context is None: + context = ssl.SSLContext(conf.ssl_version) + context.load_cert_chain(certfile=conf.certfile, keyfile=conf.keyfile) + context.verify_mode = conf.cert_reqs + if conf.ciphers: + context.set_ciphers(conf.ciphers) + if conf.ca_certs: + context.load_verify_locations(cafile=conf.ca_certs) + + return context + +def ssl_wrap_socket(sock, conf): + return ssl_context(conf).wrap_socket(sock, server_side=True, + suppress_ragged_eofs=conf.suppress_ragged_eofs, + do_handshake_on_connect=conf.do_handshake_on_connect) diff --git a/gunicorn/workers/geventlet.py b/gunicorn/workers/geventlet.py index ea82f3d6..844a1e06 100644 --- a/gunicorn/workers/geventlet.py +++ b/gunicorn/workers/geventlet.py @@ -21,6 +21,7 @@ import eventlet.wsgi import greenlet from gunicorn.workers.base_async import AsyncWorker +from gunicorn.sock import ssl_wrap_socket # ALREADY_HANDLED is removed in 0.30.3+ now it's `WSGI_LOCAL.already_handled: bool` # https://github.com/eventlet/eventlet/pull/544 @@ -153,9 +154,7 @@ class EventletWorker(AsyncWorker): def handle(self, listener, client, addr): if self.cfg.is_ssl: - client = eventlet.wrap_ssl(client, server_side=True, - **self.cfg.ssl_options) - + client = ssl_wrap_socket(client, self.cfg) super().handle(listener, client, addr) def run(self): diff --git a/gunicorn/workers/ggevent.py b/gunicorn/workers/ggevent.py index 3941814f..81a339a8 100644 --- a/gunicorn/workers/ggevent.py +++ b/gunicorn/workers/ggevent.py @@ -24,6 +24,7 @@ from gevent import hub, monkey, socket, pywsgi import gunicorn from gunicorn.http.wsgi import base_environ +from gunicorn.sock import ssl_context from gunicorn.workers.base_async import AsyncWorker VERSION = "gevent/%s gunicorn/%s" % (gevent.__version__, gunicorn.__version__) @@ -58,7 +59,7 @@ class GeventWorker(AsyncWorker): ssl_args = {} if self.cfg.is_ssl: - ssl_args = dict(server_side=True, **self.cfg.ssl_options) + ssl_args = dict(ssl_context=ssl_context(self.cfg)) for s in self.sockets: s.setblocking(1) diff --git a/gunicorn/workers/gthread.py b/gunicorn/workers/gthread.py index d5318115..73ea6fdf 100644 --- a/gunicorn/workers/gthread.py +++ b/gunicorn/workers/gthread.py @@ -27,6 +27,7 @@ from threading import RLock from . import base from .. import http from .. import util +from .. import sock from ..http import wsgi @@ -49,8 +50,7 @@ class TConn(object): if self.parser is None: # wrap the socket if needed if self.cfg.is_ssl: - self.sock = ssl.wrap_socket(self.sock, server_side=True, - **self.cfg.ssl_options) + self.sock = sock.ssl_wrap_socket(self.sock, self.cfg) # initialize the parser self.parser = http.RequestParser(self.cfg, self.sock, self.client) diff --git a/gunicorn/workers/gtornado.py b/gunicorn/workers/gtornado.py index 9dd3d7bc..9da49957 100644 --- a/gunicorn/workers/gtornado.py +++ b/gunicorn/workers/gtornado.py @@ -17,6 +17,7 @@ from tornado.ioloop import IOLoop, PeriodicCallback from tornado.wsgi import WSGIContainer from gunicorn.workers.base import Worker from gunicorn import __version__ as gversion +from gunicorn.sock import ssl_context # Tornado 5.0 updated its IOLoop, and the `io_loop` arguments to many @@ -139,16 +140,11 @@ class TornadoWorker(Worker): server_class = _HTTPServer if self.cfg.is_ssl: - _ssl_opt = copy.deepcopy(self.cfg.ssl_options) - # tornado refuses initialization if ssl_options contains following - # options - del _ssl_opt["do_handshake_on_connect"] - del _ssl_opt["suppress_ragged_eofs"] if TORNADO5: - server = server_class(app, ssl_options=_ssl_opt) + server = server_class(app, ssl_options=ssl_context(self.cfg)) else: server = server_class(app, io_loop=self.ioloop, - ssl_options=_ssl_opt) + ssl_options=ssl_context(self.cfg)) else: if TORNADO5: server = server_class(app) diff --git a/gunicorn/workers/sync.py b/gunicorn/workers/sync.py index eeb7f633..4bdf0003 100644 --- a/gunicorn/workers/sync.py +++ b/gunicorn/workers/sync.py @@ -14,6 +14,7 @@ import sys import gunicorn.http as http import gunicorn.http.wsgi as wsgi +import gunicorn.sock as sock import gunicorn.util as util import gunicorn.workers.base as base @@ -128,9 +129,7 @@ class SyncWorker(base.Worker): req = None try: if self.cfg.is_ssl: - client = ssl.wrap_socket(client, server_side=True, - **self.cfg.ssl_options) - + client = sock.ssl_wrap_socket(client, self.cfg) parser = http.RequestParser(self.cfg, client, addr) req = next(parser) self.handle_request(listener, req, client, addr)