Beispiel #1
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 REMOTE_DESTINATION
        if not self._access(user, path, "w"):
            return NOT_ALLOWED
        to_path = storage.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 NOT_ALLOWED
        to_path = to_path[len(base_prefix):]
        if not self._access(user, to_path, "w"):
            return NOT_ALLOWED

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

            to_item = next(self.Collection.discover(to_path), None)
            if isinstance(to_item, storage.BaseCollection):
                return FORBIDDEN
            to_parent_path = storage.sanitize_path(
                "/%s/" % posixpath.dirname(to_path.strip("/")))
            to_collection = next(self.Collection.discover(to_parent_path),
                                 None)
            if not to_collection:
                return CONFLICT
            tag = item.collection.get_meta("tag")
            if not tag or tag != to_collection.get_meta("tag"):
                return FORBIDDEN
            if to_item and environ.get("HTTP_OVERWRITE", "F") != "T":
                return 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(to_path.strip("/"))
            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 BAD_REQUEST
            return client.NO_CONTENT if to_item else client.CREATED, {}, None
Beispiel #2
0
 def collect_allowed_items(self, items, user):
     """Get items from request that user is allowed to access."""
     read_allowed_items = []
     write_allowed_items = []
     for item in items:
         if isinstance(item, storage.BaseCollection):
             path = storage.sanitize_path("/%s/" % item.path)
             can_read = self.Rights.authorized(user, path, "r")
             can_write = self.Rights.authorized(user, path, "w")
             target = "collection %r" % item.path
         else:
             path = storage.sanitize_path("/%s/%s" %
                                          (item.collection.path, item.href))
             can_read = self.Rights.authorized_item(user, path, "r")
             can_write = self.Rights.authorized_item(user, path, "w")
             target = "item %r from %r" % (item.href, item.collection.path)
         text_status = []
         if can_read:
             text_status.append("read")
             read_allowed_items.append(item)
         if can_write:
             text_status.append("write")
             write_allowed_items.append(item)
         self.logger.debug(
             "%s has %s access to %s",
             repr(user) if user else "anonymous user",
             " and ".join(text_status) if text_status else "NO",
             target,
         )
     return read_allowed_items, write_allowed_items
Beispiel #3
0
 def collect_allowed_items(self, items, user):
     """Get items from request that user is allowed to access."""
     read_allowed_items = []
     write_allowed_items = []
     for item in items:
         if isinstance(item, storage.BaseCollection):
             path = storage.sanitize_path("/%s/" % item.path)
             can_read = self.Rights.authorized(user, path, "r")
             can_write = self.Rights.authorized(user, path, "w")
             target = "collection %r" % item.path
         else:
             path = storage.sanitize_path("/%s/%s" % (item.collection.path,
                                                      item.href))
             can_read = self.Rights.authorized_item(user, path, "r")
             can_write = self.Rights.authorized_item(user, path, "w")
             target = "item %r from %r" % (item.href, item.collection.path)
         text_status = []
         if can_read:
             text_status.append("read")
             read_allowed_items.append(item)
         if can_write:
             text_status.append("write")
             write_allowed_items.append(item)
         self.logger.debug(
             "%s has %s access to %s",
             repr(user) if user else "anonymous user",
             " and ".join(text_status) if text_status else "NO", target)
     return read_allowed_items, write_allowed_items
Beispiel #4
0
 def collect_allowed_items(self, items, user):
     """Get items from request that user is allowed to access."""
     for item in items:
         if isinstance(item, storage.BaseCollection):
             path = storage.sanitize_path("/%s/" % item.path)
             if item.get_meta("tag"):
                 permissions = self.Rights.authorized(user, path, "rw")
                 target = "collection with tag %r" % item.path
             else:
                 permissions = self.Rights.authorized(user, path, "RW")
                 target = "collection %r" % item.path
         else:
             path = storage.sanitize_path("/%s/" % item.collection.path)
             permissions = self.Rights.authorized(user, path, "rw")
             target = "item %r from %r" % (item.href, item.collection.path)
         if rights.intersect_permissions(permissions, "Ww"):
             permission = "w"
             status = "write"
         elif rights.intersect_permissions(permissions, "Rr"):
             permission = "r"
             status = "read"
         else:
             permission = ""
             status = "NO"
         logger.debug("%s has %s access to %s",
                      repr(user) if user else "anonymous user", status,
                      target)
         if permission:
             yield item, permission
Beispiel #5
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"]:
            self.logger.info("Unsupported destination address: %r", raw_dest)
            # Remote destination server, not supported
            return REMOTE_DESTINATION
        if not self._access(user, path, "w"):
            return NOT_ALLOWED
        to_path = storage.sanitize_path(to_url.path)
        if not (to_path + "/").startswith(base_prefix + "/"):
            self.logger.warning(
                "Destination %r from MOVE request on %r does"
                "n't start with base prefix", to_path, path)
            return NOT_ALLOWED
        to_path = to_path[len(base_prefix):]
        if not self._access(user, to_path, "w"):
            return NOT_ALLOWED

        with self.Collection.acquire_lock("w", user):
            item = next(self.Collection.discover(path), None)
            if not self._access(user, path, "w", item):
                return NOT_ALLOWED
            if not self._access(user, to_path, "w", item):
                return NOT_ALLOWED
            if not item:
                return NOT_FOUND
            if isinstance(item, storage.BaseCollection):
                return WEBDAV_PRECONDITION_FAILED

            to_item = next(self.Collection.discover(to_path), None)
            if (isinstance(to_item, storage.BaseCollection)
                    or to_item and environ.get("HTTP_OVERWRITE", "F") != "T"):
                return WEBDAV_PRECONDITION_FAILED
            to_parent_path = storage.sanitize_path(
                "/%s/" % posixpath.dirname(to_path.strip("/")))
            to_collection = next(self.Collection.discover(to_parent_path),
                                 None)
            if not to_collection:
                return WEBDAV_PRECONDITION_FAILED
            to_href = posixpath.basename(to_path.strip("/"))
            try:
                self.Collection.move(item, to_collection, to_href)
            except ValueError as e:
                self.logger.warning("Bad MOVE request on %r: %s",
                                    path,
                                    e,
                                    exc_info=True)
                return BAD_REQUEST
            return client.CREATED, {}, None
