Exemplo n.º 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 httputils.REMOTE_DESTINATION
        if not self.access(user, path, "w"):
            return httputils.NOT_ALLOWED
        to_path = pathutils.sanitize_path(to_url.path)
        if not (to_path + "/").startswith(base_prefix + "/"):
            logger.warning(
                "Destination %r from MOVE request on %r doesn't "
                "start with base prefix", to_path, path)
            return httputils.NOT_ALLOWED
        to_path = to_path[len(base_prefix):]
        if not self.access(user, to_path, "w"):
            return httputils.NOT_ALLOWED

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

            to_item = next(self.Collection.discover(to_path), None)
            if isinstance(to_item, storage.BaseCollection):
                return httputils.FORBIDDEN
            to_parent_path = pathutils.unstrip_path(
                posixpath.dirname(pathutils.strip_path(to_path)), True)
            to_collection = next(self.Collection.discover(to_parent_path),
                                 None)
            if not to_collection:
                return httputils.CONFLICT
            tag = item.collection.get_meta("tag")
            if not tag or tag != to_collection.get_meta("tag"):
                return httputils.FORBIDDEN
            if to_item and environ.get("HTTP_OVERWRITE", "F") != "T":
                return httputils.PRECONDITION_FAILED
            if (to_item and item.uid != to_item.uid or not to_item
                    and to_collection.path != item.collection.path
                    and to_collection.has_uid(item.uid)):
                return self.webdav_error_response(
                    "C" if tag == "VCALENDAR" else "CR", "no-uid-conflict")
            to_href = posixpath.basename(pathutils.strip_path(to_path))
            try:
                self.Collection.move(item, to_collection, to_href)
            except ValueError as e:
                logger.warning("Bad MOVE request on %r: %s",
                               path,
                               e,
                               exc_info=True)
                return httputils.BAD_REQUEST
            return client.NO_CONTENT if to_item else client.CREATED, {}, None
Exemplo n.º 2
0
        def response(status, headers=(), answer=None):
            headers = dict(headers)
            # Set content length
            if answer:
                if hasattr(answer, "encode"):
                    logger.debug("Response content:\n%s", answer)
                    headers["Content-Type"] += "; charset=%s" % self.encoding
                    answer = answer.encode(self.encoding)
                accept_encoding = [
                    encoding.strip() for encoding in
                    environ.get("HTTP_ACCEPT_ENCODING", "").split(",")
                    if encoding.strip()]

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

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

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

            # Start response
            time_end = datetime.datetime.now()
            status = "%d %s" % (
                status, client.responses.get(status, "Unknown"))
            logger.info(
                "%s response status for %r%s in %.3f seconds: %s",
                environ["REQUEST_METHOD"], environ.get("PATH_INFO", ""),
                depthinfo, (time_end - time_begin).total_seconds(), status)
            # Return response content
            return status, list(headers.items()), [answer] if answer else []
Exemplo n.º 3
0
    def do_MOVE(self, environ, base_prefix, path, user):
        """Manage MOVE request."""
        raw_dest = environ.get("HTTP_DESTINATION", "")
        to_url = urlparse(raw_dest)
        if to_url.netloc != environ["HTTP_HOST"]:
            logger.info("Unsupported destination address: %r", raw_dest)
            # Remote destination server, not supported
            return httputils.REMOTE_DESTINATION
        if not self.access(user, path, "w"):
            return httputils.NOT_ALLOWED
        to_path = pathutils.sanitize_path(to_url.path)
        if not (to_path + "/").startswith(base_prefix + "/"):
            logger.warning("Destination %r from MOVE request on %r doesn't "
                           "start with base prefix", to_path, path)
            return httputils.NOT_ALLOWED
        to_path = to_path[len(base_prefix):]
        if not self.access(user, to_path, "w"):
            return httputils.NOT_ALLOWED

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

            to_item = next(self.Collection.discover(to_path), None)
            if isinstance(to_item, storage.BaseCollection):
                return httputils.FORBIDDEN
            to_parent_path = pathutils.unstrip_path(
                posixpath.dirname(pathutils.strip_path(to_path)), True)
            to_collection = next(
                self.Collection.discover(to_parent_path), None)
            if not to_collection:
                return httputils.CONFLICT
            tag = item.collection.get_meta("tag")
            if not tag or tag != to_collection.get_meta("tag"):
                return httputils.FORBIDDEN
            if to_item and environ.get("HTTP_OVERWRITE", "F") != "T":
                return httputils.PRECONDITION_FAILED
            if (to_item and item.uid != to_item.uid or
                    not to_item and
                    to_collection.path != item.collection.path and
                    to_collection.has_uid(item.uid)):
                return self.webdav_error_response(
                    "C" if tag == "VCALENDAR" else "CR", "no-uid-conflict")
            to_href = posixpath.basename(pathutils.strip_path(to_path))
            try:
                self.Collection.move(item, to_collection, to_href)
            except ValueError as e:
                logger.warning(
                    "Bad MOVE request on %r: %s", path, e, exc_info=True)
                return httputils.BAD_REQUEST
            return client.NO_CONTENT if to_item else client.CREATED, {}, None
