diff --git a/config b/config index d04b3da..04f47c8 100644 --- a/config +++ b/config @@ -7,6 +7,10 @@ [server] # CalDAV server hostname, empty for all hostnames +# if you want to bind to multiple interfaces, seperate them with a comma +# NOTE: IPv6 adresses are configured to only allow IPv6 connections +# hence for binding to all IPv4 and IPv6 interfaces: +# host = ::, 0.0.0.0 host = # CalDAV server port port = 5232 diff --git a/radicale.py b/radicale.py index 0729334..5e183e7 100755 --- a/radicale.py +++ b/radicale.py @@ -38,6 +38,7 @@ arguments. import os import sys import optparse +import threading, signal import radicale @@ -97,8 +98,42 @@ if options.daemon: sys.exit() sys.stdout = sys.stderr = open(os.devnull, "w") +def exit (servers): + """Cleanly shutdown all servers. + + Might be called multiple times.""" + for s in servers: + s.shutdown() + # Launch calendar server server_class = radicale.HTTPSServer if options.ssl else radicale.HTTPServer -server = server_class( - (options.host, options.port), radicale.CalendarHTTPHandler) -server.serve_forever() +servers = [] +threads = [] + +for host in (x.strip() for x in options.host.split(',')): + try: + server = server_class( + (host, options.port), radicale.CalendarHTTPHandler) + servers.append(server) + + t = threading.Thread(target = server.serve_forever) + threads.append(t) + t.start() + except: + exit(servers) + raise + +# clean exit on SIGTERM +signal.signal(signal.SIGTERM, lambda *a: exit(servers)) + +try: + while threads: + threads[0].join(1) # try one second + if threading.active_count() <= len(threads): + # at least one thread died -- exit all + break +except KeyboardInterrupt: + # no unwanted traceback :) + pass +finally: + exit(servers) diff --git a/radicale/__init__.py b/radicale/__init__.py index 905257b..6e5865c 100644 --- a/radicale/__init__.py +++ b/radicale/__init__.py @@ -85,9 +85,24 @@ class HTTPServer(server.HTTPServer): # Maybe a Pylint bug, ``__init__`` calls ``server.HTTPServer.__init__`` # pylint: disable=W0231 - def __init__(self, address, handler): + def __init__(self, address, handler, bind_and_activate = True): """Create server.""" - server.HTTPServer.__init__(self, address, handler) + self.use_ipv6 = ':' in address[0] + + if self.use_ipv6: + self.address_family = socket.AF_INET6 + + # call superclass, but do NOT bind and activate, as we might change socketopts + server.HTTPServer.__init__(self, address, handler, bind_and_activate = False) + + if self.use_ipv6: + # only allow IPv6 connections to the IPv6 socket + self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) + + if bind_and_activate: + self.server_bind() + self.server_activate() + self.acl = acl.load() # pylint: enable=W0231 @@ -96,22 +111,24 @@ class HTTPSServer(HTTPServer): """HTTPS server.""" PROTOCOL = "https" - def __init__(self, address, handler): + def __init__(self, address, handler, bind_and_activate = True): """Create server by wrapping HTTP socket in an SSL socket.""" # Fails with Python 2.5, import if needed # pylint: disable=F0401 import ssl # pylint: enable=F0401 - HTTPServer.__init__(self, address, handler) + HTTPServer.__init__(self, address, handler, False) self.socket = ssl.wrap_socket( - socket.socket(self.address_family, self.socket_type), + self.socket, # we can use this, it is not bound yet server_side=True, certfile=config.get("server", "certificate"), keyfile=config.get("server", "key"), ssl_version=ssl.PROTOCOL_SSLv23) - self.server_bind() - self.server_activate() + + if bind_and_activate: + self.server_bind() + self.server_activate() class CalendarHTTPHandler(server.BaseHTTPRequestHandler):