diff --git a/config b/config index d672a0fc..4f8a458e 100644 --- a/config +++ b/config @@ -27,10 +27,8 @@ certificate = /etc/apache2/ssl/server.crt key = /etc/apache2/ssl/server.key # Reverse DNS to resolve client address in logs dns_lookup = True -# base URL if / is not the CalDAV root. If set, must start with / -base_prefix = -# base URL if Radicale is running with / as CalDAV root, but in a subdir behind a proxy (like nginx). If set, must start with / -proxy_base_prefix = +# Root URL of Radicale (starting and ending with a slash) +base_prefix = / [encoding] diff --git a/radicale/__init__.py b/radicale/__init__.py index 64f526ec..1d9fe2da 100644 --- a/radicale/__init__.py +++ b/radicale/__init__.py @@ -246,9 +246,17 @@ class Application(object): headers = pprint.pformat(self.headers_log(environ)) log.LOGGER.debug("Request headers:\n%s" % headers) - # Sanitize request URI - environ["PATH_INFO"] = self.sanitize_uri(environ["PATH_INFO"]) - log.LOGGER.debug("Sanitized path: %s", environ["PATH_INFO"]) + base_prefix = config.get("server", "base_prefix") + if environ["PATH_INFO"].startswith(base_prefix): + # Sanitize request URI + environ["PATH_INFO"] = self.sanitize_uri( + "/%s" % environ["PATH_INFO"][len(base_prefix):]) + log.LOGGER.debug("Sanitized path: %s", environ["PATH_INFO"]) + else: + # Request path not starting with base_prefix, not allowed + log.LOGGER.debug( + "Path not starting with prefix: %s", environ["PATH_INFO"]) + environ["PATH_INFO"] = None # Get content content_length = int(environ.get("CONTENT_LENGTH") or 0) diff --git a/radicale/__main__.py b/radicale/__main__.py index a94178c6..0bf4160e 100644 --- a/radicale/__main__.py +++ b/radicale/__main__.py @@ -146,6 +146,9 @@ def run(): finally: shutdown_program.set() + log.LOGGER.debug( + "Base URL prefix: %s" % config.get("server", "base_prefix")) + # Start the servers in a different loop to avoid possible race-conditions, # when a server exists but another server is added to the list at the same # time diff --git a/radicale/config.py b/radicale/config.py index df2db6c3..0fff2371 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -46,8 +46,7 @@ INITIAL_CONFIG = { "certificate": "/etc/apache2/ssl/server.crt", "key": "/etc/apache2/ssl/server.key", "dns_lookup": "True", - "base_prefix": "", - "proxy_base_prefix": ""}, + "base_prefix": "/"}, "encoding": { "request": "utf-8", "stock": "utf-8"}, diff --git a/radicale/ical.py b/radicale/ical.py index 27fd5c68..97fa9563 100644 --- a/radicale/ical.py +++ b/radicale/ical.py @@ -203,6 +203,10 @@ class Collection(object): The ``path`` is relative. """ + # path == None means wrong URL + if path is None: + return [] + # First do normpath and then strip, to prevent access to FOLDER/../ sane_path = posixpath.normpath(path.replace(os.sep, "/")).strip("/") attributes = sane_path.split("/") @@ -490,7 +494,7 @@ class Collection(object): @property def url(self): """Get the standard collection URL.""" - return "/%s/" % self.path + return "%s/" % self.path @property def version(self): diff --git a/radicale/xmlutils.py b/radicale/xmlutils.py index 6154d189..fbe5855a 100644 --- a/radicale/xmlutils.py +++ b/radicale/xmlutils.py @@ -119,9 +119,9 @@ def _response(code): return "HTTP/1.1 %i %s" % (code, client.responses[code]) -def _href_with_proxy_base_prefix(href): - href = "%s%s" % (config.get("server", "proxy_base_prefix"), href) - return href.replace("//", "/") +def _href(href): + """Return prefixed href.""" + return "%s%s" % (config.get("server", "base_prefix"), href.lstrip("/")) def name_from_path(path, collection): @@ -179,7 +179,7 @@ def delete(path, collection): multistatus.append(response) href = ET.Element(_tag("D", "href")) - href.text = _href_with_proxy_base_prefix(path) + href.text = _href(path) response.append(href) status = ET.Element(_tag("D", "status")) @@ -231,11 +231,8 @@ def _propfind_response(path, item, props, user): response = ET.Element(_tag("D", "response")) href = ET.Element(_tag("D", "href")) - if is_collection: - uri = "%s%s" % (config.get("server", "base_prefix"), item.url) - else: - uri = "%s/%s" % (path, item.name) - href.text = _href_with_proxy_base_prefix(uri) + uri = item.url if is_collection else "%s/%s" % (path, item.name) + href.text = _href(uri.replace("//", "/")) response.append(href) propstat404 = ET.Element(_tag("D", "propstat")) @@ -255,14 +252,14 @@ def _propfind_response(path, item, props, user): element.text = item.etag elif tag == _tag("D", "principal-URL"): tag = ET.Element(_tag("D", "href")) - tag.text = _href_with_proxy_base_prefix(path) + tag.text = _href(path) element.append(tag) elif tag in (_tag("D", "principal-collection-set"), _tag("C", "calendar-user-address-set"), _tag("CR", "addressbook-home-set"), _tag("C", "calendar-home-set")): tag = ET.Element(_tag("D", "href")) - tag.text = _href_with_proxy_base_prefix(path) + tag.text = _href(path) element.append(tag) elif tag == _tag("C", "supported-calendar-component-set"): # This is not a Todo @@ -274,8 +271,7 @@ def _propfind_response(path, item, props, user): # pylint: enable=W0511 elif tag == _tag("D", "current-user-principal") and user: tag = ET.Element(_tag("D", "href")) - prefixed_path = "%s/%s/" % (config.get("server", "base_prefix"), user) - tag.text = _href_with_proxy_base_prefix(prefixed_path) + tag.text = _href("/%s/" % user) element.append(tag) elif tag == _tag("D", "current-user-privilege-set"): privilege = ET.Element(_tag("D", "privilege")) @@ -399,7 +395,7 @@ def proppatch(path, xml_request, collection): multistatus.append(response) href = ET.Element(_tag("D", "href")) - href.text = _href_with_proxy_base_prefix(path) + href.text = _href(path) response.append(href) with collection.props as collection_props: @@ -442,23 +438,15 @@ def report(path, xml_request, collection): prop_element = root.find(_tag("D", "prop")) props = [prop.tag for prop in prop_element] - proxy_prefix = config.get("server", "proxy_base_prefix") - base_prefix = config.get("server", "base_prefix") - if collection: if root.tag in (_tag("C", "calendar-multiget"), _tag("CR", "addressbook-multiget")): # Read rfc4791-7.9 for info - hreferences = set() - for href_element in root.findall(_tag("D", "href")): - # skip elements that don't have the correct base prefixes - if not href_element.text.startswith(proxy_prefix): - continue - unprefixed = href_element.text[len(proxy_prefix):] - if not unprefixed.startswith(base_prefix): - continue - # we keep the base prefix here, to be aligned with other paths - hreferences.add(unprefixed) + base_prefix = config.get("server", "base_prefix") + hreferences = set( + href_element.text[len(base_prefix):] for href_element + in root.findall(_tag("D", "href")) + if href_element.text.startswith(base_prefix)) else: hreferences = (path,) # TODO: handle other filters @@ -480,9 +468,8 @@ def report(path, xml_request, collection): collection_timezones = collection.timezones for hreference in hreferences: - unprefixed_hreference = hreference[len(base_prefix):] # Check if the reference is an item or a collection - name = name_from_path(unprefixed_hreference, collection) + name = name_from_path(hreference, collection) if name: # Reference is an item path = "/".join(hreference.split("/")[:-1]) + "/" @@ -500,7 +487,7 @@ def report(path, xml_request, collection): multistatus.append(response) href = ET.Element(_tag("D", "href")) - href.text = _href_with_proxy_base_prefix("%s/%s" % (path.rstrip("/"), item.name)) + href.text = _href("%s/%s" % (path.rstrip("/"), item.name)) response.append(href) propstat = ET.Element(_tag("D", "propstat"))