summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Shadura <andrewsh@debian.org>2016-12-13 14:33:38 +0100
committerAndrew Shadura <andrewsh@debian.org>2016-12-13 14:33:38 +0100
commit1fee41512c00408eca2d513f8080eb6be460d7a8 (patch)
tree0e3de338bcd7d7ed9aafac75f67c1d7426692ee3
parent81df6498f4bf84e72e577ec609b71ec611c974ba (diff)
parent9c9b36e9c28964142ddd6b9dd0dfb415351db4aa (diff)
Merge tag 'upstream/1.0.1'
Upstream version 1.0.1
-rw-r--r--.gitignore3
-rw-r--r--.travis.yml28
-rw-r--r--CHANGES.txt395
-rw-r--r--CONTRIBUTORS.txt9
-rw-r--r--HISTORY.txt422
-rw-r--r--README.rst2
-rw-r--r--appveyor.yml14
-rw-r--r--debian/patches/01-fix-sphinxdoc-conf.patch8
-rw-r--r--docs/api.rst2
-rw-r--r--docs/arguments.rst28
-rw-r--r--docs/conf.py7
-rw-r--r--docs/index.rst45
-rw-r--r--docs/runner.rst25
-rw-r--r--rtd.txt2
-rw-r--r--setup.cfg3
-rw-r--r--setup.py12
-rw-r--r--tox.ini67
-rw-r--r--waitress/__init__.py3
-rw-r--r--waitress/adjustments.py134
-rw-r--r--waitress/buffers.py2
-rw-r--r--waitress/channel.py2
-rw-r--r--waitress/compat.py29
-rw-r--r--waitress/parser.py2
-rw-r--r--waitress/runner.py37
-rw-r--r--waitress/server.py161
-rw-r--r--waitress/task.py11
-rw-r--r--waitress/tests/test_adjustments.py144
-rw-r--r--waitress/tests/test_buffers.py2
-rw-r--r--waitress/tests/test_functional.py4
-rw-r--r--waitress/tests/test_parser.py17
-rw-r--r--waitress/tests/test_server.py36
-rw-r--r--waitress/tests/test_task.py21
32 files changed, 1228 insertions, 449 deletions
diff --git a/.gitignore b/.gitignore
index 687d1f2..e1dbc2a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,4 +8,7 @@ nosetests.xml
waitress/coverage.xml
dist/
keep/
+build/
coverage.xml
+nosetests*.xml
+py*-cover.xml
diff --git a/.travis.yml b/.travis.yml
index cb98fdd..7c837bd 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,15 +2,22 @@
language: python
sudo: false
-env:
- - TOXENV=py26
- - TOXENV=py27
- - TOXENV=py32
- - TOXENV=py33
- - TOXENV=py34
- - TOXENV=pypy
- - TOXENV=pypy3
- - TOXENV=cover
+matrix:
+ include:
+ - python: 2.7
+ env: TOXENV=py27
+ - python: 3.3
+ env: TOXENV=py33
+ - python: 3.4
+ env: TOXENV=py34
+ - python: 3.5
+ env: TOXENV=py35
+ - python: pypy
+ env: TOXENV=pypy
+ - python: pypy3
+ env: TOXENV=pypy3
+ - python: 3.5
+ env: TOXENV=py2-cover,py3-cover,coverage
install:
- travis_retry pip install tox
@@ -21,3 +28,6 @@ script:
notifications:
email:
- pyramid-checkins@lists.repoze.org
+ irc:
+ channels:
+ - "chat.freenode.net#pyramid"
diff --git a/CHANGES.txt b/CHANGES.txt
index 485cbab..8ccb0bf 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,385 +1,56 @@
-0.8.10 (2015-08-02)
--------------------
-
-- Add support for Python 3.4, 3.5b2, and PyPy3.
-
-- Use a nonglobal asyncore socket map by default, trying to prevent conflicts
- with apps and libs that use the asyncore global socket map ala
- https://github.com/Pylons/waitress/issues/63. You can get the old
- use-global-socket-map behavior back by passing ``asyncore.socket_map`` to the
- ``create_server`` function as the ``map`` argument.
-
-- Waitress violated PEP 3333 with respect to reraising an exception when
- ``start_response`` was called with an ``exc_info`` argument. It would
- reraise the exception even if no data had been sent to the client. It now
- only reraises the exception if data has actually been sent to the client.
- See https://github.com/Pylons/waitress/pull/52 and
- https://github.com/Pylons/waitress/issues/51
-
-- Add a ``docs`` section to tox.ini that, when run, ensures docs can be built.
-
-- If an ``application`` value of ``None`` is supplied to the ``create_server``
- constructor function, a ValueError is now raised eagerly instead of an error
- occuring during runtime. See https://github.com/Pylons/waitress/pull/60
-
-- Fix parsing of multi-line (folded) headers.
- See https://github.com/Pylons/waitress/issues/53 and
- https://github.com/Pylons/waitress/pull/90
-
-- Switch from the low level Python thread/_thread module to the threading
- module.
-
-- Improved exception information should module import go awry.
-
-0.8.9 (2014-05-16)
-------------------
-
-- Fix tests under Windows. NB: to run tests under Windows, you cannot run
- "setup.py test" or "setup.py nosetests". Instead you must run ``python.exe
- -c "import nose; nose.main()"``. If you try to run the tests using the
- normal method under Windows, each subprocess created by the test suite will
- attempt to run the test suite again. See
- https://github.com/nose-devs/nose/issues/407 for more information.
-
-- Give the WSGI app_iter generated when ``wsgi.file_wrapper`` is used
- (ReadOnlyFileBasedBuffer) a ``close`` method. Do not call ``close`` on an
- instance of such a class when it's used as a WSGI app_iter, however. This is
- part of a fix which prevents a leakage of file descriptors; the other part of
- the fix was in WebOb
- (https://github.com/Pylons/webob/commit/951a41ce57bd853947f842028bccb500bd5237da).
-
-- Allow trusted proxies to override ``wsgi.url_scheme`` via a request header,
- ``X_FORWARDED_PROTO``. Allows proxies which serve mixed HTTP / HTTPS
- requests to control signal which are served as HTTPS. See
- https://github.com/Pylons/waitress/pull/42.
-
-0.8.8 (2013-11-30)
-------------------
-
-- Fix some cases where the creation of extremely large output buffers (greater
- than 2GB, suspected to be buffers added via ``wsgi.file_wrapper``) might
- cause an OverflowError on Python 2. See
- https://github.com/Pylons/waitress/issues/47.
-
-- When the ``url_prefix`` adjustment starts with more than one slash, all
- slashes except one will be stripped from its beginning. This differs from
- older behavior where more than one leading slash would be preserved in
- ``url_prefix``.
-
-- If a client somehow manages to send an empty path, we no longer convert the
- empty path to a single slash in ``PATH_INFO``. Instead, the path remains
- empty. According to RFC 2616 section "5.1.2 Request-URI", the scenario of a
- client sending an empty path is actually not possible because the request URI
- portion cannot be empty.
-
-- If the ``url_prefix`` adjustment matches the request path exactly, we now
- compute ``SCRIPT_NAME`` and ``PATH_INFO`` properly. Previously, if the
- ``url_prefix`` was ``/foo`` and the path received from a client was ``/foo``,
- we would set *both* ``SCRIPT_NAME`` and ``PATH_INFO`` to ``/foo``. This was
- incorrect. Now in such a case we set ``PATH_INFO`` to the empty string and
- we set ``SCRIPT_NAME`` to ``/foo``. Note that the change we made has no
- effect on paths that do not match the ``url_prefix`` exactly (such as
- ``/foo/bar``); these continue to operate as they did. See
- https://github.com/Pylons/waitress/issues/46
-
-- Preserve header ordering of headers with the same name as per RFC 2616. See
- https://github.com/Pylons/waitress/pull/44
-
-- When waitress receives a ``Transfer-Encoding: chunked`` request, we no longer
- send the ``TRANSFER_ENCODING`` nor the ``HTTP_TRANSFER_ENCODING`` value to
- the application in the environment. Instead, we pop this header. Since we
- cope with chunked requests by buffering the data in the server, we also know
- when a chunked request has ended, and therefore we know the content length.
- We set the content-length header in the environment, such that applications
- effectively never know the original request was a T-E: chunked request; it
- will appear to them as if the request is a non-chunked request with an
- accurate content-length.
-
-- Cope with the fact that the ``Transfer-Encoding`` value is case-insensitive.
-
-- When the ``--unix-socket-perms`` option was used as an argument to
- ``waitress-serve``, a ``TypeError`` would be raised. See
- https://github.com/Pylons/waitress/issues/50.
-
-0.8.7 (2013-08-29)
-------------------
-
-- The HTTP version of the response returned by waitress when it catches an
- exception will now match the HTTP request version.
-
-- Fix: CONNECTION header will be HTTP_CONNECTION and not CONNECTION_TYPE
- (see https://github.com/Pylons/waitress/issues/13)
-
-0.8.6 (2013-08-12)
-------------------
-
-- Do alternate type of checking for UNIX socket support, instead of checking
- for platform == windows.
-
-- Functional tests now use multiprocessing module instead of subprocess module,
- speeding up test suite and making concurrent execution more reliable.
-
-- Runner now appends the current working directory to ``sys.path`` to support
- running WSGI applications from a directory (i.e., not installed in a
- virtualenv).
-
-- Add a ``url_prefix`` adjustment setting. You can use it by passing
- ``script_name='/foo'`` to ``waitress.serve`` or you can use it in a
- ``PasteDeploy`` ini file as ``script_name = /foo``. This 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. You can use this instead of
- PasteDeploy's ``prefixmiddleware`` to always prefix the path.
-
-0.8.5 (2013-05-27)
-------------------
-
-- Fix runner multisegment imports in some Python 2 revisions (see
- https://github.com/Pylons/waitress/pull/34).
-
-- For compatibility, WSGIServer is now an alias of TcpWSGIServer. The
- signature of BaseWSGIServer is now compatible with WSGIServer pre-0.8.4.
-
-0.8.4 (2013-05-24)
-------------------
-
-- Add a command-line runner called ``waitress-serve`` to allow Waitress
- to run WSGI applications without any addional machinery. This is
- essentially a thin wrapper around the ``waitress.serve()`` function.
-
-- Allow parallel testing (e.g., under ``detox`` or ``nosetests --processes``)
- using PID-dependent port / socket for functest servers.
-
-- Fix integer overflow errors on large buffers. Thanks to Marcin Kuzminski
- for the patch. See: https://github.com/Pylons/waitress/issues/22
-
-- Add support for listening on Unix domain sockets.
-
-0.8.3 (2013-04-28)
-------------------
-
-Features
-~~~~~~~~
-
-- Add an ``asyncore_loop_timeout`` adjustment value, which controls the
- ``timeout`` value passed to ``asyncore.loop``; defaults to 1.
-
-Bug Fixes
-~~~~~~~~~
-
-- The default asyncore loop timeout is now 1 second. This prevents slow
- shutdown on Windows. See https://github.com/Pylons/waitress/issues/6 . This
- shouldn't matter to anyone in particular, but it can be changed via the
- ``asyncore_loop_timeout`` adjustment (it used to previously default to 30
- seconds).
-
-- Don't complain if there's a response to a HEAD request that contains a
- Content-Length > 0. See https://github.com/Pylons/waitress/pull/7.
-
-- Fix bug in HTTP Expect/Continue support. See
- https://github.com/Pylons/waitress/issues/9 .
-
-
-0.8.2 (2012-11-14)
-------------------
-
-Bug Fixes
-~~~~~~~~~
-
-- http://corte.si/posts/code/pathod/pythonservers/index.html pointed out that
- sending a bad header resulted in an exception leading to a 500 response
- instead of the more proper 400 response without an exception.
-
-- Fix a race condition in the test suite.
-
-- Allow "ident" to be used as a keyword to ``serve()`` as per docs.
-
-- Add py33 to tox.ini.
-
-0.8.1 (2012-02-13)
+1.0.1 (2016-10-22)
------------------
-Bug Fixes
-~~~~~~~~~
-
-- A brown-bag bug prevented request concurrency. A slow request would block
- subsequent the responses of subsequent requests until the slow request's
- response was fully generated. This was due to a "task lock" being declared
- as a class attribute rather than as an instance attribute on HTTPChannel.
- Also took the opportunity to move another lock named "outbuf lock" to the
- channel instance rather than the class. See
- https://github.com/Pylons/waitress/pull/1 .
-
-0.8 (2012-01-31)
-----------------
-
-Features
+Bugfixes
~~~~~~~~
-- Support the WSGI ``wsgi.file_wrapper`` protocol as per
- http://www.python.org/dev/peps/pep-0333/#optional-platform-specific-file-handling.
- Here's a usage example::
-
- import os
+- IPv6 support on Windows was broken due to missing constants in the socket
+ module. This has been resolved by setting the constants on Windows if they
+ are missing. See https://github.com/Pylons/waitress/issues/138
- here = os.path.dirname(os.path.abspath(__file__))
+- A ValueError was raised on Windows when passing a string for the port, on
+ Windows in Python 2 using service names instead of port numbers doesn't work
+ with `getaddrinfo`. This has been resolved by attempting to convert the port
+ number to an integer, if that fails a ValueError will be raised. See
+ https://github.com/Pylons/waitress/issues/139
- def myapp(environ, start_response):
- f = open(os.path.join(here, 'myphoto.jpg'), 'rb')
- headers = [('Content-Type', 'image/jpeg')]
- start_response(
- '200 OK',
- headers
- )
- return environ['wsgi.file_wrapper'](f, 32768)
- The signature of the file wrapper constructor is ``(filelike_object,
- block_size)``. Both arguments must be passed as positional (not keyword)
- arguments. The result of creating a file wrapper should be **returned** as
- the ``app_iter`` from a WSGI application.
-
- The object passed as ``filelike_object`` to the wrapper must be a file-like
- object which supports *at least* the ``read()`` method, and the ``read()``
- method must support an optional size hint argument. It *should* support
- the ``seek()`` and ``tell()`` methods. If it does not, normal iteration
- over the filelike object using the provided block_size is used (and copying
- is done, negating any benefit of the file wrapper). It *should* support a
- ``close()`` method.
-
- The specified ``block_size`` argument to the file wrapper constructor will
- be used only when the ``filelike_object`` doesn't support ``seek`` and/or
- ``tell`` methods. Waitress needs to use normal iteration to serve the file
- in this degenerate case (as per the WSGI spec), and this block size will be
- used as the iteration chunk size. The ``block_size`` argument is optional;
- if it is not passed, a default value``32768`` is used.
-
- Waitress will set a ``Content-Length`` header on the behalf of an
- application when a file wrapper with a sufficiently filelike object is used
- if the application hasn't already set one.
-
- The machinery which handles a file wrapper currently doesn't do anything
- particularly special using fancy system calls (it doesn't use ``sendfile``
- for example); using it currently just prevents the system from needing to
- copy data to a temporary buffer in order to send it to the client. No
- copying of data is done when a WSGI app returns a file wrapper that wraps a
- sufficiently filelike object. It may do something fancier in the future.
-
-0.7 (2012-01-11)
-----------------
-
-Features
-~~~~~~~~
-
-- Default ``send_bytes`` value is now 18000 instead of 9000. The larger
- default value prevents asyncore from needing to execute select so many
- times to serve large files, speeding up file serving by about 15%-20% or
- so. This is probably only an optimization for LAN communications, and
- could slow things down across a WAN (due to higher TCP overhead), but we're
- likely to be behind a reverse proxy on a LAN anyway if in production.
-
-- Added an (undocumented) profiling feature to the ``serve()`` command.
-
-0.6.1 (2012-01-08)
+1.0.0 (2016-08-31)
------------------
-Bug Fixes
-~~~~~~~~~
-
-- Remove performance-sapping call to ``pull_trigger`` in the channel's
- ``write_soon`` method added mistakenly in 0.6.
-
-0.6 (2012-01-07)
-----------------
-
-Bug Fixes
-~~~~~~~~~
-
-- A logic error prevented the internal outbuf buffer of a channel from being
- flushed when the client could not accept the entire contents of the output
- buffer in a single succession of socket.send calls when the channel was in
- a "pending close" state. The socket in such a case would be closed
- prematurely, sometimes resulting in partially delivered content. This was
- discovered by a user using waitress behind an Nginx reverse proxy, which
- apparently is not always ready to receive data. The symptom was that he
- received "half" of a large CSS file (110K) while serving content via
- waitress behind the proxy.
-
-0.5 (2012-01-03)
-----------------
-
-Bug Fixes
-~~~~~~~~~
-
-- Fix PATH_INFO encoding/decoding on Python 3 (as per PEP 3333, tunnel
- bytes-in-unicode-as-latin-1-after-unquoting).
-
-0.4 (2012-01-02)
-----------------
-
-Features
+Bugfixes
~~~~~~~~
-- Added "design" document to docs.
-
-Bug Fixes
-~~~~~~~~~
-
-- Set default ``connection_limit`` back to 100 for benefit of maximal
- platform compatibility.
+- Removed `AI_ADDRCONFIG` from the call to `getaddrinfo`, this resolves an
+ issue whereby `getaddrinfo` wouldn't return any addresses to `bind` to on
+ hosts where there is no internet connection but localhost is requested to be
+ bound to. See https://github.com/Pylons/waitress/issues/131 for more
+ information.
-- Normalize setting of ``last_activity`` during send.
+Deprecations
+~~~~~~~~~~~~
-- Minor resource cleanups during tests.
-
-- Channel timeout cleanup was broken.
-
-0.3 (2012-01-02)
-----------------
+- Python 2.6 is no longer supported.
Features
~~~~~~~~
-- Dont hang a thread up trying to send data to slow clients.
-
-- Use self.logger to log socket errors instead of self.log_info (normalize).
+- IPv6 support
-- Remove pointless handle_error method from channel.
+- Waitress is now able to listen on multiple sockets, including IPv4 and IPv6.
+ Instead of passing in a host/port combination you now provide waitress with a
+ space delineated list, and it will create as many sockets as required.
-- Queue requests instead of tasks in a channel.
+ .. code-block:: python
-Bug Fixes
-~~~~~~~~~
+ from waitress import serve
+ serve(wsgiapp, listen='0.0.0.0:8080 [::]:9090 *:6543')
-- Expect: 100-continue responses were broken.
-
-
-0.2 (2011-12-31)
-----------------
-
-Bug Fixes
-~~~~~~~~~
-
-- Set up logging by calling logging.basicConfig() when ``serve`` is called
- (show tracebacks and other warnings to console by default).
-
-- Disallow WSGI applications to set "hop-by-hop" headers (Connection,
- Transfer-Encoding, etc).
-
-- Don't treat 304 status responses specially in HTTP/1.1 mode.
-
-- Remove out of date ``interfaces.py`` file.
-
-- Normalize logging (all output is now sent to the ``waitress`` logger rather
- than in degenerate cases some output being sent directly to stderr).
-
-Features
+Security
~~~~~~~~
-- Support HTTP/1.1 ``Transfer-Encoding: chunked`` responses.
-
-- Slightly better docs about logging.
-
-0.1 (2011-12-30)
-----------------
-
-- Initial release.
+- Waitress will now drop HTTP headers that contain an underscore in the key
+ when received from a client. This is to stop any possible underscore/dash
+ conflation that may lead to security issues. See
+ https://github.com/Pylons/waitress/pull/80 and
+ https://www.djangoproject.com/weblog/2015/jan/13/security/
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index cf44c82..a53e15d 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -121,6 +121,8 @@ Contributors
- Adam Groszer, 2013/08/15
+- Matt Russell, 2015/01/14
+
- David Glick, 2015/04/13
- Shane Hathaway, 2015-04-20
@@ -128,3 +130,10 @@ Contributors
- Steve Piercy, 2015-04-21
- Ben Warren, 2015-05-17
+
+- Bert JW Regeer, 2015-09-23
+
+- Yu Zhou, 2015-09-24
+
+- Jason Madden, 2016-03-19
+
diff --git a/HISTORY.txt b/HISTORY.txt
new file mode 100644
index 0000000..9932582
--- /dev/null
+++ b/HISTORY.txt
@@ -0,0 +1,422 @@
+0.9.0 (2016-04-15)
+------------------
+
+Deprecations
+~~~~~~~~~~~~
+
+- Python 3.2 is no longer supported by Waitress.
+
+- Python 2.6 will no longer be supported by Waitress in future releases.
+
+Security/Protections
+~~~~~~~~~~~~~~~~~~~~
+
+- Building on the changes made in pull request 117, add in checking for line
+ feed/carriage return HTTP Response Splitting in the status line, as well as
+ the key of a header. See https://github.com/Pylons/waitress/pull/124 and
+ https://github.com/Pylons/waitress/issues/122.
+
+- Waitress will no longer accept headers or status lines with
+ newline/carriage returns in them, thereby disallowing HTTP Response
+ Splitting. See https://github.com/Pylons/waitress/issues/117 for
+ more information, as well as
+ https://www.owasp.org/index.php/HTTP_Response_Splitting.
+
+Bugfixes
+~~~~~~~~
+
+- FileBasedBuffer and more important ReadOnlyFileBasedBuffer no longer report
+ False when tested with bool(), instead always returning True, and becoming
+ more iterator like.
+ See: https://github.com/Pylons/waitress/pull/82 and
+ https://github.com/Pylons/waitress/issues/76
+
+- Call prune() on the output buffer at the end of a request so that it doesn't
+ continue to grow without bounds. See
+ https://github.com/Pylons/waitress/issues/111 for more information.
+
+0.8.10 (2015-09-02)
+-------------------
+
+- Add support for Python 3.4, 3.5b2, and PyPy3.
+
+- Use a nonglobal asyncore socket map by default, trying to prevent conflicts
+ with apps and libs that use the asyncore global socket map ala
+ https://github.com/Pylons/waitress/issues/63. You can get the old
+ use-global-socket-map behavior back by passing ``asyncore.socket_map`` to the
+ ``create_server`` function as the ``map`` argument.
+
+- Waitress violated PEP 3333 with respect to reraising an exception when
+ ``start_response`` was called with an ``exc_info`` argument. It would
+ reraise the exception even if no data had been sent to the client. It now
+ only reraises the exception if data has actually been sent to the client.
+ See https://github.com/Pylons/waitress/pull/52 and
+ https://github.com/Pylons/waitress/issues/51
+
+- Add a ``docs`` section to tox.ini that, when run, ensures docs can be built.
+
+- If an ``application`` value of ``None`` is supplied to the ``create_server``
+ constructor function, a ValueError is now raised eagerly instead of an error
+ occuring during runtime. See https://github.com/Pylons/waitress/pull/60
+
+- Fix parsing of multi-line (folded) headers.
+ See https://github.com/Pylons/waitress/issues/53 and
+ https://github.com/Pylons/waitress/pull/90
+
+- Switch from the low level Python thread/_thread module to the threading
+ module.
+
+- Improved exception information should module import go awry.
+
+0.8.9 (2014-05-16)
+------------------
+
+- Fix tests under Windows. NB: to run tests under Windows, you cannot run
+ "setup.py test" or "setup.py nosetests". Instead you must run ``python.exe
+ -c "import nose; nose.main()"``. If you try to run the tests using the
+ normal method under Windows, each subprocess created by the test suite will
+ attempt to run the test suite again. See
+ https://github.com/nose-devs/nose/issues/407 for more information.
+
+- Give the WSGI app_iter generated when ``wsgi.file_wrapper`` is used
+ (ReadOnlyFileBasedBuffer) a ``close`` method. Do not call ``close`` on an
+ instance of such a class when it's used as a WSGI app_iter, however. This is
+ part of a fix which prevents a leakage of file descriptors; the other part of
+ the fix was in WebOb
+ (https://github.com/Pylons/webob/commit/951a41ce57bd853947f842028bccb500bd5237da).
+
+- Allow trusted proxies to override ``wsgi.url_scheme`` via a request header,
+ ``X_FORWARDED_PROTO``. Allows proxies which serve mixed HTTP / HTTPS
+ requests to control signal which are served as HTTPS. See
+ https://github.com/Pylons/waitress/pull/42.
+
+0.8.8 (2013-11-30)
+------------------
+
+- Fix some cases where the creation of extremely large output buffers (greater
+ than 2GB, suspected to be buffers added via ``wsgi.file_wrapper``) might
+ cause an OverflowError on Python 2. See
+ https://github.com/Pylons/waitress/issues/47.
+
+- When the ``url_prefix`` adjustment starts with more than one slash, all
+ slashes except one will be stripped from its beginning. This differs from
+ older behavior where more than one leading slash would be preserved in
+ ``url_prefix``.
+
+- If a client somehow manages to send an empty path, we no longer convert the
+ empty path to a single slash in ``PATH_INFO``. Instead, the path remains
+ empty. According to RFC 2616 section "5.1.2 Request-URI", the scenario of a
+ client sending an empty path is actually not possible because the request URI
+ portion cannot be empty.
+
+- If the ``url_prefix`` adjustment matches the request path exactly, we now
+ compute ``SCRIPT_NAME`` and ``PATH_INFO`` properly. Previously, if the
+ ``url_prefix`` was ``/foo`` and the path received from a client was ``/foo``,
+ we would set *both* ``SCRIPT_NAME`` and ``PATH_INFO`` to ``/foo``. This was
+ incorrect. Now in such a case we set ``PATH_INFO`` to the empty string and
+ we set ``SCRIPT_NAME`` to ``/foo``. Note that the change we made has no
+ effect on paths that do not match the ``url_prefix`` exactly (such as
+ ``/foo/bar``); these continue to operate as they did. See
+ https://github.com/Pylons/waitress/issues/46
+
+- Preserve header ordering of headers with the same name as per RFC 2616. See
+ https://github.com/Pylons/waitress/pull/44
+
+- When waitress receives a ``Transfer-Encoding: chunked`` request, we no longer
+ send the ``TRANSFER_ENCODING`` nor the ``HTTP_TRANSFER_ENCODING`` value to
+ the application in the environment. Instead, we pop this header. Since we
+ cope with chunked requests by buffering the data in the server, we also know
+ when a chunked request has ended, and therefore we know the content length.
+ We set the content-length header in the environment, such that applications
+ effectively never know the original request was a T-E: chunked request; it
+ will appear to them as if the request is a non-chunked request with an
+ accurate content-length.
+
+- Cope with the fact that the ``Transfer-Encoding`` value is case-insensitive.
+
+- When the ``--unix-socket-perms`` option was used as an argument to
+ ``waitress-serve``, a ``TypeError`` would be raised. See
+ https://github.com/Pylons/waitress/issues/50.
+
+0.8.7 (2013-08-29)
+------------------
+
+- The HTTP version of the response returned by waitress when it catches an
+ exception will now match the HTTP request version.
+
+- Fix: CONNECTION header will be HTTP_CONNECTION and not CONNECTION_TYPE
+ (see https://github.com/Pylons/waitress/issues/13)
+
+0.8.6 (2013-08-12)
+------------------
+
+- Do alternate type of checking for UNIX socket support, instead of checking
+ for platform == windows.
+
+- Functional tests now use multiprocessing module instead of subprocess module,
+ speeding up test suite and making concurrent execution more reliable.
+
+- Runner now appends the current working directory to ``sys.path`` to support
+ running WSGI applications from a directory (i.e., not installed in a
+ virtualenv).
+
+- Add a ``url_prefix`` adjustment setting. You can use it by passing
+ ``script_name='/foo'`` to ``waitress.serve`` or you can use it in a
+ ``PasteDeploy`` ini file as ``script_name = /foo``. This 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. You can use this instead of
+ PasteDeploy's ``prefixmiddleware`` to always prefix the path.
+
+0.8.5 (2013-05-27)
+------------------
+
+- Fix runner multisegment imports in some Python 2 revisions (see
+ https://github.com/Pylons/waitress/pull/34).
+
+- For compatibility, WSGIServer is now an alias of TcpWSGIServer. The
+ signature of BaseWSGIServer is now compatible with WSGIServer pre-0.8.4.
+
+0.8.4 (2013-05-24)
+------------------
+
+- Add a command-line runner called ``waitress-serve`` to allow Waitress
+ to run WSGI applications without any addional machinery. This is
+ essentially a thin wrapper around the ``waitress.serve()`` function.
+
+- Allow parallel testing (e.g., under ``detox`` or ``nosetests --processes``)
+ using PID-dependent port / socket for functest servers.
+
+- Fix integer overflow errors on large buffers. Thanks to Marcin Kuzminski
+ for the patch. See: https://github.com/Pylons/waitress/issues/22
+
+- Add support for listening on Unix domain sockets.
+
+0.8.3 (2013-04-28)
+------------------
+
+Features
+~~~~~~~~
+
+- Add an ``asyncore_loop_timeout`` adjustment value, which controls the
+ ``timeout`` value passed to ``asyncore.loop``; defaults to 1.
+
+Bug Fixes
+~~~~~~~~~
+
+- The default asyncore loop timeout is now 1 second. This prevents slow
+ shutdown on Windows. See https://github.com/Pylons/waitress/issues/6 . This
+ shouldn't matter to anyone in particular, but it can be changed via the
+ ``asyncore_loop_timeout`` adjustment (it used to previously default to 30
+ seconds).
+
+- Don't complain if there's a response to a HEAD request that contains a
+ Content-Length > 0. See https://github.com/Pylons/waitress/pull/7.
+
+- Fix bug in HTTP Expect/Continue support. See
+ https://github.com/Pylons/waitress/issues/9 .
+
+
+0.8.2 (2012-11-14)
+------------------
+
+Bug Fixes
+~~~~~~~~~
+
+- http://corte.si/posts/code/pathod/pythonservers/index.html pointed out that
+ sending a bad header resulted in an exception leading to a 500 response
+ instead of the more proper 400 response without an exception.
+
+- Fix a race condition in the test suite.
+
+- Allow "ident" to be used as a keyword to ``serve()`` as per docs.
+
+- Add py33 to tox.ini.
+
+0.8.1 (2012-02-13)
+------------------
+
+Bug Fixes
+~~~~~~~~~
+
+- A brown-bag bug prevented request concurrency. A slow request would block
+ subsequent the responses of subsequent requests until the slow request's
+ response was fully generated. This was due to a "task lock" being declared
+ as a class attribute rather than as an instance attribute on HTTPChannel.
+ Also took the opportunity to move another lock named "outbuf lock" to the
+ channel instance rather than the class. See
+ https://github.com/Pylons/waitress/pull/1 .
+
+0.8 (2012-01-31)
+----------------
+
+Features
+~~~~~~~~
+
+- Support the WSGI ``wsgi.file_wrapper`` protocol as per
+ http://www.python.org/dev/peps/pep-0333/#optional-platform-specific-file-handling.
+ Here's a usage example::
+
+ import os
+
+ here = os.path.dirname(os.path.abspath(__file__))
+
+ def myapp(environ, start_response):
+ f = open(os.path.join(here, 'myphoto.jpg'), 'rb')
+ headers = [('Content-Type', 'image/jpeg')]
+ start_response(
+ '200 OK',
+ headers
+ )
+ return environ['wsgi.file_wrapper'](f, 32768)
+
+ The signature of the file wrapper constructor is ``(filelike_object,
+ block_size)``. Both arguments must be passed as positional (not keyword)
+ arguments. The result of creating a file wrapper should be **returned** as
+ the ``app_iter`` from a WSGI application.
+
+ The object passed as ``filelike_object`` to the wrapper must be a file-like
+ object which supports *at least* the ``read()`` method, and the ``read()``
+ method must support an optional size hint argument. It *should* support
+ the ``seek()`` and ``tell()`` methods. If it does not, normal iteration
+ over the filelike object using the provided block_size is used (and copying
+ is done, negating any benefit of the file wrapper). It *should* support a
+ ``close()`` method.
+
+ The specified ``block_size`` argument to the file wrapper constructor will
+ be used only when the ``filelike_object`` doesn't support ``seek`` and/or
+ ``tell`` methods. Waitress needs to use normal iteration to serve the file
+ in this degenerate case (as per the WSGI spec), and this block size will be
+ used as the iteration chunk size. The ``block_size`` argument is optional;
+ if it is not passed, a default value``32768`` is used.
+
+ Waitress will set a ``Content-Length`` header on the behalf of an
+ application when a file wrapper with a sufficiently filelike object is used
+ if the application hasn't already set one.
+
+ The machinery which handles a file wrapper currently doesn't do anything
+ particularly special using fancy system calls (it doesn't use ``sendfile``
+ for example); using it currently just prevents the system from needing to
+ copy data to a temporary buffer in order to send it to the client. No
+ copying of data is done when a WSGI app returns a file wrapper that wraps a
+ sufficiently filelike object. It may do something fancier in the future.
+
+0.7 (2012-01-11)
+----------------
+
+Features
+~~~~~~~~
+
+- Default ``send_bytes`` value is now 18000 instead of 9000. The larger
+ default value prevents asyncore from needing to execute select so many
+ times to serve large files, speeding up file serving by about 15%-20% or
+ so. This is probably only an optimization for LAN communications, and
+ could slow things down across a WAN (due to higher TCP overhead), but we're
+ likely to be behind a reverse proxy on a LAN anyway if in production.
+
+- Added an (undocumented) profiling feature to the ``serve()`` command.
+
+0.6.1 (2012-01-08)
+------------------
+
+Bug Fixes
+~~~~~~~~~
+
+- Remove performance-sapping call to ``pull_trigger`` in the channel's
+ ``write_soon`` method added mistakenly in 0.6.
+
+0.6 (2012-01-07)
+----------------
+
+Bug Fixes
+~~~~~~~~~
+
+- A logic error prevented the internal outbuf buffer of a channel from being
+ flushed when the client could not accept the entire contents of the output
+ buffer in a single succession of socket.send calls when the channel was in
+ a "pending close" state. The socket in such a case would be closed
+ prematurely, sometimes resulting in partially delivered content. This was
+ discovered by a user using waitress behind an Nginx reverse proxy, which
+ apparently is not always ready to receive data. The symptom was that he
+ received "half" of a large CSS file (110K) while serving content via
+ waitress behind the proxy.
+
+0.5 (2012-01-03)
+----------------
+
+Bug Fixes
+~~~~~~~~~
+
+- Fix PATH_INFO encoding/decoding on Python 3 (as per PEP 3333, tunnel
+ bytes-in-unicode-as-latin-1-after-unquoting).
+
+0.4 (2012-01-02)
+----------------
+
+Features
+~~~~~~~~
+
+- Added "design" document to docs.
+
+Bug Fixes
+~~~~~~~~~
+
+- Set default ``connection_limit`` back to 100 for benefit of maximal
+ platform compatibility.
+
+- Normalize setting of ``last_activity`` during send.
+
+- Minor resource cleanups during tests.
+
+- Channel timeout cleanup was broken.
+
+0.3 (2012-01-02)
+----------------
+
+Features
+~~~~~~~~
+
+- Dont hang a thread up trying to send data to slow clients.
+
+- Use self.logger to log socket errors instead of self.log_info (normalize).
+
+- Remove pointless handle_error method from channel.
+
+- Queue requests instead of tasks in a channel.
+
+Bug Fixes
+~~~~~~~~~
+
+- Expect: 100-continue responses were broken.
+
+
+0.2 (2011-12-31)
+----------------
+
+Bug Fixes
+~~~~~~~~~
+
+- Set up logging by calling logging.basicConfig() when ``serve`` is called
+ (show tracebacks and other warnings to console by default).
+
+- Disallow WSGI applications to set "hop-by-hop" headers (Connection,
+ Transfer-Encoding, etc).
+
+- Don't treat 304 status responses specially in HTTP/1.1 mode.
+
+- Remove out of date ``interfaces.py`` file.
+
+- Normalize logging (all output is now sent to the ``waitress`` logger rather
+ than in degenerate cases some output being sent directly to stderr).
+
+Features
+~~~~~~~~
+
+- Support HTTP/1.1 ``Transfer-Encoding: chunked`` responses.
+
+- Slightly better docs about logging.
+
+0.1 (2011-12-30)
+----------------
+
+- Initial release.
diff --git a/README.rst b/README.rst
index e10eac8..ce9ea01 100644
--- a/README.rst
+++ b/README.rst
@@ -1,7 +1,7 @@
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.6+ and Python 3.2+. It is also known to run on PyPy 1.6.0+ on UNIX. It
+2.7+ and Python 3.3+. It is also known to run on PyPy 1.6.0+ on UNIX. It
supports HTTP/1.0 and HTTP/1.1.
For more information, see the "docs" directory of the Waitress package or
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000..6505c2b
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,14 @@
+environment:
+ matrix:
+ - PYTHON: "C:\\Python27"
+ TOXENV: "py27"
+ - PYTHON: "C:\\Python35"
+ TOXENV: "py35"
+
+install:
+ - "%PYTHON%\\python.exe -m pip install tox"
+
+build: off
+
+test_script:
+ - "%PYTHON%\\Scripts\\tox.exe"
diff --git a/debian/patches/01-fix-sphinxdoc-conf.patch b/debian/patches/01-fix-sphinxdoc-conf.patch
index 5fa8c2c..54ee370 100644
--- a/debian/patches/01-fix-sphinxdoc-conf.patch
+++ b/debian/patches/01-fix-sphinxdoc-conf.patch
@@ -17,21 +17,19 @@ Author: Andrew Shadura <andrewsh@debian.org>
#
# The short X.Y version.
-version = pkg_resources.get_distribution('waitress').version
-+version = '0.8.10'
++version = '1.0.1'
# The full version, including alpha/beta/rc tags.
release = version
-@@ -89,10 +89,10 @@
+@@ -89,9 +89,9 @@
# -----------------------
# Add and use Pylons theme
--sys.path.append(os.path.abspath('_themes'))
-html_theme = 'pylons'
-html_theme_path = pylons_sphinx_themes.get_html_themes_path()
-html_theme_options = dict(github_url='http://github.com/Pylons/waitress')
-+# sys.path.append(os.path.abspath('_themes'))
+# html_theme = 'pylons'
-+# html_theme_path = pylons_sphinx_themes.get_html_themes_path()
++# html_theme_path = # pylons_sphinx_themes.get_html_themes_path()
+# html_theme_options = dict(github_url='http://github.com/Pylons/waitress')
# The style sheet to use for HTML and HTML Help pages. A file of that name
diff --git a/docs/api.rst b/docs/api.rst
index c85d4e1..5e0a523 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -5,6 +5,6 @@
.. module:: waitress
-.. function:: serve(app, host='0.0.0.0', port=8080, unix_socket=None, unix_socket_perms='600', threads=4, url_scheme='http', url_prefix='', ident='waitress', backlog=1204, recv_bytes=8192, send_bytes=18000, outbuf_overflow=104856, inbuf_overflow=52488, connection_limit=1000, cleanup_interval=30, channel_timeout=120, log_socket_errors=True, max_request_header_size=262144, max_request_body_size=1073741824, expose_tracebacks=False)
+.. function:: serve(app, listen='0.0.0.0:8080', unix_socket=None, unix_socket_perms='600', threads=4, url_scheme='http', url_prefix='', ident='waitress', backlog=1204, recv_bytes=8192, send_bytes=18000, outbuf_overflow=104856, inbuf_overflow=52488, connection_limit=1000, cleanup_interval=30, channel_timeout=120, log_socket_errors=True, max_request_header_size=262144, max_request_body_size=1073741824, expose_tracebacks=False)
See :ref:`arguments` for more information.
diff --git a/docs/arguments.rst b/docs/arguments.rst
index cc74c0c..b827b0e 100644
--- a/docs/arguments.rst
+++ b/docs/arguments.rst
@@ -10,9 +10,37 @@ host
hostname or IP address (string) on which to listen, default ``0.0.0.0``,
which means "all IP addresses on this host".
+ .. warning::
+ May not be used with ``listen``
+
port
TCP port (integer) on which to listen, default ``8080``
+ .. warning::
+ May not be used with ``listen``
+
+listen
+ Tell waitress to listen on an host/port combination. It is to be provided
+ as a space delineated list of host/port:
+
+ Examples:
+
+ - ``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.
+
+ IPv6 IP addresses are supported by surrounding the IP address with brackets.
+
+ .. versionadded:: 1.0
+
+ipv4
+ Enable or disable IPv4 (boolean)
+
+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
diff --git a/docs/conf.py b/docs/conf.py
index 62b171a..ce3c33f 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -47,7 +47,7 @@ copyright = '2012, Agendaless Consulting <chrism@plope.com>'
# other places throughout the built documents.
#
# The short X.Y version.
-version = '0.8.10'
+version = '1.0.1'
# The full version, including alpha/beta/rc tags.
release = version
@@ -89,9 +89,8 @@ pygments_style = 'sphinx'
# -----------------------
# Add and use Pylons theme
-# sys.path.append(os.path.abspath('_themes'))
# html_theme = 'pylons'
-# html_theme_path = pylons_sphinx_themes.get_html_themes_path()
+# html_theme_path = # pylons_sphinx_themes.get_html_themes_path()
# html_theme_options = dict(github_url='http://github.com/Pylons/waitress')
# The style sheet to use for HTML and HTML Help pages. A file of that name
@@ -181,7 +180,7 @@ latex_documents = [
# The name of an image file (relative to this directory) to place at the
# top of the title page.
-latex_logo = '.static/logo_hi.gif'
+#latex_logo = '.static/logo_hi.gif'
# For "manual" documents, if this is true, then toplevel headings are
# parts, not chapters.
diff --git a/docs/index.rst b/docs/index.rst
index f79e214..cd175e3 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -15,8 +15,19 @@ 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 impot 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:
@@ -28,6 +39,13 @@ WSGI app as a single argument:
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:
@@ -49,6 +67,13 @@ lets you use Waitress's WSGI gateway from a configuration file, e.g.:
[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
@@ -65,9 +90,15 @@ 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::
+:term:`PasteDeploy` is not necessary:
+
+.. code-block:: bash
- waitress-serve --port=8041 myapp:wsgifunc
+ # 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`.
@@ -153,7 +184,7 @@ You can have the Waitress server use the ``https`` url scheme by default.:
.. code-block:: python
from waitress import serve
- serve(wsgiapp, host='0.0.0.0', port=8080, url_scheme='https')
+ 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.
@@ -188,7 +219,7 @@ account.:
.. code-block:: python
from waitress import serve
- serve(wsgiapp, host='0.0.0.0', port=8080, url_prefix='/foo')
+ 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
@@ -257,8 +288,7 @@ PasteDeploy-style configuration:
[server:main]
use = egg:waitress#main
- host = 127.0.0.1
- port = 8080
+ 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'
@@ -282,12 +312,11 @@ Change History
--------------
.. include:: ../CHANGES.txt
+.. include:: ../HISTORY.txt
Known Issues
------------
-- Does not yet support IPv6 natively.
-
- Does not support SSL natively.
Support and Development
diff --git a/docs/runner.rst b/docs/runner.rst
index 9799a17..88a7d63 100644
--- a/docs/runner.rst
+++ b/docs/runner.rst
@@ -85,6 +85,31 @@ Common options:
``--port=PORT``
TCP port on which to listen, default is '8080'
+``--listen=host:port``
+ Tell waitress to listen on an ip port combination.
+
+ Example:
+
+ --listen=127.0.0.1:8080
+ --listen=[::1]:8080
+ --listen=*:8080
+
+ This option may be used multiple times to listen on multipe sockets.
+ A wildcard for the hostname is also supported and will bind to both
+ IPv4/IPv6 depending on whether they are enabled or disabled.
+
+``--[no-]ipv4``
+ Toggle on/off IPv4 support.
+
+ This affects wildcard matching when listening on a wildcard address/port
+ combination.
+
+``--[no-]ipv6``
+ Toggle on/off IPv6 support.
+
+ This affects wildcard matching when listening on a wildcard address/port
+ combination.
+
``--unix-socket=PATH``
Path of Unix socket. If a socket path is specified, a Unix domain
socket is made instead of the usual inet domain socket.
diff --git a/rtd.txt b/rtd.txt
index 0441b70..ff62117 100644
--- a/rtd.txt
+++ b/rtd.txt
@@ -1,3 +1,3 @@
-Sphinx >= 1.2.3
+Sphinx >= 1.3.1
repoze.sphinx.autointerface
pylons-sphinx-themes
diff --git a/setup.cfg b/setup.cfg
index 03c7194..4be5f9c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -8,6 +8,9 @@ nocapture=1
cover-package=waitress
cover-erase=1
+[bdist_wheel]
+universal = 1
+
[aliases]
dev = develop easy_install waitress[testing]
docs = develop easy_install waitress[docs]
diff --git a/setup.py b/setup.py
index 7598943..c17a7b0 100644
--- a/setup.py
+++ b/setup.py
@@ -12,7 +12,6 @@
#
##############################################################################
import os
-import sys
from setuptools import setup, find_packages
here = os.path.abspath(os.path.dirname(__file__))
@@ -33,16 +32,13 @@ testing_extras = [
'coverage',
]
-if sys.version_info[:2] == (2, 6):
- testing_extras.append('unittest2')
-
setup(
name='waitress',
- version='0.8.10',
+ version='1.0.1',
author='Zope Foundation and Contributors',
author_email='zope-dev@zope.org',
- maintainer="Chris McDonough",
- maintainer_email="chrism@plope.com",
+ maintainer="Pylons Project",
+ maintainer_email="pylons-discuss@googlegroups.com",
description='Waitress WSGI server',
long_description=README + '\n\n' + CHANGES,
license='ZPL 2.1',
@@ -54,10 +50,8 @@ setup(
'License :: OSI Approved :: Zope Public License',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
- 'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
diff --git a/tox.ini b/tox.ini
index 813cc2b..4864c46 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,24 +1,61 @@
[tox]
-envlist =
- py26,py27,pypy,py32,py33,py34,py35,pypy3,cover,docs
+envlist =
+ py27,py33,py34,py35,pypy,pypy3,
+ docs,
+ {py2,py3}-cover,coverage
[testenv]
-commands =
- pip install waitress[testing]
- nosetests --processes=4
-
-[testenv:cover]
+# Most of these are defaults but if you specify any you can't fall back
+# to defaults for others.
basepython =
- python2.6
-commands =
+ py27: python2.7
+ py33: python3.3
+ py34: python3.4
+ py35: python3.5
+ pypy: pypy
+ pypy3: pypy3
+ py2: python2.7
+ py3: python3.5
+
+commands =
pip install waitress[testing]
- nosetests --with-xunit --with-xcoverage
-deps =
- nosexcover
+ nosetests --with-xunit --xunit-file=nosetests-{envname}.xml {posargs:}
[testenv:docs]
-basepython =
- python2.7
+basepython = python3.5
+whitelist_externals = make
commands =
pip install waitress[docs]
- sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html
+ make -C docs html epub BUILDDIR={envdir} "SPHINXOPTS=-W -E"
+
+[py-cover]
+commands =
+ pip install waitress[testing]
+ coverage run --source=waitress --parallel-mode {envbindir}/nosetests
+ coverage combine
+ coverage xml -o {envname}.xml
+
+[testenv:py2-cover]
+commands =
+ {[py-cover]commands}
+setenv =
+ COVERAGE_FILE=.coverage.py2
+
+[testenv:py3-cover]
+commands =
+ {[py-cover]commands}
+setenv =
+ COVERAGE_FILE=.coverage.py3
+
+[testenv:coverage]
+basepython = python3.5
+commands =
+ coverage erase
+ coverage combine
+ coverage xml
+ coverage report --show-missing --fail-under=100 --omit=waitress/tests/fixtureapps/getline.py
+deps =
+ coverage
+setenv =
+ COVERAGE_FILE=.coverage
+
diff --git a/waitress/__init__.py b/waitress/__init__.py
index 27210d4..775fe3a 100644
--- a/waitress/__init__.py
+++ b/waitress/__init__.py
@@ -10,8 +10,7 @@ def serve(app, **kw):
logging.basicConfig()
server = _server(app, **kw)
if not _quiet: # pragma: no cover
- print('serving on http://%s:%s' % (server.effective_host,
- server.effective_port))
+ server.print_listen('Serving on http://{}:{}')
if _profile: # pragma: no cover
profile('server.run()', globals(), locals(), (), False)
else:
diff --git a/waitress/adjustments.py b/waitress/adjustments.py
index d5b237b..1a56621 100644
--- a/waitress/adjustments.py
+++ b/waitress/adjustments.py
@@ -15,7 +15,13 @@
"""
import getopt
import socket
-import sys
+
+from waitress.compat import (
+ PY2,
+ WIN,
+ string_types,
+ HAS_IPV6,
+ )
truthy = frozenset(('t', 'true', 'y', 'yes', 'on', '1'))
@@ -36,6 +42,22 @@ def asoctal(s):
"""Convert the given octal string to an actual number."""
return int(s, 8)
+def aslist_cronly(value):
+ if isinstance(value, string_types):
+ value = filter(None, [x.strip() for x in value.splitlines()])
+ return list(value)
+
+def aslist(value):
+ """ Return a list of strings, separating the input based on newlines
+ and, if flatten=True (the default), also split on spaces within
+ each line."""
+ values = aslist_cronly(value)
+ result = []
+ for value in values:
+ subvalues = value.split()
+ result.extend(subvalues)
+ return result
+
def slash_fixed_str(s):
s = s.strip()
if s:
@@ -44,6 +66,12 @@ def slash_fixed_str(s):
s = '/' + s.lstrip('/').rstrip('/')
return s
+class _str_marker(str):
+ pass
+
+class _int_marker(int):
+ pass
+
class Adjustments(object):
"""This class contains tunable parameters.
"""
@@ -51,6 +79,9 @@ class Adjustments(object):
_params = (
('host', str),
('port', int),
+ ('ipv4', asbool),
+ ('ipv6', asbool),
+ ('listen', aslist),
('threads', int),
('trusted_proxy', str),
('url_scheme', str),
@@ -77,10 +108,12 @@ class Adjustments(object):
_param_map = dict(_params)
# hostname or IP address to listen on
- host = '0.0.0.0'
+ host = _str_marker('0.0.0.0')
# TCP port to listen on
- port = 8080
+ port = _int_marker(8080)
+
+ listen = ['{}:{}'.format(host, port)]
# mumber of threads available for tasks
threads = 4
@@ -174,14 +207,96 @@ class Adjustments(object):
# The asyncore.loop flag to use poll() instead of the default select().
asyncore_use_poll = False
+ # Enable IPv4 by default
+ ipv4 = True
+
+ # Enable IPv6 by default
+ ipv6 = True
+
def __init__(self, **kw):
+
+ if 'listen' in kw and ('host' in kw or 'port' in kw):
+ raise ValueError('host and or port may not be set if listen is set.')
+
for k, v in kw.items():
if k not in self._param_map:
raise ValueError('Unknown adjustment %r' % k)
setattr(self, k, self._param_map[k](v))
- if (sys.platform[:3] == "win" and
- self.host == 'localhost'): # pragma: no cover
- self.host = ''
+
+ if (not isinstance(self.host, _str_marker) or
+ not isinstance(self.port, _int_marker)):
+ self.listen = ['{}:{}'.format(self.host, self.port)]
+
+ enabled_families = socket.AF_UNSPEC
+
+ if not self.ipv4 and not HAS_IPV6: # pragma: no cover
+ raise ValueError(
+ 'IPv4 is disabled but IPv6 is not available. Cowardly refusing to start.'
+ )
+
+ if self.ipv4 and not self.ipv6:
+ enabled_families = socket.AF_INET
+
+ if not self.ipv4 and self.ipv6 and HAS_IPV6:
+ enabled_families = socket.AF_INET6
+
+ wanted_sockets = []
+ hp_pairs = []
+ for i in self.listen:
+ if ':' in i:
+ (host, port) = i.rsplit(":", 1)
+
+ # IPv6 we need to make sure that we didn't split on the address
+ if ']' in port: # pragma: nocover
+ (host, port) = (i, str(self.port))
+ else:
+ (host, port) = (i, str(self.port))
+
+ if WIN and PY2: # pragma: no cover
+ try:
+ # Try turning the port into an integer
+ port = int(port)
+ except:
+ raise ValueError(
+ 'Windows does not support service names instead of port numbers'
+ )
+
+ try:
+ if '[' in host and ']' in host: # pragma: nocover
+ host = host.strip('[').rstrip(']')
+
+ if host == '*':
+ host = None
+
+ for s in socket.getaddrinfo(
+ host,
+ port,
+ enabled_families,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ socket.AI_PASSIVE
+ ):
+ (family, socktype, proto, _, sockaddr) = s
+
+ # It seems that getaddrinfo() may sometimes happily return
+ # the same result multiple times, this of course makes
+ # bind() very unhappy...
+ #
+ # Split on %, and drop the zone-index from the host in the
+ # sockaddr. Works around a bug in OS X whereby
+ # getaddrinfo() returns the same link-local interface with
+ # two different zone-indices (which makes no sense what so
+ # ever...) yet treats them equally when we attempt to bind().
+ if (
+ sockaddr[1] == 0 or
+ (sockaddr[0].split('%', 1)[0], sockaddr[1]) not in hp_pairs
+ ):
+ wanted_sockets.append((family, socktype, proto, sockaddr))
+ hp_pairs.append((sockaddr[0].split('%', 1)[0], sockaddr[1]))
+ except:
+ raise ValueError('Invalid host/port specified.')
+
+ self.listen = wanted_sockets
@classmethod
def parse_args(cls, argv):
@@ -203,9 +318,15 @@ class Adjustments(object):
'help': False,
'call': False,
}
+
opts, args = getopt.getopt(argv, '', long_opts)
for opt, value in opts:
param = opt.lstrip('-').replace('-', '_')
+
+ if param == 'listen':
+ kw['listen'] = '{} {}'.format(kw.get('listen', ''), value)
+ continue
+
if param.startswith('no_'):
param = param[3:]
kw[param] = 'false'
@@ -215,4 +336,5 @@ class Adjustments(object):
kw[param] = 'true'
else:
kw[param] = value
+
return kw, args
diff --git a/waitress/buffers.py b/waitress/buffers.py
index f174a79..cacc094 100644
--- a/waitress/buffers.py
+++ b/waitress/buffers.py
@@ -44,7 +44,7 @@ class FileBasedBuffer(object):
return self.remain
def __nonzero__(self):
- return self.remain > 0
+ return True
__bool__ = __nonzero__ # py3
diff --git a/waitress/channel.py b/waitress/channel.py
index c806bee..ca02511 100644
--- a/waitress/channel.py
+++ b/waitress/channel.py
@@ -249,6 +249,8 @@ class HTTPChannel(logging_dispatcher, object):
'Unexpected error when closing an outbuf')
continue # pragma: no cover (coverage bug, it is hit)
else:
+ if hasattr(outbuf, 'prune'):
+ outbuf.prune()
dobreak = True
while outbuflen > 0:
diff --git a/waitress/compat.py b/waitress/compat.py
index 9e06cde..700f7a1 100644
--- a/waitress/compat.py
+++ b/waitress/compat.py
@@ -1,5 +1,7 @@
import sys
import types
+import platform
+import warnings
try:
import urlparse
@@ -7,8 +9,12 @@ except ImportError: # pragma: no cover
from urllib import parse as urlparse
# True if we are running on Python 3.
+PY2 = sys.version_info[0] == 2
PY3 = sys.version_info[0] == 3
+# True if we are running on Windows
+WIN = platform.system() == 'Windows'
+
if PY3: # pragma: no cover
string_types = str,
integer_types = int,
@@ -109,3 +115,26 @@ try:
MAXINT = sys.maxint
except AttributeError: # pragma: no cover
MAXINT = sys.maxsize
+
+
+# Fix for issue reported in https://github.com/Pylons/waitress/issues/138,
+# Python on Windows may not define IPPROTO_IPV6 in socket.
+import socket
+
+HAS_IPV6 = socket.has_ipv6
+
+if hasattr(socket, 'IPPROTO_IPV6') and hasattr(socket, 'IPV6_V6ONLY'):
+ IPPROTO_IPV6 = socket.IPPROTO_IPV6
+ IPV6_V6ONLY = socket.IPV6_V6ONLY
+else: # pragma: no cover
+ if WIN:
+ IPPROTO_IPV6 = 41
+ IPV6_V6ONLY = 27
+ else:
+ warnings.warn(
+ 'OS does not support required IPv6 socket flags. This is requirement '
+ 'for Waitress. Please open an issue at https://github.com/Pylons/waitress. '
+ 'IPv6 support has been disabled.',
+ RuntimeWarning
+ )
+ HAS_IPV6 = False
diff --git a/waitress/parser.py b/waitress/parser.py
index 9962b83..fc71d68 100644
--- a/waitress/parser.py
+++ b/waitress/parser.py
@@ -182,6 +182,8 @@ class HTTPRequestParser(object):
index = line.find(b':')
if index > 0:
key = line[:index]
+ if b'_' in key:
+ continue
value = line[index + 1:].strip()
key1 = tostr(key.upper().replace(b'-', b'_'))
# If a header already exists, we append subsequent values
diff --git a/waitress/runner.py b/waitress/runner.py
index 04cd78f..abdb38e 100644
--- a/waitress/runner.py
+++ b/waitress/runner.py
@@ -42,9 +42,46 @@ Standard options:
Hostname or IP address on which to listen, default is '0.0.0.0',
which means "all IP addresses on this host".
+ Note: May not be used together with --listen
+
--port=PORT
TCP port on which to listen, default is '8080'
+ Note: May not be used together with --listen
+
+ --listen=ip:port
+ Tell waitress to listen on an ip port combination.
+
+ Example:
+
+ --listen=127.0.0.1:8080
+ --listen=[::1]:8080
+ --listen=*:8080
+
+ This option may be used multiple times to listen on multipe sockets.
+ A wildcard for the hostname is also supported and will bind to both
+ IPv4/IPv6 depending on whether they are enabled or disabled.
+
+ --[no-]ipv4
+ Toggle on/off IPv4 support.
+
+ Example:
+
+ --no-ipv4
+
+ This will disable IPv4 socket support. This affects wildcard matching
+ when generating the list of sockets.
+
+ --[no-]ipv6
+ Toggle on/off IPv6 support.
+
+ Example:
+
+ --no-ipv6
+
+ This will turn on IPv6 socket support. This affects wildcard matching
+ when generating a list of sockets.
+
--unix-socket=PATH
Path of Unix socket. If a socket path is specified, a Unix domain
socket is made instead of the usual inet domain socket.
diff --git a/waitress/server.py b/waitress/server.py
index 87338c8..d3fbd79 100644
--- a/waitress/server.py
+++ b/waitress/server.py
@@ -22,7 +22,14 @@ from waitress import trigger
from waitress.adjustments import Adjustments
from waitress.channel import HTTPChannel
from waitress.task import ThreadedTaskDispatcher
-from waitress.utilities import cleanup_unix_socket, logging_dispatcher
+from waitress.utilities import (
+ cleanup_unix_socket,
+ logging_dispatcher,
+ )
+from waitress.compat import (
+ IPPROTO_IPV6,
+ IPV6_V6ONLY,
+ )
def create_server(application,
map=None,
@@ -42,11 +49,90 @@ def create_server(application,
'to return a WSGI app within your application.'
)
adj = Adjustments(**kw)
+
+ if map is None: # pragma: nocover
+ map = {}
+
+ dispatcher = _dispatcher
+ if dispatcher is None:
+ dispatcher = ThreadedTaskDispatcher()
+ dispatcher.set_thread_count(adj.threads)
+
if adj.unix_socket and hasattr(socket, 'AF_UNIX'):
- cls = UnixWSGIServer
- else:
- cls = TcpWSGIServer
- return cls(application, map, _start, _sock, _dispatcher, adj)
+ sockinfo = (socket.AF_UNIX, socket.SOCK_STREAM, None, None)
+ return UnixWSGIServer(
+ application,
+ map,
+ _start,
+ _sock,
+ dispatcher=dispatcher,
+ adj=adj,
+ sockinfo=sockinfo)
+
+ effective_listen = []
+ last_serv = None
+ for sockinfo in adj.listen:
+ # When TcpWSGIServer is called, it registers itself in the map. This
+ # side-effect is all we need it for, so we don't store a reference to
+ # or return it to the user.
+ last_serv = TcpWSGIServer(
+ application,
+ map,
+ _start,
+ _sock,
+ dispatcher=dispatcher,
+ adj=adj,
+ sockinfo=sockinfo)
+ effective_listen.append((last_serv.effective_host, last_serv.effective_port))
+
+ # We are running a single server, so we can just return the last server,
+ # saves us from having to create one more object
+ if len(adj.listen) == 1:
+ # In this case we have no need to use a MultiSocketServer
+ return last_serv
+
+ # Return a class that has a utility function to print out the sockets it's
+ # listening on, and has a .run() function. All of the TcpWSGIServers
+ # registered themselves in the map above.
+ return MultiSocketServer(map, adj, effective_listen, dispatcher)
+
+
+# This class is only ever used if we have multiple listen sockets. It allows
+# the serve() API to call .run() which starts the asyncore loop, and catches
+# SystemExit/KeyboardInterrupt so that it can atempt to cleanly shut down.
+class MultiSocketServer(object):
+ asyncore = asyncore # test shim
+
+ def __init__(self,
+ map=None,
+ adj=None,
+ effective_listen=None,
+ dispatcher=None,
+ ):
+ self.adj = adj
+ self.map = map
+ self.effective_listen = effective_listen
+ self.task_dispatcher = dispatcher
+
+ def print_listen(self, format_str): # pragma: nocover
+ for l in self.effective_listen:
+ l = list(l)
+
+ if ':' in l[0]:
+ l[0] = '[{}]'.format(l[0])
+
+ print(format_str.format(*l))
+
+ def run(self):
+ try:
+ self.asyncore.loop(
+ timeout=self.adj.asyncore_loop_timeout,
+ map=self.map,
+ use_poll=self.adj.asyncore_use_poll,
+ )
+ except (SystemExit, KeyboardInterrupt):
+ self.task_dispatcher.shutdown()
+
class BaseWSGIServer(logging_dispatcher, object):
@@ -54,15 +140,15 @@ class BaseWSGIServer(logging_dispatcher, object):
next_channel_cleanup = 0
socketmod = socket # test shim
asyncore = asyncore # test shim
- family = None
def __init__(self,
application,
map=None,
_start=True, # test shim
_sock=None, # test shim
- _dispatcher=None, # test shim
+ dispatcher=None, # dispatcher
adj=None, # adjustments
+ sockinfo=None, # opaque object
**kw
):
if adj is None:
@@ -72,20 +158,30 @@ class BaseWSGIServer(logging_dispatcher, object):
# conflicts with apps and libs that use the asyncore global socket
# map ala https://github.com/Pylons/waitress/issues/63
map = {}
+ if sockinfo is None:
+ sockinfo = adj.listen[0]
+
+ self.sockinfo = sockinfo
+ self.family = sockinfo[0]
+ self.socktype = sockinfo[1]
self.application = application
self.adj = adj
self.trigger = trigger.trigger(map)
- if _dispatcher is None:
- _dispatcher = ThreadedTaskDispatcher()
- _dispatcher.set_thread_count(self.adj.threads)
- self.task_dispatcher = _dispatcher
+ if dispatcher is None:
+ dispatcher = ThreadedTaskDispatcher()
+ dispatcher.set_thread_count(self.adj.threads)
+
+ self.task_dispatcher = dispatcher
self.asyncore.dispatcher.__init__(self, _sock, map=map)
if _sock is None:
- self.create_socket(self.family, socket.SOCK_STREAM)
+ self.create_socket(self.family, self.socktype)
+ if self.family == socket.AF_INET6: # pragma: nocover
+ self.socket.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 1)
+
self.set_reuse_addr()
self.bind_server_socket()
self.effective_host, self.effective_port = self.getsockname()
- self.server_name = self.get_server_name(self.adj.host)
+ self.server_name = self.get_server_name(self.effective_host)
self.active_channels = {}
if _start:
self.accept_connections()
@@ -99,12 +195,13 @@ class BaseWSGIServer(logging_dispatcher, object):
server_name = str(ip)
else:
server_name = str(self.socketmod.gethostname())
+
# Convert to a host name if necessary.
for c in server_name:
if c != '.' and not c.isdigit():
return server_name
try:
- if server_name == '0.0.0.0':
+ if server_name == '0.0.0.0' or server_name == '::':
return 'localhost'
server_name = self.socketmod.gethostbyaddr(server_name)[0]
except socket.error: # pragma: no cover
@@ -186,25 +283,51 @@ class BaseWSGIServer(logging_dispatcher, object):
if (not channel.requests) and channel.last_activity < cutoff:
channel.will_close = True
-class TcpWSGIServer(BaseWSGIServer):
+ def print_listen(self, format_str): # pragma: nocover
+ print(format_str.format(self.effective_host, self.effective_port))
- family = socket.AF_INET
+
+class TcpWSGIServer(BaseWSGIServer):
def bind_server_socket(self):
- self.bind((self.adj.host, self.adj.port))
+ (_, _, _, sockaddr) = self.sockinfo
+ self.bind(sockaddr)
def getsockname(self):
- return self.socket.getsockname()
+ return self.socketmod.getnameinfo(
+ self.socket.getsockname(),
+ self.socketmod.NI_NUMERICSERV)
def set_socket_options(self, conn):
for (level, optname, value) in self.adj.socket_options:
conn.setsockopt(level, optname, value)
+
if hasattr(socket, 'AF_UNIX'):
class UnixWSGIServer(BaseWSGIServer):
- family = socket.AF_UNIX
+ def __init__(self,
+ application,
+ map=None,
+ _start=True, # test shim
+ _sock=None, # test shim
+ dispatcher=None, # dispatcher
+ adj=None, # adjustments
+ sockinfo=None, # opaque object
+ **kw):
+ if sockinfo is None:
+ sockinfo = (socket.AF_UNIX, socket.SOCK_STREAM, None, None)
+
+ super(UnixWSGIServer, self).__init__(
+ application,
+ map=map,
+ _start=_start,
+ _sock=_sock,
+ dispatcher=dispatcher,
+ adj=adj,
+ sockinfo=sockinfo,
+ **kw)
def bind_server_socket(self):
cleanup_unix_socket(self.adj.unix_socket)
diff --git a/waitress/task.py b/waitress/task.py
index 7136c32..4ce410c 100644
--- a/waitress/task.py
+++ b/waitress/task.py
@@ -358,6 +358,9 @@ class WSGITask(Task):
if not status.__class__ is str:
raise AssertionError('status %s is not a string' % status)
+ if '\n' in status or '\r' in status:
+ raise ValueError("carriage return/line "
+ "feed character present in status")
self.status = status
@@ -371,6 +374,14 @@ class WSGITask(Task):
raise AssertionError(
'Header value %r is not a string in %r' % (v, (k, v))
)
+
+ if '\n' in v or '\r' in v:
+ raise ValueError("carriage return/line "
+ "feed character present in header value")
+ if '\n' in k or '\r' in k:
+ raise ValueError("carriage return/line "
+ "feed character present in header name")
+
kl = k.lower()
if kl == 'content-length':
self.content_length = int(v)
diff --git a/waitress/tests/test_adjustments.py b/waitress/tests/test_adjustments.py
index f2b28c2..9446705 100644
--- a/waitress/tests/test_adjustments.py
+++ b/waitress/tests/test_adjustments.py
@@ -1,4 +1,10 @@
import sys
+import socket
+
+from waitress.compat import (
+ PY2,
+ WIN,
+ )
if sys.version_info[:2] == (2, 6): # pragma: no cover
import unittest2 as unittest
@@ -45,13 +51,35 @@ class Test_asbool(unittest.TestCase):
class TestAdjustments(unittest.TestCase):
+ def _hasIPv6(self): # pragma: nocover
+ if not socket.has_ipv6:
+ return False
+
+ try:
+ socket.getaddrinfo(
+ '::1',
+ 0,
+ socket.AF_UNSPEC,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP,
+ socket.AI_PASSIVE | socket.AI_ADDRCONFIG
+ )
+
+ return True
+ except socket.gaierror as e:
+ # Check to see what the error is
+ if e.errno == socket.EAI_ADDRFAMILY:
+ return False
+ else:
+ raise e
+
def _makeOne(self, **kw):
from waitress.adjustments import Adjustments
return Adjustments(**kw)
def test_goodvars(self):
inst = self._makeOne(
- host='host',
+ host='localhost',
port='8080',
threads='5',
trusted_proxy='192.168.1.1',
@@ -74,8 +102,11 @@ class TestAdjustments(unittest.TestCase):
unix_socket='/tmp/waitress.sock',
unix_socket_perms='777',
url_prefix='///foo/',
+ ipv4=True,
+ ipv6=False,
)
- self.assertEqual(inst.host, 'host')
+
+ self.assertEqual(inst.host, 'localhost')
self.assertEqual(inst.port, 8080)
self.assertEqual(inst.threads, 5)
self.assertEqual(inst.trusted_proxy, '192.168.1.1')
@@ -98,10 +129,96 @@ class TestAdjustments(unittest.TestCase):
self.assertEqual(inst.unix_socket, '/tmp/waitress.sock')
self.assertEqual(inst.unix_socket_perms, 0o777)
self.assertEqual(inst.url_prefix, '/foo')
+ self.assertEqual(inst.ipv4, True)
+ self.assertEqual(inst.ipv6, False)
+
+ bind_pairs = [
+ sockaddr[:2]
+ for (family, _, _, sockaddr) in inst.listen
+ if family == socket.AF_INET
+ ]
+
+ # On Travis, somehow we start listening to two sockets when resolving
+ # localhost...
+ self.assertEqual(('127.0.0.1', 8080), bind_pairs[0])
+
+ def test_goodvar_listen(self):
+ inst = self._makeOne(listen='127.0.0.1')
+
+ bind_pairs = [(host, port) for (_, _, _, (host, port)) in inst.listen]
+
+ self.assertEqual(bind_pairs, [('127.0.0.1', 8080)])
+
+ def test_default_listen(self):
+ inst = self._makeOne()
+
+ bind_pairs = [(host, port) for (_, _, _, (host, port)) in inst.listen]
+
+ self.assertEqual(bind_pairs, [('0.0.0.0', 8080)])
+
+ def test_multiple_listen(self):
+ inst = self._makeOne(listen='127.0.0.1:9090 127.0.0.1:8080')
+
+ bind_pairs = [sockaddr[:2] for (_, _, _, sockaddr) in inst.listen]
+
+ self.assertEqual(bind_pairs,
+ [('127.0.0.1', 9090),
+ ('127.0.0.1', 8080)])
+
+ def test_wildcard_listen(self):
+ inst = self._makeOne(listen='*:8080')
+
+ bind_pairs = [sockaddr[:2] for (_, _, _, sockaddr) in inst.listen]
+
+ self.assertTrue(len(bind_pairs) >= 1)
+
+ def test_ipv6_no_port(self): # pragma: nocover
+ if not self._hasIPv6():
+ return
+
+ inst = self._makeOne(listen='[::1]')
+
+ bind_pairs = [sockaddr[:2] for (_, _, _, sockaddr) in inst.listen]
+
+ self.assertEqual(bind_pairs, [('::1', 8080)])
+
+ def test_bad_port(self):
+ self.assertRaises(ValueError, self._makeOne, listen='127.0.0.1:test')
+
+ def test_service_port(self):
+ if WIN and PY2: # pragma: no cover
+ # On Windows and Python 2 this is broken, so we raise a ValueError
+ self.assertRaises(
+ ValueError,
+ self._makeOne,
+ listen='127.0.0.1:http',
+ )
+ return
+
+ inst = self._makeOne(listen='127.0.0.1:http 0.0.0.0:https')
+
+ bind_pairs = [sockaddr[:2] for (_, _, _, sockaddr) in inst.listen]
+
+ self.assertEqual(bind_pairs, [('127.0.0.1', 80), ('0.0.0.0', 443)])
+
+ def test_dont_mix_host_port_listen(self):
+ self.assertRaises(
+ ValueError,
+ self._makeOne,
+ host='localhost',
+ port='8080',
+ listen='127.0.0.1:8080',
+ )
def test_badvar(self):
self.assertRaises(ValueError, self._makeOne, nope=True)
+ def test_ipv4_disabled(self):
+ self.assertRaises(ValueError, self._makeOne, ipv4=False, listen="127.0.0.1:8080")
+
+ def test_ipv6_disabled(self):
+ self.assertRaises(ValueError, self._makeOne, ipv6=False, listen="[::]:8080")
+
class TestCLI(unittest.TestCase):
def parse(self, argv):
@@ -147,10 +264,31 @@ class TestCLI(unittest.TestCase):
self.assertDictContainsSubset({
'host': 'localhost',
'port': '80',
- 'unix_socket_perms':'777',
+ 'unix_socket_perms': '777',
}, opts)
self.assertSequenceEqual(args, [])
+ def test_listen_params(self):
+ opts, args = self.parse([
+ '--listen=test:80',
+ ])
+
+ self.assertDictContainsSubset({
+ 'listen': ' test:80'
+ }, opts)
+ self.assertSequenceEqual(args, [])
+
+ def test_multiple_listen_params(self):
+ opts, args = self.parse([
+ '--listen=test:80',
+ '--listen=test:8080',
+ ])
+
+ self.assertDictContainsSubset({
+ 'listen': ' test:80 test:8080'
+ }, opts)
+ self.assertSequenceEqual(args, [])
+
def test_bad_param(self):
import getopt
self.assertRaises(getopt.GetoptError, self.parse, ['--no-host'])
diff --git a/waitress/tests/test_buffers.py b/waitress/tests/test_buffers.py
index 8a4ce6e..46a215e 100644
--- a/waitress/tests/test_buffers.py
+++ b/waitress/tests/test_buffers.py
@@ -31,7 +31,7 @@ class TestFileBasedBuffer(unittest.TestCase):
inst.remain = 10
self.assertEqual(bool(inst), True)
inst.remain = 0
- self.assertEqual(bool(inst), False)
+ self.assertEqual(bool(inst), True)
def test_append(self):
f = io.BytesIO(b'data')
diff --git a/waitress/tests/test_functional.py b/waitress/tests/test_functional.py
index 020486a..59ef4e4 100644
--- a/waitress/tests/test_functional.py
+++ b/waitress/tests/test_functional.py
@@ -34,6 +34,8 @@ class FixtureTcpWSGIServer(server.TcpWSGIServer):
"""A version of TcpWSGIServer that relays back what it's bound to.
"""
+ family = socket.AF_INET # Testing
+
def __init__(self, application, queue, **kw): # pragma: no cover
# Coverage doesn't see this as it's ran in a separate process.
kw['port'] = 0 # Bind to any available port.
@@ -1386,6 +1388,8 @@ if hasattr(socket, 'AF_UNIX'):
"""A version of UnixWSGIServer that relays back what it's bound to.
"""
+ family = socket.AF_UNIX # Testing
+
def __init__(self, application, queue, **kw): # pragma: no cover
# Coverage doesn't see this as it's ran in a separate process.
# To permit parallel testing, use a PID-dependent socket.
diff --git a/waitress/tests/test_parser.py b/waitress/tests/test_parser.py
index 423d75a..781d7c7 100644
--- a/waitress/tests/test_parser.py
+++ b/waitress/tests/test_parser.py
@@ -408,9 +408,24 @@ Hello.
self.assertEqual(self.parser.headers, {
'CONTENT_LENGTH': '7',
'X_FORWARDED_FOR':
- '10.11.12.13, unknown,127.0.0.1, 255.255.255.255',
+ '10.11.12.13, unknown,127.0.0.1',
})
+ def testSpoofedHeadersDropped(self):
+ data = b"""\
+GET /foobar HTTP/8.4
+x-auth_user: bob
+content-length: 7
+
+Hello.
+"""
+ self.feed(data)
+ self.assertTrue(self.parser.completed)
+ self.assertEqual(self.parser.headers, {
+ 'CONTENT_LENGTH': '7',
+ })
+
+
class DummyBodyStream(object):
def getfile(self):
diff --git a/waitress/tests/test_server.py b/waitress/tests/test_server.py
index 0ff8871..39b90b3 100644
--- a/waitress/tests/test_server.py
+++ b/waitress/tests/test_server.py
@@ -34,10 +34,23 @@ class TestWSGIServer(unittest.TestCase):
_start=_start,
)
+ def _makeOneWithMulti(self, adj=None, _start=True,
+ app=dummy_app, listen="127.0.0.1:0 127.0.0.1:0"):
+ sock = DummySock()
+ task_dispatcher = DummyTaskDispatcher()
+ map = {}
+ from waitress.server import create_server
+ return create_server(
+ app,
+ listen=listen,
+ map=map,
+ _dispatcher=task_dispatcher,
+ _start=_start,
+ _sock=sock)
+
def test_ctor_app_is_None(self):
self.assertRaises(ValueError, self._makeOneWithMap, app=None)
-
def test_ctor_start_true(self):
inst = self._makeOneWithMap(_start=True)
self.assertEqual(inst.accepting, True)
@@ -72,6 +85,10 @@ class TestWSGIServer(unittest.TestCase):
result = inst.get_server_name('0.0.0.0')
self.assertEqual(result, 'localhost')
+ def test_get_server_multi(self):
+ inst = self._makeOneWithMulti()
+ self.assertEqual(inst.__class__.__name__, 'MultiSocketServer')
+
def test_run(self):
inst = self._makeOneWithMap(_start=False)
inst.asyncore = DummyAsyncore()
@@ -79,6 +96,13 @@ class TestWSGIServer(unittest.TestCase):
inst.run()
self.assertTrue(inst.task_dispatcher.was_shutdown)
+ def test_run_base_server(self):
+ inst = self._makeOneWithMulti(_start=False)
+ inst.asyncore = DummyAsyncore()
+ inst.task_dispatcher = DummyTaskDispatcher()
+ inst.run()
+ self.assertTrue(inst.task_dispatcher.was_shutdown)
+
def test_pull_trigger(self):
inst = self._makeOneWithMap(_start=False)
inst.trigger = DummyTrigger()
@@ -242,6 +266,16 @@ if hasattr(socket, 'AF_UNIX'):
[(inst, client, ('localhost', None), inst.adj)]
)
+ def test_creates_new_sockinfo(self):
+ from waitress.server import UnixWSGIServer
+ inst = UnixWSGIServer(
+ dummy_app,
+ unix_socket=self.unix_socket,
+ unix_socket_perms='600'
+ )
+
+ self.assertEqual(inst.sockinfo[0], socket.AF_UNIX)
+
class DummySock(object):
accepted = False
blocking = False
diff --git a/waitress/tests/test_task.py b/waitress/tests/test_task.py
index 6d6fcce..2a2759a 100644
--- a/waitress/tests/test_task.py
+++ b/waitress/tests/test_task.py
@@ -409,6 +409,27 @@ class TestWSGITask(unittest.TestCase):
inst.channel.server.application = app
self.assertRaises(AssertionError, inst.execute)
+ def test_execute_bad_header_value_control_characters(self):
+ def app(environ, start_response):
+ start_response('200 OK', [('a', '\n')])
+ inst = self._makeOne()
+ inst.channel.server.application = app
+ self.assertRaises(ValueError, inst.execute)
+
+ def test_execute_bad_header_name_control_characters(self):
+ def app(environ, start_response):
+ start_response('200 OK', [('a\r', 'value')])
+ inst = self._makeOne()
+ inst.channel.server.application = app
+ self.assertRaises(ValueError, inst.execute)
+
+ def test_execute_bad_status_control_characters(self):
+ def app(environ, start_response):
+ start_response('200 OK\r', [])
+ inst = self._makeOne()
+ inst.channel.server.application = app
+ self.assertRaises(ValueError, inst.execute)
+
def test_preserve_header_value_order(self):
def app(environ, start_response):
write = start_response('200 OK', [('C', 'b'), ('A', 'b'), ('A', 'a')])