propfind.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. # This file is part of Radicale Server - Calendar 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. """
  20. Radicale WSGI application.
  21. Can be used with an external WSGI server or the built-in server.
  22. """
  23. import itertools
  24. import posixpath
  25. import socket
  26. from http import client
  27. from xml.etree import ElementTree as ET
  28. from radicale import httputils, pathutils, rights, storage, xmlutils
  29. from radicale.log import logger
  30. def xml_propfind(base_prefix, path, xml_request, allowed_items, user):
  31. """Read and answer PROPFIND requests.
  32. Read rfc4918-9.1 for info.
  33. The collections parameter is a list of collections that are to be included
  34. in the output.
  35. """
  36. # A client may choose not to submit a request body. An empty PROPFIND
  37. # request body MUST be treated as if it were an 'allprop' request.
  38. top_tag = (xml_request[0] if xml_request is not None else
  39. ET.Element(xmlutils.make_tag("D", "allprop")))
  40. props = ()
  41. allprop = False
  42. propname = False
  43. if top_tag.tag == xmlutils.make_tag("D", "allprop"):
  44. allprop = True
  45. elif top_tag.tag == xmlutils.make_tag("D", "propname"):
  46. propname = True
  47. elif top_tag.tag == xmlutils.make_tag("D", "prop"):
  48. props = [prop.tag for prop in top_tag]
  49. if xmlutils.make_tag("D", "current-user-principal") in props and not user:
  50. # Ask for authentication
  51. # Returning the DAV:unauthenticated pseudo-principal as specified in
  52. # RFC 5397 doesn't seem to work with DAVdroid.
  53. return client.FORBIDDEN, None
  54. # Writing answer
  55. multistatus = ET.Element(xmlutils.make_tag("D", "multistatus"))
  56. for item, permission in allowed_items:
  57. write = permission == "w"
  58. response = xml_propfind_response(
  59. base_prefix, path, item, props, user, write=write,
  60. allprop=allprop, propname=propname)
  61. if response:
  62. multistatus.append(response)
  63. return client.MULTI_STATUS, multistatus
  64. def xml_propfind_response(base_prefix, path, item, props, user, write=False,
  65. propname=False, allprop=False):
  66. """Build and return a PROPFIND response."""
  67. if propname and allprop or (props and (propname or allprop)):
  68. raise ValueError("Only use one of props, propname and allprops")
  69. is_collection = isinstance(item, storage.BaseCollection)
  70. if is_collection:
  71. is_leaf = item.get_meta("tag") in ("VADDRESSBOOK", "VCALENDAR")
  72. collection = item
  73. else:
  74. collection = item.collection
  75. response = ET.Element(xmlutils.make_tag("D", "response"))
  76. href = ET.Element(xmlutils.make_tag("D", "href"))
  77. if is_collection:
  78. # Some clients expect collections to end with /
  79. uri = "/%s/" % item.path if item.path else "/"
  80. else:
  81. uri = "/" + posixpath.join(collection.path, item.href)
  82. href.text = xmlutils.make_href(base_prefix, uri)
  83. response.append(href)
  84. propstat404 = ET.Element(xmlutils.make_tag("D", "propstat"))
  85. propstat200 = ET.Element(xmlutils.make_tag("D", "propstat"))
  86. response.append(propstat200)
  87. prop200 = ET.Element(xmlutils.make_tag("D", "prop"))
  88. propstat200.append(prop200)
  89. prop404 = ET.Element(xmlutils.make_tag("D", "prop"))
  90. propstat404.append(prop404)
  91. if propname or allprop:
  92. props = []
  93. # Should list all properties that can be retrieved by the code below
  94. props.append(xmlutils.make_tag("D", "principal-collection-set"))
  95. props.append(xmlutils.make_tag("D", "current-user-principal"))
  96. props.append(xmlutils.make_tag("D", "current-user-privilege-set"))
  97. props.append(xmlutils.make_tag("D", "supported-report-set"))
  98. props.append(xmlutils.make_tag("D", "resourcetype"))
  99. props.append(xmlutils.make_tag("D", "owner"))
  100. if is_collection and collection.is_principal:
  101. props.append(xmlutils.make_tag("C", "calendar-user-address-set"))
  102. props.append(xmlutils.make_tag("D", "principal-URL"))
  103. props.append(xmlutils.make_tag("CR", "addressbook-home-set"))
  104. props.append(xmlutils.make_tag("C", "calendar-home-set"))
  105. if not is_collection or is_leaf:
  106. props.append(xmlutils.make_tag("D", "getetag"))
  107. props.append(xmlutils.make_tag("D", "getlastmodified"))
  108. props.append(xmlutils.make_tag("D", "getcontenttype"))
  109. props.append(xmlutils.make_tag("D", "getcontentlength"))
  110. if is_collection:
  111. if is_leaf:
  112. props.append(xmlutils.make_tag("D", "displayname"))
  113. props.append(xmlutils.make_tag("D", "sync-token"))
  114. if collection.get_meta("tag") == "VCALENDAR":
  115. props.append(xmlutils.make_tag("CS", "getctag"))
  116. props.append(
  117. xmlutils.make_tag("C", "supported-calendar-component-set"))
  118. meta = item.get_meta()
  119. for tag in meta:
  120. if tag == "tag":
  121. continue
  122. clark_tag = xmlutils.tag_from_human(tag)
  123. if clark_tag not in props:
  124. props.append(clark_tag)
  125. if propname:
  126. for tag in props:
  127. prop200.append(ET.Element(tag))
  128. props = ()
  129. for tag in props:
  130. element = ET.Element(tag)
  131. is404 = False
  132. if tag == xmlutils.make_tag("D", "getetag"):
  133. if not is_collection or is_leaf:
  134. element.text = item.etag
  135. else:
  136. is404 = True
  137. elif tag == xmlutils.make_tag("D", "getlastmodified"):
  138. if not is_collection or is_leaf:
  139. element.text = item.last_modified
  140. else:
  141. is404 = True
  142. elif tag == xmlutils.make_tag("D", "principal-collection-set"):
  143. tag = ET.Element(xmlutils.make_tag("D", "href"))
  144. tag.text = xmlutils.make_href(base_prefix, "/")
  145. element.append(tag)
  146. elif (tag in (xmlutils.make_tag("C", "calendar-user-address-set"),
  147. xmlutils.make_tag("D", "principal-URL"),
  148. xmlutils.make_tag("CR", "addressbook-home-set"),
  149. xmlutils.make_tag("C", "calendar-home-set")) and
  150. collection.is_principal and is_collection):
  151. tag = ET.Element(xmlutils.make_tag("D", "href"))
  152. tag.text = xmlutils.make_href(base_prefix, path)
  153. element.append(tag)
  154. elif tag == xmlutils.make_tag("C", "supported-calendar-component-set"):
  155. human_tag = xmlutils.tag_from_clark(tag)
  156. if is_collection and is_leaf:
  157. meta = item.get_meta(human_tag)
  158. if meta:
  159. components = meta.split(",")
  160. else:
  161. components = ("VTODO", "VEVENT", "VJOURNAL")
  162. for component in components:
  163. comp = ET.Element(xmlutils.make_tag("C", "comp"))
  164. comp.set("name", component)
  165. element.append(comp)
  166. else:
  167. is404 = True
  168. elif tag == xmlutils.make_tag("D", "current-user-principal"):
  169. if user:
  170. tag = ET.Element(xmlutils.make_tag("D", "href"))
  171. tag.text = xmlutils.make_href(base_prefix, "/%s/" % user)
  172. element.append(tag)
  173. else:
  174. element.append(ET.Element(
  175. xmlutils.make_tag("D", "unauthenticated")))
  176. elif tag == xmlutils.make_tag("D", "current-user-privilege-set"):
  177. privileges = [("D", "read")]
  178. if write:
  179. privileges.append(("D", "all"))
  180. privileges.append(("D", "write"))
  181. privileges.append(("D", "write-properties"))
  182. privileges.append(("D", "write-content"))
  183. for ns, privilege_name in privileges:
  184. privilege = ET.Element(xmlutils.make_tag("D", "privilege"))
  185. privilege.append(ET.Element(
  186. xmlutils.make_tag(ns, privilege_name)))
  187. element.append(privilege)
  188. elif tag == xmlutils.make_tag("D", "supported-report-set"):
  189. # These 3 reports are not implemented
  190. reports = [
  191. ("D", "expand-property"),
  192. ("D", "principal-search-property-set"),
  193. ("D", "principal-property-search")]
  194. if is_collection and is_leaf:
  195. reports.append(("D", "sync-collection"))
  196. if item.get_meta("tag") == "VADDRESSBOOK":
  197. reports.append(("CR", "addressbook-multiget"))
  198. reports.append(("CR", "addressbook-query"))
  199. elif item.get_meta("tag") == "VCALENDAR":
  200. reports.append(("C", "calendar-multiget"))
  201. reports.append(("C", "calendar-query"))
  202. for ns, report_name in reports:
  203. supported = ET.Element(
  204. xmlutils.make_tag("D", "supported-report"))
  205. report_tag = ET.Element(xmlutils.make_tag("D", "report"))
  206. supported_report_tag = ET.Element(
  207. xmlutils.make_tag(ns, report_name))
  208. report_tag.append(supported_report_tag)
  209. supported.append(report_tag)
  210. element.append(supported)
  211. elif tag == xmlutils.make_tag("D", "getcontentlength"):
  212. if not is_collection or is_leaf:
  213. encoding = collection.configuration.get("encoding", "request")
  214. element.text = str(len(item.serialize().encode(encoding)))
  215. else:
  216. is404 = True
  217. elif tag == xmlutils.make_tag("D", "owner"):
  218. # return empty elment, if no owner available (rfc3744-5.1)
  219. if collection.owner:
  220. tag = ET.Element(xmlutils.make_tag("D", "href"))
  221. tag.text = xmlutils.make_href(
  222. base_prefix, "/%s/" % collection.owner)
  223. element.append(tag)
  224. elif is_collection:
  225. if tag == xmlutils.make_tag("D", "getcontenttype"):
  226. if is_leaf:
  227. element.text = xmlutils.MIMETYPES[item.get_meta("tag")]
  228. else:
  229. is404 = True
  230. elif tag == xmlutils.make_tag("D", "resourcetype"):
  231. if item.is_principal:
  232. tag = ET.Element(xmlutils.make_tag("D", "principal"))
  233. element.append(tag)
  234. if is_leaf:
  235. if item.get_meta("tag") == "VADDRESSBOOK":
  236. tag = ET.Element(
  237. xmlutils.make_tag("CR", "addressbook"))
  238. element.append(tag)
  239. elif item.get_meta("tag") == "VCALENDAR":
  240. tag = ET.Element(xmlutils.make_tag("C", "calendar"))
  241. element.append(tag)
  242. tag = ET.Element(xmlutils.make_tag("D", "collection"))
  243. element.append(tag)
  244. elif tag == xmlutils.make_tag("RADICALE", "displayname"):
  245. # Only for internal use by the web interface
  246. displayname = item.get_meta("D:displayname")
  247. if displayname is not None:
  248. element.text = displayname
  249. else:
  250. is404 = True
  251. elif tag == xmlutils.make_tag("D", "displayname"):
  252. displayname = item.get_meta("D:displayname")
  253. if not displayname and is_leaf:
  254. displayname = item.path
  255. if displayname is not None:
  256. element.text = displayname
  257. else:
  258. is404 = True
  259. elif tag == xmlutils.make_tag("CS", "getctag"):
  260. if is_leaf:
  261. element.text = item.etag
  262. else:
  263. is404 = True
  264. elif tag == xmlutils.make_tag("D", "sync-token"):
  265. if is_leaf:
  266. element.text, _ = item.sync()
  267. else:
  268. is404 = True
  269. else:
  270. human_tag = xmlutils.tag_from_clark(tag)
  271. meta = item.get_meta(human_tag)
  272. if meta is not None:
  273. element.text = meta
  274. else:
  275. is404 = True
  276. # Not for collections
  277. elif tag == xmlutils.make_tag("D", "getcontenttype"):
  278. element.text = xmlutils.get_content_type(item)
  279. elif tag == xmlutils.make_tag("D", "resourcetype"):
  280. # resourcetype must be returned empty for non-collection elements
  281. pass
  282. else:
  283. is404 = True
  284. if is404:
  285. prop404.append(element)
  286. else:
  287. prop200.append(element)
  288. status200 = ET.Element(xmlutils.make_tag("D", "status"))
  289. status200.text = xmlutils.make_response(200)
  290. propstat200.append(status200)
  291. status404 = ET.Element(xmlutils.make_tag("D", "status"))
  292. status404.text = xmlutils.make_response(404)
  293. propstat404.append(status404)
  294. if len(prop404):
  295. response.append(propstat404)
  296. return response
  297. class ApplicationPropfindMixin:
  298. def _collect_allowed_items(self, items, user):
  299. """Get items from request that user is allowed to access."""
  300. for item in items:
  301. if isinstance(item, storage.BaseCollection):
  302. path = pathutils.sanitize_path("/%s/" % item.path)
  303. if item.get_meta("tag"):
  304. permissions = self.Rights.authorized(user, path, "rw")
  305. target = "collection with tag %r" % item.path
  306. else:
  307. permissions = self.Rights.authorized(user, path, "RW")
  308. target = "collection %r" % item.path
  309. else:
  310. path = pathutils.sanitize_path("/%s/" % item.collection.path)
  311. permissions = self.Rights.authorized(user, path, "rw")
  312. target = "item %r from %r" % (item.href, item.collection.path)
  313. if rights.intersect_permissions(permissions, "Ww"):
  314. permission = "w"
  315. status = "write"
  316. elif rights.intersect_permissions(permissions, "Rr"):
  317. permission = "r"
  318. status = "read"
  319. else:
  320. permission = ""
  321. status = "NO"
  322. logger.debug(
  323. "%s has %s access to %s",
  324. repr(user) if user else "anonymous user", status, target)
  325. if permission:
  326. yield item, permission
  327. def do_PROPFIND(self, environ, base_prefix, path, user):
  328. """Manage PROPFIND request."""
  329. if not self.access(user, path, "r"):
  330. return httputils.NOT_ALLOWED
  331. try:
  332. xml_content = self.read_xml_content(environ)
  333. except RuntimeError as e:
  334. logger.warning(
  335. "Bad PROPFIND request on %r: %s", path, e, exc_info=True)
  336. return httputils.BAD_REQUEST
  337. except socket.timeout as e:
  338. logger.debug("client timed out", exc_info=True)
  339. return httputils.REQUEST_TIMEOUT
  340. with self.Collection.acquire_lock("r", user):
  341. items = self.Collection.discover(
  342. path, environ.get("HTTP_DEPTH", "0"))
  343. # take root item for rights checking
  344. item = next(items, None)
  345. if not item:
  346. return httputils.NOT_FOUND
  347. if not self.access(user, path, "r", item):
  348. return httputils.NOT_ALLOWED
  349. # put item back
  350. items = itertools.chain([item], items)
  351. allowed_items = self._collect_allowed_items(items, user)
  352. headers = {"DAV": httputils.DAV_HEADERS,
  353. "Content-Type": "text/xml; charset=%s" % self.encoding}
  354. status, xml_answer = xml_propfind(
  355. base_prefix, path, xml_content, allowed_items, user)
  356. if status == client.FORBIDDEN:
  357. return httputils.NOT_ALLOWED
  358. return status, headers, self.write_xml_content(xml_answer)