Exemplo n.º 1
0
    def __init__(self, storage_, path):
        self._storage = storage_
        # Path should already be sanitized
        self._path = pathutils.sanitize_path(path).strip("/")

        attributes = _get_attributes_from_path(path)
        self.etesync = self._storage.etesync
        if len(attributes) == 2:
            self.uid = attributes[-1]
            self.journal = self.etesync.get(self.uid)
            self.collection = self.journal.collection
            if isinstance(self.collection, api.Calendar):
                self.tag = "VCALENDAR"
                self.meta_mappings = MetaMappingCalendar()
                self.content_suffix = ".ics"
            elif isinstance(self.collection, api.TaskList):
                self.tag = "VCALENDAR"
                self.meta_mappings = MetaMappingTaskList()
                self.content_suffix = ".ics"
            elif isinstance(self.collection, api.AddressBook):
                self.tag = "VADDRESSBOOK"
                self.meta_mappings = MetaMappingContacts()
                self.content_suffix = ".vcf"

            self.is_fake = False
        else:
            self.is_fake = True

        super().__init__()
Exemplo n.º 2
0
 def get(self, environ, base_prefix, path, user):
     assert path == "/.web" or path.startswith("/.web/")
     assert pathutils.sanitize_path(path) == path
     try:
         filesystem_path = pathutils.path_to_filesystem(
             self.folder, path[len("/.web"):].strip("/"))
     except ValueError as e:
         logger.debug("Web content with unsafe path %r requested: %s",
                      path, e, exc_info=True)
         return httputils.NOT_FOUND
     if os.path.isdir(filesystem_path) and not path.endswith("/"):
         location = posixpath.basename(path) + "/"
         return (client.FOUND,
                 {"Location": location, "Content-Type": "text/plain"},
                 "Redirected to %s" % location)
     if os.path.isdir(filesystem_path):
         filesystem_path = os.path.join(filesystem_path, "index.html")
     if not os.path.isfile(filesystem_path):
         return httputils.NOT_FOUND
     content_type = MIMETYPES.get(
         os.path.splitext(filesystem_path)[1].lower(), FALLBACK_MIMETYPE)
     with open(filesystem_path, "rb") as f:
         answer = f.read()
         last_modified = time.strftime(
             "%a, %d %b %Y %H:%M:%S GMT",
             time.gmtime(os.fstat(f.fileno()).st_mtime))
     headers = {
         "Content-Type": content_type,
         "Last-Modified": last_modified}
     return client.OK, headers, answer
Exemplo n.º 3
0
    def do_MOVE(self, environ, base_prefix, path, user):
        """Manage MOVE request."""
        raw_dest = environ.get("HTTP_DESTINATION", "")
        to_url = urlparse(raw_dest)
        if to_url.netloc != environ["HTTP_HOST"]:
            logger.info("Unsupported destination address: %r", raw_dest)
            # Remote destination server, not supported
            return httputils.REMOTE_DESTINATION
        if not self.access(user, path, "w"):
            return httputils.NOT_ALLOWED
        to_path = pathutils.sanitize_path(to_url.path)
        if not (to_path + "/").startswith(base_prefix + "/"):
            logger.warning(
                "Destination %r from MOVE request on %r doesn't "
                "start with base prefix", to_path, path)
            return httputils.NOT_ALLOWED
        to_path = to_path[len(base_prefix):]
        if not self.access(user, to_path, "w"):
            return httputils.NOT_ALLOWED

        with self.Collection.acquire_lock("w", user):
            item = next(self.Collection.discover(path), None)
            if not item:
                return httputils.NOT_FOUND
            if (not self.access(user, path, "w", item)
                    or not self.access(user, to_path, "w", item)):
                return httputils.NOT_ALLOWED
            if isinstance(item, storage.BaseCollection):
                # TODO: support moving collections
                return httputils.METHOD_NOT_ALLOWED

            to_item = next(self.Collection.discover(to_path), None)
            if isinstance(to_item, storage.BaseCollection):
                return httputils.FORBIDDEN
            to_parent_path = pathutils.unstrip_path(
                posixpath.dirname(pathutils.strip_path(to_path)), True)
            to_collection = next(self.Collection.discover(to_parent_path),
                                 None)
            if not to_collection:
                return httputils.CONFLICT
            tag = item.collection.get_meta("tag")
            if not tag or tag != to_collection.get_meta("tag"):
                return httputils.FORBIDDEN
            if to_item and environ.get("HTTP_OVERWRITE", "F") != "T":
                return httputils.PRECONDITION_FAILED
            if (to_item and item.uid != to_item.uid or not to_item
                    and to_collection.path != item.collection.path
                    and to_collection.has_uid(item.uid)):
                return self.webdav_error_response(
                    "C" if tag == "VCALENDAR" else "CR", "no-uid-conflict")
            to_href = posixpath.basename(pathutils.strip_path(to_path))
            try:
                self.Collection.move(item, to_collection, to_href)
            except ValueError as e:
                logger.warning("Bad MOVE request on %r: %s",
                               path,
                               e,
                               exc_info=True)
                return httputils.BAD_REQUEST
            return client.NO_CONTENT if to_item else client.CREATED, {}, None
Exemplo n.º 4
0
def _get_attributes_from_path(path):
    sane_path = pathutils.sanitize_path(path).strip("/")
    attributes = sane_path.split("/", 2)
    if not attributes[0]:
        attributes.pop()

    return attributes
