diff --git a/NEWS b/NEWS index 7893995..5eb6802 100644 --- a/NEWS +++ b/NEWS @@ -10,6 +10,7 @@ ======================== * IPv6 support +* Smart, verbose and configurable logs 0.5 - Historical Artifacts diff --git a/TODO b/TODO index 7ab5d0f..3c32723 100644 --- a/TODO +++ b/TODO @@ -11,7 +11,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 b86a632..35dd3f2 100644 --- a/config +++ b/config @@ -28,8 +28,13 @@ stock = utf-8 [acl] # Access method +<<<<<<< HEAD # Value: fake | htpasswd | authLdap type = fake +======= +# Value: None | htpasswd +type = None +>>>>>>> d9ea784e31687b03f1451bc5b543122f05c5deb1 # Personal calendars only available for logged in users (if needed) personal = False # Htpasswd filename (if needed) @@ -53,6 +58,7 @@ LDAPAppend = ou=users,dc=exmaple,dc=dom # created if not present folder = ~/.config/radicale/calendars +<<<<<<< HEAD [Logging] # Logging type # Value: syslog | file | stdout @@ -61,5 +67,15 @@ type = file logfile = ~/.config/radicale/radicale.log # Log facility 10: DEBUG, 20: INFO, 30 WARNING, 40 ERROR, 50 CRITICAL facility = 50 +======= +[logging] +# Logging configuration file +# If no config is given, simple information is printed on the standard output +# For more information about the syntax of the configuration file, see: +# http://docs.python.org/library/logging.config.html +config = /etc/radicale/logging +# Set the default logging level to debug +debug = False +>>>>>>> d9ea784e31687b03f1451bc5b543122f05c5deb1 # vim:ft=cfg diff --git a/radicale.py b/radicale.py index 507dff7..15f4ee6 100755 --- a/radicale.py +++ b/radicale.py @@ -32,8 +32,6 @@ Launch the server according to configuration and command-line options. """ -# TODO: Manage smart and configurable logs - import os import sys import optparse @@ -70,14 +68,22 @@ parser.add_option( "-c", "--certificate", default=radicale.config.get("server", "certificate"), help="set certificate file") +parser.add_option( + "-D", "--debug", action="store_true", + default=radicale.config.getboolean("logging", "debug"), + help="print debug information") options = parser.parse_args()[0] # Update Radicale configuration according to options for option in parser.option_list: key = option.dest if key: + section = "logging" if key == "debug" else "server" value = getattr(options, key) - radicale.config.set("server", key, value) + radicale.config.set(section, key, value) + +# Start logging +radicale.log.start(options.debug) # Fork if Radicale is launched as daemon if options.daemon: @@ -85,6 +91,8 @@ if options.daemon: sys.exit() sys.stdout = sys.stderr = open(os.devnull, "w") +radicale.log.LOGGER.info("Starting Radicale") + # Create calendar servers servers = [] server_class = radicale.HTTPSServer if options.ssl else radicale.HTTPServer @@ -110,6 +118,10 @@ def serve_forever(server): # a server exists but another server is added to the list at the same time for server in servers: threading.Thread(target=serve_forever, args=(server,)).start() + radicale.log.LOGGER.debug( + "Listening to %s port %s" % (server.server_name, server.server_port)) + +radicale.log.LOGGER.debug("Radicale server ready") # Main loop: wait until all servers are exited try: @@ -126,5 +138,10 @@ finally: signal.signal(signal.SIGINT, signal.SIG_IGN) signal.signal(signal.SIGTERM, signal.SIG_IGN) + radicale.log.LOGGER.info("Stopping Radicale") + for server in servers: + radicale.log.LOGGER.debug( + "Closing server listening to %s port %s" % ( + server.server_name, server.server_port)) server.shutdown() diff --git a/radicale/__init__.py b/radicale/__init__.py index 2f27d0a..ec28966 100644 --- a/radicale/__init__.py +++ b/radicale/__init__.py @@ -46,21 +46,35 @@ except ImportError: import BaseHTTPServer as server # pylint: enable=F0401 +<<<<<<< HEAD from radicale import acl, config, ical, xmlutils, log +======= +from radicale import acl, config, ical, log, xmlutils +>>>>>>> d9ea784e31687b03f1451bc5b543122f05c5deb1 VERSION = "git" +# Decorators can access ``request`` protected functions +# pylint: disable=W0212 + def _check(request, function): """Check if user has sufficient rights for performing ``request``.""" +<<<<<<< HEAD log.log(10, "Check if user has sufficient rights for performing ``request`` %s." % (request.command)) # ``_check`` decorator can access ``request`` protected functions # pylint: disable=W0212 # If we have no calendar, don't check rights if not request._calendar: +======= + # If we have no calendar or no acl, don't check rights + if not request._calendar or not request.server.acl: +>>>>>>> d9ea784e31687b03f1451bc5b543122f05c5deb1 return function(request) + log.LOGGER.info("Checking rights for %s" % request._calendar.owner) + authorization = request.headers.get("Authorization", None) if authorization: challenge = authorization.lstrip("Basic").strip().encode("ascii") @@ -72,6 +86,7 @@ def _check(request, function): if request.server.acl.has_right(request._calendar.owner, user, password): log.log(20, "Sufficient rights for performing ``request`` %s." % (request.command)) function(request) + log.LOGGER.info("%s allowed" % request._calendar.owner) else: log.log(40, "No sufficient rights for performing ``request``.") request.send_response(client.UNAUTHORIZED) @@ -79,7 +94,24 @@ def _check(request, function): "WWW-Authenticate", "Basic realm=\"Radicale Server - Password Required\"") request.end_headers() - # pylint: enable=W0212 + log.LOGGER.info("%s refused" % request._calendar.owner) + +def _log_request_content(request, function): + """Log the content of the request and store it in the request object.""" + log.LOGGER.info( + "%s request at %s recieved from %s" % ( + request.command, request.path, request.client_address[0])) + + content_length = int(request.headers.get("Content-Length", 0)) + if content_length: + request._content = request.rfile.read(content_length) + log.LOGGER.debug("Request content:\n%s" % request._content) + else: + request._content = None + + return function(request) + +# pylint: enable=W0212 class HTTPServer(server.HTTPServer): @@ -142,8 +174,19 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler): log.log(10, "HTTP requests handler for calendars.") _encoding = config.get("encoding", "request") - # Decorator checking rights before performing request + # Request handlers decorators check_rights = lambda function: lambda request: _check(request, function) + log_request_content = \ + lambda function: lambda request: _log_request_content(request, function) + + # Maybe a Pylint bug, ``__init__`` calls ``server.HTTPServer.__init__`` + # pylint: disable=W0231 + def __init__(self, request, client_address, http_server): + self._content = None + self._answer = None + server.BaseHTTPRequestHandler.__init__( + self, request, client_address, http_server) + # pylint: enable=W0231 @property def _calendar(self): @@ -180,9 +223,13 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler): pass raise UnicodeDecodeError + def log_message(self, *args, **kwargs): + """Disable inner logging management.""" + # Naming methods ``do_*`` is OK here # pylint: disable=C0103 + @log_request_content def do_GET(self): """Manage GET request.""" log.log(10, "Manage GET request.") @@ -190,6 +237,7 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler): if self._answer: self.wfile.write(self._answer) + @log_request_content @check_rights def do_HEAD(self): """Manage HEAD request.""" @@ -221,6 +269,7 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler): self.send_header("ETag", etag) self.end_headers() + @log_request_content @check_rights def do_DELETE(self): """Manage DELETE request.""" @@ -238,12 +287,14 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler): # No item or ETag precondition not verified, do not delete item self.send_response(client.PRECONDITION_FAILED) + @log_request_content @check_rights def do_MKCALENDAR(self): """Manage MKCALENDAR request.""" self.send_response(client.CREATED) self.end_headers() + @log_request_content def do_OPTIONS(self): """Manage OPTIONS request.""" log.log(10, "Manage OPTIONS request.") @@ -254,12 +305,16 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler): self.send_header("DAV", "1, calendar-access") self.end_headers() + @log_request_content def do_PROPFIND(self): """Manage PROPFIND request.""" +<<<<<<< HEAD log.log(10, "Manage PROPFIND request.") xml_request = self.rfile.read(int(self.headers["Content-Length"])) +======= +>>>>>>> d9ea784e31687b03f1451bc5b543122f05c5deb1 self._answer = xmlutils.propfind( - self.path, xml_request, self._calendar, + self.path, self._content, self._calendar, self.headers.get("depth", "infinity")) self.send_response(client.MULTI_STATUS) @@ -269,6 +324,7 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler): self.end_headers() self.wfile.write(self._answer) + @log_request_content @check_rights def do_PUT(self): """Manage PUT request.""" @@ -281,8 +337,7 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler): # Case 1: No item and no ETag precondition: Add new item # Case 2: Item and ETag precondition verified: Modify item # Case 3: Item and no Etag precondition: Force modifying item - ical_request = self._decode( - self.rfile.read(int(self.headers["Content-Length"]))) + ical_request = self._decode(self._content) xmlutils.put(self.path, ical_request, self._calendar) etag = self._calendar.get_item(item_name).etag @@ -293,12 +348,17 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler): # PUT rejected in all other cases self.send_response(client.PRECONDITION_FAILED) + @log_request_content @check_rights def do_REPORT(self): """Manage REPORT request.""" +<<<<<<< HEAD log.log(10, "Manage REPORT request.") xml_request = self.rfile.read(int(self.headers["Content-Length"])) self._answer = xmlutils.report(self.path, xml_request, self._calendar) +======= + self._answer = xmlutils.report(self.path, self._content, self._calendar) +>>>>>>> d9ea784e31687b03f1451bc5b543122f05c5deb1 self.send_response(client.MULTI_STATUS) self.send_header("Content-Length", len(self._answer)) diff --git a/radicale/acl/__init__.py b/radicale/acl/__init__.py index d33e878..6745c76 100644 --- a/radicale/acl/__init__.py +++ b/radicale/acl/__init__.py @@ -31,5 +31,9 @@ from radicale import config def load(): """Load list of available ACL managers.""" - module = __import__("radicale.acl", fromlist=[config.get("acl", "type")]) - return getattr(module, config.get("acl", "type")) + acl_type = config.get("acl", "type") + if acl_type == "None": + return None + else: + module = __import__("radicale.acl", fromlist=[acl_type]) + return getattr(module, acl_type) diff --git a/radicale/acl/fake.py b/radicale/acl/fake.py deleted file mode 100644 index 9ddd224..0000000 --- a/radicale/acl/fake.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Radicale Server - Calendar Server -# Copyright © 2008-2011 Guillaume Ayoub -# Copyright © 2008 Nicolas Kandel -# Copyright © 2008 Pascal Halter -# -# This library is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Radicale. If not, see . - -""" -Fake ACL. - -No rights management. - -""" - -def has_right(*_): - """Check if ``user``/``password`` couple is valid.""" - return True diff --git a/radicale/config.py b/radicale/config.py index 5b00543..4ef8434 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -48,13 +48,14 @@ INITIAL_CONFIG = { "request": "utf-8", "stock": "utf-8"}, "acl": { - "type": "fake", + "type": "None", "personal": "False", "filename": "/etc/radicale/users", "encryption": "crypt"}, "storage": { "folder": os.path.expanduser("~/.config/radicale/calendars")}, "logging": { +<<<<<<< HEAD "type": "stdout", "logfile": os.path.expanduser("~/.config/radicale/radicale.log"), "facility": 10}, @@ -62,6 +63,10 @@ INITIAL_CONFIG = { "LDAPServer": "127.0.0.1", "LDAPPrepend": "uid=", "LDAPAppend": "ou=users,dc=example,dc=com"}} +======= + "config": "/etc/radicale/logging", + "debug": "False"}} +>>>>>>> d9ea784e31687b03f1451bc5b543122f05c5deb1 # Create a ConfigParser and configure it _CONFIG_PARSER = ConfigParser() diff --git a/radicale/log.py b/radicale/log.py index 2692a7e..0e027e5 100644 --- a/radicale/log.py +++ b/radicale/log.py @@ -1,30 +1,50 @@ # -*- coding: utf-8 -*- +# +# This file is part of Radicale Server - Calendar Server +# Copyright © 2011 Guillaume Ayoub +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Radicale. If not, see . + +""" +Radicale logging module. + +Manage logging from a configuration file. For more information, see: +http://docs.python.org/library/logging.config.html + +""" + +import os +import sys +import logging +import logging.config -import logging, sys -from logging.handlers import SysLogHandler from radicale import config -class log: - def __init__(self): - self.logger=logging.getLogger("radicale") - self.logger.setLevel(config.get("logging", "facility")) - - loggingType=config.get("logging", "type") - if loggingType == "stdout": - handler=logging.StreamHandler(sys.stdout) - elif loggingType == "file": - handler=logging.FileHandler(config.get("logging", "logfile")) - else: - handler=logging.handlers.SysLogHandler("/dev/log") - - formatter = logging.Formatter('%(name)s %(asctime)s %(levelname)s %(message)s') - handler.setFormatter(formatter) - self.logger.addHandler(handler) - def log(self, level, msg): - self.logger.log(level, msg) +LOGGER = logging.getLogger("radicale") +FILENAME = os.path.expanduser(config.get("logging", "config")) -_LOGGING = log() - -sys.modules[__name__] = _LOGGING +def start(debug=False): + """Start the logging according to the configuration.""" + if debug: + LOGGER.setLevel(logging.DEBUG) + if os.path.exists(FILENAME): + # Configuration taken from file + logging.config.fileConfig(FILENAME) + else: + # Default configuration, standard output + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s")) + LOGGER.addHandler(handler)