Add authentication structure, with fake and htpasswd methods.

This commit is contained in:
Guillaume Ayoub 2010-01-21 18:52:53 +01:00
parent 06843adca1
commit 1998dc3b08
6 changed files with 105 additions and 31 deletions

6
TODO
View File

@ -14,8 +14,8 @@
0.2
===
* [DONE] SSL connections
* Authentications
* [DONE] SSL connection
* [DONE] Htpasswd authentication
* [DONE] Daemon mode
* [DONE] User configuration
@ -24,7 +24,7 @@
===
* Calendar collections
* Windows and MacOS tested support
* [IN PROGRESS] Windows and MacOS tested support
1.0

View File

@ -35,6 +35,7 @@ should have been included in this package.
# TODO: Manage errors (see xmlutils)
import base64
import socket
try:
from http import client, server
@ -42,11 +43,36 @@ except ImportError:
import httplib as client
import BaseHTTPServer as server
from radicale import config, support, xmlutils
from radicale import acl, config, support, xmlutils
def check(request, function):
"""Check if user has sufficient rights for performing ``request``."""
authorization = request.headers.get("Authorization", None)
if authorization:
challenge = authorization.lstrip("Basic").strip().encode("ascii")
plain = request.decode(base64.b64decode(challenge))
user, password = plain.split(":")
else:
user = password = None
if request.server.acl.has_right(user, password):
function(request)
else:
request.send_response(client.UNAUTHORIZED)
request.send_header(
"WWW-Authenticate",
"Basic realm=\"Radicale Server - Password Required\"")
request.end_headers()
# Decorator checking rights before performing request
check_rights = lambda function: lambda request: check(request, function)
class HTTPServer(server.HTTPServer):
"""HTTP server."""
pass
def __init__(self, address, handler):
"""Create server."""
server.HTTPServer.__init__(self, address, handler)
self.acl = acl.load()
class HTTPSServer(HTTPServer):
"""HTTPS server."""
@ -55,7 +81,7 @@ class HTTPSServer(HTTPServer):
# Fails with Python 2.5, import if needed
import ssl
super(HTTPSServer, self).__init__(address, handler)
HTTPServer.__init__(self, address, handler)
self.socket = ssl.wrap_socket(
socket.socket(self.address_family, self.socket_type),
server_side=True,
@ -77,6 +103,30 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler):
cal = "%s/%s" % (path[0], path[1])
return calendar.Calendar("radicale", cal)
def decode(self, text):
"""Try to decode text according to various parameters."""
# List of charsets to try
charsets = []
# First append content charset given in the request
contentType = self.headers["Content-Type"]
if contentType and "charset=" in contentType:
charsets.append(contentType.split("charset=")[1].strip())
# Then append default Radicale charset
charsets.append(self._encoding)
# Then append various fallbacks
charsets.append("utf-8")
charsets.append("iso8859-1")
# Try to decode
for charset in charsets:
try:
return text.decode(charset)
except UnicodeDecodeError:
pass
raise UnicodeDecodeError
@check_rights
def do_GET(self):
"""Manage GET request."""
answer = self.calendar.vcalendar.encode(_encoding)
@ -86,9 +136,10 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler):
self.end_headers()
self.wfile.write(answer)
@check_rights
def do_DELETE(self):
"""Manage DELETE request."""
obj = self.headers.get("if-match", None)
obj = self.headers.get("If-Match", None)
answer = xmlutils.delete(obj, self.calendar, self.path)
self.send_response(client.NO_CONTENT)
@ -114,20 +165,17 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler):
self.end_headers()
self.wfile.write(answer)
@check_rights
def do_PUT(self):
"""Manage PUT request."""
# TODO: Improve charset detection
contentType = self.headers["content-type"]
if contentType and "charset=" in contentType:
charset = contentType.split("charset=")[1].strip()
else:
charset = self._encoding
ical_request = self.rfile.read(int(self.headers["Content-Length"])).decode(charset)
obj = self.headers.get("if-match", None)
ical_request = self.decode(
self.rfile.read(int(self.headers["Content-Length"])))
obj = self.headers.get("If-Match", None)
xmlutils.put(ical_request, self.calendar, self.path, obj)
self.send_response(client.CREATED)
@check_rights
def do_REPORT(self):
"""Manage REPORT request."""
xml_request = self.rfile.read(int(self.headers["Content-Length"]))

View File

@ -27,6 +27,7 @@ configuration.
from radicale import config
_acl = __import__(config.get("acl", "type"), locals(), globals())
users = _acl.users
def load():
module = __import__("radicale.acl", globals(), locals(),
[config.get("acl", "type")])
return getattr(module, config.get("acl", "type"))

View File

@ -21,11 +21,10 @@
"""
Fake ACL.
Just load the default user "radicale", with no rights management.
No rights management.
"""
from radicale import config
def users():
"""Get the list of all users."""
return ["radicale"]
def has_right(user, password):
"""Check if ``user``/``password`` couple is valid."""
return True

View File

@ -21,14 +21,39 @@
"""
Htpasswd ACL.
Load the list of users according to the htpasswd configuration.
Load the list of login/password couples according a the configuration file
created by Apache ``htpasswd`` command. Plain-text, crypt and sha1 are
supported, but md5 is not (see ``htpasswd`` man page to understand why).
"""
# TODO: Manage rights
import base64
import crypt
import hashlib
from radicale import config
def users():
"""Get the list of all users."""
return [line.split(":")[0] for line
in open(config.get("acl", "filename")).readlines()]
def _plain(hash, password):
return hash == password
def _crypt(hash, password):
return crypt.crypt(password, hash) == hash
def _sha1(hash, password):
hash = hash.lstrip("{SHA}").encode("ascii")
password = password.encode(config.get("encoding", "stock"))
sha1 = hashlib.sha1()
sha1.update(password)
return sha1.digest() == base64.b64decode(hash)
_filename = config.get("acl", "filename")
_check_password = locals()["_%s" % config.get("acl", "encryption")]
def has_right(user, password):
"""Check if ``user``/``password`` couple is valid."""
for line in open(_filename).readlines():
if line.strip():
login, hash = line.strip().split(":")
if login == user:
return _check_password(hash, password)
return False

View File

@ -62,7 +62,8 @@ _initial = {
},
"acl": {
"type": "fake",
#"filename": "/etc/radicale/users",
"filename": "/etc/radicale/users",
"encryption": "crypt",
},
"support": {
"type": "plain",