summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBert JW Regeer <xistence@0x58.com>2016-06-24 22:42:52 -0600
committerGitHub <noreply@github.com>2016-06-24 22:42:52 -0600
commit631b5fc855f5930f2766286fdf0d379a5b3973d1 (patch)
tree0e99921ad63e412e4a97f627e883a71b440b7b34
parent60be8767a1b35c5561becbaf47f93e26632779c9 (diff)
parent3e35e6f9d63143341e089bcad4746aab1d5d3bca (diff)
downloadwaitress-631b5fc855f5930f2766286fdf0d379a5b3973d1.tar.gz
Merge pull request #128 from Pylons/feature/multiple_sockets
Feature: multiple sockets
-rw-r--r--.travis.yml2
-rw-r--r--CHANGES.txt41
-rw-r--r--HISTORY.txt37
-rw-r--r--appveyor.yml12
-rw-r--r--docs/api.rst2
-rw-r--r--docs/arguments.rst32
-rw-r--r--docs/index.rst27
-rw-r--r--docs/runner.rst25
-rw-r--r--setup.py5
-rw-r--r--tox.ini3
-rw-r--r--waitress/__init__.py3
-rw-r--r--waitress/adjustments.py115
-rw-r--r--waitress/runner.py37
-rw-r--r--waitress/server.py152
-rw-r--r--waitress/tests/test_adjustments.py130
-rw-r--r--waitress/tests/test_functional.py4
-rw-r--r--waitress/tests/test_server.py36
17 files changed, 580 insertions, 83 deletions
diff --git a/.travis.yml b/.travis.yml
index 7390683..7c837bd 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,8 +4,6 @@ sudo: false
matrix:
include:
- - python: 2.6
- env: TOXENV=py26
- python: 2.7
env: TOXENV=py27
- python: 3.3
diff --git a/CHANGES.txt b/CHANGES.txt
index ba9ae51..7a16e9d 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,36 +1,21 @@
-0.9.0 (2016-04-15)
-------------------
+Unreleased
+----------
Deprecations
~~~~~~~~~~~~
-- Python 3.2 is no longer supported by Waitress.
+- Python 2.6 is no longer supported.
-- 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
+Features
~~~~~~~~
-- 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
+- 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. Using
+ the host/port combination is deprecated but will be supported for at least
+ the next 5 minor releases.
+
+ .. code-block:: python
-- 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.
+ from waitress import serve
+ serve(wsgiapp, listen='0.0.0.0:8080 [::]:9090 *:6543')
diff --git a/HISTORY.txt b/HISTORY.txt
index 176a654..9932582 100644
--- a/HISTORY.txt
+++ b/HISTORY.txt
@@ -1,3 +1,40 @@
+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)
-------------------
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000..1350507
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,12 @@
+environment:
+ matrix:
+ - PYTHON: "C:\\Python35"
+ TOXENV: "py35"
+
+install:
+ - "%PYTHON%\\python.exe -m pip install tox"
+
+build: off
+
+test_script:
+ - "%PYTHON%\\Scripts\\tox.exe"
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..d6d75ce 100644
--- a/docs/arguments.rst
+++ b/docs/arguments.rst
@@ -10,9 +10,41 @@ 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``
+
+ .. deprecated:: 1.0
+
port
TCP port (integer) on which to listen, default ``8080``
+ .. warning::
+ May not be used with ``listen``
+
+ .. deprecated:: 1.0
+
+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/index.rst b/docs/index.rst
index 6a595ad..9d8a812 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -15,18 +15,19 @@ Here's normal usage of the server:
.. code-block:: python
from waitress import serve
- serve(wsgiapp, host='0.0.0.0', port=8080)
+ serve(wsgiapp, listen='*:8080')
-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:
+This will run waitress on port 8080 on all available IP addresses, both IPv4
+and IPv6.
+
+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)
-
-Press Ctrl-C (or Ctrl-Break on Windows) to exit the server.
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``
@@ -49,8 +50,7 @@ lets you use Waitress's WSGI gateway from a configuration file, e.g.:
[server:main]
use = egg:waitress#main
- host = 127.0.0.1
- port = 8080
+ listen = 127.0.0.1:8080
The :term:`PasteDeploy` syntax for UNIX domain sockets is analagous:
@@ -67,7 +67,7 @@ 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::
- waitress-serve --port=8041 myapp:wsgifunc
+ waitress-serve --listen=*:8041 myapp:wsgifunc
For more information on this, see :ref:`runner`.
@@ -153,7 +153,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 +188,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 +257,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'
@@ -287,8 +286,6 @@ Change History
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/setup.py b/setup.py
index d458267..7c6c564 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,9 +32,6 @@ testing_extras = [
'coverage',
]
-if sys.version_info[:2] == (2, 6):
- testing_extras.append('unittest2')
-
setup(
name='waitress',
version='0.9.1dev0',
@@ -54,7 +50,6 @@ 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.3',
diff --git a/tox.ini b/tox.ini
index 030bf7a..4864c46 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
[tox]
envlist =
- py26,py27,py33,py34,py35,pypy,pypy3,
+ py27,py33,py34,py35,pypy,pypy3,
docs,
{py2,py3}-cover,coverage
@@ -8,7 +8,6 @@ envlist =
# Most of these are defaults but if you specify any you can't fall back
# to defaults for others.
basepython =
- py26: python2.6
py27: python2.7
py33: python3.3
py34: python3.4
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..f86ece5 100644
--- a/waitress/adjustments.py
+++ b/waitress/adjustments.py
@@ -15,7 +15,8 @@
"""
import getopt
import socket
-import sys
+
+from waitress.compat import string_types
truthy = frozenset(('t', 'true', 'y', 'yes', 'on', '1'))
@@ -36,6 +37,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 +61,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 +74,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 +103,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 +202,82 @@ 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 self.ipv4 and not self.ipv6:
+ enabled_families = socket.AF_INET
+
+ if not self.ipv4 and self.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))
+
+ 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 | socket.AI_ADDRCONFIG
+ ):
+ (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 +299,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 +317,5 @@ class Adjustments(object):
kw[param] = 'true'
else:
kw[param] = value
+
return kw, args
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..7134f86 100644
--- a/waitress/server.py
+++ b/waitress/server.py
@@ -42,11 +42,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 +133,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 +151,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(socket.IPPROTO_IPV6, socket.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 +188,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 +276,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/tests/test_adjustments.py b/waitress/tests/test_adjustments.py
index f2b28c2..5c8985a 100644
--- a/waitress/tests/test_adjustments.py
+++ b/waitress/tests/test_adjustments.py
@@ -1,4 +1,5 @@
import sys
+import socket
if sys.version_info[:2] == (2, 6): # pragma: no cover
import unittest2 as unittest
@@ -45,13 +46,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 +97,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 +124,87 @@ 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):
+ 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 +250,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_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_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