summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBert JW Regeer <xistence@0x58.com>2018-11-14 15:10:35 -0700
committerGitHub <noreply@github.com>2018-11-14 15:10:35 -0700
commitecea02ff58896ceba149dba17260b05735cbe5b4 (patch)
treedf75cfb5fc2a5cd64d28180a6c8550c3fa06908c
parent86d1ad36859426853d68e12c9afb78060b873add (diff)
parent5e66b5aa0e08853c0a7230e34f318e8d7c03d56b (diff)
downloadwaitress-ecea02ff58896ceba149dba17260b05735cbe5b4.tar.gz
Merge pull request #215 from Frank-Krick/feature/allow_passing_of_sockets_for_socket_activation
Allow passing of sockets for socket activation
-rw-r--r--CONTRIBUTORS.txt2
-rw-r--r--docs/arguments.rst10
-rw-r--r--docs/index.rst1
-rw-r--r--docs/socket-activation.rst45
-rw-r--r--waitress/adjustments.py49
-rw-r--r--waitress/server.py60
-rw-r--r--waitress/tests/test_adjustments.py105
-rw-r--r--waitress/tests/test_server.py90
8 files changed, 343 insertions, 19 deletions
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 40b7960..32ea0ba 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -142,3 +142,5 @@ Contributors
- David D Lowe, 2017-06-02
- Jack Wearden, 2018-05-18
+
+- Frank Krick, 2018-10-29
diff --git a/docs/arguments.rst b/docs/arguments.rst
index dfdbc53..690daef 100644
--- a/docs/arguments.rst
+++ b/docs/arguments.rst
@@ -51,6 +51,16 @@ unix_socket_perms
Octal permissions to use for the Unix domain socket (string), default is
``600``. Only used if ``unix_socket`` is not ``None``.
+sockets
+ .. versionadded:: 1.1.1
+ A list of sockets. The sockets can be either Internet or UNIX sockets and have
+ to be bound. Internet and UNIX sockets cannot be mixed.
+ If the socket list is not empty, waitress creates one server for each socket.
+ Default is ``[]``.
+
+ .. warning::
+ May not be used with ``listen``, ``host``, ``port`` or ``unix_socket``
+
threads
number of threads used to process application logic (integer), default
``4``
diff --git a/docs/index.rst b/docs/index.rst
index e016284..1eaa42b 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -26,6 +26,7 @@ Extended Documentation
arguments
filewrapper
runner
+ socket-activation
glossary
Change History
diff --git a/docs/socket-activation.rst b/docs/socket-activation.rst
new file mode 100644
index 0000000..88fd021
--- /dev/null
+++ b/docs/socket-activation.rst
@@ -0,0 +1,45 @@
+Socket Activation
+-----------------
+
+While waitress does not support the various implementations of socket activation,
+for example using systemd or launchd, it is prepared to receive pre-bound sockets
+from init systems, process and socket managers, or other launchers that can provide
+pre-bound sockets.
+
+The following shows a code example starting waitress with two Internet sockets pre-bound sockets.
+
+.. code-block:: python
+
+ import socket
+ import waitress
+
+
+ def app(environ, start_response):
+ content_length = environ.get('CONTENT_LENGTH', None)
+ if content_length is not None:
+ content_length = int(content_length)
+ body = environ['wsgi.input'].read(content_length)
+ content_length = str(len(body))
+ start_response(
+ '200 OK',
+ [('Content-Length', content_length), ('Content-Type', 'text/plain')]
+ )
+ return [body]
+
+
+ if __name__ == '__main__':
+ sockets = [
+ socket.socket(socket.AF_INET, socket.SOCK_STREAM),
+ socket.socket(socket.AF_INET, socket.SOCK_STREAM)]
+ sockets[0].bind(('127.0.0.1', 8080))
+ sockets[1].bind(('127.0.0.1', 9090))
+ waitress.serve(app, sockets=sockets)
+ for socket in sockets:
+ socket.close()
+
+Generally, to implement socket activation for a given init system, a wrapper
+script uses the init system specific libraries to retrieve the sockets from
+the init system. Afterwards it starts waitress, passing the sockets with the parameter
+``sockets``. Note that the sockets have to be bound, which all init systems
+supporting socket activation do.
+
diff --git a/waitress/adjustments.py b/waitress/adjustments.py
index ec2fb9e..ad2d0f4 100644
--- a/waitress/adjustments.py
+++ b/waitress/adjustments.py
@@ -69,6 +69,11 @@ def slash_fixed_str(s):
def str_iftruthy(s):
return str(s) if s else None
+def as_socket_list(sockets):
+ """Checks if the elements in the list are of type socket and
+ removes them if not."""
+ return [sock for sock in sockets if isinstance(sock, socket.socket)]
+
class _str_marker(str):
pass
@@ -106,6 +111,7 @@ class Adjustments(object):
('asyncore_use_poll', asbool),
('unix_socket', str),
('unix_socket_perms', asoctal),
+ ('sockets', as_socket_list),
)
_param_map = dict(_params)
@@ -216,10 +222,29 @@ class Adjustments(object):
# Enable IPv6 by default
ipv6 = True
+ # A list of sockets that waitress will use to accept connections. They can
+ # be used for e.g. socket activation
+ sockets = []
+
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.')
+ raise ValueError('host or port may not be set if listen is set.')
+
+ if 'listen' in kw and 'sockets' in kw:
+ raise ValueError('socket may not be set if listen is set.')
+
+ if 'sockets' in kw and ('host' in kw or 'port' in kw):
+ raise ValueError('host or port may not be set if sockets is set.')
+
+ if 'sockets' in kw and 'unix_socket' in kw:
+ raise ValueError('unix_socket may not be set if sockets is set')
+
+ if 'unix_socket' in kw and ('host' in kw or 'port' in kw):
+ raise ValueError('unix_socket may not be set if host or port is set')
+
+ if 'unix_socket' in kw and 'listen' in kw:
+ raise ValueError('unix_socket may not be set if listen is set')
for k, v in kw.items():
if k not in self._param_map:
@@ -301,6 +326,8 @@ class Adjustments(object):
self.listen = wanted_sockets
+ self.check_sockets(self.sockets)
+
@classmethod
def parse_args(cls, argv):
"""Pre-parse command line arguments for input into __init__. Note that
@@ -341,3 +368,23 @@ class Adjustments(object):
kw[param] = value
return kw, args
+
+ @classmethod
+ def check_sockets(cls, sockets):
+ has_unix_socket = False
+ has_inet_socket = False
+ has_unsupported_socket = False
+ for sock in sockets:
+ if (sock.family == socket.AF_INET or sock.family == socket.AF_INET6) and \
+ sock.type == socket.SOCK_STREAM:
+ has_inet_socket = True
+ elif hasattr(socket, 'AF_UNIX') and \
+ sock.family == socket.AF_UNIX and \
+ sock.type == socket.SOCK_STREAM:
+ has_unix_socket = True
+ else:
+ has_unsupported_socket = True
+ if has_unix_socket and has_inet_socket:
+ raise ValueError('Internet and UNIX sockets may not be mixed.')
+ if has_unsupported_socket:
+ raise ValueError('Only Internet or UNIX stream sockets may be used.')
diff --git a/waitress/server.py b/waitress/server.py
index 198d5fa..7ef930e 100644
--- a/waitress/server.py
+++ b/waitress/server.py
@@ -69,23 +69,49 @@ def create_server(application,
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))
+ if not adj.sockets:
+ 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))
+
+ for sock in adj.sockets:
+ sockinfo = (sock.family, sock.type, sock.proto, sock.getsockname())
+ if sock.family == socket.AF_INET or sock.family == socket.AF_INET6:
+ last_serv = TcpWSGIServer(
+ application,
+ map,
+ _start,
+ sock,
+ dispatcher=dispatcher,
+ adj=adj,
+ bind_socket=False,
+ sockinfo=sockinfo)
+ effective_listen.append((last_serv.effective_host, last_serv.effective_port))
+ elif hasattr(socket, 'AF_UNIX') and sock.family == socket.AF_UNIX:
+ last_serv = UnixWSGIServer(
+ application,
+ map,
+ _start,
+ sock,
+ dispatcher=dispatcher,
+ adj=adj,
+ bind_socket=False,
+ 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:
+ if len(effective_listen) == 1:
# In this case we have no need to use a MultiSocketServer
return last_serv
@@ -151,6 +177,7 @@ class BaseWSGIServer(wasyncore.dispatcher, object):
dispatcher=None, # dispatcher
adj=None, # adjustments
sockinfo=None, # opaque object
+ bind_socket=True,
**kw
):
if adj is None:
@@ -181,7 +208,10 @@ class BaseWSGIServer(wasyncore.dispatcher, object):
self.socket.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 1)
self.set_reuse_addr()
- self.bind_server_socket()
+
+ if bind_socket:
+ self.bind_server_socket()
+
self.effective_host, self.effective_port = self.getsockname()
self.server_name = self.get_server_name(self.effective_host)
self.active_channels = {}
diff --git a/waitress/tests/test_adjustments.py b/waitress/tests/test_adjustments.py
index 09c60ef..05b4dbd 100644
--- a/waitress/tests/test_adjustments.py
+++ b/waitress/tests/test_adjustments.py
@@ -49,6 +49,31 @@ class Test_asbool(unittest.TestCase):
result = self._callFUT(1)
self.assertEqual(result, True)
+class Test_as_socket_list(unittest.TestCase):
+
+ def test_only_sockets_in_list(self):
+ from waitress.adjustments import as_socket_list
+ sockets = [
+ socket.socket(socket.AF_INET, socket.SOCK_STREAM),
+ socket.socket(socket.AF_INET6, socket.SOCK_STREAM)]
+ if hasattr(socket, 'AF_UNIX'):
+ sockets.append(socket.socket(socket.AF_UNIX, socket.SOCK_STREAM))
+ new_sockets = as_socket_list(sockets)
+ self.assertEqual(sockets, new_sockets)
+ for sock in sockets:
+ sock.close()
+
+ def test_not_only_sockets_in_list(self):
+ from waitress.adjustments import as_socket_list
+ sockets = [
+ socket.socket(socket.AF_INET, socket.SOCK_STREAM),
+ socket.socket(socket.AF_INET6, socket.SOCK_STREAM),
+ {'something': 'else'}]
+ new_sockets = as_socket_list(sockets)
+ self.assertEqual(new_sockets, [sockets[0], sockets[1]])
+ for sock in [sock for sock in sockets if isinstance(sock, socket.socket)]:
+ sock.close()
+
class TestAdjustments(unittest.TestCase):
def _hasIPv6(self): # pragma: nocover
@@ -99,7 +124,6 @@ class TestAdjustments(unittest.TestCase):
ident='abc',
asyncore_loop_timeout='5',
asyncore_use_poll=True,
- unix_socket='/tmp/waitress.sock',
unix_socket_perms='777',
url_prefix='///foo/',
ipv4=True,
@@ -126,7 +150,6 @@ class TestAdjustments(unittest.TestCase):
self.assertEqual(inst.asyncore_loop_timeout, 5)
self.assertEqual(inst.asyncore_use_poll, True)
self.assertEqual(inst.ident, 'abc')
- 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)
@@ -210,6 +233,66 @@ class TestAdjustments(unittest.TestCase):
listen='127.0.0.1:8080',
)
+ def test_good_sockets(self):
+ sockets = [
+ socket.socket(socket.AF_INET6, socket.SOCK_STREAM),
+ socket.socket(socket.AF_INET, socket.SOCK_STREAM)]
+ inst = self._makeOne(sockets=sockets)
+ self.assertEqual(inst.sockets, sockets)
+ sockets[0].close()
+ sockets[1].close()
+
+ def test_dont_mix_sockets_and_listen(self):
+ sockets = [socket.socket(socket.AF_INET, socket.SOCK_STREAM)]
+ self.assertRaises(
+ ValueError,
+ self._makeOne,
+ listen='127.0.0.1:8080',
+ sockets=sockets)
+ sockets[0].close()
+
+ def test_dont_mix_sockets_and_host_port(self):
+ sockets = [socket.socket(socket.AF_INET, socket.SOCK_STREAM)]
+ self.assertRaises(
+ ValueError,
+ self._makeOne,
+ host='localhost',
+ port='8080',
+ sockets=sockets)
+ sockets[0].close()
+
+ def test_dont_mix_sockets_and_unix_socket(self):
+ sockets = [socket.socket(socket.AF_INET, socket.SOCK_STREAM)]
+ self.assertRaises(
+ ValueError,
+ self._makeOne,
+ unix_socket='./tmp/test',
+ sockets=sockets)
+ sockets[0].close()
+
+ def test_dont_mix_unix_socket_and_host_port(self):
+ self.assertRaises(
+ ValueError,
+ self._makeOne,
+ unix_socket='./tmp/test',
+ host='localhost',
+ port='8080')
+
+ def test_dont_mix_unix_socket_and_listen(self):
+ self.assertRaises(
+ ValueError,
+ self._makeOne,
+ unix_socket='./tmp/test',
+ listen='127.0.0.1:8080')
+
+ def test_dont_use_unsupported_socket_types(self):
+ sockets = [socket.socket(socket.AF_INET, socket.SOCK_DGRAM)]
+ self.assertRaises(
+ ValueError,
+ self._makeOne,
+ sockets=sockets)
+ sockets[0].close()
+
def test_badvar(self):
self.assertRaises(ValueError, self._makeOne, nope=True)
@@ -303,3 +386,21 @@ class TestCLI(unittest.TestCase):
def test_bad_param(self):
import getopt
self.assertRaises(getopt.GetoptError, self.parse, ['--no-host'])
+
+
+if hasattr(socket, 'AF_UNIX'):
+ class TestUnixSocket(unittest.TestCase):
+ def _makeOne(self, **kw):
+ from waitress.adjustments import Adjustments
+ return Adjustments(**kw)
+
+ def test_dont_mix_internet_and_unix_sockets(self):
+ sockets = [
+ socket.socket(socket.AF_INET, socket.SOCK_STREAM),
+ socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)]
+ self.assertRaises(
+ ValueError,
+ self._makeOne,
+ sockets=sockets)
+ sockets[0].close()
+ sockets[1].close()
diff --git a/waitress/tests/test_server.py b/waitress/tests/test_server.py
index 0aa217a..7cd6345 100644
--- a/waitress/tests/test_server.py
+++ b/waitress/tests/test_server.py
@@ -50,6 +50,21 @@ class TestWSGIServer(unittest.TestCase):
_sock=sock)
return self.inst
+ def _makeWithSockets(self, application=dummy_app, _dispatcher=None, map=None,
+ _start=True, _sock=None, _server=None, sockets=None):
+ from waitress.server import create_server
+ _sockets = []
+ if sockets is not None:
+ _sockets = sockets
+ self.inst = create_server(
+ application,
+ map=map,
+ _dispatcher=_dispatcher,
+ _start=_start,
+ _sock=_sock,
+ sockets=_sockets)
+ return self.inst
+
def tearDown(self):
if self.inst is not None:
self.inst.close()
@@ -237,6 +252,47 @@ class TestWSGIServer(unittest.TestCase):
self.assertNotEqual(Adjustments.port, 1234)
self.assertEqual(self.inst.adj.port, 1234)
+ def test_create_with_one_tcp_socket(self):
+ from waitress.server import TcpWSGIServer
+ sockets = [socket.socket(socket.AF_INET, socket.SOCK_STREAM)]
+ sockets[0].bind(('127.0.0.1', 0))
+ inst = self._makeWithSockets(_start=False, sockets=sockets)
+ self.assertTrue(isinstance(inst, TcpWSGIServer))
+
+ def test_create_with_multiple_tcp_sockets(self):
+ from waitress.server import MultiSocketServer
+ sockets = [
+ socket.socket(socket.AF_INET, socket.SOCK_STREAM),
+ socket.socket(socket.AF_INET, socket.SOCK_STREAM)]
+ sockets[0].bind(('127.0.0.1', 0))
+ sockets[1].bind(('127.0.0.1', 0))
+ inst = self._makeWithSockets(_start=False, sockets=sockets)
+ self.assertTrue(isinstance(inst, MultiSocketServer))
+ self.assertEqual(len(inst.effective_listen), 2)
+
+ def test_create_with_one_socket_should_not_bind_socket(self):
+ innersock = DummySock()
+ sockets = [DummySock(acceptresult=(innersock, None))]
+ sockets[0].bind(('127.0.0.1', 80))
+ sockets[0].bind_called = False
+ inst = self._makeWithSockets(_start=False, sockets=sockets)
+ self.assertEqual(inst.socket.bound, ('127.0.0.1', 80))
+ self.assertFalse(inst.socket.bind_called)
+
+ def test_create_with_one_socket_handle_accept_noerror(self):
+ innersock = DummySock()
+ sockets = [DummySock(acceptresult=(innersock, None))]
+ sockets[0].bind(('127.0.0.1', 80))
+ inst = self._makeWithSockets(sockets=sockets)
+ L = []
+ inst.channel_class = lambda *arg, **kw: L.append(arg)
+ inst.adj = DummyAdj
+ inst.handle_accept()
+ self.assertEqual(sockets[0].accepted, True)
+ self.assertEqual(innersock.opts, [('level', 'optname', 'value')])
+ self.assertEqual(L, [(inst, innersock, None, inst.adj)])
+
+
if hasattr(socket, 'AF_UNIX'):
class TestUnixWSGIServer(unittest.TestCase):
@@ -255,6 +311,21 @@ if hasattr(socket, 'AF_UNIX'):
)
return self.inst
+ def _makeWithSockets(self, application=dummy_app, _dispatcher=None, map=None,
+ _start=True, _sock=None, _server=None, sockets=None):
+ from waitress.server import create_server
+ _sockets = []
+ if sockets is not None:
+ _sockets = sockets
+ self.inst = create_server(
+ application,
+ map=map,
+ _dispatcher=_dispatcher,
+ _start=_start,
+ _sock=_sock,
+ sockets=_sockets)
+ return self.inst
+
def tearDown(self):
self.inst.close()
@@ -297,18 +368,35 @@ if hasattr(socket, 'AF_UNIX'):
self.assertEqual(self.inst.sockinfo[0], socket.AF_UNIX)
-class DummySock(object):
+ def test_create_with_unix_socket(self):
+ from waitress.server import MultiSocketServer, BaseWSGIServer, \
+ TcpWSGIServer, UnixWSGIServer
+ sockets = [
+ socket.socket(socket.AF_UNIX, socket.SOCK_STREAM),
+ socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)]
+ inst = self._makeWithSockets(sockets=sockets, _start=False)
+ self.assertTrue(isinstance(inst, MultiSocketServer))
+ server = list(filter(lambda s: isinstance(s, BaseWSGIServer), inst.map.values()))
+ self.assertTrue(isinstance(server[0], UnixWSGIServer))
+ self.assertTrue(isinstance(server[1], UnixWSGIServer))
+
+
+class DummySock(socket.socket):
accepted = False
blocking = False
family = socket.AF_INET
+ type = socket.SOCK_STREAM
+ proto = 0
def __init__(self, toraise=None, acceptresult=(None, None)):
self.toraise = toraise
self.acceptresult = acceptresult
self.bound = None
self.opts = []
+ self.bind_called = False
def bind(self, addr):
+ self.bind_called = True
self.bound = addr
def accept(self):