mirror of
https://github.com/frappe/gunicorn.git
synced 2026-01-14 11:09:11 +08:00
Merge pull request #2649 from Nordix/sslcontext
Update SSLContext handling
This commit is contained in:
commit
f955a0c18c
@ -320,8 +320,6 @@ The log config file written in JSON.
|
||||
``logconfig_dict``
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**Command line:** ``--log-config-dict``
|
||||
|
||||
**Default:** ``{}``
|
||||
|
||||
The log config dictionary to use, using the standard Python
|
||||
@ -343,7 +341,7 @@ For more context you can look at the default configuration dictionary for loggin
|
||||
|
||||
**Command line:** ``--log-syslog-to SYSLOG_ADDR``
|
||||
|
||||
**Default:** ``'unix:///var/run/syslog'``
|
||||
**Default:** ``'udp://localhost:514'``
|
||||
|
||||
Address to send syslog messages.
|
||||
|
||||
@ -932,6 +930,29 @@ 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, default_ssl_context_factory):
|
||||
return default_ssl_context_factory()
|
||||
|
||||
Called when SSLContext is needed.
|
||||
|
||||
Allows fully customized SSL context to be used in place of the default
|
||||
context.
|
||||
|
||||
The callable needs to accept an instance variable for the Config and
|
||||
a factory function that returns default SSLContext which is initialized
|
||||
with certificates, private key, cert_reqs, and ciphers according to
|
||||
config and can be further customized by the callable.
|
||||
The callable needs to return SSLContext object.
|
||||
|
||||
Server Mechanics
|
||||
----------------
|
||||
|
||||
@ -994,9 +1015,7 @@ Set the ``SO_REUSEPORT`` flag on the listening socket.
|
||||
|
||||
**Default:** ``'.'``
|
||||
|
||||
Change directory to specified directory before loading apps.
|
||||
|
||||
Default is the current directory.
|
||||
Change directory to specified directory before loading apps.
|
||||
|
||||
.. _daemon:
|
||||
|
||||
@ -1157,10 +1176,16 @@ temporary directory.
|
||||
**Default:** ``{'X-FORWARDED-PROTOCOL': 'ssl', 'X-FORWARDED-PROTO': 'https', 'X-FORWARDED-SSL': 'on'}``
|
||||
|
||||
A dictionary containing headers and values that the front-end proxy
|
||||
uses to indicate HTTPS requests. These tell Gunicorn to set
|
||||
uses to indicate HTTPS requests. If the source IP is permitted by
|
||||
``forwarded-allow-ips`` (below), *and* at least one request header matches
|
||||
a key-value pair listed in this dictionary, then Gunicorn will set
|
||||
``wsgi.url_scheme`` to ``https``, so your application can tell that the
|
||||
request is secure.
|
||||
|
||||
If the other headers listed in this dictionary are not present in the request, they will be ignored,
|
||||
but if the other headers are present and do not match the provided values, then
|
||||
the request will fail to parse. See the note below for more detailed examples of this behaviour.
|
||||
|
||||
The dictionary should map upper-case header names to exact string
|
||||
values. The value comparisons are case-sensitive, unlike the header
|
||||
names, so make sure they're exactly what your front-end proxy sends
|
||||
@ -1188,6 +1213,68 @@ you still trust the environment).
|
||||
By default, the value of the ``FORWARDED_ALLOW_IPS`` environment
|
||||
variable. If it is not defined, the default is ``"127.0.0.1"``.
|
||||
|
||||
.. note::
|
||||
|
||||
The interplay between the request headers, the value of ``forwarded_allow_ips``, and the value of
|
||||
``secure_scheme_headers`` is complex. Various scenarios are documented below to further elaborate. In each case, we
|
||||
have a request from the remote address 134.213.44.18, and the default value of ``secure_scheme_headers``:
|
||||
|
||||
.. code::
|
||||
|
||||
secure_scheme_headers = {
|
||||
'X-FORWARDED-PROTOCOL': 'ssl',
|
||||
'X-FORWARDED-PROTO': 'https',
|
||||
'X-FORWARDED-SSL': 'on'
|
||||
}
|
||||
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
:align: center
|
||||
:widths: auto
|
||||
|
||||
* - ``forwarded-allow-ips``
|
||||
- Secure Request Headers
|
||||
- Result
|
||||
- Explanation
|
||||
* - .. code::
|
||||
|
||||
["127.0.0.1"]
|
||||
- .. code::
|
||||
|
||||
X-Forwarded-Proto: https
|
||||
- .. code::
|
||||
|
||||
wsgi.url_scheme = "http"
|
||||
- IP address was not allowed
|
||||
* - .. code::
|
||||
|
||||
"*"
|
||||
- <none>
|
||||
- .. code::
|
||||
|
||||
wsgi.url_scheme = "http"
|
||||
- IP address allowed, but no secure headers provided
|
||||
* - .. code::
|
||||
|
||||
"*"
|
||||
- .. code::
|
||||
|
||||
X-Forwarded-Proto: https
|
||||
- .. code::
|
||||
|
||||
wsgi.url_scheme = "https"
|
||||
- IP address allowed, one request header matched
|
||||
* - .. code::
|
||||
|
||||
["134.213.44.18"]
|
||||
- .. code::
|
||||
|
||||
X-Forwarded-Ssl: on
|
||||
X-Forwarded-Proto: http
|
||||
- ``InvalidSchemeHeaders()`` raised
|
||||
- IP address allowed, but the two secure headers disagreed on if HTTPS was used
|
||||
|
||||
.. _pythonpath:
|
||||
|
||||
``pythonpath``
|
||||
@ -1360,8 +1447,9 @@ A positive integer generally in the ``2-4 x $(NUM_CORES)`` range.
|
||||
You'll want to vary this a bit to find the best for your particular
|
||||
application's work load.
|
||||
|
||||
By default, the value of the ``WEB_CONCURRENCY`` environment variable.
|
||||
If it is not defined, the default is ``1``.
|
||||
By default, the value of the ``WEB_CONCURRENCY`` environment variable,
|
||||
which is set by some Platform-as-a-Service providers such as Heroku. If
|
||||
it is not defined, the default is ``1``.
|
||||
|
||||
.. _worker-class:
|
||||
|
||||
|
||||
@ -214,3 +214,27 @@ def worker_int(worker):
|
||||
|
||||
def worker_abort(worker):
|
||||
worker.log.info("worker received SIGABRT signal")
|
||||
|
||||
def ssl_context(conf, default_ssl_context_factory):
|
||||
import ssl
|
||||
|
||||
# The default SSLContext returned by the factory function is initialized
|
||||
# with the TLS parameters from config, including TLS certificates and other
|
||||
# parameters.
|
||||
context = default_ssl_context_factory()
|
||||
|
||||
# The SSLContext can be further customized, for example by enforcing
|
||||
# minimum TLS version.
|
||||
context.minimum_version = ssl.TLSVersion.TLSv1_3
|
||||
|
||||
# Server can also 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 = default_ssl_context_factory()
|
||||
new_context.load_cert_chain(certfile="foo.pem", keyfile="foo-key.pem")
|
||||
socket.context = new_context
|
||||
|
||||
context.sni_callback = sni_callback
|
||||
|
||||
return context
|
||||
|
||||
@ -1523,6 +1523,8 @@ class LogConfigDict(Setting):
|
||||
|
||||
Format: https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig
|
||||
|
||||
For more context you can look at the default configuration dictionary for logging, which can be found at ``gunicorn.glogging.CONFIG_DEFAULTS``.
|
||||
|
||||
.. versionadded:: 19.8
|
||||
"""
|
||||
|
||||
@ -2004,6 +2006,28 @@ 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(2)
|
||||
type = callable
|
||||
|
||||
def ssl_context(config, default_ssl_context_factory):
|
||||
return default_ssl_context_factory()
|
||||
|
||||
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 an instance variable for the Config and
|
||||
a factory function that returns default SSLContext which is initialized
|
||||
with certificates, private key, cert_reqs, and ciphers according to
|
||||
config and can be further customized by the callable.
|
||||
The callable needs to return SSLContext object.
|
||||
"""
|
||||
|
||||
class ProxyProtocol(Setting):
|
||||
name = "proxy_protocol"
|
||||
|
||||
@ -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):
|
||||
def default_ssl_context_factory():
|
||||
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
|
||||
|
||||
return conf.ssl_context(conf, default_ssl_context_factory)
|
||||
|
||||
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)
|
||||
|
||||
@ -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
|
||||
@ -152,9 +153,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):
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -27,6 +27,7 @@ from threading import RLock
|
||||
from . import base
|
||||
from .. import http
|
||||
from .. import util
|
||||
from .. import sock
|
||||
from ..http import wsgi
|
||||
|
||||
|
||||
@ -52,8 +53,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)
|
||||
|
||||
@ -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
|
||||
@ -140,16 +141,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)
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user