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``
|
``logconfig_dict``
|
||||||
~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
**Command line:** ``--log-config-dict``
|
|
||||||
|
|
||||||
**Default:** ``{}``
|
**Default:** ``{}``
|
||||||
|
|
||||||
The log config dictionary to use, using the standard Python
|
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``
|
**Command line:** ``--log-syslog-to SYSLOG_ADDR``
|
||||||
|
|
||||||
**Default:** ``'unix:///var/run/syslog'``
|
**Default:** ``'udp://localhost:514'``
|
||||||
|
|
||||||
Address to send syslog messages.
|
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.
|
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
|
Server Mechanics
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
@ -994,9 +1015,7 @@ Set the ``SO_REUSEPORT`` flag on the listening socket.
|
|||||||
|
|
||||||
**Default:** ``'.'``
|
**Default:** ``'.'``
|
||||||
|
|
||||||
Change directory to specified directory before loading apps.
|
Change directory to specified directory before loading apps.
|
||||||
|
|
||||||
Default is the current directory.
|
|
||||||
|
|
||||||
.. _daemon:
|
.. _daemon:
|
||||||
|
|
||||||
@ -1157,10 +1176,16 @@ temporary directory.
|
|||||||
**Default:** ``{'X-FORWARDED-PROTOCOL': 'ssl', 'X-FORWARDED-PROTO': 'https', 'X-FORWARDED-SSL': 'on'}``
|
**Default:** ``{'X-FORWARDED-PROTOCOL': 'ssl', 'X-FORWARDED-PROTO': 'https', 'X-FORWARDED-SSL': 'on'}``
|
||||||
|
|
||||||
A dictionary containing headers and values that the front-end proxy
|
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
|
``wsgi.url_scheme`` to ``https``, so your application can tell that the
|
||||||
request is secure.
|
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
|
The dictionary should map upper-case header names to exact string
|
||||||
values. The value comparisons are case-sensitive, unlike the header
|
values. The value comparisons are case-sensitive, unlike the header
|
||||||
names, so make sure they're exactly what your front-end proxy sends
|
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
|
By default, the value of the ``FORWARDED_ALLOW_IPS`` environment
|
||||||
variable. If it is not defined, the default is ``"127.0.0.1"``.
|
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:
|
||||||
|
|
||||||
``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
|
You'll want to vary this a bit to find the best for your particular
|
||||||
application's work load.
|
application's work load.
|
||||||
|
|
||||||
By default, the value of the ``WEB_CONCURRENCY`` environment variable.
|
By default, the value of the ``WEB_CONCURRENCY`` environment variable,
|
||||||
If it is not defined, the default is ``1``.
|
which is set by some Platform-as-a-Service providers such as Heroku. If
|
||||||
|
it is not defined, the default is ``1``.
|
||||||
|
|
||||||
.. _worker-class:
|
.. _worker-class:
|
||||||
|
|
||||||
|
|||||||
@ -214,3 +214,27 @@ 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, 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
|
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
|
.. versionadded:: 19.8
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -2004,6 +2006,28 @@ 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(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):
|
class ProxyProtocol(Setting):
|
||||||
name = "proxy_protocol"
|
name = "proxy_protocol"
|
||||||
|
|||||||
@ -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):
|
||||||
|
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
|
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
|
||||||
@ -152,9 +153,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):
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
@ -52,8 +53,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)
|
||||||
|
|||||||
@ -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
|
||||||
@ -140,16 +141,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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user