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