Beispiel #6
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"]:
            self.logger.info("Unsupported destination address: %r", raw_dest)
            # Remote destination server, not supported
            return REMOTE_DESTINATION
        if not self._access(user, path, "w"):
            return NOT_ALLOWED
        to_path = storage.sanitize_path(to_url.path)
        if not (to_path + "/").startswith(base_prefix + "/"):
            self.logger.warning("Destination %r from MOVE request on %r does"
                                "n't start with base prefix", to_path, path)
            return NOT_ALLOWED
        to_path = to_path[len(base_prefix):]
        if not self._access(user, to_path, "w"):
            return NOT_ALLOWED

        with self.Collection.acquire_lock("w", user):
            item = next(self.Collection.discover(path), None)
            if not self._access(user, path, "w", item):
                return NOT_ALLOWED
            if not self._access(user, to_path, "w", item):
                return NOT_ALLOWED
            if not item:
                return NOT_FOUND
            if isinstance(item, storage.BaseCollection):
                return WEBDAV_PRECONDITION_FAILED

            to_item = next(self.Collection.discover(to_path), None)
            if (isinstance(to_item, storage.BaseCollection) or
                    to_item and environ.get("HTTP_OVERWRITE", "F") != "T"):
                return WEBDAV_PRECONDITION_FAILED
            to_parent_path = storage.sanitize_path(
                "/%s/" % posixpath.dirname(to_path.strip("/")))
            to_collection = next(
                self.Collection.discover(to_parent_path), None)
            if not to_collection:
                return WEBDAV_PRECONDITION_FAILED
            to_href = posixpath.basename(to_path.strip("/"))
            try:
                self.Collection.move(item, to_collection, to_href)
            except ValueError as e:
                self.logger.warning(
                    "Bad MOVE request on %r: %s", path, e, exc_info=True)
                return BAD_REQUEST
            return client.CREATED, {}, None
Beispiel #7
0
    def __init__(self, path, principal=False, folder=None, tag=None):
        attributes = _get_attributes_from_path(path)
        self.etesync = self.__class__.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"

            if tag is not None and tag != self.tag:
                raise RuntimeError("Tag mismatch")

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

        # Needed by Radicale
        self.path = sanitize_path(path).strip("/")
Beispiel #8
0
def _get_attributes_from_path(path):
    sane_path = sanitize_path(path).strip("/")
    attributes = sane_path.split("/", 2)
    if not attributes[0]:
        attributes.pop()

    return attributes
Beispiel #9
0
 def authorized(self, user, path, permissions):
     self.logger.debug(
         "User %r is trying to access path %r. Permissions: %r",
         user,
         path,
         permissions,
     )
     # everybody can access the root collection
     if path == "/":
         self.logger.debug("Accessing root path. Access granted.")
         return True
     user = user or ""
     sane_path = sanitize_path(path)
     sane_path = sane_path.lstrip("/")
     pathowner, subpath = sane_path.split("/", maxsplit=1)
     if user == pathowner:
         self.logger.debug("User %r is pathowner. Access granted.", user)
         return True
     else:
         # Check if pathowner is group of user
         in_group = self.user_in_group(user, pathowner)
         if in_group:
             self.logger.debug(
                 "User %r is in pathowner group %r. Access granted.", user,
                 pathowner)
         else:
             self.logger.debug(
                 "Access to path %r is not granted to user %r.", pathowner,
                 user)
         return in_group
Beispiel #10
0
def _is_principal(path):
    sane_path = 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
Beispiel #11
0
 def authorized(self, user, path, permissions):
     if self._verify_user and not user:
         return ""
     sane_path = storage.sanitize_path(path).strip("/")
     if "/" not in sane_path:
         return intersect_permissions(permissions, "RW")
     if sane_path.count("/") == 1:
         return intersect_permissions(permissions, "rw")
     return ""
Beispiel #12
0
 def __init__(self,
              path,
              principal=None,
              folder=None,
              filesystem_path=None):
     self.path = sanitize_path(path).strip("/")
     self._meta_cache = {}
     self._last_modified = None
     self.attributes = self.path.split("/") if self.path else []
     self.deep_path = len(self.attributes)
Beispiel #13
0
    def discover(cls,
                 path,
                 depth="0",
                 child_context_manager=(
                     lambda path, href=None: contextlib.ExitStack())):
        logger.debug("discover")
        sane_path = sanitize_path(path).strip("/")
        attributes = sane_path.split("/") if sane_path else []
        deep_path = len(attributes)

        if deep_path:
            try:
                user = User.objects.get(username=attributes[0])
            except User.DoesNotExist:
                return

        # Zkontrolujeme jestli je kolekce/item a jestli existuje
        if deep_path == 2:
            if user.collections.filter(name=attributes[-1]).exists():
                href = None
            else:
                return
        elif deep_path == 3:
            try:
                collection = user.collections.get(name=attributes[1])
            except DBCollection.DoesNotExist:
                return
            if collection.items.filter(name=attributes[-1]).exists():
                href = attributes.pop()
            else:
                return
        else:
            href = None

        collection = cls(sane_path)

        if href:
            yield collection.get(href)
            return

        yield collection

        if depth == "0":
            return

        for href in collection.list():
            with child_context_manager(sane_path, href):
                yield collection.get(href)

        for i in user.collections.all():
            path = "{0}/{1}".format(user.username, i.name)
            with child_context_manager(path):
                yield cls(path)
Beispiel #14
0
 def authorized(self, user, path, permission):
     user = user or ""
     sane_path = storage.sanitize_path(path).strip("/")
     # Prevent "regex injection"
     user_escaped = re.escape(user)
     sane_path_escaped = re.escape(sane_path)
     regex = configparser.ConfigParser({
         "login": user_escaped,
         "path": sane_path_escaped
     })
     try:
         if not regex.read(self.filename):
             raise RuntimeError("No such file: %r" % self.filename)
     except Exception as e:
         raise RuntimeError("Failed to load rights file %r: %s" %
                            (self.filename, e)) from e
     for section in regex.sections():
         try:
             re_user_pattern = regex.get(section, "user")
             re_collection_pattern = regex.get(section, "collection")
             # Emulate fullmatch
             user_match = re.match(r"(?:%s)\Z" % re_user_pattern, user)
             collection_match = user_match and re.match(
                 r"(?:%s)\Z" % re_collection_pattern.format(
                     *map(re.escape, user_match.groups())), sane_path)
         except Exception as e:
             raise RuntimeError("Error in section %r of rights file %r: "
                                "%s" % (section, self.filename, e)) from e
         if user_match and collection_match:
             self.logger.debug(
                 "Rule %r:%r matches %r:%r from section %r",
                 user,
                 sane_path,
                 re_user_pattern,
                 re_collection_pattern,
                 section,
             )
             return permission in regex.get(section, "permission")
         else:
             self.logger.debug(
                 "Rule %r:%r doesn't match %r:%r from section"
                 " %r",
                 user,
                 sane_path,
                 re_user_pattern,
                 re_collection_pattern,
                 section,
             )
     self.logger.info("Rights: %r:%r doesn't match any section", user,
                      sane_path)
     return False
