Merge branch 'Unrud-url'

This commit is contained in:
Guillaume Ayoub 2017-02-26 14:44:40 +01:00
commit 9f3d4f3e05
5 changed files with 76 additions and 77 deletions

6
config
View File

@ -51,12 +51,6 @@
# Reverse DNS to resolve client address in logs # Reverse DNS to resolve client address in logs
#dns_lookup = True #dns_lookup = True
# Root URL of Radicale (starting and ending with a slash)
#base_prefix = /
# Possibility to allow URLs cleaned by a HTTP server, without the base_prefix
#can_skip_base_prefix = False
# Message displayed in the client when a password is needed # Message displayed in the client when a password is needed
#realm = Radicale - Password Required #realm = Radicale - Password Required

View File

@ -171,7 +171,7 @@ class RequestHandler(wsgiref.simple_server.WSGIRequestHandler):
def get_environ(self): def get_environ(self):
env = super().get_environ() env = super().get_environ()
# Parent class only tries latin1 encoding # Parent class only tries latin1 encoding
env["PATH_INFO"] = urllib.parse.unquote(self.path.split("?", 1)[0]) env["PATH_INFO"] = unquote(self.path.split("?", 1)[0])
return env return env
def handle(self): def handle(self):
@ -308,22 +308,18 @@ class Application:
headers = pprint.pformat(self.headers_log(environ)) headers = pprint.pformat(self.headers_log(environ))
self.logger.debug("Request headers:\n%s", headers) self.logger.debug("Request headers:\n%s", headers)
# Strip base_prefix from request URI # Let reverse proxies overwrite SCRIPT_NAME
base_prefix = self.configuration.get("server", "base_prefix") if "HTTP_X_SCRIPT_NAME" in environ:
if environ["PATH_INFO"].startswith(base_prefix): environ["SCRIPT_NAME"] = environ["HTTP_X_SCRIPT_NAME"]
environ["PATH_INFO"] = environ["PATH_INFO"][len(base_prefix):] self.logger.debug("Script name overwritten by client: %s",
elif self.configuration.get("server", "can_skip_base_prefix"): environ["SCRIPT_NAME"])
self.logger.debug( # Sanitize base prefix
"Prefix already stripped from path: %s", environ["PATH_INFO"]) environ["SCRIPT_NAME"] = storage.sanitize_path(
else: environ.get("SCRIPT_NAME", "")).rstrip("/")
# Request path not starting with base_prefix, not allowed self.logger.debug("Sanitized script name: %s", environ["SCRIPT_NAME"])
self.logger.debug( base_prefix = environ["SCRIPT_NAME"]
"Path not starting with prefix: %s", environ["PATH_INFO"])
return response(*NOT_ALLOWED)
# Sanitize request URI # Sanitize request URI
environ["PATH_INFO"] = storage.sanitize_path( environ["PATH_INFO"] = storage.sanitize_path(environ["PATH_INFO"])
unquote(environ["PATH_INFO"]))
self.logger.debug("Sanitized path: %s", environ["PATH_INFO"]) self.logger.debug("Sanitized path: %s", environ["PATH_INFO"])
path = environ["PATH_INFO"] path = environ["PATH_INFO"]
@ -377,7 +373,8 @@ class Application:
if is_valid_user: if is_valid_user:
try: try:
status, headers, answer = function(environ, path, user) status, headers, answer = function(
environ, base_prefix, path, user)
except socket.timeout: except socket.timeout:
return response(*REQUEST_TIMEOUT) return response(*REQUEST_TIMEOUT)
else: else:
@ -424,7 +421,7 @@ class Application:
content = None content = None
return content return content
def do_DELETE(self, environ, path, user): def do_DELETE(self, environ, base_prefix, path, user):
"""Manage DELETE request.""" """Manage DELETE request."""
if not self._access(user, path, "w"): if not self._access(user, path, "w"):
return NOT_ALLOWED return NOT_ALLOWED
@ -439,12 +436,13 @@ class Application:
# ETag precondition not verified, do not delete item # ETag precondition not verified, do not delete item
return PRECONDITION_FAILED return PRECONDITION_FAILED
if isinstance(item, self.Collection): if isinstance(item, self.Collection):
answer = xmlutils.delete(path, item) answer = xmlutils.delete(base_prefix, path, item)
else: else:
answer = xmlutils.delete(path, item.collection, item.href) answer = xmlutils.delete(
base_prefix, path, item.collection, item.href)
return client.OK, {"Content-Type": "text/xml"}, answer return client.OK, {"Content-Type": "text/xml"}, answer
def do_GET(self, environ, path, user): def do_GET(self, environ, base_prefix, path, user):
"""Manage GET request.""" """Manage GET request."""
# Display a "Radicale works!" message if the root URL is requested # Display a "Radicale works!" message if the root URL is requested
if not path.strip("/"): if not path.strip("/"):
@ -473,12 +471,13 @@ class Application:
answer = item.serialize() answer = item.serialize()
return client.OK, headers, answer return client.OK, headers, answer
def do_HEAD(self, environ, path, user): def do_HEAD(self, environ, base_prefix, path, user):
"""Manage HEAD request.""" """Manage HEAD request."""
status, headers, answer = self.do_GET(environ, path, user) status, headers, answer = self.do_GET(
environ, base_prefix, path, user)
return status, headers, None return status, headers, None
def do_MKCALENDAR(self, environ, path, user): def do_MKCALENDAR(self, environ, base_prefix, path, user):
"""Manage MKCALENDAR request.""" """Manage MKCALENDAR request."""
if not self.authorized(user, path, "w"): if not self.authorized(user, path, "w"):
return NOT_ALLOWED return NOT_ALLOWED
@ -494,7 +493,7 @@ class Application:
self.Collection.create_collection(path, props=props) self.Collection.create_collection(path, props=props)
return client.CREATED, {}, None return client.CREATED, {}, None
def do_MKCOL(self, environ, path, user): def do_MKCOL(self, environ, base_prefix, path, user):
"""Manage MKCOL request.""" """Manage MKCOL request."""
if not self.authorized(user, path, "w"): if not self.authorized(user, path, "w"):
return NOT_ALLOWED return NOT_ALLOWED
@ -507,7 +506,7 @@ class Application:
self.Collection.create_collection(path, props=props) self.Collection.create_collection(path, props=props)
return client.CREATED, {}, None return client.CREATED, {}, None
def do_MOVE(self, environ, path, user): def do_MOVE(self, environ, base_prefix, path, user):
"""Manage MOVE request.""" """Manage MOVE request."""
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"]:
@ -544,7 +543,7 @@ class Application:
self.Collection.move(item, to_collection, to_href) self.Collection.move(item, to_collection, to_href)
return client.CREATED, {}, None return client.CREATED, {}, None
def do_OPTIONS(self, environ, path, user): def do_OPTIONS(self, environ, base_prefix, path, user):
"""Manage OPTIONS request.""" """Manage OPTIONS request."""
headers = { headers = {
"Allow": ", ".join( "Allow": ", ".join(
@ -552,7 +551,7 @@ class Application:
"DAV": DAV_HEADERS} "DAV": DAV_HEADERS}
return client.OK, headers, None return client.OK, headers, None
def do_PROPFIND(self, environ, path, user): def do_PROPFIND(self, environ, base_prefix, path, user):
"""Manage PROPFIND request.""" """Manage PROPFIND request."""
if not self._access(user, path, "r"): if not self._access(user, path, "r"):
return NOT_ALLOWED return NOT_ALLOWED
@ -571,13 +570,13 @@ class Application:
read_items, write_items = self.collect_allowed_items(items, user) read_items, write_items = self.collect_allowed_items(items, user)
headers = {"DAV": DAV_HEADERS, "Content-Type": "text/xml"} headers = {"DAV": DAV_HEADERS, "Content-Type": "text/xml"}
status, answer = xmlutils.propfind( status, answer = xmlutils.propfind(
path, content, read_items, write_items, user) base_prefix, path, content, read_items, write_items, user)
if status == client.FORBIDDEN: if status == client.FORBIDDEN:
return NOT_ALLOWED return NOT_ALLOWED
else: else:
return status, headers, answer return status, headers, answer
def do_PROPPATCH(self, environ, path, user): def do_PROPPATCH(self, environ, base_prefix, path, user):
"""Manage PROPPATCH request.""" """Manage PROPPATCH request."""
if not self.authorized(user, path, "w"): if not self.authorized(user, path, "w"):
return NOT_ALLOWED return NOT_ALLOWED
@ -587,10 +586,10 @@ class Application:
if not isinstance(item, self.Collection): if not isinstance(item, self.Collection):
return WEBDAV_PRECONDITION_FAILED 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(base_prefix, path, content, item)
return client.MULTI_STATUS, headers, answer return client.MULTI_STATUS, headers, answer
def do_PUT(self, environ, path, user): def do_PUT(self, environ, base_prefix, path, user):
"""Manage PUT request.""" """Manage PUT request."""
if not self._access(user, path, "w"): if not self._access(user, path, "w"):
return NOT_ALLOWED return NOT_ALLOWED
@ -642,7 +641,7 @@ class Application:
headers = {"ETag": new_item.etag} headers = {"ETag": new_item.etag}
return client.CREATED, headers, None return client.CREATED, headers, None
def do_REPORT(self, environ, path, user): def do_REPORT(self, environ, base_prefix, path, user):
"""Manage REPORT request.""" """Manage REPORT request."""
if not self._access(user, path, "w"): if not self._access(user, path, "w"):
return NOT_ALLOWED return NOT_ALLOWED
@ -658,5 +657,5 @@ class Application:
else: else:
collection = item.collection collection = item.collection
headers = {"Content-Type": "text/xml"} headers = {"Content-Type": "text/xml"}
answer = xmlutils.report(path, content, collection) answer = xmlutils.report(base_prefix, path, content, collection)
return client.MULTI_STATUS, headers, answer return client.MULTI_STATUS, headers, answer

View File

@ -151,9 +151,6 @@ def serve(configuration, logger):
atexit.register(cleanup) atexit.register(cleanup)
logger.info("Starting Radicale") logger.info("Starting Radicale")
logger.debug(
"Base URL prefix: %s", configuration.get("server", "base_prefix"))
# Create collection servers # Create collection servers
servers = {} servers = {}
if configuration.getboolean("server", "ssl"): if configuration.getboolean("server", "ssl"):

View File

@ -74,12 +74,6 @@ INITIAL_CONFIG = OrderedDict([
("dns_lookup", { ("dns_lookup", {
"value": "True", "value": "True",
"help": "use reverse DNS to resolve client address in logs"}), "help": "use reverse DNS to resolve client address in logs"}),
("base_prefix", {
"value": "/",
"help": "root URL of Radicale, starting and ending with a slash"}),
("can_skip_base_prefix", {
"value": "False",
"help": "allow URLs cleaned by a HTTP server"}),
("realm", { ("realm", {
"value": "Radicale - Password Required", "value": "Radicale - Password Required",
"help": "message displayed when a password is needed"})])), "help": "message displayed when a password is needed"})])),