Exemplo n.º 5
0
 def get(self, environ, base_prefix, path, user):
     assert path == "/.web" or path.startswith("/.web/")
     assert pathutils.sanitize_path(path) == path
     try:
         filesystem_path = pathutils.path_to_filesystem(
             self.folder, path[len("/.web"):].strip("/"))
     except ValueError as e:
         logger.debug("Web content with unsafe path %r requested: %s",
                      path,
                      e,
                      exc_info=True)
         return httputils.NOT_FOUND
     if os.path.isdir(filesystem_path) and not path.endswith("/"):
         location = posixpath.basename(path) + "/"
         return (client.FOUND, {
             "Location": location,
             "Content-Type": "text/plain"
         }, "Redirected to %s" % location)
     if os.path.isdir(filesystem_path):
         filesystem_path = os.path.join(filesystem_path, "index.html")
     if not os.path.isfile(filesystem_path):
         return httputils.NOT_FOUND
     content_type = MIMETYPES.get(
         os.path.splitext(filesystem_path)[1].lower(), FALLBACK_MIMETYPE)
     with open(filesystem_path, "rb") as f:
         answer = f.read()
         last_modified = time.strftime(
             "%a, %d %b %Y %H:%M:%S GMT",
             time.gmtime(os.fstat(f.fileno()).st_mtime))
     headers = {
         "Content-Type": content_type,
         "Last-Modified": last_modified
     }
     return client.OK, headers, answer
Exemplo n.º 6
0
    def __init__(self, storage_, path):
        self._storage = storage_
        # Path should already be sanitized
        self._path = pathutils.sanitize_path(path).strip("/")

        attributes = _get_attributes_from_path(path)
        self.etesync = self._storage.etesync
        if len(attributes) == 2:
            self.uid = attributes[-1]
            self.collection = self.etesync.get(self.uid)
            col_type = self.collection.col_type
            if col_type == "etebase.vevent":
                self.tag = "VCALENDAR"
                self.meta_mappings = MetaMappingCalendar()
                self.content_suffix = ".ics"
            elif col_type == "etebase.vtodo":
                self.tag = "VCALENDAR"
                self.meta_mappings = MetaMappingTaskList()
                self.content_suffix = ".ics"
            elif col_type == "etebase.vcard":
                self.tag = "VADDRESSBOOK"
                self.meta_mappings = MetaMappingContacts()
                self.content_suffix = ".vcf"

            self.is_fake = False
        else:
            self.is_fake = True

        super().__init__()
Exemplo n.º 7
0
def _is_principal(path):
    sane_path = pathutils.sanitize_path(path).strip("/")
    attributes = sane_path.split("/")
    if not attributes[0]:
        attributes.pop()

    # It's a principal if all we have is the user
    return len(attributes) == 1
Exemplo n.º 8
0
    def do_MOVE(self, environ, base_prefix, path, user):
        """Manage MOVE request."""
        raw_dest = environ.get("HTTP_DESTINATION", "")
        to_url = urlparse(raw_dest)
        if to_url.netloc != environ["HTTP_HOST"]:
            logger.info("Unsupported destination address: %r", raw_dest)
            # Remote destination server, not supported
            return httputils.REMOTE_DESTINATION
        if not self.access(user, path, "w"):
            return httputils.NOT_ALLOWED
        to_path = pathutils.sanitize_path(to_url.path)
        if not (to_path + "/").startswith(base_prefix + "/"):
            logger.warning("Destination %r from MOVE request on %r doesn't "
                           "start with base prefix", to_path, path)
            return httputils.NOT_ALLOWED
        to_path = to_path[len(base_prefix):]
        if not self.access(user, to_path, "w"):
            return httputils.NOT_ALLOWED

        with self.Collection.acquire_lock("w", user):
            item = next(self.Collection.discover(path), None)
            if not item:
                return httputils.NOT_FOUND
            if (not self.access(user, path, "w", item) or
                    not self.access(user, to_path, "w", item)):
                return httputils.NOT_ALLOWED
            if isinstance(item, storage.BaseCollection):
                # TODO: support moving collections
                return httputils.METHOD_NOT_ALLOWED

            to_item = next(self.Collection.discover(to_path), None)
            if isinstance(to_item, storage.BaseCollection):
                return httputils.FORBIDDEN
            to_parent_path = pathutils.unstrip_path(
                posixpath.dirname(pathutils.strip_path(to_path)), True)
            to_collection = next(
                self.Collection.discover(to_parent_path), None)
            if not to_collection:
                return httputils.CONFLICT
            tag = item.collection.get_meta("tag")
            if not tag or tag != to_collection.get_meta("tag"):
                return httputils.FORBIDDEN
            if to_item and environ.get("HTTP_OVERWRITE", "F") != "T":
                return httputils.PRECONDITION_FAILED
            if (to_item and item.uid != to_item.uid or
                    not to_item and
                    to_collection.path != item.collection.path and
                    to_collection.has_uid(item.uid)):
                return self.webdav_error_response(
                    "C" if tag == "VCALENDAR" else "CR", "no-uid-conflict")
            to_href = posixpath.basename(pathutils.strip_path(to_path))
            try:
                self.Collection.move(item, to_collection, to_href)
            except ValueError as e:
                logger.warning(
                    "Bad MOVE request on %r: %s", path, e, exc_info=True)
                return httputils.BAD_REQUEST
            return client.NO_CONTENT if to_item else client.CREATED, {}, None
