Server IP : 85.214.239.14 / Your IP : 3.144.119.149 Web Server : Apache/2.4.62 (Debian) System : Linux h2886529.stratoserver.net 4.9.0 #1 SMP Tue Jan 9 19:45:01 MSK 2024 x86_64 User : www-data ( 33) PHP Version : 7.4.18 Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare, MySQL : OFF | cURL : OFF | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /srv/radicale/env/lib64/python3.5/site-packages/radicale/app/ |
Upload File : |
# This file is part of Radicale Server - Calendar Server # Copyright © 2008 Nicolas Kandel # Copyright © 2008 Pascal Halter # Copyright © 2008-2017 Guillaume Ayoub # Copyright © 2017-2019 Unrud <unrud@outlook.com> # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Radicale. If not, see <http://www.gnu.org/licenses/>. """ Radicale WSGI application. Can be used with an external WSGI server (see ``radicale.application()``) or the built-in server (see ``radicale.server`` module). """ import base64 import datetime import io import logging import posixpath import pprint import random import time import zlib from http import client from xml.etree import ElementTree as ET import defusedxml.ElementTree as DefusedET import pkg_resources from radicale import (auth, httputils, log, pathutils, rights, storage, web, xmlutils) from radicale.app.delete import ApplicationDeleteMixin from radicale.app.get import ApplicationGetMixin from radicale.app.head import ApplicationHeadMixin from radicale.app.mkcalendar import ApplicationMkcalendarMixin from radicale.app.mkcol import ApplicationMkcolMixin from radicale.app.move import ApplicationMoveMixin from radicale.app.options import ApplicationOptionsMixin from radicale.app.propfind import ApplicationPropfindMixin from radicale.app.proppatch import ApplicationProppatchMixin from radicale.app.put import ApplicationPutMixin from radicale.app.report import ApplicationReportMixin from radicale.log import logger VERSION = pkg_resources.get_distribution("radicale").version class Application( ApplicationDeleteMixin, ApplicationGetMixin, ApplicationHeadMixin, ApplicationMkcalendarMixin, ApplicationMkcolMixin, ApplicationMoveMixin, ApplicationOptionsMixin, ApplicationPropfindMixin, ApplicationProppatchMixin, ApplicationPutMixin, ApplicationReportMixin): """WSGI application.""" def __init__(self, configuration): """Initialize Application. ``configuration`` see ``radicale.config`` module. The ``configuration`` must not change during the lifetime of this object, it is kept as an internal reference. """ super().__init__() self.configuration = configuration self._auth = auth.load(configuration) self._storage = storage.load(configuration) self._rights = rights.load(configuration) self._web = web.load(configuration) self._encoding = configuration.get("encoding", "request") def _headers_log(self, environ): """Sanitize headers for logging.""" request_environ = dict(environ) # Mask passwords mask_passwords = self.configuration.get("logging", "mask_passwords") authorization = request_environ.get("HTTP_AUTHORIZATION", "") if mask_passwords and authorization.startswith("Basic"): request_environ["HTTP_AUTHORIZATION"] = "Basic **masked**" if request_environ.get("HTTP_COOKIE"): request_environ["HTTP_COOKIE"] = "**masked**" return request_environ def _decode(self, text, environ): """Try to magically decode ``text`` according to given ``environ``.""" # List of charsets to try charsets = [] # First append content charset given in the request content_type = environ.get("CONTENT_TYPE") if content_type and "charset=" in content_type: charsets.append( content_type.split("charset=")[1].split(";")[0].strip()) # Then append default Radicale charset charsets.append(self._encoding) # Then append various fallbacks charsets.append("utf-8") charsets.append("iso8859-1") # Try to decode for charset in charsets: try: return text.decode(charset) except UnicodeDecodeError: pass raise UnicodeDecodeError def __call__(self, environ, start_response): with log.register_stream(environ["wsgi.errors"]): try: status, headers, answers = self._handle_request(environ) except Exception as e: try: method = str(environ["REQUEST_METHOD"]) except Exception: method = "unknown" try: path = str(environ.get("PATH_INFO", "")) except Exception: path = "" logger.error("An exception occurred during %s request on %r: " "%s", method, path, e, exc_info=True) status, headers, answer = httputils.INTERNAL_SERVER_ERROR answer = answer.encode("ascii") status = "%d %s" % ( status.value, client.responses.get(status, "Unknown")) headers = [ ("Content-Length", str(len(answer)))] + list(headers) answers = [answer] start_response(status, headers) return answers 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 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.get("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 with self._storage.acquire_lock("r", user): principal = next(self._storage.discover( principal_path, depth="1"), None) if not principal: if "W" in self._rights.authorization(user, principal_path): with self._storage.acquire_lock("w", user): try: self._storage.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.get("server", "_internal_server"): # Verify content length content_length = int(environ.get("CONTENT_LENGTH") or 0) if content_length: max_content_length = self.configuration.get( "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 _read_raw_content(self, environ): content_length = int(environ.get("CONTENT_LENGTH") or 0) if not content_length: return b"" content = environ["wsgi.input"].read(content_length) if len(content) < content_length: raise RuntimeError("Request body too short: %d" % len(content)) return content def _read_content(self, environ): content = self._decode(self._read_raw_content(environ), environ) logger.debug("Request content:\n%s", content) return content def _read_xml_content(self, environ): content = self._decode(self._read_raw_content(environ), environ) if not content: return None try: xml_content = DefusedET.fromstring(content) except ET.ParseError as e: logger.debug("Request content (Invalid XML):\n%s", content) raise RuntimeError("Failed to parse XML: %s" % e) from e if logger.isEnabledFor(logging.DEBUG): logger.debug("Request content:\n%s", xmlutils.pretty_xml(xml_content)) return xml_content def _write_xml_content(self, xml_content): if logger.isEnabledFor(logging.DEBUG): logger.debug("Response content:\n%s", xmlutils.pretty_xml(xml_content)) f = io.BytesIO() ET.ElementTree(xml_content).write(f, encoding=self._encoding, xml_declaration=True) return f.getvalue() def _webdav_error_response(self, human_tag, status=httputils.WEBDAV_PRECONDITION_FAILED[0]): """Generate XML error response.""" headers = {"Content-Type": "text/xml; charset=%s" % self._encoding} content = self._write_xml_content(xmlutils.webdav_error(human_tag)) return status, headers, content class Access: """Helper class to check access rights of an item""" def __init__(self, rights, user, path): self._rights = rights self.user = user self.path = path self.parent_path = pathutils.unstrip_path( posixpath.dirname(pathutils.strip_path(path)), True) self.permissions = self._rights.authorization(self.user, self.path) self._parent_permissions = None @property def parent_permissions(self): if self.path == self.parent_path: return self.permissions if self._parent_permissions is None: self._parent_permissions = self._rights.authorization( self.user, self.parent_path) return self._parent_permissions def check(self, permission, item=None): if permission not in "rw": raise ValueError("Invalid permission argument: %r" % permission) if not item: permissions = permission + permission.upper() parent_permissions = permission elif isinstance(item, storage.BaseCollection): if item.get_meta("tag"): permissions = permission else: permissions = permission.upper() parent_permissions = "" else: permissions = "" parent_permissions = permission return bool(rights.intersect(self.permissions, permissions) or ( self.path != self.parent_path and rights.intersect(self.parent_permissions, parent_permissions)))