Exemplo n.º 4
0
 def inspect(self):
     """Inspect all external config sources and write problems to logger."""
     for config, source, internal in self._configs:
         if internal:
             continue
         if config is self.SOURCE_MISSING:
             logger.info("Skipped missing %s", source)
         else:
             logger.info("Parsed %s", source)
Exemplo n.º 5
0
def load_plugin(internal_types, module_name, class_name, configuration):
    type_ = configuration.get(module_name, "type")
    if type_ in internal_types:
        module = "radicale.%s.%s" % (module_name, type_)
    else:
        module = type_
    try:
        class_ = getattr(import_module(module), class_name)
    except Exception as e:
        raise RuntimeError("Failed to load %s module %r: %s" %
                           (module_name, module, e)) from e
    logger.info("%s type is %r", module_name, module)
    return class_(configuration)
Exemplo n.º 6
0
    def log_config_sources(self):
        """
        A helper function that writes a description of all config sources
        to logger.

        Configs set to ``Configuration.SOURCE_MISSING`` are described as
        missing.

        """
        for config, source, _ in self._configs:
            if config is self.SOURCE_MISSING:
                logger.info("Skipped missing %s", source)
            else:
                logger.info("Loaded %s", source)
Exemplo n.º 7
0
def load(configuration):
    """Load the web module chosen in configuration."""
    web_type = configuration.get("web", "type")
    if web_type in INTERNAL_TYPES:
        module = "radicale.web.%s" % web_type
    else:
        module = web_type
    try:
        class_ = import_module(module).Web
    except Exception as e:
        raise RuntimeError("Failed to load web module %r: %s" %
                           (module, e)) from e
    logger.info("Web type is %r", web_type)
    return class_(configuration)
Exemplo n.º 8
0
def load(configuration):
    """Load the rights manager chosen in configuration."""
    rights_type = configuration.get("rights", "type")
    if rights_type in INTERNAL_TYPES:
        module = "radicale.rights.%s" % rights_type
    else:
        module = rights_type
    try:
        class_ = import_module(module).Rights
    except Exception as e:
        raise RuntimeError("Failed to load rights module %r: %s" %
                           (module, e)) from e
    logger.info("Rights type is %r", rights_type)
    return class_(configuration)
Exemplo n.º 9
0
def load(configuration):
    """Load the authentication manager chosen in configuration."""
    auth_type = configuration.get("auth", "type")
    if auth_type in INTERNAL_TYPES:
        module = "radicale.auth.%s" % auth_type
    else:
        module = auth_type
    try:
        class_ = import_module(module).Auth
    except Exception as e:
        raise RuntimeError("Failed to load authentication module %r: %s" %
                           (module, e)) from e
    logger.info("Authentication type is %r", auth_type)
    return class_(configuration)
Exemplo n.º 10
0
def load(configuration):
    """Load the web module chosen in configuration."""
    web_type = configuration.get("web", "type")
    if web_type == "none":
        web_class = NoneWeb
    elif web_type == "internal":
        web_class = Web
    else:
        try:
            web_class = import_module(web_type).Web
        except Exception as e:
            raise RuntimeError("Failed to load web module %r: %s" %
                               (web_type, e)) from e
    logger.info("Web type is %r", web_type)
    return web_class(configuration)
