report.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
  1. # This file is part of Radicale - CalDAV and CardDAV server
  2. # Copyright © 2008 Nicolas Kandel
  3. # Copyright © 2008 Pascal Halter
  4. # Copyright © 2008-2017 Guillaume Ayoub
  5. # Copyright © 2017-2018 Unrud <unrud@outlook.com>
  6. #
  7. # This library is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This library is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  19. import contextlib
  20. import copy
  21. import datetime
  22. import posixpath
  23. import socket
  24. import xml.etree.ElementTree as ET
  25. from http import client
  26. from typing import (Any, Callable, Iterable, Iterator, List, Optional,
  27. Sequence, Tuple, Union)
  28. from urllib.parse import unquote, urlparse
  29. import vobject
  30. import vobject.base
  31. from vobject.base import ContentLine
  32. import radicale.item as radicale_item
  33. from radicale import httputils, pathutils, storage, types, xmlutils
  34. from radicale.app.base import Access, ApplicationBase
  35. from radicale.item import filter as radicale_filter
  36. from radicale.log import logger
  37. def free_busy_report(base_prefix: str, path: str, xml_request: Optional[ET.Element],
  38. collection: storage.BaseCollection, encoding: str,
  39. unlock_storage_fn: Callable[[], None],
  40. max_occurrence: int
  41. ) -> Tuple[int, Union[ET.Element, str]]:
  42. # NOTE: this function returns both an Element and a string because
  43. # free-busy reports are an edge-case on the return type according
  44. # to the spec.
  45. multistatus = ET.Element(xmlutils.make_clark("D:multistatus"))
  46. if xml_request is None:
  47. return client.MULTI_STATUS, multistatus
  48. root = xml_request
  49. if (root.tag == xmlutils.make_clark("C:free-busy-query") and
  50. collection.tag != "VCALENDAR"):
  51. logger.warning("Invalid REPORT method %r on %r requested",
  52. xmlutils.make_human_tag(root.tag), path)
  53. return client.FORBIDDEN, xmlutils.webdav_error("D:supported-report")
  54. time_range_element = root.find(xmlutils.make_clark("C:time-range"))
  55. assert isinstance(time_range_element, ET.Element)
  56. # Build a single filter from the free busy query for retrieval
  57. # TODO: filter for VFREEBUSY in additional to VEVENT but
  58. # test_filter doesn't support that yet.
  59. vevent_cf_element = ET.Element(xmlutils.make_clark("C:comp-filter"),
  60. attrib={'name': 'VEVENT'})
  61. vevent_cf_element.append(time_range_element)
  62. vcalendar_cf_element = ET.Element(xmlutils.make_clark("C:comp-filter"),
  63. attrib={'name': 'VCALENDAR'})
  64. vcalendar_cf_element.append(vevent_cf_element)
  65. filter_element = ET.Element(xmlutils.make_clark("C:filter"))
  66. filter_element.append(vcalendar_cf_element)
  67. filters = (filter_element,)
  68. # First pull from storage
  69. retrieved_items = list(collection.get_filtered(filters))
  70. # !!! Don't access storage after this !!!
  71. unlock_storage_fn()
  72. cal = vobject.iCalendar()
  73. collection_tag = collection.tag
  74. while retrieved_items:
  75. # Second filtering before evaluating occurrences.
  76. # ``item.vobject_item`` might be accessed during filtering.
  77. # Don't keep reference to ``item``, because VObject requires a lot of
  78. # memory.
  79. item, filter_matched = retrieved_items.pop(0)
  80. if not filter_matched:
  81. try:
  82. if not test_filter(collection_tag, item, filter_element):
  83. continue
  84. except ValueError as e:
  85. raise ValueError("Failed to free-busy filter item %r from %r: %s" %
  86. (item.href, collection.path, e)) from e
  87. except Exception as e:
  88. raise RuntimeError("Failed to free-busy filter item %r from %r: %s" %
  89. (item.href, collection.path, e)) from e
  90. fbtype = None
  91. if item.component_name == 'VEVENT':
  92. transp = getattr(item.vobject_item.vevent, 'transp', None)
  93. if transp and transp.value != 'OPAQUE':
  94. continue
  95. status = getattr(item.vobject_item.vevent, 'status', None)
  96. if not status or status.value == 'CONFIRMED':
  97. fbtype = 'BUSY'
  98. elif status.value == 'CANCELLED':
  99. fbtype = 'FREE'
  100. elif status.value == 'TENTATIVE':
  101. fbtype = 'BUSY-TENTATIVE'
  102. else:
  103. # Could do fbtype = status.value for x-name, I prefer this
  104. fbtype = 'BUSY'
  105. # TODO: coalesce overlapping periods
  106. if max_occurrence > 0:
  107. n_occurrences = max_occurrence+1
  108. else:
  109. n_occurrences = 0
  110. occurrences = radicale_filter.time_range_fill(item.vobject_item,
  111. time_range_element,
  112. "VEVENT",
  113. n=n_occurrences)
  114. if len(occurrences) >= max_occurrence:
  115. raise ValueError("FREEBUSY occurrences limit of {} hit"
  116. .format(max_occurrence))
  117. for occurrence in occurrences:
  118. vfb = cal.add('vfreebusy')
  119. vfb.add('dtstamp').value = item.vobject_item.vevent.dtstamp.value
  120. vfb.add('dtstart').value, vfb.add('dtend').value = occurrence
  121. if fbtype:
  122. vfb.add('fbtype').value = fbtype
  123. return (client.OK, cal.serialize())
  124. def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element],
  125. collection: storage.BaseCollection, encoding: str,
  126. unlock_storage_fn: Callable[[], None]
  127. ) -> Tuple[int, ET.Element]:
  128. """Read and answer REPORT requests that return XML.
  129. Read rfc3253-3.6 for info.
  130. """
  131. multistatus = ET.Element(xmlutils.make_clark("D:multistatus"))
  132. if xml_request is None:
  133. return client.MULTI_STATUS, multistatus
  134. root = xml_request
  135. if root.tag in (xmlutils.make_clark("D:principal-search-property-set"),
  136. xmlutils.make_clark("D:principal-property-search"),
  137. xmlutils.make_clark("D:expand-property")):
  138. # We don't support searching for principals or indirect retrieving of
  139. # properties, just return an empty result.
  140. # InfCloud asks for expand-property reports (even if we don't announce
  141. # support for them) and stops working if an error code is returned.
  142. logger.warning("Unsupported REPORT method %r on %r requested",
  143. xmlutils.make_human_tag(root.tag), path)
  144. return client.MULTI_STATUS, multistatus
  145. if (root.tag == xmlutils.make_clark("C:calendar-multiget") and
  146. collection.tag != "VCALENDAR" or
  147. root.tag == xmlutils.make_clark("CR:addressbook-multiget") and
  148. collection.tag != "VADDRESSBOOK" or
  149. root.tag == xmlutils.make_clark("D:sync-collection") and
  150. collection.tag not in ("VADDRESSBOOK", "VCALENDAR")):
  151. logger.warning("Invalid REPORT method %r on %r requested",
  152. xmlutils.make_human_tag(root.tag), path)
  153. return client.FORBIDDEN, xmlutils.webdav_error("D:supported-report")
  154. props: Union[ET.Element, List] = root.find(xmlutils.make_clark("D:prop")) or []
  155. hreferences: Iterable[str]
  156. if root.tag in (
  157. xmlutils.make_clark("C:calendar-multiget"),
  158. xmlutils.make_clark("CR:addressbook-multiget")):
  159. # Read rfc4791-7.9 for info
  160. hreferences = set()
  161. for href_element in root.findall(xmlutils.make_clark("D:href")):
  162. temp_url_path = urlparse(href_element.text).path
  163. assert isinstance(temp_url_path, str)
  164. href_path = pathutils.sanitize_path(unquote(temp_url_path))
  165. if (href_path + "/").startswith(base_prefix + "/"):
  166. hreferences.add(href_path[len(base_prefix):])
  167. else:
  168. logger.warning("Skipping invalid path %r in REPORT request on "
  169. "%r", href_path, path)
  170. elif root.tag == xmlutils.make_clark("D:sync-collection"):
  171. old_sync_token_element = root.find(
  172. xmlutils.make_clark("D:sync-token"))
  173. old_sync_token = ""
  174. if old_sync_token_element is not None and old_sync_token_element.text:
  175. old_sync_token = old_sync_token_element.text.strip()
  176. logger.debug("Client provided sync token: %r", old_sync_token)
  177. try:
  178. sync_token, names = collection.sync(old_sync_token)
  179. except ValueError as e:
  180. # Invalid sync token
  181. logger.warning("Client provided invalid sync token %r: %s",
  182. old_sync_token, e, exc_info=True)
  183. # client.CONFLICT doesn't work with some clients (e.g. InfCloud)
  184. return (client.FORBIDDEN,
  185. xmlutils.webdav_error("D:valid-sync-token"))
  186. hreferences = (pathutils.unstrip_path(
  187. posixpath.join(collection.path, n)) for n in names)
  188. # Append current sync token to response
  189. sync_token_element = ET.Element(xmlutils.make_clark("D:sync-token"))
  190. sync_token_element.text = sync_token
  191. multistatus.append(sync_token_element)
  192. else:
  193. hreferences = (path,)
  194. filters = (
  195. root.findall(xmlutils.make_clark("C:filter")) +
  196. root.findall(xmlutils.make_clark("CR:filter")))
  197. # Retrieve everything required for finishing the request.
  198. retrieved_items = list(retrieve_items(
  199. base_prefix, path, collection, hreferences, filters, multistatus))
  200. collection_tag = collection.tag
  201. # !!! Don't access storage after this !!!
  202. unlock_storage_fn()
  203. while retrieved_items:
  204. # ``item.vobject_item`` might be accessed during filtering.
  205. # Don't keep reference to ``item``, because VObject requires a lot of
  206. # memory.
  207. item, filters_matched = retrieved_items.pop(0)
  208. if filters and not filters_matched:
  209. try:
  210. if not all(test_filter(collection_tag, item, filter_)
  211. for filter_ in filters):
  212. continue
  213. except ValueError as e:
  214. raise ValueError("Failed to filter item %r from %r: %s" %
  215. (item.href, collection.path, e)) from e
  216. except Exception as e:
  217. raise RuntimeError("Failed to filter item %r from %r: %s" %
  218. (item.href, collection.path, e)) from e
  219. found_props = []
  220. not_found_props = []
  221. for prop in props:
  222. element = ET.Element(prop.tag)
  223. if prop.tag == xmlutils.make_clark("D:getetag"):
  224. element.text = item.etag
  225. found_props.append(element)
  226. elif prop.tag == xmlutils.make_clark("D:getcontenttype"):
  227. element.text = xmlutils.get_content_type(item, encoding)
  228. found_props.append(element)
  229. elif prop.tag in (
  230. xmlutils.make_clark("C:calendar-data"),
  231. xmlutils.make_clark("CR:address-data")):
  232. element.text = item.serialize()
  233. expand = prop.find(xmlutils.make_clark("C:expand"))
  234. if expand is not None and item.component_name == 'VEVENT':
  235. start = expand.get('start')
  236. end = expand.get('end')
  237. if (start is None) or (end is None):
  238. return client.FORBIDDEN, \
  239. xmlutils.webdav_error("C:expand")
  240. start = datetime.datetime.strptime(
  241. start, '%Y%m%dT%H%M%SZ'
  242. ).replace(tzinfo=datetime.timezone.utc)
  243. end = datetime.datetime.strptime(
  244. end, '%Y%m%dT%H%M%SZ'
  245. ).replace(tzinfo=datetime.timezone.utc)
  246. expanded_element = _expand(
  247. element, copy.copy(item), start, end)
  248. found_props.append(expanded_element)
  249. else:
  250. found_props.append(element)
  251. else:
  252. not_found_props.append(element)
  253. assert item.href
  254. uri = pathutils.unstrip_path(
  255. posixpath.join(collection.path, item.href))
  256. multistatus.append(xml_item_response(
  257. base_prefix, uri, found_props=found_props,
  258. not_found_props=not_found_props, found_item=True))
  259. return client.MULTI_STATUS, multistatus
  260. def _expand(
  261. element: ET.Element,
  262. item: radicale_item.Item,
  263. start: datetime.datetime,
  264. end: datetime.datetime,
  265. ) -> ET.Element:
  266. dt_format = '%Y%m%dT%H%M%SZ'
  267. if type(item.vobject_item.vevent.dtstart.value) is datetime.date:
  268. # If an event comes to us with a dt_start specified as a date
  269. # then in the response we return the date, not datetime
  270. dt_format = '%Y%m%d'
  271. duration = None
  272. if hasattr(item.vobject_item.vevent, "dtend"):
  273. duration = item.vobject_item.vevent.dtend.value - item.vobject_item.vevent.dtstart.value
  274. expanded_item, rruleset = _make_vobject_expanded_item(item, dt_format)
  275. if rruleset:
  276. recurrences = rruleset.between(start, end, inc=True)
  277. expanded: vobject.base.Component = copy.copy(expanded_item.vobject_item)
  278. is_expanded_filled: bool = False
  279. for recurrence_dt in recurrences:
  280. recurrence_utc = recurrence_dt.astimezone(datetime.timezone.utc)
  281. vevent = copy.deepcopy(expanded.vevent)
  282. vevent.recurrence_id = ContentLine(
  283. name='RECURRENCE-ID',
  284. value=recurrence_utc.strftime(dt_format), params={}
  285. )
  286. vevent.dtstart = ContentLine(
  287. name='DTSTART',
  288. value=recurrence_utc.strftime(dt_format), params={}
  289. )
  290. if duration:
  291. vevent.dtend = ContentLine(
  292. name='DTEND',
  293. value=(recurrence_utc + duration).strftime(dt_format), params={}
  294. )
  295. if is_expanded_filled is False:
  296. expanded.vevent = vevent
  297. is_expanded_filled = True
  298. else:
  299. expanded.add(vevent)
  300. element.text = expanded.serialize()
  301. else:
  302. element.text = expanded_item.vobject_item.serialize()
  303. return element
  304. def _make_vobject_expanded_item(
  305. item: radicale_item.Item,
  306. dt_format: str,
  307. ) -> Tuple[radicale_item.Item, Optional[Any]]:
  308. # https://www.rfc-editor.org/rfc/rfc4791#section-9.6.5
  309. # The returned calendar components MUST NOT use recurrence
  310. # properties (i.e., EXDATE, EXRULE, RDATE, and RRULE) and MUST NOT
  311. # have reference to or include VTIMEZONE components. Date and local
  312. # time with reference to time zone information MUST be converted
  313. # into date with UTC time.
  314. item = copy.copy(item)
  315. vevent = item.vobject_item.vevent
  316. if type(vevent.dtstart.value) is datetime.date:
  317. start_utc = datetime.datetime.fromordinal(
  318. vevent.dtstart.value.toordinal()
  319. ).replace(tzinfo=datetime.timezone.utc)
  320. else:
  321. start_utc = vevent.dtstart.value.astimezone(datetime.timezone.utc)
  322. vevent.dtstart = ContentLine(name='DTSTART', value=start_utc, params=[])
  323. dt_end = getattr(vevent, 'dtend', None)
  324. if dt_end is not None:
  325. if type(vevent.dtend.value) is datetime.date:
  326. end_utc = datetime.datetime.fromordinal(
  327. dt_end.value.toordinal()
  328. ).replace(tzinfo=datetime.timezone.utc)
  329. else:
  330. end_utc = dt_end.value.astimezone(datetime.timezone.utc)
  331. vevent.dtend = ContentLine(name='DTEND', value=end_utc, params={})
  332. rruleset = None
  333. if hasattr(item.vobject_item.vevent, 'rrule'):
  334. rruleset = vevent.getrruleset()
  335. # There is something strange behaviour during serialization native datetime, so converting manually
  336. vevent.dtstart.value = vevent.dtstart.value.strftime(dt_format)
  337. if dt_end is not None:
  338. vevent.dtend.value = vevent.dtend.value.strftime(dt_format)
  339. timezones_to_remove = []
  340. for component in item.vobject_item.components():
  341. if component.name == 'VTIMEZONE':
  342. timezones_to_remove.append(component)
  343. for timezone in timezones_to_remove:
  344. item.vobject_item.remove(timezone)
  345. try:
  346. delattr(item.vobject_item.vevent, 'rrule')
  347. delattr(item.vobject_item.vevent, 'exdate')
  348. delattr(item.vobject_item.vevent, 'exrule')
  349. delattr(item.vobject_item.vevent, 'rdate')
  350. except AttributeError:
  351. pass
  352. return item, rruleset
  353. def xml_item_response(base_prefix: str, href: str,
  354. found_props: Sequence[ET.Element] = (),
  355. not_found_props: Sequence[ET.Element] = (),
  356. found_item: bool = True) -> ET.Element:
  357. response = ET.Element(xmlutils.make_clark("D:response"))
  358. href_element = ET.Element(xmlutils.make_clark("D:href"))
  359. href_element.text = xmlutils.make_href(base_prefix, href)
  360. response.append(href_element)
  361. if found_item:
  362. for code, props in ((200, found_props), (404, not_found_props)):
  363. if props:
  364. propstat = ET.Element(xmlutils.make_clark("D:propstat"))
  365. status = ET.Element(xmlutils.make_clark("D:status"))
  366. status.text = xmlutils.make_response(code)
  367. prop_element = ET.Element(xmlutils.make_clark("D:prop"))
  368. for prop in props:
  369. prop_element.append(prop)
  370. propstat.append(prop_element)
  371. propstat.append(status)
  372. response.append(propstat)
  373. else:
  374. status = ET.Element(xmlutils.make_clark("D:status"))
  375. status.text = xmlutils.make_response(404)
  376. response.append(status)
  377. return response
  378. def retrieve_items(
  379. base_prefix: str, path: str, collection: storage.BaseCollection,
  380. hreferences: Iterable[str], filters: Sequence[ET.Element],
  381. multistatus: ET.Element) -> Iterator[Tuple[radicale_item.Item, bool]]:
  382. """Retrieves all items that are referenced in ``hreferences`` from
  383. ``collection`` and adds 404 responses for missing and invalid items
  384. to ``multistatus``."""
  385. collection_requested = False
  386. def get_names() -> Iterator[str]:
  387. """Extracts all names from references in ``hreferences`` and adds
  388. 404 responses for invalid references to ``multistatus``.
  389. If the whole collections is referenced ``collection_requested``
  390. gets set to ``True``."""
  391. nonlocal collection_requested
  392. for hreference in hreferences:
  393. try:
  394. name = pathutils.name_from_path(hreference, collection)
  395. except ValueError as e:
  396. logger.warning("Skipping invalid path %r in REPORT request on "
  397. "%r: %s", hreference, path, e)
  398. response = xml_item_response(base_prefix, hreference,
  399. found_item=False)
  400. multistatus.append(response)
  401. continue
  402. if name:
  403. # Reference is an item
  404. yield name
  405. else:
  406. # Reference is a collection
  407. collection_requested = True
  408. for name, item in collection.get_multi(get_names()):
  409. if not item:
  410. uri = pathutils.unstrip_path(posixpath.join(collection.path, name))
  411. response = xml_item_response(base_prefix, uri, found_item=False)
  412. multistatus.append(response)
  413. else:
  414. yield item, False
  415. if collection_requested:
  416. yield from collection.get_filtered(filters)
  417. def test_filter(collection_tag: str, item: radicale_item.Item,
  418. filter_: ET.Element) -> bool:
  419. """Match an item against a filter."""
  420. if (collection_tag == "VCALENDAR" and
  421. filter_.tag != xmlutils.make_clark("C:%s" % filter_)):
  422. if len(filter_) == 0:
  423. return True
  424. if len(filter_) > 1:
  425. raise ValueError("Filter with %d children" % len(filter_))
  426. if filter_[0].tag != xmlutils.make_clark("C:comp-filter"):
  427. raise ValueError("Unexpected %r in filter" % filter_[0].tag)
  428. return radicale_filter.comp_match(item, filter_[0])
  429. if (collection_tag == "VADDRESSBOOK" and
  430. filter_.tag != xmlutils.make_clark("CR:%s" % filter_)):
  431. for child in filter_:
  432. if child.tag != xmlutils.make_clark("CR:prop-filter"):
  433. raise ValueError("Unexpected %r in filter" % child.tag)
  434. test = filter_.get("test", "anyof")
  435. if test == "anyof":
  436. return any(radicale_filter.prop_match(item.vobject_item, f, "CR")
  437. for f in filter_)
  438. if test == "allof":
  439. return all(radicale_filter.prop_match(item.vobject_item, f, "CR")
  440. for f in filter_)
  441. raise ValueError("Unsupported filter test: %r" % test)
  442. raise ValueError("Unsupported filter %r for %r" %
  443. (filter_.tag, collection_tag))
  444. class ApplicationPartReport(ApplicationBase):
  445. def do_REPORT(self, environ: types.WSGIEnviron, base_prefix: str,
  446. path: str, user: str) -> types.WSGIResponse:
  447. """Manage REPORT request."""
  448. access = Access(self._rights, user, path)
  449. if not access.check("r"):
  450. return httputils.NOT_ALLOWED
  451. try:
  452. xml_content = self._read_xml_request_body(environ)
  453. except RuntimeError as e:
  454. logger.warning("Bad REPORT request on %r: %s", path, e,
  455. exc_info=True)
  456. return httputils.BAD_REQUEST
  457. except socket.timeout:
  458. logger.debug("Client timed out", exc_info=True)
  459. return httputils.REQUEST_TIMEOUT
  460. with contextlib.ExitStack() as lock_stack:
  461. lock_stack.enter_context(self._storage.acquire_lock("r", user))
  462. item = next(iter(self._storage.discover(path)), None)
  463. if not item:
  464. return httputils.NOT_FOUND
  465. if not access.check("r", item):
  466. return httputils.NOT_ALLOWED
  467. if isinstance(item, storage.BaseCollection):
  468. collection = item
  469. else:
  470. assert item.collection is not None
  471. collection = item.collection
  472. if xml_content is not None and \
  473. xml_content.tag == xmlutils.make_clark("C:free-busy-query"):
  474. max_occurrence = self.configuration.get("reporting", "max_freebusy_occurrence")
  475. try:
  476. status, body = free_busy_report(
  477. base_prefix, path, xml_content, collection, self._encoding,
  478. lock_stack.close, max_occurrence)
  479. except ValueError as e:
  480. logger.warning(
  481. "Bad REPORT request on %r: %s", path, e, exc_info=True)
  482. return httputils.BAD_REQUEST
  483. headers = {"Content-Type": "text/calendar; charset=%s" % self._encoding}
  484. return status, headers, str(body)
  485. else:
  486. try:
  487. status, xml_answer = xml_report(
  488. base_prefix, path, xml_content, collection, self._encoding,
  489. lock_stack.close)
  490. except ValueError as e:
  491. logger.warning(
  492. "Bad REPORT request on %r: %s", path, e, exc_info=True)
  493. return httputils.BAD_REQUEST
  494. headers = {"Content-Type": "text/xml; charset=%s" % self._encoding}
  495. return status, headers, self._xml_response(xml_answer)