Exemplo n.º 9
0
    def __init__(self, collection_path=None, collection=None,
                 vobject_item=None, href=None, last_modified=None, text=None,
                 etag=None, uid=None, name=None, component_name=None,
                 time_range=None):
        """Initialize an item.

        ``collection_path`` the path of the parent collection (optional if
        ``collection`` is set).

        ``collection`` the parent collection (optional).

        ``href`` the href of the item.

        ``last_modified`` the HTTP-datetime of when the item was modified.

        ``text`` the text representation of the item (optional if
        ``vobject_item`` is set).

        ``vobject_item`` the vobject item (optional if ``text`` is set).

        ``etag`` the etag of the item (optional). See ``get_etag``.

        ``uid`` the UID of the object (optional). See ``get_uid_from_object``.

        ``name`` the name of the item (optional). See ``vobject_item.name``.

        ``component_name`` the name of the primary component (optional).
        See ``find_tag``.

        ``time_range`` the enclosing time range.
        See ``find_tag_and_time_range``.

        """
        if text is None and vobject_item is None:
            raise ValueError(
                "at least one of 'text' or 'vobject_item' must be set")
        if collection_path is None:
            if collection is None:
                raise ValueError("at least one of 'collection_path' or "
                                 "'collection' must be set")
            collection_path = collection.path
        assert collection_path == pathutils.strip_path(
            pathutils.sanitize_path(collection_path))
        self._collection_path = collection_path
        self.collection = collection
        self.href = href
        self.last_modified = last_modified
        self._text = text
        self._vobject_item = vobject_item
        self._etag = etag
        self._uid = uid
        self._name = name
        self._component_name = component_name
        self._time_range = time_range
Exemplo n.º 10
0
    def __init__(self, collection_path=None, collection=None,
                 vobject_item=None, href=None, last_modified=None, text=None,
                 etag=None, uid=None, name=None, component_name=None,
                 time_range=None):
        """Initialize an item.

        ``collection_path`` the path of the parent collection (optional if
        ``collection`` is set).

        ``collection`` the parent collection (optional).

        ``href`` the href of the item.

        ``last_modified`` the HTTP-datetime of when the item was modified.

        ``text`` the text representation of the item (optional if
        ``vobject_item`` is set).

        ``vobject_item`` the vobject item (optional if ``text`` is set).

        ``etag`` the etag of the item (optional). See ``get_etag``.

        ``uid`` the UID of the object (optional). See ``get_uid_from_object``.

        ``name`` the name of the item (optional). See ``vobject_item.name``.

        ``component_name`` the name of the primary component (optional).
        See ``find_tag``.

        ``time_range`` the enclosing time range.
        See ``find_tag_and_time_range``.

        """
        if text is None and vobject_item is None:
            raise ValueError(
                "at least one of 'text' or 'vobject_item' must be set")
        if collection_path is None:
            if collection is None:
                raise ValueError("at least one of 'collection_path' or "
                                 "'collection' must be set")
            collection_path = collection.path
        assert collection_path == pathutils.strip_path(
            pathutils.sanitize_path(collection_path))
        self._collection_path = collection_path
        self.collection = collection
        self.href = href
        self.last_modified = last_modified
        self._text = text
        self._vobject_item = vobject_item
        self._etag = etag
        self._uid = uid
        self._name = name
        self._component_name = component_name
        self._time_range = time_range
