summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBert JW Regeer <xistence@0x58.com>2018-12-02 22:50:44 -0700
committerGitHub <noreply@github.com>2018-12-02 22:50:44 -0700
commit8cfacc1914313d972ecdd133eda18b9b81aafd7f (patch)
tree4463a2fdda0ac794098e266754c911c66c634f26
parent77bb0026eebaa94845cc1a967171ca2f3e3fdfe0 (diff)
parentd34190998d6190851915162198fc18d8e0f29940 (diff)
downloadwaitress-8cfacc1914313d972ecdd133eda18b9b81aafd7f.tar.gz
Merge pull request #209 from Pylons/bugfix/socket-server-name-port
Use Forwarded/X-Forwarded-{For,Host,By,Port,Proto} to fixup WSGI environ
-rw-r--r--CHANGES.txt27
-rw-r--r--docs/arguments.rst230
-rw-r--r--docs/reverse-proxy.rst140
-rw-r--r--waitress/adjustments.py97
-rw-r--r--waitress/task.py321
-rw-r--r--waitress/tests/test_adjustments.py59
-rw-r--r--waitress/tests/test_task.py555
-rw-r--r--waitress/tests/test_utilities.py33
-rw-r--r--waitress/utilities.py79
9 files changed, 1336 insertions, 205 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 0f688a0..1056b47 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -4,6 +4,33 @@ Unreleased
Features
~~~~~~~~
+- Waitress has increased its support of the X-Forwarded-* headers and includes
+ Forwarded (RFC7239) support. This may be used to allow proxy servers to
+ influence the WSGI environment. See
+ https://github.com/Pylons/waitress/pull/209
+
+ This also provides a new security feature when using Waitress behind a proxy
+ in that it is possible to remove untrusted proxy headers thereby making sure
+ that downstream WSGI applications don't accidentally use those proxy headers
+ to make security decisions.
+
+ The documentation has more information, see the following new arguments:
+
+ - trusted_proxy_count
+ - trusted_proxy_headers
+ - clear_untrusted_proxy_headers
+ - log_untrusted_proxy_headers (useful for debugging)
+
+ Be aware that the defaults for these are currently backwards compatible with
+ older versions of Waitress, this will change in a future release of waitress.
+ If you expect to need this behaviour please explicitly set these variables in
+ your configuration, or pin this version of waitress.
+
+- Waitress can now accept a list of sockets that are already pre-bound rather
+ than creating its own to allow for socket activation. Support for init
+ systems/other systems that create said activated sockets is not included. See
+ https://github.com/Pylons/waitress/pull/215
+
- Server header can be omitted by specifying `ident=None` or `ident=''`.
See https://github.com/Pylons/waitress/pull/187
diff --git a/docs/arguments.rst b/docs/arguments.rst
index 690daef..2665087 100644
--- a/docs/arguments.rst
+++ b/docs/arguments.rst
@@ -7,7 +7,7 @@ Here are the arguments you can pass to the `waitress.serve`` function or use
in :term:`PasteDeploy` configuration (interchangeably):
host
- hostname or IP address (string) on which to listen, default ``0.0.0.0``,
+ Hostname or IP address (string) on which to listen, default ``0.0.0.0``,
which means "all IP addresses on this host".
.. warning::
@@ -20,19 +20,20 @@ port
May not be used with ``listen``
listen
- .. versionadded:: 1.0
- Tell waitress to listen on combinations of ``host:port`` arguments.
- Combinations should be a quoted, space-delimited list, as in the following examples.
+ Tell waitress to listen on combinations of ``host:port`` arguments.
+ Combinations should be a quoted, space-delimited list, as in the following examples.
+
+ .. code-block:: python
- .. code-block:: python
+ listen="127.0.0.1:8080 [::1]:8080"
+ listen="*:8080 *:6543"
- listen="127.0.0.1:8080 [::1]:8080"
- listen="*:8080 *:6543"
+ A wildcard for the hostname is also supported and will bind to both
+ IPv4/IPv6 depending on whether they are enabled or disabled.
- A wildcard for the hostname is also supported and will bind to both
- IPv4/IPv6 depending on whether they are enabled or disabled.
+ IPv6 IP addresses are supported by surrounding the IP address with brackets.
- IPv6 IP addresses are supported by surrounding the IP address with brackets.
+ .. versionadded:: 1.0
ipv4
Enable or disable IPv4 (boolean)
@@ -41,76 +42,171 @@ ipv6
Enable or disable IPv6 (boolean)
unix_socket
- Path of Unix socket (string), default is ``None``. If a socket path is
- specified, a Unix domain socket is made instead of the usual inet domain
- socket.
+ Path of Unix socket (string). If a socket path is specified, a Unix domain
+ socket is made instead of the usual inet domain socket.
Not available on Windows.
+ Default: ``None``
+
unix_socket_perms
- Octal permissions to use for the Unix domain socket (string), default is
- ``600``. Only used if ``unix_socket`` is not ``None``.
+ Octal permissions to use for the Unix domain socket (string).
+ Only used if ``unix_socket`` is not ``None``.
+
+ Default: ``'600'``
sockets
+ A list of sockets. The sockets can be either Internet or UNIX sockets and have
+ to be bound. Internet and UNIX sockets cannot be mixed.
+ If the socket list is not empty, waitress creates one server for each socket.
+
+ Default: ``[]``
+
.. versionadded:: 1.1.1
- A list of sockets. The sockets can be either Internet or UNIX sockets and have
- to be bound. Internet and UNIX sockets cannot be mixed.
- If the socket list is not empty, waitress creates one server for each socket.
- Default is ``[]``.
- .. warning::
- May not be used with ``listen``, ``host``, ``port`` or ``unix_socket``
+ .. warning::
+ May not be used with ``listen``, ``host``, ``port`` or ``unix_socket``
threads
- number of threads used to process application logic (integer), default
- ``4``
+ The number of threads used to process application logic (integer).
+
+ Default: ``4``
trusted_proxy
- IP address of a client allowed to override ``url_scheme`` via the
- ``X_FORWARDED_PROTO`` header.
+ IP address of a remote peer allowed to override various WSGI environment
+ variables using proxy headers.
+
+ For unix sockets, set this value to ``localhost`` instead of an IP address.
+
+ Default: ``None``
+
+trusted_proxy_count
+ How many proxies we trust when chained. For example,
+
+ ``X-Forwarded-For: 192.0.2.1, "[2001:db8::1]"``
+
+ or
+
+ ``Forwarded: for=192.0.2.1, For="[2001:db8::1]"``
+
+ means there were (potentially), two proxies involved. If we know there is
+ only 1 valid proxy, then that initial IP address "192.0.2.1" is not trusted
+ and we completely ignore it.
+
+ If there are two trusted proxies in the path, this value should be set to
+ 2. If there are more proxies, this value should be set higher.
+
+ Default: ``1``
+
+ .. versionadded:: 1.2.0
+
+trusted_proxy_headers
+ Which of the proxy headers should we trust, this is a set where you
+ either specify "forwarded" or one or more of "x-forwarded-host", "x-forwarded-for",
+ "x-forwarded-proto", "x-forwarded-port", "x-forwarded-by".
+
+ This list of trusted headers is used when ``trusted_proxy`` is set and will
+ allow waitress to modify the WSGI environment using the values provided by
+ the proxy.
+
+ .. versionadded:: 1.2.0
+
+ .. warning::
+ If ``trusted_proxy`` is set, the default is ``x-forwarded-proto`` to
+ match older versions of Waitress. Users should explicitly opt-in by
+ selecting the headers to be trusted as future versions of waitress will
+ use an empty default.
+
+ .. warning::
+ It is an error to set this value without setting ``trusted_proxy``.
+
+log_untrusted_proxy_headers
+ Should waitress log warning messages about proxy headers that are being
+ sent from upstream that are not trusted by ``trusted_proxy_headers`` but
+ are being cleared due to ``clear_untrusted_proxy_headers``?
+
+ This may be useful for debugging if you expect your upstream proxy server
+ to only send specific headers.
+
+ Default: ``False``
+
+ .. versionadded:: 1.2.0
+
+ .. warning::
+ It is a no-op to set this value without also setting
+ ``clear_untrusted_proxy_headers`` and ``trusted_proxy``
+
+clear_untrusted_proxy_headers
+ This tells Waitress to remove any untrusted proxy headers ("Forwarded",
+ "X-Forwared-For", "X-Forwarded-By", "X-Forwarded-Host", "X-Forwarded-Port",
+ "X-Forwarded-Proto") not explicitly allowed by ``trusted_proxy_headers``.
+
+ Default: ``False``
+
+ .. versionadded:: 1.2.0
+
+ .. warning::
+ The default value is set to ``False`` for backwards compatibility. In
+ future versions of Waitress this default will be changed to ``True``.
+ Warnings will be raised unless the user explicitly provides a value for
+ this option, allowing the user to opt-in to the new safety features
+ automatically.
+
+ .. warning::
+ It is an error to set this value without setting ``trusted_proxy``.
url_scheme
- default ``wsgi.url_scheme`` value (string), default ``http``; can be
+ The value of ``wsgi.url_scheme`` in the environ. This can be
overridden per-request by the value of the ``X_FORWARDED_PROTO`` header,
but only if the client address matches ``trusted_proxy``.
+ Default: ``http``
+
ident
- server identity (string) used in "Server:" header in responses, default
- ``waitress``
+ Server identity (string) used in "Server:" header in responses.
+
+ Default: ``waitress``
backlog
- backlog is the value waitress passes to pass to socket.listen()
- (integer), default ``1024``. This is the maximum number of incoming TCP
+ The value waitress passes to pass to ``socket.listen()`` (integer).
+ This is the maximum number of incoming TCP
connections that will wait in an OS queue for an available channel. From
listen(1): "If a connection request arrives when the queue is full, the
client may receive an error with an indication of ECONNREFUSED or, if the
underlying protocol supports retransmission, the request may be ignored
so that a later reattempt at connection succeeds."
+ Default: ``1024``
+
recv_bytes
- recv_bytes is the argument waitress passes to socket.recv() (integer),
- default ``8192``
+ The argument waitress passes to ``socket.recv()`` (integer).
+
+ Default: ``8192``
send_bytes
- send_bytes is the number of bytes to send to socket.send() (integer),
- default ``18000``. Multiples of 9000 should avoid partly-filled TCP
+ The number of bytes to send to ``socket.send()`` (integer).
+ Multiples of 9000 should avoid partly-filled TCP
packets, but don't set this larger than the TCP write buffer size. In
- Linux, /proc/sys/net/ipv4/tcp_wmem controls the minimum, default, and
+ Linux, ``/proc/sys/net/ipv4/tcp_wmem`` controls the minimum, default, and
maximum sizes of TCP write buffers.
+ Default: ``18000``
+
outbuf_overflow
A tempfile should be created if the pending output is larger than
- outbuf_overflow, which is measured in bytes. The default is 1MB
- (``1048576``). This is conservative.
+ outbuf_overflow, which is measured in bytes. The default is conservative.
+
+ Default: ``1048576`` (1MB)
inbuf_overflow
A tempfile should be created if the pending input is larger than
- inbuf_overflow, which is measured in bytes. The default is 512K
- (``524288``). This is conservative.
+ inbuf_overflow, which is measured in bytes. The default is conservative.
+
+ Default: ``524288`` (512K)
connection_limit
Stop creating new channels if too many are already active (integer).
- Default is ``100``. Each channel consumes at least one file descriptor,
+ Each channel consumes at least one file descriptor,
and, depending on the input and output body sizes, potentially up to
three, plus whatever file descriptors your application logic happens to
open. The default is conservative, but you may need to increase the
@@ -120,46 +216,62 @@ connection_limit
connections that can be waiting for processing; the ``backlog`` argument
controls that.
+ Default: ``100``
+
cleanup_interval
- Minimum seconds between cleaning up inactive channels (integer), default
- ``30``. See "channel_timeout".
+ Minimum seconds between cleaning up inactive channels (integer).
+ See also ``channel_timeout``.
+
+ Default: ``30``
channel_timeout
- Maximum seconds to leave an inactive connection open (integer), default
- ``120``. "Inactive" is defined as "has received no data from a client
+ Maximum seconds to leave an inactive connection open (integer).
+ "Inactive" is defined as "has received no data from a client
and has sent no data to a client".
+ Default: ``120``
+
log_socket_errors
- Boolean: turn off to not log premature client disconnect tracebacks.
- Default: ``True``.
+ Set to ``False`` to not log premature client disconnect tracebacks.
+
+ Default: ``True``
max_request_header_size
- maximum number of bytes of all request headers combined (integer), 256K
- (``262144``) default)
+ Maximum number of bytes of all request headers combined (integer).
+
+ Default: ``262144`` (256K)
max_request_body_size
- maximum number of bytes in request body (integer), 1GB (``1073741824``)
- default.
+ Maximum number of bytes in request body (integer).
+
+ Default: ``1073741824`` (1GB)
expose_tracebacks
- Boolean: expose tracebacks of unhandled exceptions to client. Default:
- ``False``.
+ Set to ``True`` to expose tracebacks of unhandled exceptions to client.
+
+ Default: ``False``
asyncore_loop_timeout
+ The ``timeout`` value (seconds) passed to ``asyncore.loop`` to run the mainloop.
+
+ Default: ``1``
+
.. versionadded:: 0.8.3
- The ``timeout`` value (seconds) passed to ``asyncore.loop`` to run the mainloop.
- Default: 1.
asyncore_use_poll
+ Set to ``True`` to switch from using ``select()`` to ``poll()`` in ``asyncore.loop``.
+ By default ``asyncore.loop()`` uses ``select()`` which has a limit of 1024 file descriptors.
+ ``select()`` and ``poll()`` provide basically the same functionality, but ``poll()`` doesn't have the file descriptors limit.
+
+ Default: ``False``
+
.. versionadded:: 0.8.6
- Boolean: switch from using ``select()`` to ``poll()`` in ``asyncore.loop``.
- By default ``asyncore.loop()`` uses ``select()`` which has a limit of 1024 file descriptors.
- ``select()`` and ``poll()`` provide basically the same functionality, but ``poll()`` doesn't have the file descriptors limit.
- Default: False.
url_prefix
String: the value used as the WSGI ``SCRIPT_NAME`` value. Setting this to
anything except the empty string will cause the WSGI ``SCRIPT_NAME`` value
to be the value passed minus any trailing slashes you add, and it will
cause the ``PATH_INFO`` of any request which is prefixed with this value to
- be stripped of the prefix. Default: the empty string.
+ be stripped of the prefix.
+
+ Default: ``''``
diff --git a/docs/reverse-proxy.rst b/docs/reverse-proxy.rst
index 2532180..7081fda 100644
--- a/docs/reverse-proxy.rst
+++ b/docs/reverse-proxy.rst
@@ -1,4 +1,4 @@
-..index:: reverse, proxy, TLS, SSL, https
+.. index:: reverse, proxy, TLS, SSL, https
.. _using-behind-a-reverse-proxy:
@@ -15,7 +15,8 @@ proxies often have lots of useful deployment knobs.
If you're using Waitress behind a reverse proxy, you'll almost always want
your reverse proxy to pass along the ``Host`` header sent by the client to
Waitress, in either case, as it will be used by most applications to generate
-correct URLs.
+correct URLs. You may also use the proxy headers if passing the Host directly
+is not possible, or there are multiple proxies involved.
For example, when using nginx as a reverse proxy, you might add the following
lines in a ``location`` section.
@@ -39,15 +40,9 @@ application. You can do this in one of three ways:
1. You can pass a ``url_scheme`` configuration variable to the
``waitress.serve`` function.
-2. You can configure the proxy reverse server to pass a header,
- ``X_FORWARDED_PROTO``, whose value will be set for that request as
- the ``wsgi.url_scheme`` environment value. Note that you must also
- conigure ``waitress.serve`` by passing the IP address of that proxy
- as its ``trusted_proxy``.
-
-3. You can use Paste's ``PrefixMiddleware`` in conjunction with
- configuration settings on the reverse proxy server.
-
+2. You can pass certain well known proxy headers from your proxy server and
+ use waitress's ``trusted_proxy`` support to automatically configure the
+ WSGI environment.
Using ``url_scheme`` to set ``wsgi.url_scheme``
-----------------------------------------------
@@ -62,25 +57,59 @@ You can have the Waitress server use the ``https`` url scheme by default.:
This works if all URLs generated by your application should use the ``https``
scheme.
-Passing the ``X_FORWARDED_PROTO`` header to set ``wsgi.url_scheme``
--------------------------------------------------------------------
+Passing the proxy headers to setup the WSGI environment
+-------------------------------------------------------
If your proxy accepts both HTTP and HTTPS URLs, and you want your application
-to generate the appropriate url based on the incoming scheme, also set up
-your proxy to send a ``X-Forwarded-Proto`` with the original URL scheme along
-with each proxied request. For example, when using nginx::
+to generate the appropriate url based on the incoming scheme, you'll want to
+pass waitress ``X-Forwarded-Proto``, however Waitress is also able to update
+the environment using ``X-Forwarded-Proto``, ``X-Forwarded-For``,
+``X-Forwarded-Host``, and ``X-Forwarded-Port``::
+
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Host $host:$server_port;
+ proxy_set_header X-Forwarded-Port $server_port;
+
+when using Apache, ``mod_proxy`` automatically forwards the following headers::
- proxy_set_header X-Forwarded-Proto $scheme;
+ X-Forwarded-For
+ X-Forwarded-Host
+ X-Forwarded-Server
-or via Apache::
+You will also want to add to Apache::
RequestHeader set X-Forwarded-Proto https
+Configure waitress's ``trusted_proxy_headers`` as appropriate::
+
+ trusted_proxy_headers = "x-forwarded-for, x-forwarded-host, x-forwarded-proto, x-forwarded-port"
+
+At this point waitress will set up the WSGI environment using the information
+specified in the trusted proxy headers. This will setup the following
+variables::
+
+ HTTP_HOST
+ SERVER_NAME
+ SERVER_PORT
+ REMOTE_ADDR
+ REMOTE_PORT (if available)
+ wsgi.url_scheme
+
+Waitress also has support for the `Forwarded (RFC7239) HTTP header
+<https://tools.ietf.org/html/rfc7239>`_ which is better defined than the ad-hoc
+``X-Forwarded-*``, however support is not nearly as widespread yet.
+``Forwarded`` supports similar functionality as the different individual
+headers, and is mutually exclusive to using the ``X-Forwarded-*`` headers.
+
+To configure waitress to use the ``Forwarded`` header, set::
+
+ trusted_proxy_headers = "forwarded"
+
.. note::
You must also configure the Waitress server's ``trusted_proxy`` to
- contain the IP address of the proxy in order for this header to override
- the default URL scheme.
+ contain the IP address of the proxy.
Using ``url_prefix`` to influence ``SCRIPT_NAME`` and ``PATH_INFO``
@@ -101,74 +130,3 @@ it will cause the ``PATH_INFO`` of any request which is prefixed with this
value to be stripped of the prefix. This is useful in proxying scenarios where
you wish to forward all traffic to a Waitress server but need URLs generated by
downstream applications to be prefixed with a particular path segment.
-
-
-Using Paste's ``PrefixMiddleware`` to set ``wsgi.url_scheme``
--------------------------------------------------------------
-
-If only some of the URLs generated by your application should use the
-``https`` scheme (and some should use ``http``), you'll need to use Paste's
-``PrefixMiddleware`` as well as change some configuration settings on your
-proxy. To use ``PrefixMiddleware``, wrap your application before serving it
-using Waitress:
-
-.. code-block:: python
-
- from waitress import serve
- from paste.deploy.config import PrefixMiddleware
- app = PrefixMiddleware(app)
- serve(app)
-
-Once you wrap your application in the the ``PrefixMiddleware``, the
-middleware will notice certain headers sent from your proxy and will change
-the ``wsgi.url_scheme`` and possibly other WSGI environment variables
-appropriately.
-
-Once your application is wrapped by the prefix middleware, you should
-instruct your proxy server to send along the original ``Host`` header from
-the client to your Waitress server, as well as sending along a
-``X-Forwarded-Proto`` header with the appropriate value for
-``wsgi.url_scheme``.
-
-If your proxy accepts both HTTP and HTTPS URLs, and you want your application
-to generate the appropriate url based on the incoming scheme, also set up
-your proxy to send a ``X-Forwarded-Proto`` with the original URL scheme along
-with each proxied request. For example, when using nginx.
-
-.. code-block:: nginx
-
- proxy_set_header X-Forwarded-Proto $scheme;
-
-It's permitted to set an ``X-Forwarded-For`` header too; the
-``PrefixMiddleware`` uses this to adjust other environment variables (you'll
-have to read its docs to find out which ones, I don't know what they are). For
-the ``X-Forwarded-For`` header.
-
-.. code-block:: nginx
-
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-
-Note that you can wrap your application in the PrefixMiddleware declaratively
-in a :term:`PasteDeploy` configuration file too, if your web framework uses
-PasteDeploy-style configuration:
-
-.. code-block:: ini
-
- [app:myapp]
- use = egg:mypackage#myapp
-
- [filter:paste_prefix]
- use = egg:PasteDeploy#prefix
-
- [pipeline:main]
- pipeline =
- paste_prefix
- myapp
-
- [server:main]
- use = egg:waitress#main
- listen = 127.0.0.1:8080
-
-Note that you can also set ``PATH_INFO`` and ``SCRIPT_NAME`` using
-PrefixMiddleware too (its original purpose, really) instead of using Waitress'
-``url_prefix`` adjustment. See the PasteDeploy docs for more information.
diff --git a/waitress/adjustments.py b/waitress/adjustments.py
index ad2d0f4..c1aa66d 100644
--- a/waitress/adjustments.py
+++ b/waitress/adjustments.py
@@ -15,8 +15,10 @@
"""
import getopt
import socket
+import warnings
-from waitress.compat import (
+from .utilities import PROXY_HEADERS
+from .compat import (
PY2,
WIN,
string_types,
@@ -25,6 +27,8 @@ from waitress.compat import (
truthy = frozenset(('t', 'true', 'y', 'yes', 'on', '1'))
+KNOWN_PROXY_HEADERS = {header.lower().replace('_', '-') for header in PROXY_HEADERS}
+
def asbool(s):
""" Return the boolean value ``True`` if the case-lowered value of string
input ``s`` is any of ``t``, ``true``, ``y``, ``on``, or ``1``, otherwise
@@ -58,6 +62,9 @@ def aslist(value):
result.extend(subvalues)
return result
+def asset(value):
+ return set(aslist(value))
+
def slash_fixed_str(s):
s = s.strip()
if s:
@@ -80,6 +87,9 @@ class _str_marker(str):
class _int_marker(int):
pass
+class _bool_marker(object):
+ pass
+
class Adjustments(object):
"""This class contains tunable parameters.
"""
@@ -91,7 +101,11 @@ class Adjustments(object):
('ipv6', asbool),
('listen', aslist),
('threads', int),
- ('trusted_proxy', str),
+ ('trusted_proxy', str_iftruthy),
+ ('trusted_proxy_count', int),
+ ('trusted_proxy_headers', asset),
+ ('log_untrusted_proxy_headers', asbool),
+ ('clear_untrusted_proxy_headers', asbool),
('url_scheme', str),
('url_prefix', slash_fixed_str),
('backlog', int),
@@ -130,6 +144,35 @@ class Adjustments(object):
# Host allowed to overrid ``wsgi.url_scheme`` via header
trusted_proxy = None
+ # How many proxies we trust when chained
+ #
+ # X-Forwarded-For: 192.0.2.1, "[2001:db8::1]"
+ #
+ # or
+ #
+ # Forwarded: for=192.0.2.1, For="[2001:db8::1]"
+ #
+ # means there were (potentially), two proxies involved. If we know there is
+ # only 1 valid proxy, then that initial IP address "192.0.2.1" is not
+ # trusted and we completely ignore it. If there are two trusted proxies in
+ # the path, this value should be set to a higher number.
+ trusted_proxy_count = 1
+
+ # Which of the proxy headers should we trust, this is a set where you
+ # either specify forwarded or one or more of forwarded-host, forwarded-for,
+ # forwarded-proto, forwarded-port.
+ trusted_proxy_headers = set()
+
+ # Would you like waitress to log warnings about untrusted proxy headers
+ # that were encountered while processing the proxy headers? This only makes
+ # sense to set when you have a trusted_proxy, and you expect the upstream
+ # proxy server to filter invalid headers
+ log_untrusted_proxy_headers = False
+
+ # Should waitress clear any proxy headers that are not deemed trusted from
+ # the environ? Change to True by default in 2.x
+ clear_untrusted_proxy_headers = _bool_marker
+
# default ``wsgi.url_scheme`` value
url_scheme = 'http'
@@ -324,6 +367,56 @@ class Adjustments(object):
except:
raise ValueError('Invalid host/port specified.')
+ if (
+ self.trusted_proxy is None and
+ (
+ self.trusted_proxy_headers or
+ (self.clear_untrusted_proxy_headers is not _bool_marker)
+ )
+ ):
+ raise ValueError(
+ "The values trusted_proxy_headers and clear_untrusted_proxy_headers "
+ "have no meaning without setting trusted_proxy. Cowardly refusing to "
+ "continue."
+ )
+
+ if self.trusted_proxy_headers:
+ self.trusted_proxy_headers = {header.lower() for header in self.trusted_proxy_headers}
+
+ unknown_values = self.trusted_proxy_headers - KNOWN_PROXY_HEADERS
+ if unknown_values:
+ raise ValueError(
+ "Received unknown trusted_proxy_headers value (%s) expected one "
+ "of %s" % (", ".join(unknown_values), ", ".join(KNOWN_PROXY_HEADERS))
+ )
+
+ if (
+ 'forwarded' in self.trusted_proxy_headers and
+ self.trusted_proxy_headers - {'forwarded'}
+ ):
+ raise ValueError(
+ "The Forwarded proxy header and the "
+ "X-Forwarded-{By,Host,Proto,Port,For} headers are mutually "
+ "exclusive. Can't trust both!"
+ )
+ elif self.trusted_proxy is not None:
+ warnings.warn(
+ 'No proxy headers were marked as trusted, but trusted_proxy was set. '
+ 'Implicitly trusting X-Forwarded-Proto for backwards compatibility. '
+ 'This will be removed in future versions of waitress.',
+ DeprecationWarning
+ )
+ self.trusted_proxy_headers = {'x-forwarded-proto'}
+
+ if self.trusted_proxy and self.clear_untrusted_proxy_headers is _bool_marker:
+ warnings.warn(
+ 'In future versions of Waitress clear_untrusted_proxy_headers will be '
+ 'set to True by default. You may opt-out by setting this value to '
+ 'False, or opt-in explicitly by setting this to True.',
+ DeprecationWarning
+ )
+ self.clear_untrusted_proxy_headers = False
+
self.listen = wanted_sockets
self.check_sockets(self.sockets)
diff --git a/waitress/task.py b/waitress/task.py
index 258fb3e..68aa69a 100644
--- a/waitress/task.py
+++ b/waitress/task.py
@@ -17,19 +17,16 @@ import sys
import threading
import time
-from waitress.buffers import ReadOnlyFileBasedBuffer
-
-from waitress.compat import (
- tobytes,
- Queue,
- Empty,
- reraise,
-)
-
-from waitress.utilities import (
+from .buffers import ReadOnlyFileBasedBuffer
+from .compat import Empty, Queue, reraise, tobytes
+from .utilities import (
+ Forwarded,
+ PROXY_HEADERS,
build_http_date,
+ clear_untrusted_headers,
logger,
queue_logger,
+ undquote,
)
rename_headers = { # or keep them without the HTTP_ prefix added
@@ -48,6 +45,7 @@ hop_by_hop = frozenset((
'upgrade'
))
+
class JustTesting(Exception):
pass
@@ -508,6 +506,229 @@ class WSGITask(Task):
if hasattr(app_iter, 'close'):
app_iter.close()
+ def parse_proxy_headers(
+ self,
+ environ,
+ headers,
+ trusted_proxy_count=1,
+ trusted_proxy_headers=None,
+ ):
+ if trusted_proxy_headers is None:
+ trusted_proxy_headers = set()
+
+ forwarded_for = []
+ forwarded_host = forwarded_proto = forwarded_port = forwarded = ""
+ client_addr = None
+ untrusted_headers = set(PROXY_HEADERS)
+
+ def warn_unspecified_behavior(header):
+ self.logger.warning(
+ "Found multiple values in %s, this has unspecified behaviour. "
+ "Ignoring header value.",
+ header,
+ )
+
+ if "x-forwarded-for" in trusted_proxy_headers and "X_FORWARDED_FOR" in headers:
+ forwarded_for = []
+
+ for forward_hop in headers["X_FORWARDED_FOR"].split(","):
+ forward_hop = forward_hop.strip()
+ forward_hop = undquote(forward_hop)
+
+ # Make sure that all IPv6 addresses are surrounded by brackets
+
+ if ":" in forward_hop and forward_hop[-1] != "]":
+ forwarded_for.append("[{}]".format(forward_hop))
+ else:
+ forwarded_for.append(forward_hop)
+
+ forwarded_for = forwarded_for[-trusted_proxy_count:]
+ client_addr = forwarded_for[0]
+
+ untrusted_headers.remove("X_FORWARDED_FOR")
+
+ if "x-forwarded-host" in trusted_proxy_headers and "X_FORWARDED_HOST" in headers:
+ forwarded_host_multiple = []
+
+ for forward_host in headers["X_FORWARDED_HOST"].split(","):
+ forward_host = forward_host.strip()
+ forward_host = undquote(forward_host)
+ forwarded_host_multiple.append(forward_host)
+
+ forwarded_host_multiple = forwarded_host_multiple[-trusted_proxy_count:]
+ forwarded_host = forwarded_host_multiple[0]
+
+ untrusted_headers.remove("X_FORWARDED_HOST")
+
+ if "x-forwarded-proto" in trusted_proxy_headers:
+ forwarded_proto = undquote(headers.get("X_FORWARDED_PROTO", ""))
+ untrusted_headers.remove("X_FORWARDED_PROTO")
+
+ if "," in forwarded_proto:
+ forwarded_proto = ""
+ warn_unspecified_behavior("X-Forwarded-Proto")
+
+ if "x-forwarded-port" in trusted_proxy_headers:
+ forwarded_port = undquote(headers.get("X_FORWARDED_PORT", ""))
+ untrusted_headers.remove("X_FORWARDED_PORT")
+
+ if "," in forwarded_port:
+ forwarded_port = ""
+ warn_unspecified_behavior("X-Forwarded-Port")
+
+ if "x-forwarded-by" in trusted_proxy_headers:
+ # Waitress itself does not use X-Forwarded-By, but we can not
+ # remove it so it can get set in the environ
+ untrusted_headers.remove("X_FORWARDED_BY")
+
+ if "forwarded" in trusted_proxy_headers:
+ forwarded = headers.get("FORWARDED", None)
+ untrusted_headers = PROXY_HEADERS - {"FORWARDED"}
+
+ # If the Forwarded header exists, it gets priority
+ if forwarded:
+ proxies = []
+
+ for forwarded_element in forwarded.split(","):
+ # Remove whitespace that may have been introduced when
+ # appending a new entry
+ forwarded_element = forwarded_element.strip()
+
+ forwarded_for = forwarded_host = forwarded_proto = ""
+ forwarded_port = forwarded_by = ""
+
+ for pair in forwarded_element.split(";"):
+ pair = pair.lower()
+
+ if not pair:
+ continue
+
+ token, equals, value = pair.partition("=")
+
+ if equals != "=":
+ raise ValueError("Invalid forwarded-pair in Forwarded element")
+
+ if token.strip() != token:
+ raise ValueError("token may not be surrounded by whitespace")
+
+ if value.strip() != value:
+ raise ValueError("value may not be surrounded by whitespace")
+
+ if token == "by":
+ forwarded_by = undquote(value)
+
+ elif token == "for":
+ forwarded_for = undquote(value)
+
+ elif token == "host":
+ forwarded_host = undquote(value)
+
+ elif token == "proto":
+ forwarded_proto = undquote(value)
+
+ else:
+ self.logger.warning("Unknown Forwarded token: %s" % token)
+
+ proxies.append(
+ Forwarded(
+ forwarded_by, forwarded_for, forwarded_host, forwarded_proto
+ )
+ )
+
+ proxies = proxies[-trusted_proxy_count:]
+
+ # Iterate backwards and fill in some values, the oldest entry that
+ # contains the information we expect is the one we use. We expect
+ # that intermediate proxies may re-write the host header or proto,
+ # but the oldest entry is the one that contains the information the
+ # client expects when generating URL's
+ #
+ # Forwarded: for="[2001:db8::1]";host="example.com:8443";proto="https"
+ # Forwarded: for=192.0.2.1;host="example.internal:8080"
+ #
+ # (After HTTPS header folding) should mean that we use as values:
+ #
+ # Host: example.com
+ # Protocol: https
+ # Port: 8443
+
+ for proxy in proxies[::-1]:
+ client_addr = proxy.for_ or client_addr
+ forwarded_host = proxy.host or forwarded_host
+ forwarded_proto = proxy.proto or forwarded_proto
+
+ if forwarded_proto:
+ forwarded_proto = forwarded_proto.lower()
+
+ if forwarded_proto not in {"http", "https"}:
+ raise ValueError(
+ 'Invalid "Forwarded Proto=" or "X-Forwarded-Proto" value.'
+ )
+
+ # Set the URL scheme to the proxy provided proto
+ environ["wsgi.url_scheme"] = forwarded_proto
+
+ if not forwarded_port:
+ if forwarded_proto == "http":
+ forwarded_port = "80"
+
+ if forwarded_proto == "https":
+ forwarded_port = "443"
+
+ if forwarded_host:
+ forwarded_host = forwarded_host.strip()
+
+ if ":" in forwarded_host and forwarded_host[-1] != "]":
+ host, port = forwarded_host.rsplit(":", 1)
+ host, port = host.strip(), str(port)
+
+ # We trust the port in the Forwarded Host/X-Forwarded-Host over
+ # X-Forwarded-Port, or whatever we got from Forwarded
+ # Proto/X-Forwarded-Proto.
+
+ if forwarded_port != port:
+ forwarded_port = port
+
+ # We trust the proxy server's forwarded Host
+ environ["SERVER_NAME"] = host
+ environ["HTTP_HOST"] = forwarded_host
+ else:
+ # We trust the proxy server's forwarded Host
+ environ["SERVER_NAME"] = forwarded_host
+ environ["HTTP_HOST"] = forwarded_host
+
+ if forwarded_port:
+ if forwarded_port not in {"443", "80"}:
+ environ["HTTP_HOST"] = "{}:{}".format(
+ forwarded_host, forwarded_port
+ )
+ elif (
+ forwarded_port == "80" and environ["wsgi.url_scheme"] != "http"
+ ):
+ environ["HTTP_HOST"] = "{}:{}".format(
+ forwarded_host, forwarded_port
+ )
+ elif (
+ forwarded_port == "443"
+ and environ["wsgi.url_scheme"] != "https"
+ ):
+ environ["HTTP_HOST"] = "{}:{}".format(
+ forwarded_host, forwarded_port
+ )
+
+ if forwarded_port:
+ environ["SERVER_PORT"] = str(forwarded_port)
+
+ if client_addr:
+ if ":" in client_addr and client_addr[-1] != "]":
+ addr, port = client_addr.rsplit(":", 1)
+ environ["REMOTE_ADDR"] = addr.strip()
+ environ["REMOTE_PORT"] = port.strip()
+ else:
+ environ["REMOTE_ADDR"] = client_addr.strip()
+
+ return untrusted_headers
+
def get_environment(self):
"""Returns a WSGI environment."""
environ = self.environ
@@ -542,25 +763,61 @@ class WSGITask(Task):
if path.startswith(url_prefix_with_trailing_slash):
path = path[len(url_prefix):]
- environ = {}
- environ['REQUEST_METHOD'] = request.command.upper()
- environ['SERVER_PORT'] = str(server.effective_port)
- environ['SERVER_NAME'] = server.server_name
- environ['SERVER_SOFTWARE'] = server.adj.ident
- environ['SERVER_PROTOCOL'] = 'HTTP/%s' % self.version
- environ['SCRIPT_NAME'] = url_prefix
- environ['PATH_INFO'] = path
- environ['QUERY_STRING'] = request.query
- host = environ['REMOTE_ADDR'] = channel.addr[0]
+ environ = {
+ 'REQUEST_METHOD': request.command.upper(),
+ 'SERVER_PORT': str(server.effective_port),
+ 'SERVER_NAME': server.server_name,
+ 'SERVER_SOFTWARE': server.adj.ident,
+ 'SERVER_PROTOCOL': 'HTTP/%s' % self.version,
+ 'SCRIPT_NAME': url_prefix,
+ 'PATH_INFO': path,
+ 'QUERY_STRING': request.query,
+ 'wsgi.url_scheme': request.url_scheme,
+
+ # the following environment variables are required by the WSGI spec
+ 'wsgi.version': (1, 0),
+
+ # apps should use the logging module
+ 'wsgi.errors': sys.stderr,
+ 'wsgi.multithread': True,
+ 'wsgi.multiprocess': False,
+ 'wsgi.run_once': False,
+ 'wsgi.input': request.get_body_stream(),
+ 'wsgi.file_wrapper': ReadOnlyFileBasedBuffer,
+ 'wsgi.input_terminated': True, # wsgi.input is EOF terminated
+ }
+ remote_peer = environ['REMOTE_ADDR'] = channel.addr[0]
headers = dict(request.headers)
- if host == server.adj.trusted_proxy:
- wsgi_url_scheme = headers.pop('X_FORWARDED_PROTO',
- request.url_scheme)
+
+ untrusted_headers = PROXY_HEADERS
+ if remote_peer == server.adj.trusted_proxy:
+ untrusted_headers = self.parse_proxy_headers(
+ environ,
+ headers,
+ trusted_proxy_count=server.adj.trusted_proxy_count,
+ trusted_proxy_headers=server.adj.trusted_proxy_headers,
+ )
else:
- wsgi_url_scheme = request.url_scheme
- if wsgi_url_scheme not in ('http', 'https'):
- raise ValueError('Invalid X_FORWARDED_PROTO value')
+ # If we are not relying on a proxy, we still want to try and set
+ # the REMOTE_PORT to something useful, maybe None though.
+ environ["REMOTE_PORT"] = str(channel.addr[1])
+
+ # Nah, we aren't actually going to look up the reverse DNS for
+ # REMOTE_ADDR, but we will happily set this environment variable for
+ # the WSGI application. Spec says we can just set this to REMOTE_ADDR,
+ # so we do.
+ environ["REMOTE_HOST"] = environ["REMOTE_ADDR"]
+
+ # Clear out the untrusted proxy headers
+ if server.adj.clear_untrusted_proxy_headers:
+ clear_untrusted_headers(
+ headers,
+ untrusted_headers,
+ log_warning=server.adj.log_untrusted_proxy_headers,
+ logger=self.logger,
+ )
+
for key, value in headers.items():
value = value.strip()
mykey = rename_headers.get(key, None)
@@ -569,16 +826,6 @@ class WSGITask(Task):
if mykey not in environ:
environ[mykey] = value
- # the following environment variables are required by the WSGI spec
- environ['wsgi.version'] = (1, 0)
- environ['wsgi.url_scheme'] = wsgi_url_scheme
- environ['wsgi.errors'] = sys.stderr # apps should use the logging module
- environ['wsgi.multithread'] = True
- environ['wsgi.multiprocess'] = False
- environ['wsgi.run_once'] = False
- environ['wsgi.input'] = request.get_body_stream()
- environ['wsgi.file_wrapper'] = ReadOnlyFileBasedBuffer
- environ['wsgi.input_terminated'] = True # wsgi.input is EOF terminated
-
+ # cache the environ for this request
self.environ = environ
return environ
diff --git a/waitress/tests/test_adjustments.py b/waitress/tests/test_adjustments.py
index 05b4dbd..f841efc 100644
--- a/waitress/tests/test_adjustments.py
+++ b/waitress/tests/test_adjustments.py
@@ -1,5 +1,6 @@
import sys
import socket
+import warnings
from waitress.compat import (
PY2,
@@ -108,6 +109,9 @@ class TestAdjustments(unittest.TestCase):
port='8080',
threads='5',
trusted_proxy='192.168.1.1',
+ trusted_proxy_headers={'forwarded'},
+ trusted_proxy_count=2,
+ log_untrusted_proxy_headers=True,
url_scheme='https',
backlog='20',
recv_bytes='200',
@@ -134,6 +138,9 @@ class TestAdjustments(unittest.TestCase):
self.assertEqual(inst.port, 8080)
self.assertEqual(inst.threads, 5)
self.assertEqual(inst.trusted_proxy, '192.168.1.1')
+ self.assertEqual(inst.trusted_proxy_headers, {'forwarded'})
+ self.assertEqual(inst.trusted_proxy_count, 2)
+ self.assertEqual(inst.log_untrusted_proxy_headers, True)
self.assertEqual(inst.url_scheme, 'https')
self.assertEqual(inst.backlog, 20)
self.assertEqual(inst.recv_bytes, 200)
@@ -293,6 +300,58 @@ class TestAdjustments(unittest.TestCase):
sockets=sockets)
sockets[0].close()
+ def test_dont_mix_forwarded_with_x_forwarded(self):
+ with self.assertRaises(ValueError) as cm:
+ self._makeOne(trusted_proxy='localhost', trusted_proxy_headers={'forwarded', 'x-forwarded-for'})
+
+ self.assertIn('The Forwarded proxy header', str(cm.exception))
+
+ def test_unknown_trusted_proxy_header(self):
+ with self.assertRaises(ValueError) as cm:
+ self._makeOne(trusted_proxy='localhost', trusted_proxy_headers={'forwarded', 'x-forwarded-unknown'})
+
+ self.assertIn(
+ 'unknown trusted_proxy_headers value (x-forwarded-unknown)',
+ str(cm.exception)
+ )
+
+ def test_trusted_proxy_headers_no_trusted_proxy(self):
+ with self.assertRaises(ValueError) as cm:
+ self._makeOne(trusted_proxy_headers={'forwarded'})
+
+ self.assertIn(
+ 'Cowardly refusing to continue.',
+ str(cm.exception)
+ )
+
+ def test_trusted_proxy_headers_string_list(self):
+ inst = self._makeOne(trusted_proxy='localhost', trusted_proxy_headers='x-forwarded-for x-forwarded-by')
+ self.assertEqual(inst.trusted_proxy_headers, {'x-forwarded-for', 'x-forwarded-by'})
+
+ def test_trusted_proxy_headers_string_list_newlines(self):
+ inst = self._makeOne(trusted_proxy='localhost', trusted_proxy_headers='x-forwarded-for\nx-forwarded-by\nx-forwarded-host')
+ self.assertEqual(inst.trusted_proxy_headers, {'x-forwarded-for', 'x-forwarded-by', 'x-forwarded-host'})
+
+ def test_no_trusted_proxy_headers_trusted_proxy(self):
+ with warnings.catch_warnings(record=True) as w:
+ warnings.resetwarnings()
+ warnings.simplefilter("always")
+ self._makeOne(trusted_proxy='localhost')
+
+ self.assertGreaterEqual(len(w), 1)
+ self.assertTrue(issubclass(w[0].category, DeprecationWarning))
+ self.assertIn("Implicitly trusting X-Forwarded-Proto", str(w[0]))
+
+ def test_clear_untrusted_proxy_headers(self):
+ with warnings.catch_warnings(record=True) as w:
+ warnings.resetwarnings()
+ warnings.simplefilter("always")
+ self._makeOne(trusted_proxy='localhost', trusted_proxy_headers={'x-forwarded-for'})
+
+ self.assertGreaterEqual(len(w), 1)
+ self.assertTrue(issubclass(w[0].category, DeprecationWarning))
+ self.assertIn("clear_untrusted_proxy_headers will be set to True", str(w[0]))
+
def test_badvar(self):
self.assertRaises(ValueError, self._makeOne, nope=True)
diff --git a/waitress/tests/test_task.py b/waitress/tests/test_task.py
index 0d837f0..c141d79 100644
--- a/waitress/tests/test_task.py
+++ b/waitress/tests/test_task.py
@@ -752,11 +752,13 @@ class TestWSGITask(unittest.TestCase):
# nail the keys of environ
self.assertEqual(sorted(environ.keys()), [
'CONTENT_LENGTH', 'CONTENT_TYPE', 'HTTP_CONNECTION', 'HTTP_X_FOO',
- 'PATH_INFO', 'QUERY_STRING', 'REMOTE_ADDR', 'REQUEST_METHOD',
- 'SCRIPT_NAME', 'SERVER_NAME', 'SERVER_PORT', 'SERVER_PROTOCOL',
- 'SERVER_SOFTWARE', 'wsgi.errors', 'wsgi.file_wrapper', 'wsgi.input',
- 'wsgi.input_terminated', 'wsgi.multiprocess', 'wsgi.multithread',
- 'wsgi.run_once', 'wsgi.url_scheme', 'wsgi.version'])
+ 'PATH_INFO', 'QUERY_STRING', 'REMOTE_ADDR', 'REMOTE_HOST',
+ 'REMOTE_PORT', 'REQUEST_METHOD', 'SCRIPT_NAME', 'SERVER_NAME',
+ 'SERVER_PORT', 'SERVER_PROTOCOL', 'SERVER_SOFTWARE', 'wsgi.errors',
+ 'wsgi.file_wrapper', 'wsgi.input', 'wsgi.input_terminated',
+ 'wsgi.multiprocess', 'wsgi.multithread', 'wsgi.run_once',
+ 'wsgi.url_scheme', 'wsgi.version'
+ ])
self.assertEqual(environ['REQUEST_METHOD'], 'GET')
self.assertEqual(environ['SERVER_PORT'], '80')
@@ -768,6 +770,8 @@ class TestWSGITask(unittest.TestCase):
self.assertEqual(environ['PATH_INFO'], '/')
self.assertEqual(environ['QUERY_STRING'], 'abc')
self.assertEqual(environ['REMOTE_ADDR'], '127.0.0.1')
+ self.assertEqual(environ['REMOTE_HOST'], '127.0.0.1')
+ self.assertEqual(environ['REMOTE_PORT'], '39830')
self.assertEqual(environ['CONTENT_TYPE'], 'abc')
self.assertEqual(environ['CONTENT_LENGTH'], '10')
self.assertEqual(environ['HTTP_X_FOO'], 'BAR')
@@ -799,8 +803,9 @@ class TestWSGITask(unittest.TestCase):
def test_get_environment_values_w_scheme_override_trusted(self):
import sys
inst = self._makeOne()
- inst.channel.addr = ['192.168.1.1']
+ inst.channel.addr = ['192.168.1.1', 8080]
inst.channel.server.adj.trusted_proxy = '192.168.1.1'
+ inst.channel.server.adj.trusted_proxy_headers = {'x-forwarded-proto'}
request = DummyParser()
request.headers = {
'CONTENT_TYPE': 'abc',
@@ -816,14 +821,16 @@ class TestWSGITask(unittest.TestCase):
# nail the keys of environ
self.assertEqual(sorted(environ.keys()), [
'CONTENT_LENGTH', 'CONTENT_TYPE', 'HTTP_CONNECTION', 'HTTP_X_FOO',
- 'PATH_INFO', 'QUERY_STRING', 'REMOTE_ADDR', 'REQUEST_METHOD',
- 'SCRIPT_NAME', 'SERVER_NAME', 'SERVER_PORT', 'SERVER_PROTOCOL',
- 'SERVER_SOFTWARE', 'wsgi.errors', 'wsgi.file_wrapper', 'wsgi.input',
+ 'HTTP_X_FORWARDED_PROTO', 'PATH_INFO', 'QUERY_STRING',
+ 'REMOTE_ADDR', 'REMOTE_HOST', 'REQUEST_METHOD', 'SCRIPT_NAME',
+ 'SERVER_NAME', 'SERVER_PORT', 'SERVER_PROTOCOL', 'SERVER_SOFTWARE',
+ 'wsgi.errors', 'wsgi.file_wrapper', 'wsgi.input',
'wsgi.input_terminated', 'wsgi.multiprocess', 'wsgi.multithread',
- 'wsgi.run_once', 'wsgi.url_scheme', 'wsgi.version'])
+ 'wsgi.run_once', 'wsgi.url_scheme', 'wsgi.version'
+ ])
self.assertEqual(environ['REQUEST_METHOD'], 'GET')
- self.assertEqual(environ['SERVER_PORT'], '80')
+ self.assertEqual(environ['SERVER_PORT'], '443')
self.assertEqual(environ['SERVER_NAME'], 'localhost')
self.assertEqual(environ['SERVER_SOFTWARE'], 'waitress')
self.assertEqual(environ['SERVER_PROTOCOL'], 'HTTP/1.0')
@@ -847,8 +854,9 @@ class TestWSGITask(unittest.TestCase):
def test_get_environment_values_w_bogus_scheme_override(self):
inst = self._makeOne()
- inst.channel.addr = ['192.168.1.1']
+ inst.channel.addr = ['192.168.1.1', 80]
inst.channel.server.adj.trusted_proxy = '192.168.1.1'
+ inst.channel.server.adj.trusted_proxy_headers = {'x-forwarded-proto'}
request = DummyParser()
request.headers = {
'CONTENT_TYPE': 'abc',
@@ -861,6 +869,519 @@ class TestWSGITask(unittest.TestCase):
inst.request = request
self.assertRaises(ValueError, inst.get_environment)
+ def test_get_environment_warning_other_proxy_headers(self):
+ inst = self._makeOne()
+ inst.logger = DummyLogger()
+
+ inst.request.headers = {
+ 'X_FORWARDED_FOR': '[2001:db8::1]',
+ 'FORWARDED': 'For=198.51.100.2;host=example.com:8080;proto=https'
+ }
+ inst.channel.addr = ['192.168.1.1', 80]
+ inst.channel.server.adj.trusted_proxy = '192.168.1.1'
+ inst.channel.server.adj.trusted_proxy_count = 1
+ inst.channel.server.adj.trusted_proxy_headers = {'forwarded'}
+ inst.channel.server.adj.log_untrusted_proxy_headers = True
+ environ = inst.get_environment()
+
+ self.assertEqual(len(inst.logger.logged), 1)
+ self.assertNotIn('HTTP_X_FORWARDED_FOR', environ)
+
+ self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
+ self.assertEqual(environ['SERVER_NAME'], 'example.com')
+ self.assertEqual(environ['HTTP_HOST'], 'example.com:8080')
+ self.assertEqual(environ['SERVER_PORT'], '8080')
+ self.assertEqual(environ['wsgi.url_scheme'], 'https')
+
+ def test_get_environment_contains_all_headers_including_untrusted(self):
+ inst = self._makeOne()
+ inst.logger = DummyLogger()
+
+ inst.request.headers = {
+ 'X_FORWARDED_FOR': '198.51.100.2',
+ 'X_FORWARDED_BY': 'Waitress',
+ 'X_FORWARDED_PROTO': 'https',
+ 'X_FORWARDED_HOST': 'example.org',
+ }
+ headers_orig = inst.request.headers.copy()
+ inst.channel.addr = ['192.168.1.1', 80]
+ inst.channel.server.adj.trusted_proxy = '192.168.1.1'
+ inst.channel.server.adj.trusted_proxy_count = 1
+ inst.channel.server.adj.trusted_proxy_headers = {'x-forwarded-by'}
+ inst.channel.server.adj.clear_untrusted_proxy_headers = False
+ environ = inst.get_environment()
+
+ for k, expected in headers_orig.items():
+ result = environ['HTTP_%s' % k]
+ self.assertEqual(result, expected)
+
+ def test_get_environment_contains_only_trusted_headers(self):
+ inst = self._makeOne()
+ inst.logger = DummyLogger()
+
+ inst.request.headers = {
+ 'X_FORWARDED_FOR': '198.51.100.2',
+ 'X_FORWARDED_BY': 'Waitress',
+ 'X_FORWARDED_PROTO': 'https',
+ 'X_FORWARDED_HOST': 'example.org',
+ }
+ inst.channel.addr = ['192.168.1.1', 80]
+ inst.channel.server.adj.trusted_proxy = '192.168.1.1'
+ inst.channel.server.adj.trusted_proxy_count = 1
+ inst.channel.server.adj.trusted_proxy_headers = {'x-forwarded-by'}
+ inst.channel.server.adj.clear_untrusted_proxy_headers = True
+ environ = inst.get_environment()
+
+ self.assertEqual(environ['HTTP_X_FORWARDED_BY'], 'Waitress')
+ self.assertNotIn('HTTP_X_FORWARDED_FOR', environ)
+ self.assertNotIn('HTTP_X_FORWARDED_PROTO', environ)
+ self.assertNotIn('HTTP_X_FORWARDED_HOST', environ)
+
+ def test_get_environment_clears_headers_if_untrusted_proxy(self):
+ inst = self._makeOne()
+ inst.logger = DummyLogger()
+
+ inst.request.headers = {
+ 'X_FORWARDED_FOR': '198.51.100.2',
+ 'X_FORWARDED_BY': 'Waitress',
+ 'X_FORWARDED_PROTO': 'https',
+ 'X_FORWARDED_HOST': 'example.org',
+ }
+ inst.channel.addr = ['192.168.1.255', 80]
+ inst.channel.server.adj.trusted_proxy = '192.168.1.1'
+ inst.channel.server.adj.trusted_proxy_count = 1
+ inst.channel.server.adj.trusted_proxy_headers = {'x-forwarded-by'}
+ inst.channel.server.adj.clear_untrusted_proxy_headers = True
+ environ = inst.get_environment()
+
+ self.assertNotIn('HTTP_X_FORWARDED_BY', environ)
+ self.assertNotIn('HTTP_X_FORWARDED_FOR', environ)
+ self.assertNotIn('HTTP_X_FORWARDED_PROTO', environ)
+ self.assertNotIn('HTTP_X_FORWARDED_HOST', environ)
+
+ def test_parse_proxy_headers_forwarded_for(self):
+ inst = self._makeOne()
+
+ headers = {
+ 'X_FORWARDED_FOR': '192.0.2.1'
+ }
+ environ = {}
+ inst.parse_proxy_headers(
+ environ,
+ headers,
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'x-forwarded-for'}
+ )
+
+ self.assertEqual(environ['REMOTE_ADDR'], '192.0.2.1')
+
+ def test_parse_proxy_headers_forwarded_for_v6_missing_brackets(self):
+ inst = self._makeOne()
+
+ headers = {
+ 'X_FORWARDED_FOR': '2001:db8::0'
+ }
+ environ = {}
+ inst.parse_proxy_headers(
+ environ,
+ headers,
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'x-forwarded-for'}
+ )
+
+ self.assertEqual(environ['REMOTE_ADDR'], '[2001:db8::0]')
+
+ def test_parse_proxy_headers_forwared_for_multiple(self):
+ inst = self._makeOne()
+
+ headers = {
+ 'X_FORWARDED_FOR': '192.0.2.1, 198.51.100.2, 203.0.113.1'
+ }
+ environ = {}
+ inst.parse_proxy_headers(
+ environ,
+ headers,
+ trusted_proxy_count=2,
+ trusted_proxy_headers={'x-forwarded-for'}
+ )
+ self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
+
+ def test_parse_forwarded_multiple_proxies_trust_only_two(self):
+ inst = self._makeOne()
+
+ headers = {
+ 'FORWARDED': 'For=192.0.2.1;host=fake.com, For=198.51.100.2;host=example.com:8080, For=203.0.113.1'
+ }
+ environ = {}
+ inst.parse_proxy_headers(
+ environ,
+ headers,
+ trusted_proxy_count=2,
+ trusted_proxy_headers={'forwarded'}
+ )
+
+ self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
+ self.assertEqual(environ['SERVER_NAME'], 'example.com')
+ self.assertEqual(environ['HTTP_HOST'], 'example.com:8080')
+ self.assertEqual(environ['SERVER_PORT'], '8080')
+
+ def test_parse_forwarded_multiple_proxies(self):
+ inst = self._makeOne()
+
+ headers = {
+ 'FORWARDED': 'for="[2001:db8::1]";host="example.com:8443";proto="https", for=192.0.2.1;host="example.internal:8080"'
+ }
+ environ = {}
+ inst.parse_proxy_headers(
+ environ,
+ headers,
+ trusted_proxy_count=2,
+ trusted_proxy_headers={'forwarded'}
+ )
+
+ self.assertEqual(environ['REMOTE_ADDR'], '[2001:db8::1]')
+ self.assertEqual(environ['SERVER_NAME'], 'example.com')
+ self.assertEqual(environ['HTTP_HOST'], 'example.com:8443')
+ self.assertEqual(environ['SERVER_PORT'], '8443')
+ self.assertEqual(environ['wsgi.url_scheme'], 'https')
+
+ def test_parse_forwarded_multiple_proxies_minimal(self):
+ inst = self._makeOne()
+
+ headers = {
+ 'FORWARDED': 'for="[2001:db8::1]";proto="https", for=192.0.2.1;host="example.org"'
+ }
+ environ = {}
+ inst.parse_proxy_headers(
+ environ,
+ headers,
+ trusted_proxy_count=2,
+ trusted_proxy_headers={'forwarded'}
+ )
+
+ self.assertEqual(environ['REMOTE_ADDR'], '[2001:db8::1]')
+ self.assertEqual(environ['SERVER_NAME'], 'example.org')
+ self.assertEqual(environ['HTTP_HOST'], 'example.org')
+ self.assertEqual(environ['SERVER_PORT'], '443')
+ self.assertEqual(environ['wsgi.url_scheme'], 'https')
+
+ def test_parse_proxy_headers_forwarded_host_with_port(self):
+ inst = self._makeOne()
+
+ headers = {
+ 'X_FORWARDED_FOR': '192.0.2.1, 198.51.100.2, 203.0.113.1',
+ 'X_FORWARDED_PROTO': 'http',
+ 'X_FORWARDED_HOST': 'example.com:8080',
+ }
+ environ = {}
+ inst.parse_proxy_headers(
+ environ,
+ headers,
+ trusted_proxy_count=2,
+ trusted_proxy_headers={'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-host'}
+ )
+
+ self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
+ self.assertEqual(environ['SERVER_NAME'], 'example.com')
+ self.assertEqual(environ['HTTP_HOST'], 'example.com:8080')
+ self.assertEqual(environ['SERVER_PORT'], '8080')
+
+ def test_parse_proxy_headers_forwarded_host_without_port(self):
+ inst = self._makeOne()
+
+ headers = {
+ 'X_FORWARDED_FOR': '192.0.2.1, 198.51.100.2, 203.0.113.1',
+ 'X_FORWARDED_PROTO': 'http',
+ 'X_FORWARDED_HOST': 'example.com',
+ }
+ environ = {}
+ inst.parse_proxy_headers(
+ environ,
+ headers,
+ trusted_proxy_count=2,
+ trusted_proxy_headers={'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-host'}
+ )
+
+ self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
+ self.assertEqual(environ['SERVER_NAME'], 'example.com')
+ self.assertEqual(environ['HTTP_HOST'], 'example.com')
+ self.assertEqual(environ['SERVER_PORT'], '80')
+
+ def test_parse_proxy_headers_forwarded_host_with_forwarded_port(self):
+ inst = self._makeOne()
+
+ headers = {
+ 'X_FORWARDED_FOR': '192.0.2.1, 198.51.100.2, 203.0.113.1',
+ 'X_FORWARDED_PROTO': 'http',
+ 'X_FORWARDED_HOST': 'example.com',
+ 'X_FORWARDED_PORT': '8080'
+ }
+ environ = {}
+ inst.parse_proxy_headers(
+ environ,
+ headers,
+ trusted_proxy_count=2,
+ trusted_proxy_headers={'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-host', 'x-forwarded-port'}
+ )
+
+ self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
+ self.assertEqual(environ['SERVER_NAME'], 'example.com')
+ self.assertEqual(environ['HTTP_HOST'], 'example.com:8080')
+ self.assertEqual(environ['SERVER_PORT'], '8080')
+
+ def test_parse_proxy_headers_forwarded_host_multiple_with_forwarded_port(self):
+ inst = self._makeOne()
+
+ headers = {
+ 'X_FORWARDED_FOR': '192.0.2.1, 198.51.100.2, 203.0.113.1',
+ 'X_FORWARDED_PROTO': 'http',
+ 'X_FORWARDED_HOST': 'example.com, example.org',
+ 'X_FORWARDED_PORT': '8080'
+ }
+ environ = {}
+ inst.parse_proxy_headers(
+ environ,
+ headers,
+ trusted_proxy_count=2,
+ trusted_proxy_headers={'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-host', 'x-forwarded-port'}
+ )
+
+ self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
+ self.assertEqual(environ['SERVER_NAME'], 'example.com')
+ self.assertEqual(environ['HTTP_HOST'], 'example.com:8080')
+ self.assertEqual(environ['SERVER_PORT'], '8080')
+
+ def test_parse_proxy_headers_forwarded_host_multiple_with_forwarded_port_limit_one_trusted(self):
+ inst = self._makeOne()
+
+ headers = {
+ 'X_FORWARDED_FOR': '192.0.2.1, 198.51.100.2, 203.0.113.1',
+ 'X_FORWARDED_PROTO': 'http',
+ 'X_FORWARDED_HOST': 'example.com, example.org',
+ 'X_FORWARDED_PORT': '8080'
+ }
+ environ = {}
+ inst.parse_proxy_headers(
+ environ,
+ headers,
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-host', 'x-forwarded-port'}
+ )
+
+ self.assertEqual(environ['REMOTE_ADDR'], '203.0.113.1')
+ self.assertEqual(environ['SERVER_NAME'], 'example.org')
+ self.assertEqual(environ['HTTP_HOST'], 'example.org:8080')
+ self.assertEqual(environ['SERVER_PORT'], '8080')
+
+ def test_parse_forwarded(self):
+ inst = self._makeOne()
+
+ headers = {
+ 'FORWARDED': 'For=198.51.100.2:5858;host=example.com:8080;proto=https'
+ }
+ environ = {}
+ inst.parse_proxy_headers(
+ environ,
+ headers,
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'forwarded'}
+ )
+
+ self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
+ self.assertEqual(environ['REMOTE_PORT'], '5858')
+ self.assertEqual(environ['SERVER_NAME'], 'example.com')
+ self.assertEqual(environ['HTTP_HOST'], 'example.com:8080')
+ self.assertEqual(environ['SERVER_PORT'], '8080')
+ self.assertEqual(environ['wsgi.url_scheme'], 'https')
+
+ def test_parse_forwarded_empty_pair(self):
+ inst = self._makeOne()
+
+ headers = {
+ 'FORWARDED': 'For=198.51.100.2;;proto=https;by=_unused'
+ }
+ environ = {}
+ inst.parse_proxy_headers(
+ environ,
+ headers,
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'forwarded'}
+ )
+
+ self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
+
+ def test_parse_forwarded_pair_token_whitespace(self):
+ inst = self._makeOne()
+
+ headers = {
+ 'FORWARDED': 'For=198.51.100.2; proto =https'
+ }
+ environ = {}
+
+ with self.assertRaises(ValueError):
+ inst.parse_proxy_headers(
+ environ,
+ headers,
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'forwarded'}
+ )
+
+ def test_parse_forwarded_pair_value_whitespace(self):
+ inst = self._makeOne()
+
+ headers = {
+ 'FORWARDED': 'For= "198.51.100.2"; proto =https'
+ }
+ environ = {}
+
+ with self.assertRaises(ValueError):
+ inst.parse_proxy_headers(
+ environ,
+ headers,
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'forwarded'}
+ )
+
+ def test_parse_forwarded_pair_no_equals(self):
+ inst = self._makeOne()
+
+ headers = {
+ 'FORWARDED': 'For'
+ }
+ environ = {}
+
+ with self.assertRaises(ValueError):
+ inst.parse_proxy_headers(
+ environ,
+ headers,
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'forwarded'}
+ )
+
+ def test_parse_forwarded_warning_unknown_token(self):
+ inst = self._makeOne()
+ inst.logger = DummyLogger()
+
+ headers = {
+ 'FORWARDED': 'For=198.51.100.2;host=example.com:8080;proto=https;unknown="yolo"'
+ }
+ environ = {}
+ inst.parse_proxy_headers(
+ environ,
+ headers,
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'forwarded'}
+ )
+
+ self.assertEqual(len(inst.logger.logged), 1)
+ self.assertIn('Unknown Forwarded token', inst.logger.logged[0])
+
+ self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
+ self.assertEqual(environ['SERVER_NAME'], 'example.com')
+ self.assertEqual(environ['HTTP_HOST'], 'example.com:8080')
+ self.assertEqual(environ['SERVER_PORT'], '8080')
+ self.assertEqual(environ['wsgi.url_scheme'], 'https')
+
+ def test_parse_no_valid_proxy_headers(self):
+ inst = self._makeOne()
+ inst.logger = DummyLogger()
+
+ headers = {
+ 'X_FORWARDED_FOR': '198.51.100.2',
+ 'FORWARDED': 'For=198.51.100.2;host=example.com:8080;proto=https'
+ }
+ environ = {}
+ inst.parse_proxy_headers(
+ environ,
+ headers,
+ trusted_proxy_count=1,
+ )
+
+ self.assertEqual(environ, {})
+
+ def test_parse_multiple_x_forwarded_proto(self):
+ inst = self._makeOne()
+ inst.logger = DummyLogger()
+
+ headers = {
+ 'X_FORWARDED_PROTO': 'http, https',
+ }
+ environ = {}
+ inst.parse_proxy_headers(
+ environ,
+ headers,
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'x-forwarded-proto'}
+ )
+
+ self.assertEqual(environ, {})
+ self.assertEqual(len(inst.logger.logged), 1)
+ self.assertIn("Found multiple values in X-Forwarded-Proto", inst.logger.logged[0])
+
+ def test_parse_multiple_x_forwarded_port(self):
+ inst = self._makeOne()
+ inst.logger = DummyLogger()
+
+ headers = {
+ 'X_FORWARDED_PORT': '443, 80',
+ }
+ environ = {}
+ inst.parse_proxy_headers(
+ environ,
+ headers,
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'x-forwarded-port'}
+ )
+
+ self.assertEqual(environ, {})
+ self.assertEqual(len(inst.logger.logged), 1)
+ self.assertIn("Found multiple values in X-Forwarded-Port", inst.logger.logged[0])
+
+ def test_parse_forwarded_port_wrong_proto_port_80(self):
+ inst = self._makeOne()
+ inst.logger = DummyLogger()
+
+ headers = {
+ 'X_FORWARDED_PORT': '80',
+ 'X_FORWARDED_PROTO': 'https',
+ 'X_FORWARDED_HOST': 'example.com',
+ }
+ environ = {}
+ inst.parse_proxy_headers(
+ environ,
+ headers,
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'x-forwarded-port', 'x-forwarded-host', 'x-forwarded-proto'}
+ )
+
+ self.assertEqual(environ['SERVER_NAME'], 'example.com')
+ self.assertEqual(environ['HTTP_HOST'], 'example.com:80')
+ self.assertEqual(environ['SERVER_PORT'], '80')
+ self.assertEqual(environ['wsgi.url_scheme'], 'https')
+
+ def test_parse_forwarded_port_wrong_proto_port_443(self):
+ inst = self._makeOne()
+ inst.logger = DummyLogger()
+
+ headers = {
+ 'X_FORWARDED_PORT': '443',
+ 'X_FORWARDED_PROTO': 'http',
+ 'X_FORWARDED_HOST': 'example.com',
+ }
+ environ = {}
+ inst.parse_proxy_headers(
+ environ,
+ headers,
+ trusted_proxy_count=1,
+ trusted_proxy_headers={'x-forwarded-port', 'x-forwarded-host', 'x-forwarded-proto'}
+ )
+
+ self.assertEqual(environ['SERVER_NAME'], 'example.com')
+ self.assertEqual(environ['HTTP_HOST'], 'example.com:443')
+ self.assertEqual(environ['SERVER_PORT'], '443')
+ self.assertEqual(environ['wsgi.url_scheme'], 'http')
+
+
class TestErrorTask(unittest.TestCase):
def _makeOne(self, channel=None, request=None):
@@ -969,6 +1490,10 @@ class DummyAdj(object):
port = 80
url_prefix = ''
trusted_proxy = None
+ trusted_proxy_count = 1
+ trusted_proxy_headers = set()
+ log_untrusted_proxy_headers = True
+ clear_untrusted_proxy_headers = True
class DummyServer(object):
server_name = 'localhost'
@@ -981,7 +1506,7 @@ class DummyChannel(object):
closed_when_done = False
adj = DummyAdj()
creation_time = 0
- addr = ['127.0.0.1']
+ addr = ('127.0.0.1', 39830)
def __init__(self, server=None):
if server is None:
@@ -1020,8 +1545,8 @@ class DummyLogger(object):
def __init__(self):
self.logged = []
- def warning(self, msg):
- self.logged.append(msg)
+ def warning(self, msg, *args):
+ self.logged.append(msg % args)
def exception(self, msg):
self.logged.append(msg)
diff --git a/waitress/tests/test_utilities.py b/waitress/tests/test_utilities.py
index 95b39f3..f7691f0 100644
--- a/waitress/tests/test_utilities.py
+++ b/waitress/tests/test_utilities.py
@@ -99,3 +99,36 @@ class TestBadRequest(unittest.TestCase):
inst = self._makeOne()
self.assertEqual(inst.body, 1)
+class Test_undquote(unittest.TestCase):
+
+ def _callFUT(self, value):
+ from waitress.utilities import undquote
+ return undquote(value)
+
+ def test_empty(self):
+ self.assertEqual(self._callFUT(''), '')
+
+ def test_quoted(self):
+ self.assertEqual(self._callFUT('"test"'), 'test')
+
+ def test_unquoted(self):
+ self.assertEqual(self._callFUT('test'), 'test')
+
+ def test_quoted_backslash_quote(self):
+ self.assertEqual(self._callFUT('"\\""'), '"')
+
+ def test_quoted_htab(self):
+ self.assertEqual(self._callFUT("\"\t\""), "\t")
+
+ def test_quoted_backslash_htab(self):
+ self.assertEqual(self._callFUT("\"\\\t\""), "\t")
+
+ def test_quoted_backslash_invalid(self):
+ self.assertRaises(ValueError, self._callFUT, '"\\"')
+
+ def test_invalid_quoting(self):
+ self.assertRaises(ValueError, self._callFUT, '"test')
+
+ def test_invalid_quoting_single_quote(self):
+ self.assertRaises(ValueError, self._callFUT, '"')
+
diff --git a/waitress/utilities.py b/waitress/utilities.py
index 2c212a0..2d56e35 100644
--- a/waitress/utilities.py
+++ b/waitress/utilities.py
@@ -14,17 +14,27 @@
"""Utility functions
"""
+import calendar
import errno
import logging
import os
import re
import stat
import time
-import calendar
+from collections import namedtuple
logger = logging.getLogger('waitress')
queue_logger = logging.getLogger('waitress.queue')
+PROXY_HEADERS = frozenset({
+ 'X_FORWARDED_FOR',
+ 'X_FORWARDED_HOST',
+ 'X_FORWARDED_PROTO',
+ 'X_FORWARDED_PORT',
+ 'X_FORWARDED_BY',
+ 'FORWARDED',
+})
+
def find_double_newline(s):
"""Returns the position just after a double newline in the given string."""
pos1 = s.find(b'\n\r\n') # One kind of double newline
@@ -204,3 +214,70 @@ class RequestEntityTooLarge(BadRequest):
class InternalServerError(Error):
code = 500
reason = 'Internal Server Error'
+
+
+# RFC 5234 Appendix B.1 "Core Rules":
+# VCHAR = %x21-7E
+# ; visible (printing) characters
+vchar_re = '\x21-\x7e'
+
+# RFC 7230 Section 3.2.6 "Field Value Components":
+# quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
+# qdtext = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text
+# obs-text = %x80-FF
+# quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
+obs_text_re = '\x80-\xff'
+
+# The '\\' between \x5b and \x5d is needed to escape \x5d (']')
+qdtext_re = '[\t \x21\x23-\x5b\\\x5d-\x7e' + obs_text_re + ']'
+
+quoted_pair_re = r'\\' + '([\t ' + vchar_re + obs_text_re + '])'
+quoted_string_re = \
+ '"(?:(?:' + qdtext_re + ')|(?:' + quoted_pair_re + '))*"'
+
+quoted_string = re.compile(quoted_string_re)
+quoted_pair = re.compile(quoted_pair_re)
+
+
+def undquote(value):
+ if value.startswith('"') and value.endswith('"'):
+ # So it claims to be DQUOTE'ed, let's validate that
+ matches = quoted_string.match(value)
+
+ if matches and matches.end() == len(value):
+ # Remove the DQUOTE's from the value
+ value = value[1:-1]
+
+ # Remove all backslashes that are followed by a valid vchar or
+ # obs-text
+ value = quoted_pair.sub(r'\1', value)
+
+ return value
+ elif not value.startswith('"') and not value.endswith('"'):
+ return value
+
+ raise ValueError('Invalid quoting in value')
+
+
+Forwarded = namedtuple('Forwarded', ['by', 'for_', 'host', 'proto'])
+
+
+def clear_untrusted_headers(
+ headers, untrusted_headers, log_warning=False, logger=logger
+):
+ untrusted_headers_removed = [
+ header
+ for header in untrusted_headers
+ if headers.pop(header, False) is not False
+ ]
+
+ if any(untrusted_headers_removed) and log_warning:
+ untrusted_headers_removed = [
+ "-".join([x.capitalize() for x in header.split("_")])
+ for header in untrusted_headers_removed
+ ]
+ logger.warning(
+ "Removed untrusted headers (%s). Waitress recommends these be "
+ "removed upstream.",
+ ", ".join(untrusted_headers_removed),
+ )