Exemplo n.º 11
0
def load(configuration):
    """Load the storage manager chosen in configuration."""
    storage_type = configuration.get("storage", "type")
    if storage_type in INTERNAL_TYPES:
        module = "radicale.storage.%s" % storage_type
    else:
        module = storage_type
    try:
        class_ = import_module(module).Collection
    except Exception as e:
        raise RuntimeError("Failed to load storage module %r: %s" %
                           (module, e)) from e
    logger.info("Storage type is %r", storage_type)

    class CollectionCopy(class_):
        """Collection copy, avoids overriding the original class attributes."""
    CollectionCopy.configuration = configuration
    CollectionCopy.static_init()
    return CollectionCopy
Exemplo n.º 12
0
def load(configuration):
    """Load the rights manager chosen in configuration."""
    rights_type = configuration.get("rights", "type")
    if rights_type == "authenticated":
        rights_class = AuthenticatedRights
    elif rights_type == "owner_write":
        rights_class = OwnerWriteRights
    elif rights_type == "owner_only":
        rights_class = OwnerOnlyRights
    elif rights_type == "from_file":
        rights_class = Rights
    else:
        try:
            rights_class = import_module(rights_type).Rights
        except Exception as e:
            raise RuntimeError("Failed to load rights module %r: %s" %
                               (rights_type, e)) from e
    logger.info("Rights type is %r", rights_type)
    return rights_class(configuration)
