コード例 #1
0
def name_from_path(path, collection):
    """Return Radicale item name from ``path``."""
    path = path.strip("/") + "/"
    start = collection.path + "/"
    if not path.startswith(start):
        raise ValueError("%r doesn't start with %r" % (path, start))
    name = path[len(start) :][:-1]
    if name and not storage.is_safe_path_component(name):
        raise ValueError("%r is not a component in collection %r" % (name, collection.path))
    return name
コード例 #2
0
ファイル: xmlutils.py プロジェクト: JulienPalard/Radicale
def name_from_path(path, collection):
    """Return Radicale item name from ``path``."""
    path = path.strip("/") + "/"
    start = collection.path + "/"
    if not path.startswith(start):
        raise ValueError("%r doesn't start with %r" % (path, start))
    name = path[len(start):][:-1]
    if name and not storage.is_safe_path_component(name):
        raise ValueError("%r is not a component in collection %r" %
                         (name, collection.path))
    return name
コード例 #3
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)
コード例 #4
0
ファイル: __init__.py プロジェクト: JulienPalard/Radicale
    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)