Update SSLContext handling

* Change deprecated ssl.wrap_socket() to SSLContext.wrap_context().
* Add new server hook to allow user to create custom SSLContext.
* Updated the documentation.

Signed-off-by: Tero Saarni <tero.saarni@est.tech>
This commit is contained in:
Tero Saarni 2021-09-10 19:59:15 +03:00
parent ff58e0c6da
commit 5a581c0b14
9 changed files with 99 additions and 18 deletions

View File

@ -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.

View File

@ -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

View File

@ -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"

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)