View File

@ -31,7 +31,7 @@ import xml.etree.ElementTree as ET
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from http import client from http import client
from urllib.parse import unquote, urlparse from urllib.parse import quote, unquote, urlparse
from . import storage from . import storage
@ -102,11 +102,9 @@ def _response(code):
return "HTTP/1.1 %i %s" % (code, client.responses[code]) return "HTTP/1.1 %i %s" % (code, client.responses[code])
def _href(collection, href): def _href(base_prefix, href):
"""Return prefixed href.""" """Return prefixed href."""
return "%s%s" % ( return quote("%s%s" % (base_prefix, href))
collection.configuration.get("server", "base_prefix"),
href.lstrip("/"))
def _date_to_datetime(date_): def _date_to_datetime(date_):
@ -425,7 +423,11 @@ def name_from_path(path, collection):
start = collection.path + "/" start = collection.path + "/"
if not path.startswith(start): if not path.startswith(start):
raise ValueError("'%s' doesn't start with '%s'" % (path, start)) raise ValueError("'%s' doesn't start with '%s'" % (path, start))
return path[len(start):].rstrip("/") name = path[len(start):][:-1]
if name and not storage.is_safe_path_component(name):
raise ValueError("'%s' is not a component in collection '%s'" %
(path, collection.path))
return name
def props_from_request(root, actions=("set", "remove")): def props_from_request(root, actions=("set", "remove")):
@ -466,7 +468,7 @@ def props_from_request(root, actions=("set", "remove")):
return result return result
def delete(path, collection, href=None): def delete(base_prefix, path, collection, href=None):
"""Read and answer DELETE requests. """Read and answer DELETE requests.
Read rfc4918-9.6 for info. Read rfc4918-9.6 for info.
@ -479,7 +481,7 @@ def delete(path, collection, href=None):
multistatus.append(response) multistatus.append(response)
href = ET.Element(_tag("D", "href")) href = ET.Element(_tag("D", "href"))
href.text = _href(collection, path) href.text = _href(base_prefix, path)
response.append(href) response.append(href)
status = ET.Element(_tag("D", "status")) status = ET.Element(_tag("D", "status"))
@ -489,7 +491,8 @@ def delete(path, collection, href=None):
return _pretty_xml(multistatus) return _pretty_xml(multistatus)
def propfind(path, xml_request, read_collections, write_collections, user): def propfind(base_prefix, path, xml_request, read_collections,
write_collections, user):
"""Read and answer PROPFIND requests. """Read and answer PROPFIND requests.
Read rfc4918-9.1 for info. Read rfc4918-9.1 for info.
@ -522,19 +525,19 @@ def propfind(path, xml_request, read_collections, write_collections, user):
for collection in write_collections: for collection in write_collections:
collections.append(collection) collections.append(collection)
response = _propfind_response( response = _propfind_response(
path, collection, props, user, write=True) base_prefix, path, collection, props, user, write=True)
multistatus.append(response) multistatus.append(response)
for collection in read_collections: for collection in read_collections:
if collection in collections: if collection in collections:
continue continue
response = _propfind_response( response = _propfind_response(
path, collection, props, user, write=False) base_prefix, path, collection, props, user, write=False)
multistatus.append(response) multistatus.append(response)
return client.MULTI_STATUS, _pretty_xml(multistatus) return client.MULTI_STATUS, _pretty_xml(multistatus)
def _propfind_response(path, item, props, user, write=False): def _propfind_response(base_prefix, path, item, props, user, write=False):
"""Build and return a PROPFIND response.""" """Build and return a PROPFIND response."""
is_collection = isinstance(item, storage.BaseCollection) is_collection = isinstance(item, storage.BaseCollection)
if is_collection: if is_collection:
@ -552,7 +555,7 @@ def _propfind_response(path, item, props, user, write=False):
else: else:
uri = "/" + posixpath.join(collection.path, item.href) uri = "/" + posixpath.join(collection.path, item.href)
href.text = _href(collection, uri) href.text = _href(base_prefix, uri)
response.append(href) response.append(href)
propstat404 = ET.Element(_tag("D", "propstat")) propstat404 = ET.Element(_tag("D", "propstat"))
@ -574,7 +577,7 @@ def _propfind_response(path, item, props, user, write=False):
element.text = item.last_modified element.text = item.last_modified
elif tag == _tag("D", "principal-collection-set"): elif tag == _tag("D", "principal-collection-set"):
tag = ET.Element(_tag("D", "href")) tag = ET.Element(_tag("D", "href"))
tag.text = _href(collection, "/") tag.text = _href(base_prefix, "/")
element.append(tag) element.append(tag)
elif (tag in (_tag("C", "calendar-user-address-set"), elif (tag in (_tag("C", "calendar-user-address-set"),
_tag("D", "principal-URL"), _tag("D", "principal-URL"),
@ -582,7 +585,7 @@ def _propfind_response(path, item, props, user, write=False):
_tag("C", "calendar-home-set")) and _tag("C", "calendar-home-set")) and
collection.is_principal and is_collection): collection.is_principal and is_collection):
tag = ET.Element(_tag("D", "href")) tag = ET.Element(_tag("D", "href"))
tag.text = _href(collection, path) tag.text = _href(base_prefix, path)
element.append(tag) element.append(tag)
elif tag == _tag("C", "supported-calendar-component-set"): elif tag == _tag("C", "supported-calendar-component-set"):
human_tag = _tag_from_clark(tag) human_tag = _tag_from_clark(tag)
@ -600,7 +603,7 @@ def _propfind_response(path, item, props, user, write=False):
is404 = True is404 = True
elif tag == _tag("D", "current-user-principal"): elif tag == _tag("D", "current-user-principal"):
tag = ET.Element(_tag("D", "href")) tag = ET.Element(_tag("D", "href"))
tag.text = _href(collection, ("/%s/" % user) if user else "/") tag.text = _href(base_prefix, ("/%s/" % user) if user else "/")
element.append(tag) element.append(tag)
elif tag == _tag("D", "current-user-privilege-set"): elif tag == _tag("D", "current-user-privilege-set"):
privilege = ET.Element(_tag("D", "privilege")) privilege = ET.Element(_tag("D", "privilege"))
@ -721,7 +724,7 @@ def _add_propstat_to(element, tag, status_number):
propstat.append(status) propstat.append(status)
def proppatch(path, xml_request, collection): def proppatch(base_prefix, path, xml_request, collection):
"""Read and answer PROPPATCH requests. """Read and answer PROPPATCH requests.
Read rfc4918-9.2 for info. Read rfc4918-9.2 for info.
@ -736,7 +739,7 @@ def proppatch(path, xml_request, collection):
multistatus.append(response) multistatus.append(response)
href = ET.Element(_tag("D", "href")) href = ET.Element(_tag("D", "href"))
href.text = _href(collection, path) href.text = _href(base_prefix, path)
response.append(href) response.append(href)
for short_name in props_to_remove: for short_name in props_to_remove:
@ -749,7 +752,7 @@ def proppatch(path, xml_request, collection):
return _pretty_xml(multistatus) return _pretty_xml(multistatus)
def report(path, xml_request, collection): def report(base_prefix, path, xml_request, collection):
"""Read and answer REPORT requests. """Read and answer REPORT requests.
Read rfc3253-3.6 for info. Read rfc3253-3.6 for info.
@ -766,12 +769,15 @@ def report(path, xml_request, collection):
_tag("C", "calendar-multiget"), _tag("C", "calendar-multiget"),
_tag("CR", "addressbook-multiget")): _tag("CR", "addressbook-multiget")):
# Read rfc4791-7.9 for info # Read rfc4791-7.9 for info
base_prefix = collection.configuration.get("server", "base_prefix")
hreferences = set() hreferences = set()
for href_element in root.findall(_tag("D", "href")): for href_element in root.findall(_tag("D", "href")):
href_path = unquote(urlparse(href_element.text).path) href_path = storage.sanitize_path(
if href_path.startswith(base_prefix): unquote(urlparse(href_element.text).path))
hreferences.add(href_path[len(base_prefix) - 1:]) if (href_path + "/").startswith(base_prefix + "/"):
hreferences.add(href_path[len(base_prefix):])
else:
collection.logger.info(
"Skipping invalid path: %s", href_path)
else: else:
hreferences = (path,) hreferences = (path,)
filters = ( filters = (
@ -783,12 +789,20 @@ def report(path, xml_request, collection):
multistatus = ET.Element(_tag("D", "multistatus")) multistatus = ET.Element(_tag("D", "multistatus"))
for hreference in hreferences: for hreference in hreferences:
try:
name = name_from_path(hreference, collection) name = name_from_path(hreference, collection)
except ValueError:
collection.logger.info("Skipping invalid path: %s", hreference)
response = _item_response(base_prefix, hreference,
found_item=False)
multistatus.append(response)
continue
if name: if name:
# Reference is an item # Reference is an item
item = collection.get(name) item = collection.get(name)
if not item: if not item:
response = _item_response(hreference, found_item=False) response = _item_response(base_prefix, hreference,
found_item=False)
multistatus.append(response) multistatus.append(response)
continue continue
items = [item] items = [item]
@ -829,17 +843,18 @@ def report(path, xml_request, collection):
uri = "/" + posixpath.join(collection.path, item.href) uri = "/" + posixpath.join(collection.path, item.href)
multistatus.append(_item_response( multistatus.append(_item_response(
uri, found_props=found_props, base_prefix, uri, found_props=found_props,
not_found_props=not_found_props, found_item=True)) not_found_props=not_found_props, found_item=True))
return _pretty_xml(multistatus) return _pretty_xml(multistatus)
def _item_response(href, found_props=(), not_found_props=(), found_item=True): def _item_response(base_prefix, href, found_props=(), not_found_props=(),
found_item=True):
response = ET.Element(_tag("D", "response")) response = ET.Element(_tag("D", "response"))
href_tag = ET.Element(_tag("D", "href")) href_tag = ET.Element(_tag("D", "href"))
href_tag.text = href href_tag.text = _href(base_prefix, href)
response.append(href_tag) response.append(href_tag)
if found_item: if found_item: