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. 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 Server Mechanics
---------------- ----------------
@ -1506,4 +1526,3 @@ set this to a higher value.
.. note:: .. note::
``sync`` worker does not support persistent connections and will ``sync`` worker does not support persistent connections and will
ignore this option. ignore this option.

View File

@ -214,3 +214,33 @@ def worker_int(worker):
def worker_abort(worker): def worker_abort(worker):
worker.log.info("worker received SIGABRT signal") 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. 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): class ProxyProtocol(Setting):
name = "proxy_protocol" name = "proxy_protocol"

View File

@ -6,6 +6,7 @@
import errno import errno
import os import os
import socket import socket
import ssl
import stat import stat
import sys import sys
import time import time
@ -203,10 +204,27 @@ def create_sockets(conf, log, fds=None):
return listeners return listeners
def close_sockets(listeners, unlink=True): def close_sockets(listeners, unlink=True):
for sock in listeners: for sock in listeners:
sock_name = sock.getsockname() sock_name = sock.getsockname()
sock.close() sock.close()
if unlink and _sock_type(sock_name) is UnixSocket: if unlink and _sock_type(sock_name) is UnixSocket:
os.unlink(sock_name) 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 import greenlet
from gunicorn.workers.base_async import AsyncWorker 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` # ALREADY_HANDLED is removed in 0.30.3+ now it's `WSGI_LOCAL.already_handled: bool`
# https://github.com/eventlet/eventlet/pull/544 # https://github.com/eventlet/eventlet/pull/544
@ -153,9 +154,7 @@ class EventletWorker(AsyncWorker):
def handle(self, listener, client, addr): def handle(self, listener, client, addr):
if self.cfg.is_ssl: if self.cfg.is_ssl:
client = eventlet.wrap_ssl(client, server_side=True, client = ssl_wrap_socket(client, self.cfg)
**self.cfg.ssl_options)
super().handle(listener, client, addr) super().handle(listener, client, addr)
def run(self): def run(self):

View File

@ -24,6 +24,7 @@ from gevent import hub, monkey, socket, pywsgi
import gunicorn import gunicorn
from gunicorn.http.wsgi import base_environ from gunicorn.http.wsgi import base_environ
from gunicorn.sock import ssl_context
from gunicorn.workers.base_async import AsyncWorker from gunicorn.workers.base_async import AsyncWorker
VERSION = "gevent/%s gunicorn/%s" % (gevent.__version__, gunicorn.__version__) VERSION = "gevent/%s gunicorn/%s" % (gevent.__version__, gunicorn.__version__)
@ -58,7 +59,7 @@ class GeventWorker(AsyncWorker):
ssl_args = {} ssl_args = {}
if self.cfg.is_ssl: 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: for s in self.sockets:
s.setblocking(1) s.setblocking(1)

View File

@ -27,6 +27,7 @@ from threading import RLock
from . import base from . import base
from .. import http from .. import http
from .. import util from .. import util
from .. import sock
from ..http import wsgi from ..http import wsgi
@ -49,8 +50,7 @@ class TConn(object):
if self.parser is None: if self.parser is None:
# wrap the socket if needed # wrap the socket if needed
if self.cfg.is_ssl: if self.cfg.is_ssl:
self.sock = ssl.wrap_socket(self.sock, server_side=True, self.sock = sock.ssl_wrap_socket(self.sock, self.cfg)
**self.cfg.ssl_options)
# initialize the parser # initialize the parser
self.parser = http.RequestParser(self.cfg, self.sock, self.client) 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 tornado.wsgi import WSGIContainer
from gunicorn.workers.base import Worker from gunicorn.workers.base import Worker
from gunicorn import __version__ as gversion 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 # Tornado 5.0 updated its IOLoop, and the `io_loop` arguments to many
@ -139,16 +140,11 @@ class TornadoWorker(Worker):
server_class = _HTTPServer server_class = _HTTPServer
if self.cfg.is_ssl: 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: if TORNADO5:
server = server_class(app, ssl_options=_ssl_opt) server = server_class(app, ssl_options=ssl_context(self.cfg))
else: else:
server = server_class(app, io_loop=self.ioloop, server = server_class(app, io_loop=self.ioloop,
ssl_options=_ssl_opt) ssl_options=ssl_context(self.cfg))
else: else:
if TORNADO5: if TORNADO5:
server = server_class(app) server = server_class(app)

View File

@ -14,6 +14,7 @@ import sys
import gunicorn.http as http import gunicorn.http as http
import gunicorn.http.wsgi as wsgi import gunicorn.http.wsgi as wsgi
import gunicorn.sock as sock
import gunicorn.util as util import gunicorn.util as util
import gunicorn.workers.base as base import gunicorn.workers.base as base
@ -128,9 +129,7 @@ class SyncWorker(base.Worker):
req = None req = None
try: try:
if self.cfg.is_ssl: if self.cfg.is_ssl:
client = ssl.wrap_socket(client, server_side=True, client = sock.ssl_wrap_socket(client, self.cfg)
**self.cfg.ssl_options)
parser = http.RequestParser(self.cfg, client, addr) parser = http.RequestParser(self.cfg, client, addr)
req = next(parser) req = next(parser)
self.handle_request(listener, req, client, addr) self.handle_request(listener, req, client, addr)