View
@@ -1,312 +1,32 @@
.. _index:
========
Waitress
--------
========
Waitress is meant to be a production-quality pure-Python WSGI server with
very acceptable performance. It has no dependencies except ones which live
in the Python standard library. It runs on CPython on Unix and Windows under
Python 2.7+ and Python 3.3+. It is also known to run on PyPy 1.6.0 on UNIX.
Python 2.7+ and Python 3.4+. It is also known to run on PyPy 1.6.0 on UNIX.
It supports HTTP/1.0 and HTTP/1.1.
Usage
-----
Here's normal usage of the server:
.. code-block:: python
from waitress import serve
serve(wsgiapp, listen='*:8080')
This will run waitress on port 8080 on all available IP addresses, both IPv4
and IPv6.
.. code-block:: python
from waitress import serve
serve(wsgiapp, host='0.0.0.0', port=8080)
This will run waitress on port 8080 on all available IPv4 addresses.
If you want to serve your application on all IP addresses, on port 8080, you
can omit the ``host`` and ``port`` arguments and just call ``serve`` with the
WSGI app as a single argument:
.. code-block:: python
from waitress import serve
serve(wsgiapp)
Press Ctrl-C (or Ctrl-Break on Windows) to exit the server.
The default is to bind to any IPv4 address on port 8080:
.. code-block:: python
from waitress import serve
serve(wsgiapp)
If you want to serve your application through a UNIX domain socket (to serve
a downstream HTTP server/proxy, e.g. nginx, lighttpd, etc.), call ``serve``
with the ``unix_socket`` argument:
.. code-block:: python
from waitress import serve
serve(wsgiapp, unix_socket='/path/to/unix.sock')
Needless to say, this configuration won't work on Windows.
Exceptions generated by your application will be shown on the console by
default. See :ref:`logging` to change this.
There's an entry point for :term:`PasteDeploy` (``egg:waitress#main``) that
lets you use Waitress's WSGI gateway from a configuration file, e.g.:
.. code-block:: ini
[server:main]
use = egg:waitress#main
listen = 127.0.0.1:8080
Using ``host`` and ``port`` is also supported:
.. code-block:: ini
[server:main]
host = 127.0.0.1
port = 8080
The :term:`PasteDeploy` syntax for UNIX domain sockets is analagous:
.. code-block:: ini
[server:main]
use = egg:waitress#main
unix_socket = /path/to/unix.sock
You can find more settings to tweak (arguments to ``waitress.serve`` or
equivalent settings in PasteDeploy) in :ref:`arguments`.
Additionally, there is a command line runner called ``waitress-serve``, which
can be used in development and in situations where the likes of
:term:`PasteDeploy` is not necessary:
.. code-block:: bash
# Listen on both IPv4 and IPv6 on port 8041
waitress-serve --listen=*:8041 myapp:wsgifunc
# Listen on only IPv4 on port 8041
waitress-serve --port=8041 myapp:wsgifunc
For more information on this, see :ref:`runner`.
.. _logging:
Logging
-------
``waitress.serve`` calls ``logging.basicConfig()`` to set up logging to the
console when the server starts up. Assuming no other logging configuration
has already been done, this sets the logging default level to
``logging.WARNING``. The Waitress logger will inherit the root logger's
level information (it logs at level ``WARNING`` or above).
Waitress sends its logging output (including application exception
renderings) to the Python logger object named ``waitress``. You can
influence the logger level and output stream using the normal Python
``logging`` module API. For example:
.. code-block:: python
import logging
logger = logging.getLogger('waitress')
logger.setLevel(logging.INFO)
Within a PasteDeploy configuration file, you can use the normal Python
``logging`` module ``.ini`` file format to change similar Waitress logging
options. For example:
.. code-block:: ini
[logger_waitress]
level = INFO
Using Behind a Reverse Proxy
----------------------------
Often people will set up "pure Python" web servers behind reverse proxies,
especially if they need SSL support (Waitress does not natively support SSL).
Even if you don't need SSL support, it's not uncommon to see Waitress and
other pure-Python web servers set up to "live" behind a reverse proxy; these
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.
For example, when using Nginx as a reverse proxy, you might add the following
lines in a ``location`` section::
proxy_set_header Host $host;
The Apache directive named ``ProxyPreserveHost`` does something similar when
used as a reverse proxy.
Unfortunately, even if you pass the ``Host`` header, the Host header does not
contain enough information to regenerate the original URL sent by the client.
For example, if your reverse proxy accepts HTTPS requests (and therefore URLs
which start with ``https://``), the URLs generated by your application when
used behind a reverse proxy served by Waitress might inappropriately be
``http://foo`` rather than ``https://foo``. To fix this, you'll want to
change the ``wsgi.url_scheme`` in the WSGI environment before it reaches your
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.
Using ``url_scheme`` to set ``wsgi.url_scheme``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can have the Waitress server use the ``https`` url scheme by default.:
.. code-block:: python
from waitress import serve
serve(wsgiapp, listen='0.0.0.0:8080', url_scheme='https')
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``
-------------------------------------------------------------------
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::
proxy_set_header X-Forwarded-Proto $scheme;
or via Apache::
RequestHeader set X-Forwarded-Proto https
.. 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.
Using ``url_prefix`` to influence ``SCRIPT_NAME`` and ``PATH_INFO``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can have the Waitress server use a particular url prefix by default for all
URLs generated by downstream applications that take ``SCRIPT_NAME`` into
account.:
.. code-block:: python
from waitress import serve
serve(wsgiapp, listen='0.0.0.0:8080', url_prefix='/foo')
Setting this to any value except the empty string will cause the WSGI
``SCRIPT_NAME`` value to be that value, 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. 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::
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::
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.
Extended Documentation
----------------------
.. toctree::
:maxdepth: 1
design.rst
differences.rst
api.rst
arguments.rst
filewrapper.rst
runner.rst
glossary.rst
usage
logging
reverse-proxy
design
differences
api
arguments
filewrapper
runner
glossary
Change History
--------------
@@ -317,33 +37,31 @@ Change History
Known Issues
------------
- Does not support SSL natively.
- Does not support TLS natively. See :ref:`using-behind-a-reverse-proxy` for more information.
Support and Development
-----------------------
The `Pylons Project web site <http://pylonsproject.org/>`_ is the main online
The `Pylons Project web site <https://pylonsproject.org/>`_ is the main online
source of Waitress support and development information.
To report bugs, use the `issue tracker
<http://github.com/Pylons/waitress/issues>`_.
<https://github.com/Pylons/waitress/issues>`_.
If you've got questions that aren't answered by this documentation,
contact the `Pylons-devel maillist
<http://groups.google.com/group/pylons-devel>`_ or join the `#pyramid
IRC channel <irc://irc.freenode.net/#pyramid>`_.
contact the `Pylons-discuss maillist
<https://groups.google.com/forum/#!forum/pylons-discuss>`_ or join the `#pyramid
IRC channel <https://webchat.freenode.net/?channels=pyramid>`_.
Browse and check out tagged and trunk versions of Waitress via
the `Waitress GitHub repository <http://github.com/Pylons/waitress/>`_.
the `Waitress GitHub repository <https://github.com/Pylons/waitress/>`_.
To check out the trunk via ``git``, use this command:
.. code-block:: text
git clone git@github.com:Pylons/waitress.git
To find out how to become a contributor to Waitress, please see the
`contributor's section of the documentation
<http://docs.pylonsproject.org/index.html#contributing>`_.
To find out how to become a contributor to Waitress, please see the guidelines in `contributing.md <https://github.com/Pylons/waitress/blob/master/contributing.md>`_ and `How to Contribute Source Code and Documentation <https://pylonsproject.org/community-how-to-contribute.html>`_.
Why?
----
@@ -373,7 +91,7 @@ framework distribution simply for its server component is awkward. The test
suite of the CherryPy server also depends on the CherryPy web framework, so
even if we forked its server component into a separate distribution, we would
have still needed to backfill for all of its tests. The CherryPy team has
started work on `Cheroot <https://bitbucket.org/cherrypy/cheroot>`_, which
started work on `Cheroot <https://bitbucket.org/cherrypy/cheroot/src/default/>`_, which
should solve this problem, however.
Waitress is a fork of the WSGI-related components which existed in
View
@@ -0,0 +1,190 @@
.. _access-logging:
==============
Access Logging
==============
The WSGI design is modular. Waitress logs error conditions, debugging
output, etc., but not web traffic. For web traffic logging, Paste
provides `TransLogger
<https://web.archive.org/web/20160707041338/http://pythonpaste.org/modules/translogger.html>`_
:term:`middleware`. TransLogger produces logs in the `Apache Combined
Log Format <https://httpd.apache.org/docs/current/logs.html#combined>`_.
.. _logging-to-the-console-using-python:
Logging to the Console Using Python
-----------------------------------
``waitress.serve`` calls ``logging.basicConfig()`` to set up logging to the
console when the server starts up. Assuming no other logging configuration
has already been done, this sets the logging default level to
``logging.WARNING``. The Waitress logger will inherit the root logger's
level information (it logs at level ``WARNING`` or above).
Waitress sends its logging output (including application exception
renderings) to the Python logger object named ``waitress``. You can
influence the logger level and output stream using the normal Python
``logging`` module API. For example:
.. code-block:: python
import logging
logger = logging.getLogger('waitress')
logger.setLevel(logging.INFO)
Within a PasteDeploy configuration file, you can use the normal Python
``logging`` module ``.ini`` file format to change similar Waitress logging
options. For example:
.. code-block:: ini
[logger_waitress]
level = INFO
.. _logging-to-the-console-using-pastedeploy:
Logging to the Console Using PasteDeploy
----------------------------------------
TransLogger will automatically setup a logging handler to the console when called with no arguments.
It "just works" in environments that don't configure logging.
This is by virtue of its default configuration setting of ``setup_console_handler = True``.
.. TODO:
.. .. _logging-to-a-file-using-python:
.. Logging to a File Using Python
.. ------------------------------
.. Show how to configure the WSGI logger via python.
.. _logging-to-a-file-using-pastedeploy:
Logging to a File Using PasteDeploy
------------------------------------
TransLogger does not write to files, and the Python logging system
must be configured to do this. The Python class :class:`FileHandler`
logging handler can be used alongside TransLogger to create an
``access.log`` file similar to Apache's.
Like any standard :term:`middleware` with a Paste entry point,
TransLogger can be configured to wrap your application using ``.ini``
file syntax. First add a
``[filter:translogger]`` section, then use a ``[pipeline:main]``
section file to form a WSGI pipeline with both the translogger and
your application in it. For instance, if you have this:
.. code-block:: ini
[app:wsgiapp]
use = egg:mypackage#wsgiapp
[server:main]
use = egg:waitress#main
host = 127.0.0.1
port = 8080
Add this:
.. code-block:: ini
[filter:translogger]
use = egg:Paste#translogger
setup_console_handler = False
[pipeline:main]
pipeline = translogger
wsgiapp
Using PasteDeploy this way to form and serve a pipeline is equivalent to
wrapping your app in a TransLogger instance via the bottom of the ``main``
function of your project's ``__init__`` file:
.. code-block:: python
from mypackage import wsgiapp
from waitress import serve
from paste.translogger import TransLogger
serve(TransLogger(wsgiapp, setup_console_handler=False))
.. note::
TransLogger will automatically set up a logging handler to the console when
called with no arguments, so it "just works" in environments that don't
configure logging. Since our logging handlers are configured, we disable
the automation via ``setup_console_handler = False``.
With the filter in place, TransLogger's logger (named the ``wsgi`` logger) will
propagate its log messages to the parent logger (the root logger), sending
its output to the console when we request a page:
.. code-block:: text
00:50:53,694 INFO [wsgiapp] Returning: Hello World!
(content-type: text/plain)
00:50:53,695 INFO [wsgi] 192.168.1.111 - - [11/Aug/2011:20:09:33 -0700] "GET /hello
HTTP/1.1" 404 - "-"
"Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.6) Gecko/20070725
Firefox/2.0.0.6"
To direct TransLogger to an ``access.log`` FileHandler, we need the
following to add a FileHandler (named ``accesslog``) to the list of
handlers, and ensure that the ``wsgi`` logger is configured and uses
this handler accordingly:
.. code-block:: ini
# Begin logging configuration
[loggers]
keys = root, wsgiapp, wsgi
[handlers]
keys = console, accesslog
[logger_wsgi]
level = INFO
handlers = accesslog
qualname = wsgi
propagate = 0
[handler_accesslog]
class = FileHandler
args = ('%(here)s/access.log','a')
level = INFO
formatter = generic
As mentioned above, non-root loggers by default propagate their log records
to the root logger's handlers (currently the console handler). Setting
``propagate`` to ``0`` (``False``) here disables this; so the ``wsgi`` logger
directs its records only to the ``accesslog`` handler.
Finally, there's no need to use the ``generic`` formatter with
TransLogger, as TransLogger itself provides all the information we
need. We'll use a formatter that passes-through the log messages as
is. Add a new formatter called ``accesslog`` by including the
following in your configuration file:
.. code-block:: ini
[formatters]
keys = generic, accesslog
[formatter_accesslog]
format = %(message)s
Finally alter the existing configuration to wire this new
``accesslog`` formatter into the FileHandler:
.. code-block:: ini
[handler_accesslog]
class = FileHandler
args = ('%(here)s/access.log','a')
level = INFO
formatter = accesslog
View
@@ -0,0 +1,174 @@
..index:: reverse, proxy, TLS, SSL, https
.. _using-behind-a-reverse-proxy:
============================
Using Behind a Reverse Proxy
============================
Often people will set up "pure Python" web servers behind reverse proxies,
especially if they need TLS support (Waitress does not natively support TLS).
Even if you don't need TLS support, it's not uncommon to see Waitress and
other pure-Python web servers set up to "live" behind a reverse proxy; these
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.
For example, when using nginx as a reverse proxy, you might add the following
lines in a ``location`` section.
.. code-block:: nginx
proxy_set_header Host $host;
The Apache directive named ``ProxyPreserveHost`` does something similar when
used as a reverse proxy.
Unfortunately, even if you pass the ``Host`` header, the Host header does not
contain enough information to regenerate the original URL sent by the client.
For example, if your reverse proxy accepts HTTPS requests (and therefore URLs
which start with ``https://``), the URLs generated by your application when
used behind a reverse proxy served by Waitress might inappropriately be
``http://foo`` rather than ``https://foo``. To fix this, you'll want to
change the ``wsgi.url_scheme`` in the WSGI environment before it reaches your
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.
Using ``url_scheme`` to set ``wsgi.url_scheme``
-----------------------------------------------
You can have the Waitress server use the ``https`` url scheme by default.:
.. code-block:: python
from waitress import serve
serve(wsgiapp, listen='0.0.0.0:8080', url_scheme='https')
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``
-------------------------------------------------------------------
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::
proxy_set_header X-Forwarded-Proto $scheme;
or via Apache::
RequestHeader set X-Forwarded-Proto https
.. 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.
Using ``url_prefix`` to influence ``SCRIPT_NAME`` and ``PATH_INFO``
-------------------------------------------------------------------
You can have the Waitress server use a particular url prefix by default for all
URLs generated by downstream applications that take ``SCRIPT_NAME`` into
account.:
.. code-block:: python
from waitress import serve
serve(wsgiapp, listen='0.0.0.0:8080', url_prefix='/foo')
Setting this to any value except the empty string will cause the WSGI
``SCRIPT_NAME`` value to be that value, 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. 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.
View
@@ -3,14 +3,10 @@
waitress-serve
--------------
Waitress comes bundled with a thin command-line wrapper around the
``waitress.serve`` function called ``waitress-serve``. This is useful for
development, and in production situations where serving of static assets is
delegated to a reverse proxy, such as Nginx or Apache.
.. versionadded:: 0.8.4
.. note::
This feature is new as of Waitress 0.8.4.
Waitress comes bundled with a thin command-line wrapper around the ``waitress.serve`` function called ``waitress-serve``.
This is useful for development, and in production situations where serving of static assets is delegated to a reverse proxy, such as nginx or Apache.
``waitress-serve`` takes the very same :ref:`arguments <arguments>` as the
``waitress.serve`` function, but where the function's arguments have
View
@@ -0,0 +1,83 @@
.. _usage:
=====
Usage
=====
The following code will run waitress on port 8080 on all available IP addresses, both IPv4 and IPv6.
.. code-block:: python
from waitress import serve
serve(wsgiapp, listen='*:8080')
Press :kbd:`Ctrl-C` (or :kbd:`Ctrl-Break` on Windows) to exit the server.
The following will run waitress on port 8080 on all available IPv4 addresses, but not IPv6.
.. code-block:: python
from waitress import serve
serve(wsgiapp, host='0.0.0.0', port=8080)
By default Waitress binds to any IPv4 address on port 8080.
You can omit the ``host`` and ``port`` arguments and just call ``serve`` with the WSGI app as a single argument:
.. code-block:: python
from waitress import serve
serve(wsgiapp)
If you want to serve your application through a UNIX domain socket (to serve a downstream HTTP server/proxy such as nginx, lighttpd, and so on), call ``serve`` with the ``unix_socket`` argument:
.. code-block:: python
from waitress import serve
serve(wsgiapp, unix_socket='/path/to/unix.sock')
Needless to say, this configuration won't work on Windows.
Exceptions generated by your application will be shown on the console by
default. See :ref:`access-logging` to change this.
There's an entry point for :term:`PasteDeploy` (``egg:waitress#main``) that
lets you use Waitress's WSGI gateway from a configuration file, e.g.:
.. code-block:: ini
[server:main]
use = egg:waitress#main
listen = 127.0.0.1:8080
Using ``host`` and ``port`` is also supported:
.. code-block:: ini
[server:main]
host = 127.0.0.1
port = 8080
The :term:`PasteDeploy` syntax for UNIX domain sockets is analagous:
.. code-block:: ini
[server:main]
use = egg:waitress#main
unix_socket = /path/to/unix.sock
You can find more settings to tweak (arguments to ``waitress.serve`` or
equivalent settings in PasteDeploy) in :ref:`arguments`.
Additionally, there is a command line runner called ``waitress-serve``, which
can be used in development and in situations where the likes of
:term:`PasteDeploy` is not necessary:
.. code-block:: bash
# Listen on both IPv4 and IPv6 on port 8041
waitress-serve --listen=*:8041 myapp:wsgifunc
# Listen on only IPv4 on port 8041
waitress-serve --port=8041 myapp:wsgifunc
For more information on this, see :ref:`runner`.
View
@@ -87,6 +87,8 @@ class Adjustments(object):
('listen', aslist),
('threads', int),
('trusted_proxy', str),
('trusted_proxy_count', int),
('trusted_proxy_forwarded', asbool),
('url_scheme', str),
('url_prefix', slash_fixed_str),
('backlog', int),
@@ -124,6 +126,24 @@ 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 can get set to a higher number
trusted_proxy_count = 1
# Should we use/trust the Forwarded header instead of the older
# X-Forwarded-{For,By,Host,Proto} headers
trusted_proxy_forwarded = False
# default ``wsgi.url_scheme`` value
url_scheme = 'http'
View
@@ -274,7 +274,7 @@ def get_header_lines(header):
for line in lines:
if line.startswith((b' ', b'\t')):
if not r:
# http://corte.si/posts/code/pathod/pythonservers/index.html
# https://corte.si/posts/code/pathod/pythonservers/index.html
raise ParsingError('Malformed header line "%s"' % tostr(line))
r[-1] += line
else:
View
@@ -137,7 +137,8 @@ def close(self):
class BaseWSGIServer(wasyncore.dispatcher, object):
# By default we don't trust any of the proxy headers sent by a client
trusted_proxy = False
channel_class = HTTPChannel
next_channel_cleanup = 0
socketmod = socket # test shim
@@ -338,6 +339,8 @@ def set_socket_options(self, conn):
if hasattr(socket, 'AF_UNIX'):
class UnixWSGIServer(BaseWSGIServer):
# We can implicitly trust proxy headers received over a Unix socket.
trusted_proxy = True
def __init__(self,
application,
View
@@ -17,19 +17,14 @@
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,
build_http_date,
logger,
queue_logger,
undquote,
)
rename_headers = { # or keep them without the HTTP_ prefix added
@@ -48,6 +43,16 @@
'upgrade'
))
PROXY_HEADERS = frozenset({
'X_FORWARDED_FOR',
'X_FORWARDED_HOST',
'X_FORWARDED_PROTO',
'X_FORWARDED_PORT',
'X_FORWARDED_BY',
'FORWARDED',
})
class JustTesting(Exception):
pass
@@ -508,6 +513,193 @@ def start_response(status, headers, exc_info=None):
if hasattr(app_iter, 'close'):
app_iter.close()
def parse_proxy_headers(
self,
environ,
headers,
trusted_proxy_count=1,
trusted_proxy_forwarded=False
):
forwarded_for = None
client_addr = None
forwarded_for = None
if '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]
forwarded_host = undquote(headers.get('X_FORWARDED_HOST', ''))
forwarded_proto = undquote(headers.get('X_FORWARDED_PROTO', ''))
forwarded_port = undquote(headers.get('X_FORWARDED_PORT', ''))
forwarded = headers.get('FORWARDED', None)
# If the Forwarded header exists, it gets priority if the setting is
# turned on, and will warn if the other headers were not None and
# discard them.
if forwarded and trusted_proxy_forwarded:
if (
forwarded_for or
forwarded_host or
forwarded_proto or
forwarded_port
):
self.logger.warning(
'The Forwarded header was found to exist alongside '
'one or more of the older X-Forwarded-Host, '
'X-Forwarded-Proto, X-Forwarded-For, '
'X-Forwarded-Port, X-Forwarded-By headers. Waitress will '
'ignore the older style headers, but this could be a '
'security issue. Please make sure to remove the invalid '
'headers before passing the request to Waitress.')
# Remove all other proxy headers other than Forwarded
for header in PROXY_HEADERS - {'FORWARDED'}:
headers.pop(header, None)
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 = None
forwarded_port = forwarded_by = None
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
elif forwarded:
self.logger.warning(
'The Forwarded header was present, but is not considered '
'trusted. Waitress will remove the Forwarded header value.')
headers.pop('FORWARDED')
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 and forwarded_port not in {'443', '80'}:
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()
def get_environment(self):
"""Returns a WSGI environment."""
environ = self.environ
@@ -551,16 +743,28 @@ def get_environment(self):
environ['SCRIPT_NAME'] = url_prefix
environ['PATH_INFO'] = path
environ['QUERY_STRING'] = request.query
host = environ['REMOTE_ADDR'] = channel.addr[0]
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)
if remote_peer == server.adj.trusted_proxy or server.trusted_proxy:
self.parse_proxy_headers(
environ,
headers,
trusted_proxy_count=server.adj.trusted_proxy_count,
trusted_proxy_forwarded=server.adj.trusted_proxy_forwarded
)
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']
for key, value in headers.items():
value = value.strip()
mykey = rename_headers.get(key, None)
@@ -571,14 +775,19 @@ def get_environment(self):
# 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
# May have already been set by the proxy
if 'wsgi.url_scheme' not in environ:
environ['wsgi.url_scheme'] = request.url_scheme
# apps should use the logging module
environ['wsgi.errors'] = sys.stderr
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
environ['wsgi.input_terminated'] = True # wsgi.input is EOF terminated
self.environ = environ
return environ
View
@@ -653,6 +653,7 @@ class DummyAdjustments(object):
max_request_header_size = 10000
class DummyServer(object):
trusted_proxy = False
trigger_pulled = False
adj = DummyAdjustments()
View
@@ -155,7 +155,7 @@ def test_date_and_server(self):
self.assertTrue(headers.get('date'))
def test_bad_host_header(self):
# http://corte.si/posts/code/pathod/pythonservers/index.html
# https://corte.si/posts/code/pathod/pythonservers/index.html
to_send = ("GET / HTTP/1.0\n"
" Host: 0\n\n")
to_send = tobytes(to_send)
View
@@ -286,7 +286,7 @@ def test_get_header_lines_tabbed(self):
self.assertEqual(result, [b'slam\tslim'])
def test_get_header_lines_malformed(self):
# http://corte.si/posts/code/pathod/pythonservers/index.html
# https://corte.si/posts/code/pathod/pythonservers/index.html
from waitress.parser import ParsingError
self.assertRaises(ParsingError,
self._callFUT, b' Host: localhost\r\n\r\n')
View
@@ -752,11 +752,13 @@ def test_get_environment_values(self):
# 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 @@ def test_get_environment_values(self):
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,7 +803,7 @@ def test_get_environment_values_w_scheme_override_untrusted(self):
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'
request = DummyParser()
request.headers = {
@@ -816,14 +820,16 @@ def test_get_environment_values_w_scheme_override_trusted(self):
# 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')
@@ -861,6 +867,289 @@ def test_get_environment_values_w_bogus_scheme_override(self):
inst.request = request
self.assertRaises(ValueError, inst.get_environment)
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_forwarded=False
)
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_forwarded=False
)
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
)
self.assertEqual(environ['REMOTE_ADDR'], '198.51.100.2')
def test_parse_forwarded_multiple_proxies(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_forwarded=True
)
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_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_forwarded=False,
)
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_forwarded=False,
)
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_forwarded=False,
)
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(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_forwarded=True
)
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_warning_other_proxy_headers(self):
inst = self._makeOne()
inst.logger = DummyLogger()
headers = {
'X_FORWARDED_FOR': '[2001:db8::1]',
'FORWARDED': 'For=198.51.100.2;host=example.com:8080;proto=https'
}
environ = {}
inst.parse_proxy_headers(
environ,
headers,
trusted_proxy_count=1,
trusted_proxy_forwarded=True
)
self.assertEqual(len(inst.logger.logged), 1)
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_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_forwarded=True
)
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_forwarded=True
)
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_forwarded=True
)
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_forwarded=True
)
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_forwarded=True
)
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_forwarded_warning_forwarded_exists(self):
inst = self._makeOne()
inst.logger = DummyLogger()
headers = {
'FORWARDED': 'For=198.51.100.2'
}
environ = {}
inst.parse_proxy_headers(
environ,
headers,
trusted_proxy_count=1,
trusted_proxy_forwarded=False
)
self.assertEqual(len(inst.logger.logged), 1)
self.assertIn('Forwarded header was present', inst.logger.logged[0])
self.assertNotIn('FORWARDED', headers)
class TestErrorTask(unittest.TestCase):
def _makeOne(self, channel=None, request=None):
@@ -969,8 +1258,11 @@ class DummyAdj(object):
port = 80
url_prefix = ''
trusted_proxy = None
trusted_proxy_count = 1
trusted_proxy_forwarded = False
class DummyServer(object):
trusted_proxy = False
server_name = 'localhost'
effective_port = 80
@@ -981,7 +1273,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:
View
@@ -99,3 +99,36 @@ def test_it(self):
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, '"')
View
@@ -14,13 +14,14 @@
"""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')
@@ -204,3 +205,49 @@ 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'])