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

View File

@ -35,6 +35,7 @@ should have been included in this package.
# TODO: Manage errors (see xmlutils) # TODO: Manage errors (see xmlutils)
import base64
import socket import socket
try: try:
from http import client, server from http import client, server
@ -42,11 +43,36 @@ except ImportError:
import httplib as client import httplib as client
import BaseHTTPServer as server 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): class HTTPServer(server.HTTPServer):
"""HTTP server.""" """HTTP server."""
pass def __init__(self, address, handler):
"""Create server."""
server.HTTPServer.__init__(self, address, handler)
self.acl = acl.load()
class HTTPSServer(HTTPServer): class HTTPSServer(HTTPServer):
"""HTTPS server.""" """HTTPS server."""
@ -55,7 +81,7 @@ class HTTPSServer(HTTPServer):
# Fails with Python 2.5, import if needed # Fails with Python 2.5, import if needed
import ssl import ssl
super(HTTPSServer, self).__init__(address, handler) HTTPServer.__init__(self, address, handler)
self.socket = ssl.wrap_socket( self.socket = ssl.wrap_socket(
socket.socket(self.address_family, self.socket_type), socket.socket(self.address_family, self.socket_type),
server_side=True, server_side=True,
@ -77,6 +103,30 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler):
cal = "%s/%s" % (path[0], path[1]) cal = "%s/%s" % (path[0], path[1])
return calendar.Calendar("radicale", cal) 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): def do_GET(self):
"""Manage GET request.""" """Manage GET request."""
answer = self.calendar.vcalendar.encode(_encoding) answer = self.calendar.vcalendar.encode(_encoding)
@ -86,9 +136,10 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler):
self.end_headers() self.end_headers()
self.wfile.write(answer) self.wfile.write(answer)
@check_rights
def do_DELETE(self): def do_DELETE(self):
"""Manage DELETE request.""" """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) answer = xmlutils.delete(obj, self.calendar, self.path)
self.send_response(client.NO_CONTENT) self.send_response(client.NO_CONTENT)
@ -114,20 +165,17 @@ class CalendarHTTPHandler(server.BaseHTTPRequestHandler):
self.end_headers() self.end_headers()
self.wfile.write(answer) self.wfile.write(answer)
@check_rights
def do_PUT(self): def do_PUT(self):
"""Manage PUT request.""" """Manage PUT request."""
# TODO: Improve charset detection ical_request = self.decode(
contentType = self.headers["content-type"] self.rfile.read(int(self.headers["Content-Length"])))
if contentType and "charset=" in contentType: obj = self.headers.get("If-Match", None)
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)
xmlutils.put(ical_request, self.calendar, self.path, obj) xmlutils.put(ical_request, self.calendar, self.path, obj)
self.send_response(client.CREATED) self.send_response(client.CREATED)
@check_rights
def do_REPORT(self): def do_REPORT(self):
"""Manage REPORT request.""" """Manage REPORT request."""
xml_request = self.rfile.read(int(self.headers["Content-Length"])) xml_request = self.rfile.read(int(self.headers["Content-Length"]))

View File

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

View File

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

View File

@ -21,14 +21,39 @@
""" """
Htpasswd ACL. 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 from radicale import config
def users(): def _plain(hash, password):
"""Get the list of all users.""" return hash == password
return [line.split(":")[0] for line
in open(config.get("acl", "filename")).readlines()] 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": { "acl": {
"type": "fake", "type": "fake",
#"filename": "/etc/radicale/users", "filename": "/etc/radicale/users",
"encryption": "crypt",
}, },
"support": { "support": {
"type": "plain", "type": "plain",