Exemplo n.º 11
0
def xml_report(base_prefix, path, xml_request, collection, encoding,
               unlock_storage_fn):
    """Read and answer REPORT requests.

    Read rfc3253-3.6 for info.

    """
    multistatus = ET.Element(xmlutils.make_clark("D:multistatus"))
    if xml_request is None:
        return client.MULTI_STATUS, multistatus
    root = xml_request
    if root.tag in (xmlutils.make_clark("D:principal-search-property-set"),
                    xmlutils.make_clark("D:principal-property-search"),
                    xmlutils.make_clark("D:expand-property")):
        # We don't support searching for principals or indirect retrieving of
        # properties, just return an empty result.
        # InfCloud asks for expand-property reports (even if we don't announce
        # support for them) and stops working if an error code is returned.
        logger.warning("Unsupported REPORT method %r on %r requested",
                       xmlutils.make_human_tag(root.tag), path)
        return client.MULTI_STATUS, multistatus
    if (root.tag == xmlutils.make_clark("C:calendar-multiget")
            and collection.get_meta("tag") != "VCALENDAR"
            or root.tag == xmlutils.make_clark("CR:addressbook-multiget")
            and collection.get_meta("tag") != "VADDRESSBOOK"
            or root.tag == xmlutils.make_clark("D:sync-collection") and
            collection.get_meta("tag") not in ("VADDRESSBOOK", "VCALENDAR")):
        logger.warning("Invalid REPORT method %r on %r requested",
                       xmlutils.make_human_tag(root.tag), path)
        return (client.FORBIDDEN, xmlutils.webdav_error("D:supported-report"))
    prop_element = root.find(xmlutils.make_clark("D:prop"))
    props = ([prop.tag
              for prop in prop_element] if prop_element is not None else [])

    if root.tag in (xmlutils.make_clark("C:calendar-multiget"),
                    xmlutils.make_clark("CR:addressbook-multiget")):
        # Read rfc4791-7.9 for info
        hreferences = set()
        for href_element in root.findall(xmlutils.make_clark("D:href")):
            href_path = pathutils.sanitize_path(
                unquote(urlparse(href_element.text).path))
            if (href_path + "/").startswith(base_prefix + "/"):
                hreferences.add(href_path[len(base_prefix):])
            else:
                logger.warning(
                    "Skipping invalid path %r in REPORT request on "
                    "%r", href_path, path)
    elif root.tag == xmlutils.make_clark("D:sync-collection"):
        old_sync_token_element = root.find(xmlutils.make_clark("D:sync-token"))
        old_sync_token = ""
        if old_sync_token_element is not None and old_sync_token_element.text:
            old_sync_token = old_sync_token_element.text.strip()
        logger.debug("Client provided sync token: %r", old_sync_token)
        try:
            sync_token, names = collection.sync(old_sync_token)
        except ValueError as e:
            # Invalid sync token
            logger.warning("Client provided invalid sync token %r: %s",
                           old_sync_token,
                           e,
                           exc_info=True)
            # client.CONFLICT doesn't work with some clients (e.g. InfCloud)
            return (client.FORBIDDEN,
                    xmlutils.webdav_error("D:valid-sync-token"))
        hreferences = (pathutils.unstrip_path(
            posixpath.join(collection.path, n)) for n in names)
        # Append current sync token to response
        sync_token_element = ET.Element(xmlutils.make_clark("D:sync-token"))
        sync_token_element.text = sync_token
        multistatus.append(sync_token_element)
    else:
        hreferences = (path, )
    filters = (root.findall(xmlutils.make_clark("C:filter")) +
               root.findall(xmlutils.make_clark("CR:filter")))

    def retrieve_items(collection, hreferences, multistatus):
        """Retrieves all items that are referenced in ``hreferences`` from
           ``collection`` and adds 404 responses for missing and invalid items
           to ``multistatus``."""
        collection_requested = False

        def get_names():
            """Extracts all names from references in ``hreferences`` and adds
               404 responses for invalid references to ``multistatus``.
               If the whole collections is referenced ``collection_requested``
               gets set to ``True``."""
            nonlocal collection_requested
            for hreference in hreferences:
                try:
                    name = pathutils.name_from_path(hreference, collection)
                except ValueError as e:
                    logger.warning(
                        "Skipping invalid path %r in REPORT request"
                        " on %r: %s", hreference, path, e)
                    response = xml_item_response(base_prefix,
                                                 hreference,
                                                 found_item=False)
                    multistatus.append(response)
                    continue
                if name:
                    # Reference is an item
                    yield name
                else:
                    # Reference is a collection
                    collection_requested = True

        for name, item in collection.get_multi(get_names()):
            if not item:
                uri = pathutils.unstrip_path(
                    posixpath.join(collection.path, name))
                response = xml_item_response(base_prefix,
                                             uri,
                                             found_item=False)
                multistatus.append(response)
            else:
                yield item, False
        if collection_requested:
            yield from collection.get_filtered(filters)

    # Retrieve everything required for finishing the request.
    retrieved_items = list(retrieve_items(collection, hreferences,
                                          multistatus))
    collection_tag = collection.get_meta("tag")
    # Don't access storage after this!
    unlock_storage_fn()

    def match(item, filter_):
        tag = collection_tag
        if (tag == "VCALENDAR"
                and filter_.tag != xmlutils.make_clark("C:%s" % filter_)):
            if len(filter_) == 0:
                return True
            if len(filter_) > 1:
                raise ValueError("Filter with %d children" % len(filter_))
            if filter_[0].tag != xmlutils.make_clark("C:comp-filter"):
                raise ValueError("Unexpected %r in filter" % filter_[0].tag)
            return radicale_filter.comp_match(item, filter_[0])
        if (tag == "VADDRESSBOOK"
                and filter_.tag != xmlutils.make_clark("CR:%s" % filter_)):
            for child in filter_:
                if child.tag != xmlutils.make_clark("CR:prop-filter"):
                    raise ValueError("Unexpected %r in filter" % child.tag)
            test = filter_.get("test", "anyof")
            if test == "anyof":
                return any(
                    radicale_filter.prop_match(item.vobject_item, f, "CR")
                    for f in filter_)
            if test == "allof":
                return all(
                    radicale_filter.prop_match(item.vobject_item, f, "CR")
                    for f in filter_)
            raise ValueError("Unsupported filter test: %r" % test)
        raise ValueError("Unsupported filter %r for %r" % (filter_.tag, tag))

    while retrieved_items:
        # ``item.vobject_item`` might be accessed during filtering.
        # Don't keep reference to ``item``, because VObject requires a lot of
        # memory.
        item, filters_matched = retrieved_items.pop(0)
        if filters and not filters_matched:
            try:
                if not all(match(item, filter_) for filter_ in filters):
                    continue
            except ValueError as e:
                raise ValueError("Failed to filter item %r from %r: %s" %
                                 (item.href, collection.path, e)) from e
            except Exception as e:
                raise RuntimeError("Failed to filter item %r from %r: %s" %
                                   (item.href, collection.path, e)) from e

        found_props = []
        not_found_props = []

        for tag in props:
            element = ET.Element(tag)
            if tag == xmlutils.make_clark("D:getetag"):
                element.text = item.etag
                found_props.append(element)
            elif tag == xmlutils.make_clark("D:getcontenttype"):
                element.text = xmlutils.get_content_type(item, encoding)
                found_props.append(element)
            elif tag in (xmlutils.make_clark("C:calendar-data"),
                         xmlutils.make_clark("CR:address-data")):
                element.text = item.serialize()
                found_props.append(element)
            else:
                not_found_props.append(element)

        uri = pathutils.unstrip_path(posixpath.join(collection.path,
                                                    item.href))
        multistatus.append(
            xml_item_response(base_prefix,
                              uri,
                              found_props=found_props,
                              not_found_props=not_found_props,
                              found_item=True))

    return client.MULTI_STATUS, multistatus
Exemplo n.º 12
0
 def get(self, environ, base_prefix, path, user):
     assert path == "/.web" or path.startswith("/.web/")
     assert pathutils.sanitize_path(path) == path
     if path != "/.web":
         return httputils.NOT_FOUND
     return client.OK, {"Content-Type": "text/plain"}, "Radicale works!"
