commit
e25373fa85
@ -50,7 +50,24 @@ from . import auth, rights, storage, xmlutils
|
|||||||
|
|
||||||
VERSION = "2.0.0rc0"
|
VERSION = "2.0.0rc0"
|
||||||
|
|
||||||
NOT_ALLOWED = (client.FORBIDDEN, {}, None)
|
NOT_ALLOWED = (client.FORBIDDEN, {"Content-type": "text/plain"},
|
||||||
|
"Access to the requested resource forbidden.")
|
||||||
|
NOT_FOUND = (client.NOT_FOUND, {"Content-type": "text/plain"},
|
||||||
|
"The requested resource could not be found.")
|
||||||
|
WEBDAV_PRECONDITION_FAILED = (client.CONFLICT, {"Content-type": "text/plain"},
|
||||||
|
"WebDAV precondition failed.")
|
||||||
|
PRECONDITION_FAILED = (client.PRECONDITION_FAILED,
|
||||||
|
{"Content-type": "text/plain"}, "Precondition failed.")
|
||||||
|
REQUEST_TIMEOUT = (client.REQUEST_TIMEOUT, {"Content-type": "text/plain"},
|
||||||
|
"Connection timed out.")
|
||||||
|
REQUEST_ENTITY_TOO_LARGE = (client.REQUEST_ENTITY_TOO_LARGE,
|
||||||
|
{"Content-type": "text/plain"},
|
||||||
|
"Request body too large.")
|
||||||
|
REMOTE_DESTINATION = (client.BAD_GATEWAY, {"Content-type": "text/plain"},
|
||||||
|
"Remote destination not supported.")
|
||||||
|
DIRECTORY_LISTING = (client.FORBIDDEN, {"Content-type": "text/plain"},
|
||||||
|
"Directory listings are not supported.")
|
||||||
|
|
||||||
DAV_HEADERS = "1, 2, 3, calendar-access, addressbook, extended-mkcol"
|
DAV_HEADERS = "1, 2, 3, calendar-access, addressbook, extended-mkcol"
|
||||||
|
|
||||||
|
|
||||||
@ -296,7 +313,7 @@ class Application:
|
|||||||
|
|
||||||
# If "/.well-known" is not available, clients query "/"
|
# If "/.well-known" is not available, clients query "/"
|
||||||
if path == "/.well-known" or path.startswith("/.well-known/"):
|
if path == "/.well-known" or path.startswith("/.well-known/"):
|
||||||
return response(client.NOT_FOUND, {})
|
return response(*NOT_FOUND)
|
||||||
|
|
||||||
if user and not storage.is_safe_path_component(user):
|
if user and not storage.is_safe_path_component(user):
|
||||||
# Prevent usernames like "user/calendar.ics"
|
# Prevent usernames like "user/calendar.ics"
|
||||||
@ -326,13 +343,13 @@ class Application:
|
|||||||
if max_content_length and content_length > max_content_length:
|
if max_content_length and content_length > max_content_length:
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
"Request body too large: %d", content_length)
|
"Request body too large: %d", content_length)
|
||||||
return response(client.REQUEST_ENTITY_TOO_LARGE)
|
return response(*REQUEST_ENTITY_TOO_LARGE)
|
||||||
|
|
||||||
if is_valid_user:
|
if is_valid_user:
|
||||||
try:
|
try:
|
||||||
status, headers, answer = function(environ, path, user)
|
status, headers, answer = function(environ, path, user)
|
||||||
except socket.timeout:
|
except socket.timeout:
|
||||||
return response(client.REQUEST_TIMEOUT)
|
return response(*REQUEST_TIMEOUT)
|
||||||
else:
|
else:
|
||||||
status, headers, answer = NOT_ALLOWED
|
status, headers, answer = NOT_ALLOWED
|
||||||
|
|
||||||
@ -342,10 +359,9 @@ class Application:
|
|||||||
self.logger.info("%s refused" % (user or "Anonymous user"))
|
self.logger.info("%s refused" % (user or "Anonymous user"))
|
||||||
status = client.UNAUTHORIZED
|
status = client.UNAUTHORIZED
|
||||||
realm = self.configuration.get("server", "realm")
|
realm = self.configuration.get("server", "realm")
|
||||||
headers = {
|
headers.update ({
|
||||||
"WWW-Authenticate":
|
"WWW-Authenticate":
|
||||||
"Basic realm=\"%s\"" % realm}
|
"Basic realm=\"%s\"" % realm})
|
||||||
answer = None
|
|
||||||
|
|
||||||
# Set content length
|
# Set content length
|
||||||
if answer:
|
if answer:
|
||||||
@ -407,11 +423,11 @@ class Application:
|
|||||||
if not self._access(user, path, "w", item):
|
if not self._access(user, path, "w", item):
|
||||||
return NOT_ALLOWED
|
return NOT_ALLOWED
|
||||||
if not item:
|
if not item:
|
||||||
return client.GONE, {}, None
|
return NOT_FOUND
|
||||||
if_match = environ.get("HTTP_IF_MATCH", "*")
|
if_match = environ.get("HTTP_IF_MATCH", "*")
|
||||||
if if_match not in ("*", item.etag):
|
if if_match not in ("*", item.etag):
|
||||||
# ETag precondition not verified, do not delete item
|
# ETag precondition not verified, do not delete item
|
||||||
return client.PRECONDITION_FAILED, {}, None
|
return PRECONDITION_FAILED
|
||||||
if isinstance(item, self.Collection):
|
if isinstance(item, self.Collection):
|
||||||
answer = xmlutils.delete(path, item)
|
answer = xmlutils.delete(path, item)
|
||||||
else:
|
else:
|
||||||
@ -430,9 +446,11 @@ class Application:
|
|||||||
if not self._access(user, path, "r", item):
|
if not self._access(user, path, "r", item):
|
||||||
return NOT_ALLOWED
|
return NOT_ALLOWED
|
||||||
if not item:
|
if not item:
|
||||||
return client.NOT_FOUND, {}, None
|
return NOT_FOUND
|
||||||
if isinstance(item, self.Collection):
|
if isinstance(item, self.Collection):
|
||||||
collection = item
|
collection = item
|
||||||
|
if collection.get_meta("tag") not in ("VADDRESSBOOK", "VCALENDAR"):
|
||||||
|
return DIRECTORY_LISTING
|
||||||
else:
|
else:
|
||||||
collection = item.collection
|
collection = item.collection
|
||||||
content_type = xmlutils.MIMETYPES.get(
|
content_type = xmlutils.MIMETYPES.get(
|
||||||
@ -457,7 +475,7 @@ class Application:
|
|||||||
with self.Collection.acquire_lock("w", user):
|
with self.Collection.acquire_lock("w", user):
|
||||||
item = next(self.Collection.discover(path), None)
|
item = next(self.Collection.discover(path), None)
|
||||||
if item:
|
if item:
|
||||||
return client.CONFLICT, {}, None
|
return WEBDAV_PRECONDITION_FAILED
|
||||||
props = xmlutils.props_from_request(content)
|
props = xmlutils.props_from_request(content)
|
||||||
props["tag"] = "VCALENDAR"
|
props["tag"] = "VCALENDAR"
|
||||||
# TODO: use this?
|
# TODO: use this?
|
||||||
@ -473,7 +491,7 @@ class Application:
|
|||||||
with self.Collection.acquire_lock("w", user):
|
with self.Collection.acquire_lock("w", user):
|
||||||
item = next(self.Collection.discover(path), None)
|
item = next(self.Collection.discover(path), None)
|
||||||
if item:
|
if item:
|
||||||
return client.CONFLICT, {}, None
|
return WEBDAV_PRECONDITION_FAILED
|
||||||
props = xmlutils.props_from_request(content)
|
props = xmlutils.props_from_request(content)
|
||||||
self.Collection.create_collection(path, props=props)
|
self.Collection.create_collection(path, props=props)
|
||||||
return client.CREATED, {}, None
|
return client.CREATED, {}, None
|
||||||
@ -483,7 +501,7 @@ class Application:
|
|||||||
to_url = urlparse(environ["HTTP_DESTINATION"])
|
to_url = urlparse(environ["HTTP_DESTINATION"])
|
||||||
if to_url.netloc != environ["HTTP_HOST"]:
|
if to_url.netloc != environ["HTTP_HOST"]:
|
||||||
# Remote destination server, not supported
|
# Remote destination server, not supported
|
||||||
return client.BAD_GATEWAY, {}, None
|
return REMOTE_DESTINATION
|
||||||
if not self._access(user, path, "w"):
|
if not self._access(user, path, "w"):
|
||||||
return NOT_ALLOWED
|
return NOT_ALLOWED
|
||||||
to_path = storage.sanitize_path(to_url.path)
|
to_path = storage.sanitize_path(to_url.path)
|
||||||
@ -497,20 +515,20 @@ class Application:
|
|||||||
if not self._access(user, to_path, "w", item):
|
if not self._access(user, to_path, "w", item):
|
||||||
return NOT_ALLOWED
|
return NOT_ALLOWED
|
||||||
if not item:
|
if not item:
|
||||||
return client.GONE, {}, None
|
return NOT_FOUND
|
||||||
if isinstance(item, self.Collection):
|
if isinstance(item, self.Collection):
|
||||||
return client.CONFLICT, {}, None
|
return WEBDAV_PRECONDITION_FAILED
|
||||||
|
|
||||||
to_item = next(self.Collection.discover(to_path), None)
|
to_item = next(self.Collection.discover(to_path), None)
|
||||||
if (isinstance(to_item, self.Collection) or
|
if (isinstance(to_item, self.Collection) or
|
||||||
to_item and environ.get("HTTP_OVERWRITE", "F") != "T"):
|
to_item and environ.get("HTTP_OVERWRITE", "F") != "T"):
|
||||||
return client.CONFLICT, {}, None
|
return WEBDAV_PRECONDITION_FAILED
|
||||||
to_parent_path = storage.sanitize_path(
|
to_parent_path = storage.sanitize_path(
|
||||||
"/%s/" % posixpath.dirname(to_path.strip("/")))
|
"/%s/" % posixpath.dirname(to_path.strip("/")))
|
||||||
to_collection = next(
|
to_collection = next(
|
||||||
self.Collection.discover(to_parent_path), None)
|
self.Collection.discover(to_parent_path), None)
|
||||||
if not to_collection:
|
if not to_collection:
|
||||||
return client.CONFLICT, {}, None
|
return WEBDAV_PRECONDITION_FAILED
|
||||||
to_href = posixpath.basename(to_path.strip("/"))
|
to_href = posixpath.basename(to_path.strip("/"))
|
||||||
self.Collection.move(item, to_collection, to_href)
|
self.Collection.move(item, to_collection, to_href)
|
||||||
return client.CREATED, {}, None
|
return client.CREATED, {}, None
|
||||||
@ -536,7 +554,7 @@ class Application:
|
|||||||
if not self._access(user, path, "r", item):
|
if not self._access(user, path, "r", item):
|
||||||
return NOT_ALLOWED
|
return NOT_ALLOWED
|
||||||
if not item:
|
if not item:
|
||||||
return client.NOT_FOUND, {}, None
|
return NOT_FOUND
|
||||||
# put item back
|
# put item back
|
||||||
items = itertools.chain([item], items)
|
items = itertools.chain([item], items)
|
||||||
read_items, write_items = self.collect_allowed_items(items, user)
|
read_items, write_items = self.collect_allowed_items(items, user)
|
||||||
@ -556,7 +574,7 @@ class Application:
|
|||||||
with self.Collection.acquire_lock("w", user):
|
with self.Collection.acquire_lock("w", user):
|
||||||
item = next(self.Collection.discover(path), None)
|
item = next(self.Collection.discover(path), None)
|
||||||
if not isinstance(item, self.Collection):
|
if not isinstance(item, self.Collection):
|
||||||
return client.CONFLICT, {}, None
|
return WEBDAV_PRECONDITION_FAILED
|
||||||
headers = {"DAV": DAV_HEADERS, "Content-Type": "text/xml"}
|
headers = {"DAV": DAV_HEADERS, "Content-Type": "text/xml"}
|
||||||
answer = xmlutils.proppatch(path, content, item)
|
answer = xmlutils.proppatch(path, content, item)
|
||||||
return client.MULTI_STATUS, headers, answer
|
return client.MULTI_STATUS, headers, answer
|
||||||
@ -587,15 +605,15 @@ class Application:
|
|||||||
etag = environ.get("HTTP_IF_MATCH", "")
|
etag = environ.get("HTTP_IF_MATCH", "")
|
||||||
if not item and etag:
|
if not item and etag:
|
||||||
# Etag asked but no item found: item has been removed
|
# Etag asked but no item found: item has been removed
|
||||||
return client.PRECONDITION_FAILED, {}, None
|
return PRECONDITION_FAILED
|
||||||
if item and etag and item.etag != etag:
|
if item and etag and item.etag != etag:
|
||||||
# Etag asked but item not matching: item has changed
|
# Etag asked but item not matching: item has changed
|
||||||
return client.PRECONDITION_FAILED, {}, None
|
return PRECONDITION_FAILED
|
||||||
|
|
||||||
match = environ.get("HTTP_IF_NONE_MATCH", "") == "*"
|
match = environ.get("HTTP_IF_NONE_MATCH", "") == "*"
|
||||||
if item and match:
|
if item and match:
|
||||||
# Creation asked but item found: item can't be replaced
|
# Creation asked but item found: item can't be replaced
|
||||||
return client.PRECONDITION_FAILED, {}, None
|
return PRECONDITION_FAILED
|
||||||
|
|
||||||
items = list(vobject.readComponents(content or ""))
|
items = list(vobject.readComponents(content or ""))
|
||||||
content_type = environ.get("CONTENT_TYPE", "").split(";")[0]
|
content_type = environ.get("CONTENT_TYPE", "").split(";")[0]
|
||||||
@ -623,7 +641,7 @@ class Application:
|
|||||||
if not self._access(user, path, "w", item):
|
if not self._access(user, path, "w", item):
|
||||||
return NOT_ALLOWED
|
return NOT_ALLOWED
|
||||||
if not item:
|
if not item:
|
||||||
return client.NOT_FOUND, {}, None
|
return NOT_FOUND
|
||||||
if isinstance(item, self.Collection):
|
if isinstance(item, self.Collection):
|
||||||
collection = item
|
collection = item
|
||||||
else:
|
else:
|
||||||
|
@ -736,8 +736,8 @@ class BaseRequestsMixIn:
|
|||||||
def test_principal_collection_creation(self):
|
def test_principal_collection_creation(self):
|
||||||
"""Verify existence of the principal collection."""
|
"""Verify existence of the principal collection."""
|
||||||
status, headers, answer = self.request(
|
status, headers, answer = self.request(
|
||||||
"GET", "/user/", REMOTE_USER="user")
|
"PROPFIND", "/user/", REMOTE_USER="user")
|
||||||
assert status == 200
|
assert status == 207
|
||||||
|
|
||||||
def test_existence_of_root_collections(self):
|
def test_existence_of_root_collections(self):
|
||||||
"""Verify that the root collection always exists."""
|
"""Verify that the root collection always exists."""
|
||||||
@ -762,8 +762,8 @@ class BaseRequestsMixIn:
|
|||||||
"created_by_hook"))
|
"created_by_hook"))
|
||||||
status, headers, answer = self.request("MKCOL", "/calendar.ics/")
|
status, headers, answer = self.request("MKCOL", "/calendar.ics/")
|
||||||
assert status == 201
|
assert status == 201
|
||||||
status, headers, answer = self.request("GET", "/created_by_hook/")
|
status, headers, answer = self.request("PROPFIND", "/created_by_hook/")
|
||||||
assert status == 200
|
assert status == 207
|
||||||
|
|
||||||
def test_hook_read_access(self):
|
def test_hook_read_access(self):
|
||||||
"""Verify that hook is not run for read accesses."""
|
"""Verify that hook is not run for read accesses."""
|
||||||
@ -791,8 +791,8 @@ class BaseRequestsMixIn:
|
|||||||
"created_by_hook"))
|
"created_by_hook"))
|
||||||
status, headers, answer = self.request("GET", "/", REMOTE_USER="user")
|
status, headers, answer = self.request("GET", "/", REMOTE_USER="user")
|
||||||
assert status == 200
|
assert status == 200
|
||||||
status, headers, answer = self.request("GET", "/created_by_hook/")
|
status, headers, answer = self.request("PROPFIND", "/created_by_hook/")
|
||||||
assert status == 200
|
assert status == 207
|
||||||
|
|
||||||
def test_hook_fail(self):
|
def test_hook_fail(self):
|
||||||
"""Verify that a request fails if the hook fails."""
|
"""Verify that a request fails if the hook fails."""
|
||||||
|
Loading…
Reference in New Issue
Block a user