Beispiel #15
0
 def do_MKCOL(self, environ, base_prefix, path, user):
     """Manage MKCOL request."""
     permissions = self.Rights.authorized(user, path, "Ww")
     if not permissions:
         return NOT_ALLOWED
     try:
         xml_content = self._read_xml_content(environ)
     except RuntimeError as e:
         logger.warning("Bad MKCOL request on %r: %s",
                        path,
                        e,
                        exc_info=True)
         return BAD_REQUEST
     except socket.timeout as e:
         logger.debug("client timed out", exc_info=True)
         return REQUEST_TIMEOUT
     # Prepare before locking
     props = xmlutils.props_from_request(xml_content)
     try:
         storage.check_and_sanitize_props(props)
     except ValueError as e:
         logger.warning("Bad MKCOL request on %r: %s",
                        path,
                        e,
                        exc_info=True)
         return BAD_REQUEST
     if (props.get("tag") and "w" not in permissions
             or not props.get("tag") and "W" not in permissions):
         return NOT_ALLOWED
     with self.Collection.acquire_lock("w", user):
         item = next(self.Collection.discover(path), None)
         if item:
             return METHOD_NOT_ALLOWED
         parent_path = storage.sanitize_path(
             "/%s/" % posixpath.dirname(path.strip("/")))
         parent_item = next(self.Collection.discover(parent_path), None)
         if not parent_item:
             return CONFLICT
         if (not isinstance(parent_item, storage.BaseCollection)
                 or parent_item.get_meta("tag")):
             return FORBIDDEN
         try:
             self.Collection.create_collection(path, props=props)
         except ValueError as e:
             logger.warning("Bad MKCOL request on %r: %s",
                            path,
                            e,
                            exc_info=True)
             return BAD_REQUEST
         return client.CREATED, {}, None
Beispiel #16
0
 def do_MKCALENDAR(self, environ, base_prefix, path, user):
     """Manage MKCALENDAR request."""
     if not self.Rights.authorized(user, path, "w"):
         return NOT_ALLOWED
     try:
         xml_content = self._read_xml_content(environ)
     except RuntimeError as e:
         logger.warning("Bad MKCALENDAR request on %r: %s",
                        path,
                        e,
                        exc_info=True)
         return BAD_REQUEST
     except socket.timeout as e:
         logger.debug("client timed out", exc_info=True)
         return REQUEST_TIMEOUT
     # Prepare before locking
     props = xmlutils.props_from_request(xml_content)
     props["tag"] = "VCALENDAR"
     # TODO: use this?
     # timezone = props.get("C:calendar-timezone")
     try:
         storage.check_and_sanitize_props(props)
     except ValueError as e:
         logger.warning("Bad MKCALENDAR request on %r: %s",
                        path,
                        e,
                        exc_info=True)
     with self.Collection.acquire_lock("w", user):
         item = next(self.Collection.discover(path), None)
         if item:
             return self._webdav_error_response("D",
                                                "resource-must-be-null")
         parent_path = storage.sanitize_path(
             "/%s/" % posixpath.dirname(path.strip("/")))
         parent_item = next(self.Collection.discover(parent_path), None)
         if not parent_item:
             return CONFLICT
         if (not isinstance(parent_item, storage.BaseCollection)
                 or parent_item.get_meta("tag")):
             return FORBIDDEN
         try:
             self.Collection.create_collection(path, props=props)
         except ValueError as e:
             logger.warning("Bad MKCALENDAR request on %r: %s",
                            path,
                            e,
                            exc_info=True)
             return BAD_REQUEST
         return client.CREATED, {}, None
Beispiel #17
0
 def authorized(self, user, path, permissions):
     if self._verify_user and not user:
         return ""
     sane_path = storage.sanitize_path(path).strip("/")
     if not sane_path:
         return intersect_permissions(permissions, "R")
     if self._verify_user:
         owned = user == sane_path.split("/", maxsplit=1)[0]
     else:
         owned = True
     if "/" not in sane_path:
         return intersect_permissions(permissions, "RW" if owned else "R")
     if sane_path.count("/") == 1:
         return intersect_permissions(permissions, "rw" if owned else "r")
     return ""
Beispiel #18
0
    def discover(cls, path, depth="0"):
        collections = list(super().discover(path, depth))
        for collection in collections:
            yield collection

        if depth == "0":
            return

        attributes = _get_attributes_from_path(path)

        if len(attributes) == 0:
            return
        elif len(attributes) == 1:
            known_paths = [collection.path for collection in collections]
            for sync_type in ["contacts", "calendars"]:
                for collection in Decsync.list_collections(
                        cls.decsync_dir, sync_type):
                    child_path = storage.sanitize_path(path + "/" + sync_type +
                                                       "-" +
                                                       collection).strip("/")
                    if child_path in known_paths:
                        continue
                    if Decsync.get_static_info(cls.decsync_dir, sync_type,
                                               collection, "deleted") == True:
                        continue

                    props = {}
                    if sync_type == "contacts":
                        props["tag"] = "VADDRESSBOOK"
                    elif sync_type == "calendars":
                        props["tag"] = "VCALENDAR"
                        props["C:supported-calendar-component-set"] = "VEVENT"
                    else:
                        raise RuntimeError("Unknown sync type " + sync_type)
                    child = super().create_collection(child_path, props=props)
                    child.decsync.init_stored_entries()
                    child.decsync.execute_stored_entries_for_path(["info"],
                                                                  child)
                    child.decsync.execute_stored_entries_for_path(
                        ["resources"], child)
                    yield child
        elif len(attributes) == 2:
            return
        else:
            raise ValueError("Invalid number of attributes")
Beispiel #19
0
    def create_collection(cls, href, collection=None, props=None):
        logger.debug("create_collection")
        sane_path = sanitize_path(href).strip("/")
        if props:
            username, name = sane_path.split("/")
            tags = dict(props)
            name = tags.get("D:displayname", name)
            try:
                user = User.objects.get(username=username)
            except User.DoesNotExist:
                return

            collection = DBCollection.objects.create(
                name=name,
                tags=tags,
            )

            collection.users.add(user)

        return cls(sane_path)
