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
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
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)