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
|
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
|
||||||
|
@ -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"]))
|
||||||
|
@ -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"))
|
||||||
|
@ -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"]
|
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user