Beispiel #20
0
 def authorized(self, user, path, permission):
     user = user or ""
     sane_path = storage.sanitize_path(path).strip("/")
     # Prevent "regex injection"
     user_escaped = re.escape(user)
     sane_path_escaped = re.escape(sane_path)
     regex = configparser.ConfigParser(
         {"login": user_escaped, "path": sane_path_escaped})
     try:
         if not regex.read(self.filename):
             raise RuntimeError("No such file: %r" %
                                self.filename)
     except Exception as e:
         raise RuntimeError("Failed to load rights file %r: %s" %
                            (self.filename, e)) from e
     for section in regex.sections():
         try:
             re_user_pattern = regex.get(section, "user")
             re_collection_pattern = regex.get(section, "collection")
             # Emulate fullmatch
             user_match = re.match(r"(?:%s)\Z" % re_user_pattern, user)
             collection_match = user_match and re.match(
                 r"(?:%s)\Z" % re_collection_pattern.format(
                     *map(re.escape, user_match.groups())), sane_path)
         except Exception as e:
             raise RuntimeError("Error in section %r of rights file %r: "
                                "%s" % (section, self.filename, e)) from e
         if user_match and collection_match:
             self.logger.debug("Rule %r:%r matches %r:%r from section %r",
                               user, sane_path, re_user_pattern,
                               re_collection_pattern, section)
             return permission in regex.get(section, "permission")
         else:
             self.logger.debug("Rule %r:%r doesn't match %r:%r from section"
                               " %r", user, sane_path, re_user_pattern,
                               re_collection_pattern, section)
     self.logger.info(
         "Rights: %r:%r doesn't match any section", user, sane_path)
     return False
Beispiel #21
0
 def _access(self, user, path, permission, item=None):
     if permission not in "rw":
         raise ValueError("Invalid permission argument: %r" % permission)
     if not item:
         permissions = permission + permission.upper()
         parent_permissions = permission
     elif isinstance(item, storage.BaseCollection):
         if item.get_meta("tag"):
             permissions = permission
         else:
             permissions = permission.upper()
         parent_permissions = ""
     else:
         permissions = ""
         parent_permissions = permission
     if permissions and self.Rights.authorized(user, path, permissions):
         return True
     if parent_permissions:
         parent_path = storage.sanitize_path(
             "/%s/" % posixpath.dirname(path.strip("/")))
         if self.Rights.authorized(user, parent_path, parent_permissions):
             return True
     return False
Beispiel #22
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"):
                    self.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"))
            self.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()
        self.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))
        self.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"]
            self.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 = storage.sanitize_path(unsafe_base_prefix).rstrip("/")
        self.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 = storage.sanitize_path(environ.get("PATH_INFO", ""))
        self.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(*NOT_FOUND)

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

        if not user:
            is_authenticated = True
        elif not storage.is_safe_path_component(user):
            # Prevent usernames like "user/calendar.ics"
            self.logger.info("Refused unsafe username: %r", user)
            is_authenticated = False
        else:
            is_authenticated = self.Auth.is_authenticated2(
                login, user, password)
            if not is_authenticated:
                self.logger.info("Failed login attempt: %r", user)
                # 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())
                    self.logger.debug("Sleeping %.3f seconds", random_delay)
                    time.sleep(random_delay)
            else:
                self.logger.info("Successful login: %r", user)

        # Create principal collection
        if user and is_authenticated:
            principal_path = "/%s/" % user
            if self.Rights.authorized(user, principal_path, "w"):
                if not self.Database.user_exists(user):
                    self.Database.create_user(user)

            else:
                self.logger.warning(
                    "Access to principal path %r denied by "
                    "rights backend", principal_path)

        # 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:
                self.logger.info("Request body too large: %d", content_length)
                return response(*REQUEST_ENTITY_TOO_LARGE)

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

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

        return response(status, headers, answer)
Beispiel #23
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"):
                    self.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"))
            self.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()
        self.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))
        self.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"]
            self.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 = storage.sanitize_path(unsafe_base_prefix).rstrip("/")
        self.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 = storage.sanitize_path(environ.get("PATH_INFO", ""))
        self.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(*NOT_FOUND)

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

        if not user:
            is_authenticated = True
        elif not storage.is_safe_path_component(user):
            # Prevent usernames like "user/calendar.ics"
            self.logger.info("Refused unsafe username: %r", user)
            is_authenticated = False
        else:
            is_authenticated = self.Auth.is_authenticated2(login, user,
                                                           password)
            if not is_authenticated:
                self.logger.info("Failed login attempt: %r", user)
                # 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())
                    self.logger.debug("Sleeping %.3f seconds", random_delay)
                    time.sleep(random_delay)
            else:
                self.logger.info("Successful login: %r", user)

        # Create principal collection
        if user and is_authenticated:
            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:
                            self.logger.warning("Failed to create principal "
                                                "collection %r: %s", user, e)
                            is_authenticated = False
            else:
                self.logger.warning("Access to principal path %r denied by "
                                    "rights backend", principal_path)

        # 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:
                self.logger.info(
                    "Request body too large: %d", content_length)
                return response(*REQUEST_ENTITY_TOO_LARGE)

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

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

        return response(status, headers, answer)
