diff --git a/NEWS b/NEWS index fa927ff..7893995 100644 --- a/NEWS +++ b/NEWS @@ -6,10 +6,17 @@ NEWS ------ -0.5 - *Not released yet* +0.6 - *Not released yet* ======================== +* IPv6 support + + +0.5 - Historical Artifacts +========================== + * Calendar depth +* iPhone support * MacOS and Windows support * HEAD requests management * htpasswd user from calendar path diff --git a/TODO b/TODO index f394b8a..7ab5d0f 100644 --- a/TODO +++ b/TODO @@ -6,17 +6,12 @@ TODO ------ -0.5 -=== - -* iCal and iPhone support - - 0.6 === * [IN PROGRESS] Group calendars * [IN PROGRESS] LDAP and databases auth support +* [IN PROGRESS] Smart, verbose and configurable logs * CalDAV rights * Read-only access for foreign users diff --git a/config b/config index 6380a7f..b86a632 100644 --- a/config +++ b/config @@ -6,10 +6,11 @@ # The current values are the default ones [server] -# CalDAV server hostname, empty for all hostnames -host = -# CalDAV server port -port = 5232 +# CalDAV server hostnames separated by a comma +# IPv4 syntax: address:port +# IPv6 syntax: [address]:port +# IPv6 adresses are configured to only allow IPv6 connections +hosts = 0.0.0.0:5232 # Daemon flag daemon = False # SSL flag, enable HTTPS protocol diff --git a/radicale.py b/radicale.py index 03f6a14..bbc386f 100755 --- a/radicale.py +++ b/radicale.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # -*- coding: utf-8 -*- # # This file is part of Radicale Server - Calendar Server @@ -26,10 +26,9 @@ # pylint: disable-msg=W0406 """ -Radicale Server entry point. +Radicale CalDAV Server. -Launch the Radicale Server according to configuration and command-line -arguments. +Launch the server according to configuration and command-line options. """ @@ -38,15 +37,13 @@ arguments. import os import sys import optparse +import signal +import threading import radicale # Get command-line options -parser = optparse.OptionParser() -parser.add_option( - "-v", "--version", action="store_true", - default=False, - help="show version and exit") +parser = optparse.OptionParser(version=radicale.VERSION) parser.add_option( "-d", "--daemon", action="store_true", default=radicale.config.getboolean("server", "daemon"), @@ -55,13 +52,9 @@ parser.add_option( "-f", "--foreground", action="store_false", dest="daemon", help="launch in foreground (opposite of --daemon)") parser.add_option( - "-H", "--host", - default=radicale.config.get("server", "host"), - help="set server hostname") -parser.add_option( - "-p", "--port", type="int", - default=radicale.config.getint("server", "port"), - help="set server port") + "-H", "--hosts", + default=radicale.config.get("server", "hosts"), + help="set server hostnames and ports") parser.add_option( "-s", "--ssl", action="store_true", default=radicale.config.getboolean("server", "ssl"), @@ -72,11 +65,11 @@ parser.add_option( parser.add_option( "-k", "--key", default=radicale.config.get("server", "key"), - help="private key file ") + help="set private key file") parser.add_option( "-c", "--certificate", default=radicale.config.get("server", "certificate"), - help="certificate file ") + help="set certificate file") options = parser.parse_args()[0] # Update Radicale configuration according to options @@ -86,19 +79,42 @@ for option in parser.option_list: value = getattr(options, key) radicale.config.set("server", key, value) -# Print version and exit if the option is given -if options.version: - print(radicale.VERSION) - sys.exit() - # Fork if Radicale is launched as daemon if options.daemon: if os.fork(): sys.exit() sys.stdout = sys.stderr = open(os.devnull, "w") -# Launch calendar server +# Launch calendar servers +servers = [] server_class = radicale.HTTPSServer if options.ssl else radicale.HTTPServer -server = server_class( - (options.host, options.port), radicale.CalendarHTTPHandler) -server.serve_forever() + +def exit(): + """Cleanly shutdown servers.""" + while servers: + servers.pop().shutdown() + +def serve_forever(server): + """Serve a server forever with no traceback on keyboard interrupts.""" + try: + server.serve_forever() + except KeyboardInterrupt: + # No unwanted traceback + pass + finally: + exit() + +# Clean exit on SIGTERM +signal.signal(signal.SIGTERM, lambda *_: exit()) + +for host in options.hosts.split(','): + address, port = host.strip().rsplit(':', 1) + address, port = address.strip('[] '), int(port) + servers.append(server_class((address, port), radicale.CalendarHTTPHandler)) + +for server in servers[:-1]: + # More servers to come, launch a new thread + threading.Thread(target=serve_forever, args=(server,)).start() + +# Last server, no more thread +serve_forever(servers[-1]) diff --git a/radicale/__init__.py b/radicale/__init__.py index 5253ba5..a17efac 100644 --- a/radicale/__init__.py +++ b/radicale/__init__.py @@ -88,10 +88,25 @@ 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.""" log.log(10, "Create HTTP server.") - server.HTTPServer.__init__(self, address, handler) + ipv6 = ":" in address[0] + + if ipv6: + self.address_family = socket.AF_INET6 + + # Do not bind and activate, as we might change socketopts + server.HTTPServer.__init__(self, address, handler, False) + + if 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 @@ -100,7 +115,7 @@ 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.""" log.log(10, "Create server by wrapping HTTP socket in an SSL socket.") # Fails with Python 2.5, import if needed @@ -108,15 +123,17 @@ class HTTPSServer(HTTPServer): 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, 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): diff --git a/radicale/config.py b/radicale/config.py index 24a7cb0..5b00543 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -39,8 +39,7 @@ except ImportError: # Default configuration INITIAL_CONFIG = { "server": { - "host": "", - "port": "5232", + "hosts": "0.0.0.0:5232", "daemon": "False", "ssl": "False", "certificate": "/etc/apache2/ssl/server.crt", diff --git a/setup.py b/setup.py index 2d9b09c..fe74516 100755 --- a/setup.py +++ b/setup.py @@ -27,8 +27,8 @@ it requires few software dependances and is pre-configured to work out-of-the-box. The Radicale Project runs on most of the UNIX-like platforms (Linux, BSD, -MacOS X) and Windows. It is known to work with Evolution 2.30+, Lightning 0.9+ -and Sunbird 0.9+. It is free and open-source software, released under GPL +MacOS X) and Windows. It is known to work with Evolution, Lightning, iPhone +and Android clients. It is free and open-source software, released under GPL version 3. For further information, please visit the `Radicale Website @@ -91,4 +91,5 @@ setup( "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.0", "Programming Language :: Python :: 3.1", + "Programming Language :: Python :: 3.2", "Topic :: Office/Business :: Groupware"])