Exemplo n.º 13
0
def make_href(base_prefix, href):
    """Return prefixed href."""
    assert href == pathutils.sanitize_path(href)
    return quote("%s%s" % (base_prefix, href))
Exemplo n.º 14
0
    def _handle_request(self, environ):
        """Manage a request."""
        def response(status, headers=(), answer=None):
            headers = dict(headers)
            # Set content length
            if answer:
                if hasattr(answer, "encode"):
                    logger.debug("Response content:\n%s", answer)
                    headers["Content-Type"] += "; charset=%s" % self.encoding
                    answer = answer.encode(self.encoding)
                accept_encoding = [
                    encoding.strip() for encoding in
                    environ.get("HTTP_ACCEPT_ENCODING", "").split(",")
                    if encoding.strip()]

                if "gzip" in accept_encoding:
                    zcomp = zlib.compressobj(wbits=16 + zlib.MAX_WBITS)
                    answer = zcomp.compress(answer) + zcomp.flush()
                    headers["Content-Encoding"] = "gzip"

                headers["Content-Length"] = str(len(answer))

            # Add extra headers set in configuration
            if self.configuration.has_section("headers"):
                for key in self.configuration.options("headers"):
                    headers[key] = self.configuration.get("headers", key)

            # Start response
            time_end = datetime.datetime.now()
            status = "%d %s" % (
                status, client.responses.get(status, "Unknown"))
            logger.info(
                "%s response status for %r%s in %.3f seconds: %s",
                environ["REQUEST_METHOD"], environ.get("PATH_INFO", ""),
                depthinfo, (time_end - time_begin).total_seconds(), status)
            # Return response content
            return status, list(headers.items()), [answer] if answer else []

        remote_host = "unknown"
        if environ.get("REMOTE_HOST"):
            remote_host = repr(environ["REMOTE_HOST"])
        elif environ.get("REMOTE_ADDR"):
            remote_host = environ["REMOTE_ADDR"]
        if environ.get("HTTP_X_FORWARDED_FOR"):
            remote_host = "%r (forwarded by %s)" % (
                environ["HTTP_X_FORWARDED_FOR"], remote_host)
        remote_useragent = ""
        if environ.get("HTTP_USER_AGENT"):
            remote_useragent = " using %r" % environ["HTTP_USER_AGENT"]
        depthinfo = ""
        if environ.get("HTTP_DEPTH"):
            depthinfo = " with depth %r" % environ["HTTP_DEPTH"]
        time_begin = datetime.datetime.now()
        logger.info(
            "%s request for %r%s received from %s%s",
            environ["REQUEST_METHOD"], environ.get("PATH_INFO", ""), depthinfo,
            remote_host, remote_useragent)
        headers = pprint.pformat(self._headers_log(environ))
        logger.debug("Request headers:\n%s", headers)

        # Let reverse proxies overwrite SCRIPT_NAME
        if "HTTP_X_SCRIPT_NAME" in environ:
            # script_name must be removed from PATH_INFO by the client.
            unsafe_base_prefix = environ["HTTP_X_SCRIPT_NAME"]
            logger.debug("Script name overwritten by client: %r",
                         unsafe_base_prefix)
        else:
            # SCRIPT_NAME is already removed from PATH_INFO, according to the
            # WSGI specification.
            unsafe_base_prefix = environ.get("SCRIPT_NAME", "")
        # Sanitize base prefix
        base_prefix = pathutils.sanitize_path(unsafe_base_prefix).rstrip("/")
        logger.debug("Sanitized script name: %r", base_prefix)
        # Sanitize request URI (a WSGI server indicates with an empty path,
        # that the URL targets the application root without a trailing slash)
        path = pathutils.sanitize_path(environ.get("PATH_INFO", ""))
        logger.debug("Sanitized path: %r", path)

        # Get function corresponding to method
        function = getattr(self, "do_%s" % environ["REQUEST_METHOD"].upper())

        # If "/.well-known" is not available, clients query "/"
        if path == "/.well-known" or path.startswith("/.well-known/"):
            return response(*httputils.NOT_FOUND)

        # Ask authentication backend to check rights
        login = password = ""
        external_login = self.Auth.get_external_login(environ)
        authorization = environ.get("HTTP_AUTHORIZATION", "")
        if external_login:
            login, password = external_login
            login, password = login or "", password or ""
        elif authorization.startswith("Basic"):
            authorization = authorization[len("Basic"):].strip()
            login, password = self.decode(base64.b64decode(
                authorization.encode("ascii")), environ).split(":", 1)

        user = self.Auth.login(login, password) or "" if login else ""
        if user and login == user:
            logger.info("Successful login: %r", user)
        elif user:
            logger.info("Successful login: %r -> %r", login, user)
        elif login:
            logger.info("Failed login attempt: %r", login)
            # Random delay to avoid timing oracles and bruteforce attacks
            delay = self.configuration.getfloat("auth", "delay")
            if delay > 0:
                random_delay = delay * (0.5 + random.random())
                logger.debug("Sleeping %.3f seconds", random_delay)
                time.sleep(random_delay)

        if user and not pathutils.is_safe_path_component(user):
            # Prevent usernames like "user/calendar.ics"
            logger.info("Refused unsafe username: %r", user)
            user = ""

        # Create principal collection
        if user:
            principal_path = "/%s/" % user
            if self.Rights.authorized(user, principal_path, "W"):
                with self.Collection.acquire_lock("r", user):
                    principal = next(
                        self.Collection.discover(principal_path, depth="1"),
                        None)
                if not principal:
                    with self.Collection.acquire_lock("w", user):
                        try:
                            self.Collection.create_collection(principal_path)
                        except ValueError as e:
                            logger.warning("Failed to create principal "
                                           "collection %r: %s", user, e)
                            user = ""
            else:
                logger.warning("Access to principal path %r denied by "
                               "rights backend", principal_path)

        if self.configuration.getboolean("internal", "internal_server"):
            # Verify content length
            content_length = int(environ.get("CONTENT_LENGTH") or 0)
            if content_length:
                max_content_length = self.configuration.getint(
                    "server", "max_content_length")
                if max_content_length and content_length > max_content_length:
                    logger.info("Request body too large: %d", content_length)
                    return response(*httputils.REQUEST_ENTITY_TOO_LARGE)

        if not login or user:
            status, headers, answer = function(
                environ, base_prefix, path, user)
            if (status, headers, answer) == httputils.NOT_ALLOWED:
                logger.info("Access to %r denied for %s", path,
                            repr(user) if user else "anonymous user")
        else:
            status, headers, answer = httputils.NOT_ALLOWED

        if ((status, headers, answer) == httputils.NOT_ALLOWED and not user and
                not external_login):
            # Unknown or unauthorized user
            logger.debug("Asking client for authentication")
            status = client.UNAUTHORIZED
            realm = self.configuration.get("auth", "realm")
            headers = dict(headers)
            headers.update({
                "WWW-Authenticate":
                "Basic realm=\"%s\"" % realm})

        return response(status, headers, answer)
