Просмотр исходного кода

Merge branch 'url' of https://github.com/Unrud/Radicale into Unrud-url

Guillaume Ayoub 9 лет назад
Родитель
Сommit
3213495245
5 измененных файлов с 76 добавлено и 77 удалено
  1. 0 6
      config
  2. 33 34
      radicale/__init__.py
  3. 0 3
      radicale/__main__.py
  4. 0 6
      radicale/config.py
  5. 43 28
      radicale/xmlutils.py

+ 0 - 6
config

@@ -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
 
 

+ 33 - 34
radicale/__init__.py

@@ -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
-        base_prefix = self.configuration.get("server", "base_prefix")
-        if environ["PATH_INFO"].startswith(base_prefix):
-            environ["PATH_INFO"] = environ["PATH_INFO"][len(base_prefix):]
-        elif self.configuration.get("server", "can_skip_base_prefix"):
-            self.logger.debug(
-                "Prefix already stripped from path: %s", environ["PATH_INFO"])
-        else:
-            # Request path not starting with base_prefix, not allowed
-            self.logger.debug(
-                "Path not starting with prefix: %s", environ["PATH_INFO"])
-            return response(*NOT_ALLOWED)
-
+        # Let reverse proxies overwrite SCRIPT_NAME
+        if "HTTP_X_SCRIPT_NAME" in environ:
+            environ["SCRIPT_NAME"] = environ["HTTP_X_SCRIPT_NAME"]
+            self.logger.debug("Script name overwritten by client: %s",
+                              environ["SCRIPT_NAME"])
+        # Sanitize base prefix
+        environ["SCRIPT_NAME"] = storage.sanitize_path(
+            environ.get("SCRIPT_NAME", "")).rstrip("/")
+        self.logger.debug("Sanitized script name: %s", environ["SCRIPT_NAME"])
+        base_prefix = environ["SCRIPT_NAME"]
         # Sanitize request URI
         # Sanitize request URI
-        environ["PATH_INFO"] = storage.sanitize_path(
-            unquote(environ["PATH_INFO"]))
+        environ["PATH_INFO"] = storage.sanitize_path(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

+ 0 - 3
radicale/__main__.py

@@ -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"):

+ 0 - 6
radicale/config.py

@@ -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"})])),

+ 43 - 28
radicale/xmlutils.py

@@ -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" % (
-        collection.configuration.get("server", "base_prefix"),
-        href.lstrip("/"))
+    return quote("%s%s" % (base_prefix, href))
 
 
 
 
 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)
-                if href_path.startswith(base_prefix):
-                    hreferences.add(href_path[len(base_prefix) - 1:])
+                href_path = storage.sanitize_path(
+                    unquote(urlparse(href_element.text).path))
+                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:
-        name = name_from_path(hreference, collection)
+        try:
+            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: