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
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
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
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
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
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
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("/")
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
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
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
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 ""
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)
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)
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
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
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
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 ""
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")
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)
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
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
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)
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)
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
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
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
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])
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)
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)
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)
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
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])
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)
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