Add authentication structure, with fake and htpasswd methods.
This commit is contained in:
parent
06843adca1
commit
1998dc3b08
6
TODO
6
TODO
@ -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
|
||||
|
@ -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"]))
|
||||
|
@ -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"))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -62,7 +62,8 @@ _initial = {
|
||||
},
|
||||
"acl": {
|
||||
"type": "fake",
|
||||
#"filename": "/etc/radicale/users",
|
||||
"filename": "/etc/radicale/users",
|
||||
"encryption": "crypt",
|
||||
},
|
||||
"support": {
|
||||
"type": "plain",
|
||||
|
Loading…
Reference in New Issue
Block a user