Exemplo n.º 15
0
def xml_report(base_prefix, path, xml_request, collection, unlock_storage_fn):
    """Read and answer REPORT requests.

    Read rfc3253-3.6 for info.

    """
    multistatus = ET.Element(xmlutils.make_tag("D", "multistatus"))
    if xml_request is None:
        return client.MULTI_STATUS, multistatus
    root = xml_request
    if root.tag in (
            xmlutils.make_tag("D", "principal-search-property-set"),
            xmlutils.make_tag("D", "principal-property-search"),
            xmlutils.make_tag("D", "expand-property")):
        # We don't support searching for principals or indirect retrieving of
        # properties, just return an empty result.
        # InfCloud asks for expand-property reports (even if we don't announce
        # support for them) and stops working if an error code is returned.
        logger.warning("Unsupported REPORT method %r on %r requested",
                       xmlutils.tag_from_clark(root.tag), path)
        return client.MULTI_STATUS, multistatus
    if (root.tag == xmlutils.make_tag("C", "calendar-multiget") and
            collection.get_meta("tag") != "VCALENDAR" or
            root.tag == xmlutils.make_tag("CR", "addressbook-multiget") and
            collection.get_meta("tag") != "VADDRESSBOOK" or
            root.tag == xmlutils.make_tag("D", "sync-collection") and
            collection.get_meta("tag") not in ("VADDRESSBOOK", "VCALENDAR")):
        logger.warning("Invalid REPORT method %r on %r requested",
                       xmlutils.tag_from_clark(root.tag), path)
        return (client.CONFLICT,
                xmlutils.webdav_error("D", "supported-report"))
    prop_element = root.find(xmlutils.make_tag("D", "prop"))
    props = (
        [prop.tag for prop in prop_element]
        if prop_element is not None else [])

    if root.tag in (
            xmlutils.make_tag("C", "calendar-multiget"),
            xmlutils.make_tag("CR", "addressbook-multiget")):
        # Read rfc4791-7.9 for info
        hreferences = set()
        for href_element in root.findall(xmlutils.make_tag("D", "href")):
            href_path = pathutils.sanitize_path(
                unquote(urlparse(href_element.text).path))
            if (href_path + "/").startswith(base_prefix + "/"):
                hreferences.add(href_path[len(base_prefix):])
            else:
                logger.warning("Skipping invalid path %r in REPORT request on "
                               "%r", href_path, path)
    elif root.tag == xmlutils.make_tag("D", "sync-collection"):
        old_sync_token_element = root.find(
            xmlutils.make_tag("D", "sync-token"))
        old_sync_token = ""
        if old_sync_token_element is not None and old_sync_token_element.text:
            old_sync_token = old_sync_token_element.text.strip()
        logger.debug("Client provided sync token: %r", old_sync_token)
        try:
            sync_token, names = collection.sync(old_sync_token)
        except ValueError as e:
            # Invalid sync token
            logger.warning("Client provided invalid sync token %r: %s",
                           old_sync_token, e, exc_info=True)
            return (client.CONFLICT,
                    xmlutils.webdav_error("D", "valid-sync-token"))
        hreferences = (pathutils.unstrip_path(
            posixpath.join(collection.path, n)) for n in names)
        # Append current sync token to response
        sync_token_element = ET.Element(xmlutils.make_tag("D", "sync-token"))
        sync_token_element.text = sync_token
        multistatus.append(sync_token_element)
    else:
        hreferences = (path,)
    filters = (
        root.findall("./%s" % xmlutils.make_tag("C", "filter")) +
        root.findall("./%s" % xmlutils.make_tag("CR", "filter")))

    def retrieve_items(collection, hreferences, multistatus):
        """Retrieves all items that are referenced in ``hreferences`` from
           ``collection`` and adds 404 responses for missing and invalid items
           to ``multistatus``."""
        collection_requested = False

        def get_names():
            """Extracts all names from references in ``hreferences`` and adds
               404 responses for invalid references to ``multistatus``.
               If the whole collections is referenced ``collection_requested``
               gets set to ``True``."""
            nonlocal collection_requested
            for hreference in hreferences:
                try:
                    name = pathutils.name_from_path(hreference, collection)
                except ValueError as e:
                    logger.warning("Skipping invalid path %r in REPORT request"
                                   " on %r: %s", hreference, path, e)
                    response = xml_item_response(base_prefix, hreference,
                                                 found_item=False)
                    multistatus.append(response)
                    continue
                if name:
                    # Reference is an item
                    yield name
                else:
                    # Reference is a collection
                    collection_requested = True

        for name, item in collection.get_multi(get_names()):
            if not item:
                uri = pathutils.unstrip_path(
                    posixpath.join(collection.path, name))
                response = xml_item_response(base_prefix, uri,
                                             found_item=False)
                multistatus.append(response)
            else:
                yield item, False
        if collection_requested:
            yield from collection.get_filtered(filters)

    # Retrieve everything required for finishing the request.
    retrieved_items = list(retrieve_items(collection, hreferences,
                                          multistatus))
    collection_tag = collection.get_meta("tag")
    # Don't access storage after this!
    unlock_storage_fn()

    def match(item, filter_):
        tag = collection_tag
        if (tag == "VCALENDAR" and
                filter_.tag != xmlutils.make_tag("C", filter_)):
            if len(filter_) == 0:
                return True
            if len(filter_) > 1:
                raise ValueError("Filter with %d children" % len(filter_))
            if filter_[0].tag != xmlutils.make_tag("C", "comp-filter"):
                raise ValueError("Unexpected %r in filter" % filter_[0].tag)
            return radicale_filter.comp_match(item, filter_[0])
        if (tag == "VADDRESSBOOK" and
                filter_.tag != xmlutils.make_tag("CR", filter_)):
            for child in filter_:
                if child.tag != xmlutils.make_tag("CR", "prop-filter"):
                    raise ValueError("Unexpected %r in filter" % child.tag)
            test = filter_.get("test", "anyof")
            if test == "anyof":
                return any(
                    radicale_filter.prop_match(item.vobject_item, f, "CR")
                    for f in filter_)
            if test == "allof":
                return all(
                    radicale_filter.prop_match(item.vobject_item, f, "CR")
                    for f in filter_)
            raise ValueError("Unsupported filter test: %r" % test)
            return all(radicale_filter.prop_match(item.vobject_item, f, "CR")
                       for f in filter_)
        raise ValueError("unsupported filter %r for %r" % (filter_.tag, tag))

    while retrieved_items:
        # ``item.vobject_item`` might be accessed during filtering.
        # Don't keep reference to ``item``, because VObject requires a lot of
        # memory.
        item, filters_matched = retrieved_items.pop(0)
        if filters and not filters_matched:
            try:
                if not all(match(item, filter_) for filter_ in filters):
                    continue
            except ValueError as e:
                raise ValueError("Failed to filter item %r from %r: %s" %
                                 (item.href, collection.path, e)) from e
            except Exception as e:
                raise RuntimeError("Failed to filter item %r from %r: %s" %
                                   (item.href, collection.path, e)) from e

        found_props = []
        not_found_props = []

        for tag in props:
            element = ET.Element(tag)
            if tag == xmlutils.make_tag("D", "getetag"):
                element.text = item.etag
                found_props.append(element)
            elif tag == xmlutils.make_tag("D", "getcontenttype"):
                element.text = xmlutils.get_content_type(item)
                found_props.append(element)
            elif tag in (
                    xmlutils.make_tag("C", "calendar-data"),
                    xmlutils.make_tag("CR", "address-data")):
                element.text = item.serialize()
                found_props.append(element)
            else:
                not_found_props.append(element)

        uri = pathutils.unstrip_path(
            posixpath.join(collection.path, item.href))
        multistatus.append(xml_item_response(
            base_prefix, uri, found_props=found_props,
            not_found_props=not_found_props, found_item=True))

    return client.MULTI_STATUS, multistatus
