diff options
author | Bert JW Regeer <xistence@0x58.com> | 2018-11-14 15:10:35 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-11-14 15:10:35 -0700 |
commit | ecea02ff58896ceba149dba17260b05735cbe5b4 (patch) | |
tree | df75cfb5fc2a5cd64d28180a6c8550c3fa06908c | |
parent | 86d1ad36859426853d68e12c9afb78060b873add (diff) | |
parent | 5e66b5aa0e08853c0a7230e34f318e8d7c03d56b (diff) | |
download | waitress-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.txt | 2 | ||||
-rw-r--r-- | docs/arguments.rst | 10 | ||||
-rw-r--r-- | docs/index.rst | 1 | ||||
-rw-r--r-- | docs/socket-activation.rst | 45 | ||||
-rw-r--r-- | waitress/adjustments.py | 49 | ||||
-rw-r--r-- | waitress/server.py | 60 | ||||
-rw-r--r-- | waitress/tests/test_adjustments.py | 105 | ||||
-rw-r--r-- | waitress/tests/test_server.py | 90 |
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): |