Improve handling of XML requests and responses

* Move parsing/serialization of XML requests/responses from ``xmlutils.py`` to ``__init__.py``.
  * Log XML requests/responses in pretty-printed form.
      * Previously only the responses were logged in readable form. This is useful for debugging.
      * The XML documents are only converted for pretty-printing if debugging is enabled (it's expensive)
  * Send XML responses in minimized form to clients.
  * Add **encoding** attribute to XML declaration in XML response.
  * Only decode XML requests once. (Previously they were decoded, encoded and decoded again.)
This commit is contained in:
Unrud
2017-05-07 08:17:39 +02:00
parent f1a9cf7694
commit 11c5dfdb53
2 changed files with 85 additions and 51 deletions

View File

@ -31,6 +31,7 @@ import contextlib
import datetime
import io
import itertools
import logging
import os
import posixpath
import pprint
@ -46,6 +47,7 @@ import wsgiref.simple_server
import zlib
from http import client
from urllib.parse import unquote, urlparse
from xml.etree import ElementTree as ET
import vobject
@ -463,16 +465,43 @@ class Application:
allowed |= self.authorized(user, parent_path, permission)
return allowed
def _read_content(self, environ):
def _read_raw_content(self, environ):
content_length = int(environ.get("CONTENT_LENGTH") or 0)
if content_length > 0:
content = self.decode(
environ["wsgi.input"].read(content_length), environ)
self.logger.debug("Request content:\n%s", content.strip())
else:
content = None
if not content_length:
return b""
content = environ["wsgi.input"].read(content_length)
if len(content) < content_length:
raise ValueError("Request body too short: %d" % len(content))
return content
def _read_content(self, environ):
content = self.decode(self._read_raw_content(environ), environ)
self.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 = ET.fromstring(content)
except ET.ParseError:
self.logger.debug("Request content (Invalid XML):\n%s", content)
raise
if self.logger.isEnabledFor(logging.DEBUG):
self.logger.debug("Request content:\n%s",
xmlutils.pretty_xml(xml_content))
return xml_content
def _write_xml_content(self, xml_content):
if self.logger.isEnabledFor(logging.DEBUG):
self.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 do_DELETE(self, environ, base_prefix, path, user):
"""Manage DELETE request."""
if not self._access(user, path, "w"):
@ -488,11 +517,12 @@ class Application:
# ETag precondition not verified, do not delete item
return PRECONDITION_FAILED
if isinstance(item, self.Collection):
answer = xmlutils.delete(base_prefix, path, item)
xml_answer = xmlutils.delete(base_prefix, path, item)
else:
answer = xmlutils.delete(
xml_answer = xmlutils.delete(
base_prefix, path, item.collection, item.href)
return client.OK, {"Content-Type": "text/xml"}, answer
headers = {"Content-Type": "text/xml; charset=%s" % self.encoding}
return client.OK, headers, self._write_xml_content(xml_answer)
def do_GET(self, environ, base_prefix, path, user):
"""Manage GET request."""
@ -533,12 +563,12 @@ class Application:
"""Manage MKCALENDAR request."""
if not self.authorized(user, path, "w"):
return NOT_ALLOWED
content = self._read_content(environ)
xml_content = self._read_xml_content(environ)
with self.Collection.acquire_lock("w", user):
item = next(self.Collection.discover(path), None)
if item:
return WEBDAV_PRECONDITION_FAILED
props = xmlutils.props_from_request(content)
props = xmlutils.props_from_request(xml_content)
props["tag"] = "VCALENDAR"
# TODO: use this?
# timezone = props.get("C:calendar-timezone")
@ -549,12 +579,12 @@ class Application:
"""Manage MKCOL request."""
if not self.authorized(user, path, "w"):
return NOT_ALLOWED
content = self._read_content(environ)
xml_content = self._read_xml_content(environ)
with self.Collection.acquire_lock("w", user):
item = next(self.Collection.discover(path), None)
if item:
return WEBDAV_PRECONDITION_FAILED
props = xmlutils.props_from_request(content)
props = xmlutils.props_from_request(xml_content)
self.Collection.create_collection(path, props=props)
return client.CREATED, {}, None
@ -607,7 +637,7 @@ class Application:
"""Manage PROPFIND request."""
if not self._access(user, path, "r"):
return NOT_ALLOWED
content = self._read_content(environ)
xml_content = self._read_xml_content(environ)
with self.Collection.acquire_lock("r", user):
items = self.Collection.discover(
path, environ.get("HTTP_DEPTH", "0"))
@ -620,26 +650,30 @@ class Application:
# put item back
items = itertools.chain([item], items)
read_items, write_items = self.collect_allowed_items(items, user)
headers = {"DAV": DAV_HEADERS, "Content-Type": "text/xml"}
status, answer = xmlutils.propfind(
base_prefix, path, content, read_items, write_items, user)
headers = {"DAV": DAV_HEADERS,
"Content-Type": "text/xml; charset=%s" % self.encoding}
status, xml_answer = xmlutils.propfind(
base_prefix, path, xml_content, read_items, write_items, user)
if status == client.FORBIDDEN:
return NOT_ALLOWED
else:
return status, headers, answer
return status, headers, self._write_xml_content(xml_answer)
def do_PROPPATCH(self, environ, base_prefix, path, user):
"""Manage PROPPATCH request."""
if not self.authorized(user, path, "w"):
return NOT_ALLOWED
content = self._read_content(environ)
xml_content = self._read_xml_content(environ)
with self.Collection.acquire_lock("w", user):
item = next(self.Collection.discover(path), None)
if not isinstance(item, self.Collection):
return WEBDAV_PRECONDITION_FAILED
headers = {"DAV": DAV_HEADERS, "Content-Type": "text/xml"}
answer = xmlutils.proppatch(base_prefix, path, content, item)
return client.MULTI_STATUS, headers, answer
headers = {"DAV": DAV_HEADERS,
"Content-Type": "text/xml; charset=%s" % self.encoding}
xml_answer = xmlutils.proppatch(base_prefix, path, xml_content,
item)
return (client.MULTI_STATUS, headers,
self._write_xml_content(xml_answer))
def do_PUT(self, environ, base_prefix, path, user):
"""Manage PUT request."""
@ -697,7 +731,7 @@ class Application:
"""Manage REPORT request."""
if not self._access(user, path, "r"):
return NOT_ALLOWED
content = self._read_content(environ)
xml_content = self._read_xml_content(environ)
with self.Collection.acquire_lock("r", user):
item = next(self.Collection.discover(path), None)
if not self._access(user, path, "r", item):
@ -708,6 +742,8 @@ class Application:
collection = item
else:
collection = item.collection
headers = {"Content-Type": "text/xml"}
answer = xmlutils.report(base_prefix, path, content, collection)
return client.MULTI_STATUS, headers, answer
headers = {"Content-Type": "text/xml; charset=%s" % self.encoding}
xml_answer = xmlutils.report(
base_prefix, path, xml_content, collection)
return (client.MULTI_STATUS, headers,
self._write_xml_content(xml_answer))