Exemplo n.º 16
0
 def __init__(self, path, filename=None, adapter=None):
     self._path = sanitize_path(path).strip('/')
     self.filename = filename
     self.adapter = adapter
Exemplo n.º 17
0
    def _handle_request(self, environ):
        """Manage a request."""
        def response(status, headers=(), answer=None):
            headers = dict(headers)
            # Set content length
            if answer:
                if hasattr(answer, "encode"):
                    logger.debug("Response content:\n%s", answer)
                    headers["Content-Type"] += "; charset=%s" % self._encoding
                    answer = answer.encode(self._encoding)
                accept_encoding = [
                    encoding.strip() for encoding in environ.get(
                        "HTTP_ACCEPT_ENCODING", "").split(",")
                    if encoding.strip()
                ]

                if "gzip" in accept_encoding:
                    zcomp = zlib.compressobj(wbits=16 + zlib.MAX_WBITS)
                    answer = zcomp.compress(answer) + zcomp.flush()
                    headers["Content-Encoding"] = "gzip"

                headers["Content-Length"] = str(len(answer))

            # Add extra headers set in configuration
            for key in self.configuration.options("headers"):
                headers[key] = self.configuration.get("headers", key)

            # Start response
            time_end = datetime.datetime.now()
            status = "%d %s" % (status, client.responses.get(
                status, "Unknown"))
            logger.info("%s response status for %r%s in %.3f seconds: %s",
                        environ["REQUEST_METHOD"],
                        environ.get("PATH_INFO", ""), depthinfo,
                        (time_end - time_begin).total_seconds(), status)
            # Return response content
            return status, list(headers.items()), [answer] if answer else []

        remote_host = "unknown"
        if environ.get("REMOTE_HOST"):
            remote_host = repr(environ["REMOTE_HOST"])
        elif environ.get("REMOTE_ADDR"):
            remote_host = environ["REMOTE_ADDR"]
        if environ.get("HTTP_X_FORWARDED_FOR"):
            remote_host = "%s (forwarded for %r)" % (
                remote_host, environ["HTTP_X_FORWARDED_FOR"])
        remote_useragent = ""
        if environ.get("HTTP_USER_AGENT"):
            remote_useragent = " using %r" % environ["HTTP_USER_AGENT"]
        depthinfo = ""
        if environ.get("HTTP_DEPTH"):
            depthinfo = " with depth %r" % environ["HTTP_DEPTH"]
        time_begin = datetime.datetime.now()
        logger.info("%s request for %r%s received from %s%s",
                    environ["REQUEST_METHOD"], environ.get("PATH_INFO", ""),
                    depthinfo, remote_host, remote_useragent)
        headers = pprint.pformat(self._headers_log(environ))
        logger.debug("Request headers:\n%s", headers)

        # Let reverse proxies overwrite SCRIPT_NAME
        if "HTTP_X_SCRIPT_NAME" in environ:
            # script_name must be removed from PATH_INFO by the client.
            unsafe_base_prefix = environ["HTTP_X_SCRIPT_NAME"]
            logger.debug("Script name overwritten by client: %r",
                         unsafe_base_prefix)
        else:
            # SCRIPT_NAME is already removed from PATH_INFO, according to the
            # WSGI specification.
            unsafe_base_prefix = environ.get("SCRIPT_NAME", "")
        # Sanitize base prefix
        base_prefix = pathutils.sanitize_path(unsafe_base_prefix).rstrip("/")
        logger.debug("Sanitized script name: %r", base_prefix)
        # Sanitize request URI (a WSGI server indicates with an empty path,
        # that the URL targets the application root without a trailing slash)
        path = pathutils.sanitize_path(environ.get("PATH_INFO", ""))
        logger.debug("Sanitized path: %r", path)

        # Get function corresponding to method
        function = getattr(self, "do_%s" % environ["REQUEST_METHOD"].upper(),
                           None)
        if not function:
            return response(*httputils.METHOD_NOT_ALLOWED)

        # If "/.well-known" is not available, clients query "/"
        if path == "/.well-known" or path.startswith("/.well-known/"):
            return response(*httputils.NOT_FOUND)

        # Ask authentication backend to check rights
        login = password = ""
        external_login = self._auth.get_external_login(environ)
        authorization = environ.get("HTTP_AUTHORIZATION", "")
        if external_login:
            login, password = external_login
            login, password = login or "", password or ""
        elif authorization.startswith("Basic"):
            authorization = authorization[len("Basic"):].strip()
            login, password = httputils.decode_request(
                self.configuration, environ,
                base64.b64decode(authorization.encode("ascii"))).split(":", 1)

        user = self._auth.login(login, password) or "" if login else ""
        if user and login == user:
            logger.info("Successful login: %r", user)
        elif user:
            logger.info("Successful login: %r -> %r", login, user)
        elif login:
            logger.warning("Failed login attempt from %s: %r", remote_host,
                           login)
            # Random delay to avoid timing oracles and bruteforce attacks
            delay = self.configuration.get("auth", "delay")
            if delay > 0:
                random_delay = delay * (0.5 + random.random())
                logger.debug("Sleeping %.3f seconds", random_delay)
                time.sleep(random_delay)

        if user and not pathutils.is_safe_path_component(user):
            # Prevent usernames like "user/calendar.ics"
            logger.info("Refused unsafe username: %r", user)
            user = ""

        # Create principal collection
        if user:
            principal_path = "/%s/" % user
            with self._storage.acquire_lock("r", user):
                principal = next(
                    self._storage.discover(principal_path, depth="1"), None)
            if not principal:
                if "W" in self._rights.authorization(user, principal_path):
                    with self._storage.acquire_lock("w", user):
                        try:
                            self._storage.create_collection(principal_path)
                        except ValueError as e:
                            logger.warning(
                                "Failed to create principal "
                                "collection %r: %s", user, e)
                            user = ""
                else:
                    logger.warning(
                        "Access to principal path %r denied by "
                        "rights backend", principal_path)

        if self.configuration.get("server", "_internal_server"):
            # Verify content length
            content_length = int(environ.get("CONTENT_LENGTH") or 0)
            if content_length:
                max_content_length = self.configuration.get(
                    "server", "max_content_length")
                if max_content_length and content_length > max_content_length:
                    logger.info("Request body too large: %d", content_length)
                    return response(*httputils.REQUEST_ENTITY_TOO_LARGE)

        if not login or user:
            status, headers, answer = function(environ, base_prefix, path,
                                               user)
            if (status, headers, answer) == httputils.NOT_ALLOWED:
                logger.info("Access to %r denied for %s", path,
                            repr(user) if user else "anonymous user")
        else:
            status, headers, answer = httputils.NOT_ALLOWED

        if ((status, headers, answer) == httputils.NOT_ALLOWED and not user
                and not external_login):
            # Unknown or unauthorized user
            logger.debug("Asking client for authentication")
            status = client.UNAUTHORIZED
            realm = self.configuration.get("auth", "realm")
            headers = dict(headers)
            headers.update({"WWW-Authenticate": "Basic realm=\"%s\"" % realm})

        return response(status, headers, answer)
Exemplo n.º 18
0
def make_href(base_prefix, href):
    """Return prefixed href."""
    assert href == pathutils.sanitize_path(href)
    return quote("%s%s" % (base_prefix, href))