Exemplo n.º 13
0
 def authorized(self, user, path, permissions):
     user = user or ""
     sane_path = pathutils.strip_path(path)
     # Prevent "regex injection"
     user_escaped = re.escape(user)
     sane_path_escaped = re.escape(sane_path)
     rights_config = configparser.ConfigParser({
         "login": user_escaped,
         "path": sane_path_escaped
     })
     try:
         if not rights_config.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 rights_config.sections():
         try:
             user_pattern = rights_config.get(section, "user")
             collection_pattern = rights_config.get(section, "collection")
             user_match = re.fullmatch(user_pattern, user)
             collection_match = user_match and re.fullmatch(
                 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:
             logger.debug("Rule %r:%r matches %r:%r from section %r", user,
                          sane_path, user_pattern, collection_pattern,
                          section)
             return rights.intersect_permissions(
                 permissions, rights_config.get(section, "permissions"))
         else:
             logger.debug("Rule %r:%r doesn't match %r:%r from section %r",
                          user, sane_path, user_pattern, collection_pattern,
                          section)
     logger.info("Rights: %r:%r doesn't match any section", user, sane_path)
     return ""
Exemplo n.º 14
0
 def authorization(self, user, path):
     user = user or ""
     sane_path = pathutils.strip_path(path)
     # Prevent "regex injection"
     escaped_user = re.escape(user)
     rights_config = configparser.ConfigParser()
     try:
         if not rights_config.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 rights_config.sections():
         try:
             user_pattern = rights_config.get(section, "user")
             collection_pattern = rights_config.get(section, "collection")
             # Use empty format() for harmonized handling of curly braces
             user_match = re.fullmatch(user_pattern.format(), user)
             collection_match = user_match and re.fullmatch(
                 collection_pattern.format(
                     *map(re.escape, user_match.groups()),
                     user=escaped_user), 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:
             logger.debug("Rule %r:%r matches %r:%r from section %r",
                          user, sane_path, user_pattern,
                          collection_pattern, section)
             return rights_config.get(section, "permissions")
         logger.debug("Rule %r:%r doesn't match %r:%r from section %r",
                      user, sane_path, user_pattern, collection_pattern,
                      section)
     logger.info("Rights: %r:%r doesn't match any section", user, sane_path)
     return ""
Exemplo n.º 15
0
def run():
    """Run Radicale as a standalone server."""
    log.setup()

    # Get command-line arguments
    parser = argparse.ArgumentParser(usage="radicale [OPTIONS]")

    parser.add_argument("--version", action="version", version=VERSION)
    parser.add_argument("--verify-storage",
                        action="store_true",
                        help="check the storage for errors and exit")
    parser.add_argument("-C",
                        "--config",
                        help="use a specific configuration file")
    parser.add_argument("-D",
                        "--debug",
                        action="store_true",
                        help="print debug information")

    groups = {}
    for section, values in config.INITIAL_CONFIG.items():
        group = parser.add_argument_group(section)
        groups[group] = []
        for option, data in values.items():
            kwargs = data.copy()
            long_name = "--{0}-{1}".format(section, option.replace("_", "-"))
            args = kwargs.pop("aliases", [])
            args.append(long_name)
            kwargs["dest"] = "{0}_{1}".format(section, option)
            groups[group].append(kwargs["dest"])
            del kwargs["value"]
            if "internal" in kwargs:
                del kwargs["internal"]

            if kwargs["type"] == bool:
                del kwargs["type"]
                kwargs["action"] = "store_const"
                kwargs["const"] = "True"
                opposite_args = kwargs.pop("opposite", [])
                opposite_args.append("--no{0}".format(long_name[1:]))
                group.add_argument(*args, **kwargs)

                kwargs["const"] = "False"
                kwargs["help"] = "do not {0} (opposite of {1})".format(
                    kwargs["help"], long_name)
                group.add_argument(*opposite_args, **kwargs)
            else:
                group.add_argument(*args, **kwargs)

    args = parser.parse_args()

    # Preliminary configure logging
    if args.debug:
        args.logging_level = "debug"
    if args.logging_level is not None:
        log.set_level(args.logging_level)

    if args.config is not None:
        config_paths = [args.config] if args.config else []
        ignore_missing_paths = False
    else:
        config_paths = [
            "/etc/radicale/config",
            os.path.expanduser("~/.config/radicale/config")
        ]
        if "RADICALE_CONFIG" in os.environ:
            config_paths.append(os.environ["RADICALE_CONFIG"])
        ignore_missing_paths = True
    try:
        configuration = config.load(config_paths,
                                    ignore_missing_paths=ignore_missing_paths)
    except Exception as e:
        logger.fatal("Invalid configuration: %s", e, exc_info=True)
        exit(1)

    # Update Radicale configuration according to arguments
    for group, actions in groups.items():
        section = group.title
        for action in actions:
            value = getattr(args, action)
            if value is not None:
                configuration.set(section, action.split('_', 1)[1], value)

    # Configure logging
    log.set_level(configuration.get("logging", "level"))

    if args.verify_storage:
        logger.info("Verifying storage")
        try:
            Collection = storage.load(configuration)
            with Collection.acquire_lock("r"):
                if not Collection.verify():
                    logger.fatal("Storage verifcation failed")
                    exit(1)
        except Exception as e:
            logger.fatal(
                "An exception occurred during storage verification: "
                "%s",
                e,
                exc_info=True)
            exit(1)
        return

    # Create a socket pair to notify the server of program shutdown
    shutdown_socket, shutdown_socket_out = socket.socketpair()

    # SIGTERM and SIGINT (aka KeyboardInterrupt) shutdown the server
    def shutdown(*args):
        shutdown_socket.sendall(b" ")

    signal.signal(signal.SIGTERM, shutdown)
    signal.signal(signal.SIGINT, shutdown)

    try:
        server.serve(configuration, shutdown_socket_out)
    except Exception as e:
        logger.fatal("An exception occurred during server startup: %s",
                     e,
                     exc_info=True)
        exit(1)
Exemplo n.º 16
0
def run():
    """Run Radicale as a standalone server."""

    # Raise SystemExit when signal arrives to run cleanup code
    # (like destructors, try-finish etc.), otherwise the process exits
    # without running any of them
    def signal_handler(signal_number, stack_frame):
        sys.exit(1)

    signal.signal(signal.SIGTERM, signal_handler)
    signal.signal(signal.SIGINT, signal_handler)
    if os.name == "posix":
        signal.signal(signal.SIGHUP, signal_handler)

    log.setup()

    # Get command-line arguments
    parser = argparse.ArgumentParser(prog="radicale",
                                     usage="%(prog)s [OPTIONS]")

    parser.add_argument("--version", action="version", version=VERSION)
    parser.add_argument("--verify-storage",
                        action="store_true",
                        help="check the storage for errors and exit")
    parser.add_argument("-C",
                        "--config",
                        help="use specific configuration files",
                        nargs="*")
    parser.add_argument("-D",
                        "--debug",
                        action="store_true",
                        help="print debug information")

    groups = {}
    for section, values in config.DEFAULT_CONFIG_SCHEMA.items():
        if section.startswith("_"):
            continue
        group = parser.add_argument_group(section)
        groups[group] = []
        for option, data in values.items():
            if option.startswith("_"):
                continue
            kwargs = data.copy()
            long_name = "--%s-%s" % (section, option.replace("_", "-"))
            args = kwargs.pop("aliases", [])
            args.append(long_name)
            kwargs["dest"] = "%s_%s" % (section, option)
            groups[group].append(kwargs["dest"])
            del kwargs["value"]
            with contextlib.suppress(KeyError):
                del kwargs["internal"]

            if kwargs["type"] == bool:
                del kwargs["type"]
                kwargs["action"] = "store_const"
                kwargs["const"] = "True"
                opposite_args = kwargs.pop("opposite", [])
                opposite_args.append("--no%s" % long_name[1:])
                group.add_argument(*args, **kwargs)

                kwargs["const"] = "False"
                kwargs["help"] = "do not %s (opposite of %s)" % (
                    kwargs["help"], long_name)
                group.add_argument(*opposite_args, **kwargs)
            else:
                del kwargs["type"]
                group.add_argument(*args, **kwargs)

    args = parser.parse_args()

    # Preliminary configure logging
    if args.debug:
        args.logging_level = "debug"
    with contextlib.suppress(ValueError):
        log.set_level(config.DEFAULT_CONFIG_SCHEMA["logging"]["level"]["type"](
            args.logging_level))

    # Update Radicale configuration according to arguments
    arguments_config = {}
    for group, actions in groups.items():
        section = group.title
        section_config = {}
        for action in actions:
            value = getattr(args, action)
            if value is not None:
                section_config[action.split('_', 1)[1]] = value
        if section_config:
            arguments_config[section] = section_config

    try:
        configuration = config.load(
            config.parse_compound_paths(
                config.DEFAULT_CONFIG_PATH, os.environ.get("RADICALE_CONFIG"),
                os.pathsep.join(args.config) if args.config else None))
        if arguments_config:
            configuration.update(arguments_config, "arguments")
    except Exception as e:
        logger.fatal("Invalid configuration: %s", e, exc_info=True)
        sys.exit(1)

    # Configure logging
    log.set_level(configuration.get("logging", "level"))

    # Log configuration after logger is configured
    for source, miss in configuration.sources():
        logger.info("%s %s", "Skipped missing" if miss else "Loaded", source)

    if args.verify_storage:
        logger.info("Verifying storage")
        try:
            storage_ = storage.load(configuration)
            with storage_.acquire_lock("r"):
                if not storage_.verify():
                    logger.fatal("Storage verifcation failed")
                    sys.exit(1)
        except Exception as e:
            logger.fatal(
                "An exception occurred during storage verification: "
                "%s",
                e,
                exc_info=True)
            sys.exit(1)
        return

    try:
        server.serve(configuration)
    except Exception as e:
        logger.fatal("An exception occurred during server startup: %s",
                     e,
                     exc_info=True)
        sys.exit(1)
Exemplo n.º 17
0
    def _handle_request(self, environ):
        """Manage a request."""
        def response(status, headers=(), answer=None):
            headers = dict(headers)
            # Set content length
            if answer:
                if hasattr(answer, "encode"):
                    logger.debug("Response content:\n%s", answer)
                    headers["Content-Type"] += "; charset=%s" % self.encoding
                    answer = answer.encode(self.encoding)
                accept_encoding = [
                    encoding.strip() for encoding in
                    environ.get("HTTP_ACCEPT_ENCODING", "").split(",")
                    if encoding.strip()]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return response(status, headers, answer)
Exemplo n.º 18
0
    def __init__(self, configuration):
        super().__init__(configuration.copy(PLUGIN_CONFIG_SCHEMA))

        options = configuration.options("auth")

        if "ldap_url" not in options: raise RuntimeError("The ldap_url configuration for ldap auth is required.")
        if "ldap_base" not in options: raise RuntimeError("The ldap_base configuration for ldap auth is required.")
        if "ldap_filter" not in options: raise RuntimeError("The ldap_filter configuration for ldap auth is required.")
        if "ldap_attribute" not in options: raise RuntimeError("The ldap_attribute configuration for ldap auth is required.")
        if "ldap_binddn" not in options: raise RuntimeError("The ldap_binddn configuration for ldap auth is required.")
        if "ldap_password" not in options: raise RuntimeError("The ldap_password configuration for ldap auth is required.")

        # also get rid of trailing slashes which are typical for uris
        self.ldap_url = configuration.get("auth", "ldap_url").rstrip("/")
        self.ldap_base = configuration.get("auth", "ldap_base")
        self.ldap_filter = configuration.get("auth", "ldap_filter")
        self.ldap_attribute = configuration.get("auth", "ldap_attribute")
        self.ldap_binddn = configuration.get("auth", "ldap_binddn")
        self.ldap_password = configuration.get("auth", "ldap_password")

        logger.info("LDAP auth configuration:")
        logger.info("  %r is %r", "ldap_url", self.ldap_url)
        logger.info("  %r is %r", "ldap_base", self.ldap_base)
        logger.info("  %r is %r", "ldap_filter", self.ldap_filter)
        logger.info("  %r is %r", "ldap_attribute", self.ldap_attribute)
        logger.info("  %r is %r", "ldap_binddn", self.ldap_binddn)
        logger.info("  %r is %r", "ldap_password", self.ldap_password)
Exemplo n.º 19
0
def serve(configuration, shutdown_socket):
    """Serve radicale from configuration."""
    logger.info("Starting Radicale")
    # Copy configuration before modifying
    configuration = configuration.copy()
    configuration.update({"server": {
        "_internal_server": "True"
    }},
                         "server",
                         privileged=True)

    use_ssl = configuration.get("server", "ssl")
    server_class = ParallelHTTPSServer if use_ssl else ParallelHTTPServer
    application = Application(configuration)
    servers = {}
    try:
        for address in configuration.get("server", "hosts"):
            # Try to bind sockets for IPv4 and IPv6
            possible_families = (socket.AF_INET, socket.AF_INET6)
            bind_ok = False
            for i, family in enumerate(possible_families):
                is_last = i == len(possible_families) - 1
                try:
                    server = server_class(configuration, family, address,
                                          RequestHandler)
                except OSError as e:
                    # Ignore unsupported families (only one must work)
                    if ((bind_ok or not is_last) and
                        (isinstance(e, socket.gaierror) and (
                            # Hostname does not exist or doesn't have
                            # address for address family
                            # macOS: IPv6 address for INET address family
                            e.errno == socket.EAI_NONAME or
                            # Address not for address family
                            e.errno == COMPAT_EAI_ADDRFAMILY
                            or e.errno == COMPAT_EAI_NODATA) or
                         # Workaround for PyPy
                         str(e) == "address family mismatched" or
                         # Address family not available (e.g. IPv6 disabled)
                         # macOS: IPv4 address for INET6 address family with
                         #        IPV6_V6ONLY set
                         e.errno == errno.EADDRNOTAVAIL or
                         # Address family not supported
                         e.errno == errno.EAFNOSUPPORT or
                         # Protocol not supported
                         e.errno == errno.EPROTONOSUPPORT)):
                        continue
                    raise RuntimeError("Failed to start server %r: %s" %
                                       (format_address(address), e)) from e
                servers[server.socket] = server
                bind_ok = True
                server.set_app(application)
                logger.info("Listening on %r%s",
                            format_address(server.server_address),
                            " with SSL" if use_ssl else "")
        assert servers, "no servers started"

        # Mainloop
        select_timeout = None
        if os.name == "nt":
            # Fallback to busy waiting. (select(...) blocks SIGINT on Windows.)
            select_timeout = 1.0
        max_connections = configuration.get("server", "max_connections")
        logger.info("Radicale server ready")
        while True:
            rlist = []
            # Wait for finished clients
            for server in servers.values():
                rlist.extend(server.client_sockets)
            # Accept new connections if max_connections is not reached
            if max_connections <= 0 or len(rlist) < max_connections:
                rlist.extend(servers)
            # Use socket to get notified of program shutdown
            rlist.append(shutdown_socket)
            rlist, _, _ = select.select(rlist, [], [], select_timeout)
            rlist = set(rlist)
            if shutdown_socket in rlist:
                logger.info("Stopping Radicale")
                break
            for server in servers.values():
                finished_sockets = server.client_sockets.intersection(rlist)
                for s in finished_sockets:
                    s.close()
                    server.client_sockets.remove(s)
                    rlist.remove(s)
                if finished_sockets:
                    server.service_actions()
            if rlist:
                server = servers.get(rlist.pop())
                if server:
                    server.handle_request()
    finally:
        # Wait for clients to finish and close servers
        for server in servers.values():
            for s in server.client_sockets:
                s.recv(1)
                s.close()
            server.server_close()