diff --git a/radicale/app/__init__.py b/radicale/app/__init__.py index 30de725..b0c53a9 100644 --- a/radicale/app/__init__.py +++ b/radicale/app/__init__.py @@ -99,30 +99,6 @@ class Application( 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: @@ -244,8 +220,9 @@ class Application( 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) + login, password = httputils.decode_request( + self.configuration, environ, base64.b64decode( + authorization.encode("ascii"))).split(":", 1) user = self._auth.login(login, password) or "" if login else "" if user and login == user: @@ -317,22 +294,10 @@ class Application( 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) + def _read_xml_request_body(self, environ): + content = httputils.decode_request( + self.configuration, environ, + httputils.read_raw_request_body(self.configuration, environ)) if not content: return None try: diff --git a/radicale/app/mkcalendar.py b/radicale/app/mkcalendar.py index b7e16d9..d5c6c4d 100644 --- a/radicale/app/mkcalendar.py +++ b/radicale/app/mkcalendar.py @@ -33,7 +33,7 @@ class ApplicationMkcalendarMixin: if "w" not in self._rights.authorization(user, path): return httputils.NOT_ALLOWED try: - xml_content = self._read_xml_content(environ) + xml_content = self._read_xml_request_body(environ) except RuntimeError as e: logger.warning( "Bad MKCALENDAR request on %r: %s", path, e, exc_info=True) diff --git a/radicale/app/mkcol.py b/radicale/app/mkcol.py index b4ba961..f719541 100644 --- a/radicale/app/mkcol.py +++ b/radicale/app/mkcol.py @@ -34,7 +34,7 @@ class ApplicationMkcolMixin: if not rights.intersect(permissions, "Ww"): return httputils.NOT_ALLOWED try: - xml_content = self._read_xml_content(environ) + xml_content = self._read_xml_request_body(environ) except RuntimeError as e: logger.warning( "Bad MKCOL request on %r: %s", path, e, exc_info=True) diff --git a/radicale/app/propfind.py b/radicale/app/propfind.py index 1c55b04..106e2e8 100644 --- a/radicale/app/propfind.py +++ b/radicale/app/propfind.py @@ -347,7 +347,7 @@ class ApplicationPropfindMixin: if not access.check("r"): return httputils.NOT_ALLOWED try: - xml_content = self._read_xml_content(environ) + xml_content = self._read_xml_request_body(environ) except RuntimeError as e: logger.warning( "Bad PROPFIND request on %r: %s", path, e, exc_info=True) diff --git a/radicale/app/proppatch.py b/radicale/app/proppatch.py index 6fde5eb..3ec10c3 100644 --- a/radicale/app/proppatch.py +++ b/radicale/app/proppatch.py @@ -91,7 +91,7 @@ class ApplicationProppatchMixin: if not access.check("w"): return httputils.NOT_ALLOWED try: - xml_content = self._read_xml_content(environ) + xml_content = self._read_xml_request_body(environ) except RuntimeError as e: logger.warning( "Bad PROPPATCH request on %r: %s", path, e, exc_info=True) diff --git a/radicale/app/put.py b/radicale/app/put.py index 786ac00..efaf905 100644 --- a/radicale/app/put.py +++ b/radicale/app/put.py @@ -118,7 +118,7 @@ class ApplicationPutMixin: if not access.check("w"): return httputils.NOT_ALLOWED try: - content = self._read_content(environ) + content = httputils.read_request_body(self.configuration, environ) except RuntimeError as e: logger.warning("Bad PUT request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST diff --git a/radicale/app/report.py b/radicale/app/report.py index 30dac98..18ea4e7 100644 --- a/radicale/app/report.py +++ b/radicale/app/report.py @@ -262,7 +262,7 @@ class ApplicationReportMixin: if not access.check("r"): return httputils.NOT_ALLOWED try: - xml_content = self._read_xml_content(environ) + xml_content = self._read_xml_request_body(environ) except RuntimeError as e: logger.warning( "Bad REPORT request on %r: %s", path, e, exc_info=True) diff --git a/radicale/httputils.py b/radicale/httputils.py index eb4c75c..167a86b 100644 --- a/radicale/httputils.py +++ b/radicale/httputils.py @@ -24,6 +24,8 @@ Helper functions for HTTP. from http import client +from radicale.log import logger + NOT_ALLOWED = ( client.FORBIDDEN, (("Content-Type", "text/plain"),), "Access to the requested resource forbidden.") @@ -61,3 +63,45 @@ INTERNAL_SERVER_ERROR = ( "A server error occurred. Please contact the administrator.") DAV_HEADERS = "1, 2, 3, calendar-access, addressbook, extended-mkcol" + + +def decode_request(configuration, environ, text): + """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(configuration.get("encoding", "request")) + # 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 read_raw_request_body(configuration, 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_request_body(configuration, environ): + content = decode_request( + configuration, environ, read_raw_request_body(configuration, environ)) + logger.debug("Request content:\n%s", content) + return content diff --git a/radicale/tests/custom/web.py b/radicale/tests/custom/web.py index 3a8f5bd..784614f 100644 --- a/radicale/tests/custom/web.py +++ b/radicale/tests/custom/web.py @@ -21,7 +21,7 @@ Custom web plugin. from http import client -from radicale import web +from radicale import httputils, web class Web(web.BaseWeb): @@ -29,5 +29,5 @@ class Web(web.BaseWeb): return client.OK, {"Content-Type": "text/plain"}, "custom" def post(self, environ, base_prefix, path, user): - answer = "echo:" + environ["wsgi.input"].read().decode() - return client.OK, {"Content-Type": "text/plain"}, answer + content = httputils.read_request_body(self.configuration, environ) + return client.OK, {"Content-Type": "text/plain"}, "echo:" + content diff --git a/radicale/web/__init__.py b/radicale/web/__init__.py index 6d87833..c18e8d3 100644 --- a/radicale/web/__init__.py +++ b/radicale/web/__init__.py @@ -63,5 +63,8 @@ class BaseWeb: ``user`` is empty for anonymous users. + Use ``httputils.read*_request_body(self.configuration, environ)`` to + read the body. + """ return httputils.METHOD_NOT_ALLOWED