Beispiel #24
0
def report(base_prefix, path, xml_request, collection):
    """Read and answer REPORT requests.

    Read rfc3253-3.6 for info.

    """
    logger = collection.logger
    multistatus = ET.Element(_tag("D", "multistatus"))
    if xml_request is None:
        return client.MULTI_STATUS, multistatus
    root = xml_request
    if root.tag in (
        _tag("D", "principal-search-property-set"),
        _tag("D", "principal-property-search"),
        _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", _tag_from_clark(root.tag), path)
        return client.MULTI_STATUS, multistatus
    if (
        root.tag == _tag("C", "calendar-multiget")
        and collection.get_meta("tag") != "VCALENDAR"
        or root.tag == _tag("CR", "addressbook-multiget")
        and collection.get_meta("tag") != "VADDRESSBOOK"
        or root.tag == _tag("D", "sync-collection")
        and collection.get_meta("tag") not in ("VADDRESSBOOK", "VCALENDAR")
    ):
        logger.warning("Invalid REPORT method %r on %r requested", _tag_from_clark(root.tag), path)
        return (client.CONFLICT, webdav_error("D", "supported-report"))
    prop_element = root.find(_tag("D", "prop"))
    props = [prop.tag for prop in prop_element] if prop_element is not None else []

    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")):
            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:
                logger.warning("Skipping invalid path %r in REPORT request on " "%r", href_path, path)
    elif root.tag == _tag("D", "sync-collection"):
        old_sync_token_element = root.find(_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, webdav_error("D", "valid-sync-token"))
        hreferences = ("/" + posixpath.join(collection.path, n) for n in names)
        # Append current sync token to response
        sync_token_element = ET.Element(_tag("D", "sync-token"))
        sync_token_element.text = sync_token
        multistatus.append(sync_token_element)
    else:
        hreferences = (path,)
    filters = root.findall("./%s" % _tag("C", "filter")) + root.findall("./%s" % _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 = 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 = _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

        names = get_names()
        for name, item in collection.get_multi2(names):
            if not item:
                uri = "/" + posixpath.join(collection.path, name)
                response = _item_response(base_prefix, uri, found_item=False)
                multistatus.append(response)
            else:
                yield item, False
        if collection_requested:
            yield from collection.get_all_filtered(filters)

    def match(item, filter_):
        tag = collection.get_meta("tag")
        if tag == "VCALENDAR" and filter_.tag != _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 != _tag("C", "comp-filter"):
                raise ValueError("Unexpected %r in filter" % filter_[0].tag)
            return _comp_match(item, filter_[0])
        if tag == "VADDRESSBOOK" and filter_.tag != _tag("CR", filter_):
            for child in filter_:
                if child.tag != _tag("CR", "prop-filter"):
                    raise ValueError("Unexpected %r in filter" % child.tag)
            test = filter_.get("test", "anyof")
            if test == "anyof":
                return any(_prop_match(item.item, f, "CR") for f in filter_)
            if test == "allof":
                return all(_prop_match(item.item, f, "CR") for f in filter_)
            raise ValueError("Unsupported filter test: %r" % test)
            return all(_prop_match(item.item, f, "CR") for f in filter_)
        raise ValueError("unsupported filter %r for %r" % (filter_.tag, tag))

    for item, filters_matched in retrieve_items(collection, hreferences, multistatus):
        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 == _tag("D", "getetag"):
                element.text = item.etag
                found_props.append(element)
            elif tag == _tag("D", "getcontenttype"):
                element.text = get_content_type(item)
                found_props.append(element)
            elif tag in (_tag("C", "calendar-data"), _tag("CR", "address-data")):
                element.text = item.serialize()
                found_props.append(element)
            else:
                not_found_props.append(element)

        uri = "/" + posixpath.join(collection.path, item.href)
        multistatus.append(
            _item_response(base_prefix, uri, found_props=found_props, not_found_props=not_found_props, found_item=True)
        )

    return client.MULTI_STATUS, multistatus
Beispiel #25
0
    def do_PUT(self, environ, base_prefix, path, user):
        """Manage PUT request."""
        if not self._access(user, path, "w"):
            return NOT_ALLOWED
        try:
            content = self._read_content(environ)
        except RuntimeError as e:
            logger.warning("Bad PUT request on %r: %s", path, e, exc_info=True)
            return BAD_REQUEST
        except socket.timeout as e:
            logger.debug("client timed out", exc_info=True)
            return REQUEST_TIMEOUT
        # Prepare before locking
        parent_path = storage.sanitize_path("/%s/" %
                                            posixpath.dirname(path.strip("/")))
        permissions = self.Rights.authorized(user, path, "Ww")
        parent_permissions = self.Rights.authorized(user, parent_path, "w")

        def prepare(vobject_items, tag=None, write_whole_collection=None):
            if (write_whole_collection
                    or permissions and not parent_permissions):
                write_whole_collection = True
                content_type = environ.get("CONTENT_TYPE", "").split(";")[0]
                tags = {
                    value: key
                    for key, value in xmlutils.MIMETYPES.items()
                }
                tag = storage.predict_tag_of_whole_collection(
                    vobject_items, tags.get(content_type))
                if not tag:
                    raise ValueError("Can't determine collection tag")
                collection_path = storage.sanitize_path(path).strip("/")
            elif (write_whole_collection is not None
                  and not write_whole_collection
                  or not permissions and parent_permissions):
                write_whole_collection = False
                if tag is None:
                    tag = storage.predict_tag_of_parent_collection(
                        vobject_items)
                collection_path = posixpath.dirname(
                    storage.sanitize_path(path).strip("/"))
            props = None
            stored_exc_info = None
            items = []
            try:
                if tag:
                    storage.check_and_sanitize_items(
                        vobject_items,
                        is_collection=write_whole_collection,
                        tag=tag)
                    if write_whole_collection and tag == "VCALENDAR":
                        vobject_components = []
                        vobject_item, = vobject_items
                        for content in ("vevent", "vtodo", "vjournal"):
                            vobject_components.extend(
                                getattr(vobject_item, "%s_list" % content, []))
                        vobject_components_by_uid = itertools.groupby(
                            sorted(vobject_components, key=storage.get_uid),
                            storage.get_uid)
                        for uid, components in vobject_components_by_uid:
                            vobject_collection = vobject.iCalendar()
                            for component in components:
                                vobject_collection.add(component)
                            item = storage.Item(
                                collection_path=collection_path,
                                vobject_item=vobject_collection)
                            item.prepare()
                            items.append(item)
                    elif write_whole_collection and tag == "VADDRESSBOOK":
                        for vobject_item in vobject_items:
                            item = storage.Item(
                                collection_path=collection_path,
                                vobject_item=vobject_item)
                            item.prepare()
                            items.append(item)
                    elif not write_whole_collection:
                        vobject_item, = vobject_items
                        item = storage.Item(collection_path=collection_path,
                                            vobject_item=vobject_item)
                        item.prepare()
                        items.append(item)

                if write_whole_collection:
                    props = {}
                    if tag:
                        props["tag"] = tag
                    if tag == "VCALENDAR" and vobject_items:
                        if hasattr(vobject_items[0], "x_wr_calname"):
                            calname = vobject_items[0].x_wr_calname.value
                            if calname:
                                props["D:displayname"] = calname
                        if hasattr(vobject_items[0], "x_wr_caldesc"):
                            caldesc = vobject_items[0].x_wr_caldesc.value
                            if caldesc:
                                props["C:calendar-description"] = caldesc
                    storage.check_and_sanitize_props(props)
            except Exception:
                stored_exc_info = sys.exc_info()

            # Use generator for items and delete references to free memory
            # early
            def items_generator():
                while items:
                    yield items.pop(0)

            return (items_generator(), tag, write_whole_collection, props,
                    stored_exc_info)

        try:
            vobject_items = tuple(vobject.readComponents(content or ""))
        except Exception as e:
            logger.warning("Bad PUT request on %r: %s", path, e, exc_info=True)
            return BAD_REQUEST
        (prepared_items, prepared_tag, prepared_write_whole_collection,
         prepared_props, prepared_exc_info) = prepare(vobject_items)

        with self.Collection.acquire_lock("w", user):
            item = next(self.Collection.discover(path), None)
            parent_item = next(self.Collection.discover(parent_path), None)
            if not parent_item:
                return CONFLICT

            write_whole_collection = (isinstance(item, storage.BaseCollection)
                                      or not parent_item.get_meta("tag"))

            if write_whole_collection:
                tag = prepared_tag
            else:
                tag = parent_item.get_meta("tag")

            if write_whole_collection:
                if not self.Rights.authorized(user, path, "w" if tag else "W"):
                    return NOT_ALLOWED
            elif not self.Rights.authorized(user, parent_path, "w"):
                return NOT_ALLOWED

            etag = environ.get("HTTP_IF_MATCH", "")
            if not item and etag:
                # Etag asked but no item found: item has been removed
                return PRECONDITION_FAILED
            if item and etag and item.etag != etag:
                # Etag asked but item not matching: item has changed
                return PRECONDITION_FAILED

            match = environ.get("HTTP_IF_NONE_MATCH", "") == "*"
            if item and match:
                # Creation asked but item found: item can't be replaced
                return PRECONDITION_FAILED

            if (tag != prepared_tag or
                    prepared_write_whole_collection != write_whole_collection):
                (prepared_items, prepared_tag, prepared_write_whole_collection,
                 prepared_props,
                 prepared_exc_info) = prepare(vobject_items, tag,
                                              write_whole_collection)
            props = prepared_props
            if prepared_exc_info:
                logger.warning("Bad PUT request on %r: %s",
                               path,
                               prepared_exc_info[1],
                               exc_info=prepared_exc_info)
                return BAD_REQUEST

            if write_whole_collection:
                try:
                    etag = self.Collection.create_collection(
                        path, prepared_items, props).etag
                except ValueError as e:
                    logger.warning("Bad PUT request on %r: %s",
                                   path,
                                   e,
                                   exc_info=True)
                    return BAD_REQUEST
            else:
                prepared_item, = prepared_items
                if (item and item.uid != prepared_item.uid or not item
                        and parent_item.has_uid(prepared_item.uid)):
                    return self._webdav_error_response(
                        "C" if tag == "VCALENDAR" else "CR", "no-uid-conflict")

                href = posixpath.basename(path.strip("/"))
                try:
                    etag = parent_item.upload(href, prepared_item).etag
                except ValueError as e:
                    logger.warning("Bad PUT request on %r: %s",
                                   path,
                                   e,
                                   exc_info=True)
                    return BAD_REQUEST

            headers = {"ETag": etag}
            return client.CREATED, headers, None
Beispiel #26
0
    def do_PUT(self, environ, base_prefix, path, user):
        """Manage PUT request."""
        if not self._access(user, path, "w"):
            return NOT_ALLOWED
        try:
            content = self._read_content(environ)
        except RuntimeError as e:
            self.logger.warning(
                "Bad PUT request on %r: %s", path, e, exc_info=True)
            return BAD_REQUEST
        except socket.timeout as e:
            self.logger.debug("client timed out", exc_info=True)
            return REQUEST_TIMEOUT
        with self.Collection.acquire_lock("w", user):
            parent_path = storage.sanitize_path(
                "/%s/" % posixpath.dirname(path.strip("/")))
            item = next(self.Collection.discover(path), None)
            parent_item = next(self.Collection.discover(parent_path), None)

            write_whole_collection = (
                isinstance(item, storage.BaseCollection) or
                not parent_item or (
                    not next(parent_item.list(), None) and
                    parent_item.get_meta("tag") not in (
                        "VADDRESSBOOK", "VCALENDAR")))
            if write_whole_collection:
                if not self.Rights.authorized(user, path, "w"):
                    return NOT_ALLOWED
            elif not self.Rights.authorized_item(user, path, "w"):
                return NOT_ALLOWED

            etag = environ.get("HTTP_IF_MATCH", "")
            if not item and etag:
                # Etag asked but no item found: item has been removed
                return PRECONDITION_FAILED
            if item and etag and item.etag != etag:
                # Etag asked but item not matching: item has changed
                return PRECONDITION_FAILED

            match = environ.get("HTTP_IF_NONE_MATCH", "") == "*"
            if item and match:
                # Creation asked but item found: item can't be replaced
                return PRECONDITION_FAILED

            try:
                items = tuple(vobject.readComponents(content or ""))
                if not write_whole_collection and len(items) != 1:
                    raise RuntimeError(
                        "Item contains %d components" % len(items))
                if write_whole_collection or not parent_item.get_meta("tag"):
                    content_type = environ.get("CONTENT_TYPE",
                                               "").split(";")[0]
                    tags = {value: key
                            for key, value in xmlutils.MIMETYPES.items()}
                    tag = tags.get(content_type)
                    if items and items[0].name == "VCALENDAR":
                        tag = "VCALENDAR"
                    elif items and items[0].name in ("VCARD", "VLIST"):
                        tag = "VADDRESSBOOK"
                else:
                    tag = parent_item.get_meta("tag")
                if tag == "VCALENDAR" and len(items) > 1:
                    raise RuntimeError("VCALENDAR collection contains %d "
                                       "components" % len(items))
                for i in items:
                    storage.check_and_sanitize_item(
                        i, is_collection=write_whole_collection, uid=item.uid
                        if not write_whole_collection and item else None,
                        tag=tag)
            except Exception as e:
                self.logger.warning(
                    "Bad PUT request on %r: %s", path, e, exc_info=True)
                return BAD_REQUEST

            if write_whole_collection:
                props = {}
                if tag:
                    props["tag"] = tag
                if tag == "VCALENDAR" and items:
                    if hasattr(items[0], "x_wr_calname"):
                        calname = items[0].x_wr_calname.value
                        if calname:
                            props["D:displayname"] = calname
                    if hasattr(items[0], "x_wr_caldesc"):
                        caldesc = items[0].x_wr_caldesc.value
                        if caldesc:
                            props["C:calendar-description"] = caldesc
                try:
                    storage.check_and_sanitize_props(props)
                    new_item = self.Collection.create_collection(
                        path, items, props)
                except ValueError as e:
                    self.logger.warning(
                        "Bad PUT request on %r: %s", path, e, exc_info=True)
                    return BAD_REQUEST
            else:
                href = posixpath.basename(path.strip("/"))
                try:
                    if tag and not parent_item.get_meta("tag"):
                        new_props = parent_item.get_meta()
                        new_props["tag"] = tag
                        storage.check_and_sanitize_props(new_props)
                        parent_item.set_meta_all(new_props)
                    new_item = parent_item.upload(href, items[0])
                except ValueError as e:
                    self.logger.warning(
                        "Bad PUT request on %r: %s", path, e, exc_info=True)
                    return BAD_REQUEST
            headers = {"ETag": new_item.etag}
            return client.CREATED, headers, None
Beispiel #27
0
 def authorized(self, user, path, permission):
     sane_path = storage.sanitize_path(path).strip("/")
     return bool(user) and (
         permission == "r" and not sane_path or
         user == sane_path.split("/", maxsplit=1)[0])
Beispiel #28
0
 def authorized_item(self, user, path, permission):
     sane_path = storage.sanitize_path(path).strip("/")
     if "/" not in sane_path:
         return False
     return super().authorized_item(user, path, permission)
Beispiel #29
0
 def authorized_item(self, user, path, permission):
     """Check if the user is allowed to read or write the item."""
     path = storage.sanitize_path(path)
     parent_path = storage.sanitize_path("/%s/" %
                                         posixpath.dirname(path.strip("/")))
     return self.authorized(user, parent_path, permission)
Beispiel #30
0
        def prepare(vobject_items, tag=None, write_whole_collection=None):
            if (write_whole_collection
                    or permissions and not parent_permissions):
                write_whole_collection = True
                content_type = environ.get("CONTENT_TYPE", "").split(";")[0]
                tags = {
                    value: key
                    for key, value in xmlutils.MIMETYPES.items()
                }
                tag = storage.predict_tag_of_whole_collection(
                    vobject_items, tags.get(content_type))
                if not tag:
                    raise ValueError("Can't determine collection tag")
                collection_path = storage.sanitize_path(path).strip("/")
            elif (write_whole_collection is not None
                  and not write_whole_collection
                  or not permissions and parent_permissions):
                write_whole_collection = False
                if tag is None:
                    tag = storage.predict_tag_of_parent_collection(
                        vobject_items)
                collection_path = posixpath.dirname(
                    storage.sanitize_path(path).strip("/"))
            props = None
            stored_exc_info = None
            items = []
            try:
                if tag:
                    storage.check_and_sanitize_items(
                        vobject_items,
                        is_collection=write_whole_collection,
                        tag=tag)
                    if write_whole_collection and tag == "VCALENDAR":
                        vobject_components = []
                        vobject_item, = vobject_items
                        for content in ("vevent", "vtodo", "vjournal"):
                            vobject_components.extend(
                                getattr(vobject_item, "%s_list" % content, []))
                        vobject_components_by_uid = itertools.groupby(
                            sorted(vobject_components, key=storage.get_uid),
                            storage.get_uid)
                        for uid, components in vobject_components_by_uid:
                            vobject_collection = vobject.iCalendar()
                            for component in components:
                                vobject_collection.add(component)
                            item = storage.Item(
                                collection_path=collection_path,
                                vobject_item=vobject_collection)
                            item.prepare()
                            items.append(item)
                    elif write_whole_collection and tag == "VADDRESSBOOK":
                        for vobject_item in vobject_items:
                            item = storage.Item(
                                collection_path=collection_path,
                                vobject_item=vobject_item)
                            item.prepare()
                            items.append(item)
                    elif not write_whole_collection:
                        vobject_item, = vobject_items
                        item = storage.Item(collection_path=collection_path,
                                            vobject_item=vobject_item)
                        item.prepare()
                        items.append(item)

                if write_whole_collection:
                    props = {}
                    if tag:
                        props["tag"] = tag
                    if tag == "VCALENDAR" and vobject_items:
                        if hasattr(vobject_items[0], "x_wr_calname"):
                            calname = vobject_items[0].x_wr_calname.value
                            if calname:
                                props["D:displayname"] = calname
                        if hasattr(vobject_items[0], "x_wr_caldesc"):
                            caldesc = vobject_items[0].x_wr_caldesc.value
                            if caldesc:
                                props["C:calendar-description"] = caldesc
                    storage.check_and_sanitize_props(props)
            except Exception:
                stored_exc_info = sys.exc_info()

            # Use generator for items and delete references to free memory
            # early
            def items_generator():
                while items:
                    yield items.pop(0)

            return (items_generator(), tag, write_whole_collection, props,
                    stored_exc_info)
Beispiel #31
0
    def do_PUT(self, environ, base_prefix, path, user):
        """Manage PUT request."""
        if not self._access(user, path, "w"):
            return NOT_ALLOWED
        try:
            content = self._read_content(environ)
        except RuntimeError as e:
            self.logger.warning("Bad PUT request on %r: %s",
                                path,
                                e,
                                exc_info=True)
            return BAD_REQUEST
        except socket.timeout:
            self.logger.debug("client timed out", exc_info=True)
            return REQUEST_TIMEOUT
        parent_path = storage.sanitize_path("/%s/" %
                                            posixpath.dirname(path.strip("/")))
        item = next(self.Collection.discover(path), None)
        parent_item = next(self.Collection.discover(parent_path), None)
        if not parent_item:
            return CONFLICT

        write_whole_collection = isinstance(
            item, storage.BaseCollection) or not parent_item.get_meta("tag")
        if write_whole_collection:
            if not self.Rights.authorized(user, path, "w"):
                return NOT_ALLOWED
        elif not self.Rights.authorized_item(user, path, "w"):
            return NOT_ALLOWED

        etag = environ.get("HTTP_IF_MATCH", "")
        if not item and etag:
            # Etag asked but no item found: item has been removed
            return PRECONDITION_FAILED
        if item and etag and item.etag != etag:
            # Etag asked but item not matching: item has changed
            return PRECONDITION_FAILED

        match = environ.get("HTTP_IF_NONE_MATCH", "") == "*"
        if item and match:
            # Creation asked but item found: item can't be replaced
            return PRECONDITION_FAILED
        try:
            items = tuple(vobject.readComponents(content or ""))
            if write_whole_collection:
                content_type = environ.get("CONTENT_TYPE", "").split(";")[0]
                tags = {
                    value: key
                    for key, value in xmlutils.MIMETYPES.items()
                }
                tag = tags.get(content_type)
                if items and items[0].name == "VCALENDAR":
                    tag = "VCALENDAR"
                elif items and items[0].name in ("VCARD", "VLIST"):
                    tag = "VADDRESSBOOK"
            else:
                tag = parent_item.get_meta("tag")
            if tag == "VCALENDAR" and len(items) > 1:
                raise RuntimeError("VCALENDAR collection contains %d "
                                   "components" % len(items))
            for i in items:
                storage.check_and_sanitize_item(
                    i,
                    is_collection=write_whole_collection,
                    uid=item.uid
                    if not write_whole_collection and item else None,
                    tag=tag,
                )
        except Exception as e:
            self.logger.warning("Bad PUT request on %r: %s",
                                path,
                                e,
                                exc_info=True)
            return BAD_REQUEST

        if write_whole_collection:
            props = {}
            if tag:
                props["tag"] = tag
            if tag == "VCALENDAR" and items:
                if hasattr(items[0], "x_wr_calname"):
                    calname = items[0].x_wr_calname.value
                    if calname:
                        props["D:displayname"] = calname
                if hasattr(items[0], "x_wr_caldesc"):
                    caldesc = items[0].x_wr_caldesc.value
                    if caldesc:
                        props["C:calendar-description"] = caldesc
            try:
                storage.check_and_sanitize_props(props)
                new_item = self.Collection.create_collection(
                    path, items, props)
            except ValueError as e:
                self.logger.warning("Bad PUT request on %r: %s",
                                    path,
                                    e,
                                    exc_info=True)
                return BAD_REQUEST
        else:
            href = posixpath.basename(path.strip("/"))
            try:
                if tag and not parent_item.get_meta("tag"):
                    new_props = parent_item.get_meta()
                    new_props["tag"] = tag
                    storage.check_and_sanitize_props(new_props)
                    parent_item.set_meta_all(new_props)
                new_item = parent_item.upload(href, items[0])
            except ValueError as e:
                self.logger.warning("Bad PUT request on %r: %s",
                                    path,
                                    e,
                                    exc_info=True)
                return BAD_REQUEST
        headers = {"ETag": new_item.etag}
        return client.CREATED, headers, None
Beispiel #32
0
 def authorized(self, user, path, permission):
     sane_path = storage.sanitize_path(path).strip("/")
     return bool(user) and (permission == "r" and not sane_path
                            or user == sane_path.split("/", maxsplit=1)[0])
Beispiel #33
0
 def authorized_item(self, user, path, permission):
     sane_path = storage.sanitize_path(path).strip("/")
     if "/" not in sane_path:
         return False
     return super().authorized_item(user, path, permission)
Beispiel #34
0
 def authorized_item(self, user, path, permission):
     """Check if the user is allowed to read or write the item."""
     path = storage.sanitize_path(path)
     parent_path = storage.sanitize_path(
         "/%s/" % posixpath.dirname(path.strip("/")))
     return self.authorized(user, parent_path, permission)
Beispiel #35
0
def report(base_prefix, path, xml_request, collection):
    """Read and answer REPORT requests.

    Read rfc3253-3.6 for info.

    """
    logger = collection.logger
    multistatus = ET.Element(_tag("D", "multistatus"))
    if xml_request is None:
        return client.MULTI_STATUS, multistatus
    root = xml_request
    if root.tag in (
            _tag("D", "principal-search-property-set"),
            _tag("D", "principal-property-search"),
            _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",
                       _tag_from_clark(root.tag), path)
        return client.MULTI_STATUS, multistatus
    if (root.tag == _tag("C", "calendar-multiget") and
            collection.get_meta("tag") != "VCALENDAR" or
            root.tag == _tag("CR", "addressbook-multiget") and
            collection.get_meta("tag") != "VADDRESSBOOK" or
            root.tag == _tag("D", "sync-collection") and
            collection.get_meta("tag") not in ("VADDRESSBOOK", "VCALENDAR")):
        logger.warning("Invalid REPORT method %r on %r requested",
                       _tag_from_clark(root.tag), path)
        return (client.PRECONDITION_FAILED,
                _webdav_error("D", "supported-report"))
    prop_element = root.find(_tag("D", "prop"))
    props = (
        [prop.tag for prop in prop_element]
        if prop_element is not None else [])

    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")):
            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:
                logger.warning("Skipping invalid path %r in REPORT request on "
                               "%r", href_path, path)
    elif root.tag == _tag("D", "sync-collection"):
        old_sync_token_element = root.find(_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.PRECONDITION_FAILED,
                    _webdav_error("D", "valid-sync-token"))
        hreferences = ("/" + posixpath.join(collection.path, n) for n in names)
        # Append current sync token to response
        sync_token_element = ET.Element(_tag("D", "sync-token"))
        sync_token_element.text = sync_token
        multistatus.append(sync_token_element)
    else:
        hreferences = (path,)
    filters = (
        root.findall("./%s" % _tag("C", "filter")) +
        root.findall("./%s" % _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 = 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 = _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_multi2(get_names()):
            if not item:
                uri = "/" + posixpath.join(collection.path, name)
                response = _item_response(base_prefix, uri,
                                          found_item=False)
                multistatus.append(response)
            else:
                yield item, False
        if collection_requested:
            yield from collection.get_all_filtered(filters)

    def match(item, filter_):
        tag = collection.get_meta("tag")
        if (tag == "VCALENDAR" and filter_.tag != _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 != _tag("C", "comp-filter"):
                raise ValueError("Unexpected %r in filter" % filter_[0].tag)
            return _comp_match(item, filter_[0])
        if tag == "VADDRESSBOOK" and filter_.tag != _tag("CR", filter_):
            for child in filter_:
                if child.tag != _tag("CR", "prop-filter"):
                    raise ValueError("Unexpected %r in filter" % child.tag)
            test = filter_.get("test", "anyof")
            if test == "anyof":
                return any(_prop_match(item.item, f, "CR") for f in filter_)
            if test == "allof":
                return all(_prop_match(item.item, f, "CR") for f in filter_)
            raise ValueError("Unsupported filter test: %r" % test)
            return all(_prop_match(item.item, f, "CR") for f in filter_)
        raise ValueError("unsupported filter %r for %r" % (filter_.tag, tag))

    for item, filters_matched in retrieve_items(collection, hreferences,
                                                multistatus):
        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 == _tag("D", "getetag"):
                element.text = item.etag
                found_props.append(element)
            elif tag == _tag("D", "getcontenttype"):
                element.text = get_content_type(item)
                found_props.append(element)
            elif tag in (
                    _tag("C", "calendar-data"),
                    _tag("CR", "address-data")):
                element.text = item.serialize()
                found_props.append(element)
            else:
                not_found_props.append(element)

        uri = "/" + posixpath.join(collection.path, item.href)
        multistatus.append(_item_response(
            base_prefix, uri, found_props=found_props,
            not_found_props=not_found_props, found_item=True))

    return client.MULTI_STATUS, multistatus