__init__.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. # -*- coding: utf-8 -*-
  2. #
  3. # This file is part of Radicale Server - Calendar Server
  4. # Copyright © 2008-2011 Guillaume Ayoub
  5. # Copyright © 2008 Nicolas Kandel
  6. # Copyright © 2008 Pascal Halter
  7. #
  8. # This library is free software: you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation, either version 3 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This library is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License
  19. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  20. """
  21. Radicale Server module.
  22. This module offers a WSGI application class.
  23. To use this module, you should take a look at the file ``radicale.py`` that
  24. should have been included in this package.
  25. """
  26. import os
  27. import posixpath
  28. import base64
  29. import socket
  30. import wsgiref.simple_server
  31. # Manage Python2/3 different modules
  32. # pylint: disable=F0401
  33. try:
  34. from http import client, server
  35. except ImportError:
  36. import httplib as client
  37. import BaseHTTPServer as server
  38. # pylint: enable=F0401
  39. from radicale import acl, config, ical, log, xmlutils
  40. VERSION = "git"
  41. class HTTPServer(wsgiref.simple_server.WSGIServer, object):
  42. """HTTP server."""
  43. def __init__(self, address, handler, bind_and_activate=True):
  44. """Create server."""
  45. ipv6 = ":" in address[0]
  46. if ipv6:
  47. self.address_family = socket.AF_INET6
  48. # Do not bind and activate, as we might change socket options
  49. super(HTTPServer, self).__init__(address, handler, False)
  50. if ipv6:
  51. # Only allow IPv6 connections to the IPv6 socket
  52. self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
  53. if bind_and_activate:
  54. self.server_bind()
  55. self.server_activate()
  56. class HTTPSServer(HTTPServer):
  57. """HTTPS server."""
  58. def __init__(self, address, handler):
  59. """Create server by wrapping HTTP socket in an SSL socket."""
  60. # Fails with Python 2.5, import if needed
  61. # pylint: disable=F0401
  62. import ssl
  63. # pylint: enable=F0401
  64. super(HTTPSServer, self).__init__(address, handler, False)
  65. self.socket = ssl.wrap_socket(
  66. self.socket,
  67. server_side=True,
  68. certfile=config.get("server", "certificate"),
  69. keyfile=config.get("server", "key"),
  70. ssl_version=ssl.PROTOCOL_SSLv23)
  71. self.server_bind()
  72. self.server_activate()
  73. class Application(object):
  74. """WSGI application managing calendars."""
  75. def __init__(self):
  76. """Initialize application."""
  77. super(Application, self).__init__()
  78. self.acl = acl.load()
  79. self.encoding = config.get("encoding", "request")
  80. def decode(self, text, environ):
  81. """Try to magically decode ``text`` according to given ``environ``."""
  82. # List of charsets to try
  83. charsets = []
  84. # First append content charset given in the request
  85. content_type = environ.get("CONTENT_TYPE")
  86. if content_type and "charset=" in content_type:
  87. charsets.append(content_type.split("charset=")[1].strip())
  88. # Then append default Radicale charset
  89. charsets.append(self.encoding)
  90. # Then append various fallbacks
  91. charsets.append("utf-8")
  92. charsets.append("iso8859-1")
  93. # Try to decode
  94. for charset in charsets:
  95. try:
  96. return text.decode(charset)
  97. except UnicodeDecodeError:
  98. pass
  99. raise UnicodeDecodeError
  100. def __call__(self, environ, start_response):
  101. """Manage a request."""
  102. log.LOGGER.info("%s request at %s recieved" % (
  103. environ["REQUEST_METHOD"], environ["PATH_INFO"]))
  104. log.LOGGER.debug("Request headers:\n%s" % environ.items())
  105. # Get content
  106. content_length = int(environ.get("CONTENT_LENGTH") or 0)
  107. if content_length:
  108. content = self.decode(
  109. environ["wsgi.input"].read(content_length), environ)
  110. log.LOGGER.debug("Request content:\n%s" % content)
  111. else:
  112. content = None
  113. # Find calendar
  114. attributes = posixpath.normpath(
  115. environ["PATH_INFO"].strip("/")).split("/")
  116. if attributes:
  117. if attributes[-1].endswith(".ics"):
  118. attributes.pop()
  119. path = "/".join(attributes[:min(len(attributes), 2)])
  120. calendar = ical.Calendar(path)
  121. else:
  122. calendar = None
  123. # Get function corresponding to method
  124. function = getattr(self, environ["REQUEST_METHOD"].lower())
  125. # Check rights
  126. if not calendar or not self.acl:
  127. # No calendar or no acl, don't check rights
  128. status, headers, answer = function(environ, calendar, content)
  129. elif calendar.owner is None and config.getboolean("acl", "personal"):
  130. # No owner and personal calendars, don't check rights
  131. status, headers, answer = function(environ, calendar, content)
  132. else:
  133. # Ask authentication backend to check rights
  134. log.LOGGER.info(
  135. "Checking rights for calendar owned by %s" % calendar.owner)
  136. authorization = environ.get("HTTP_AUTHORIZATION", None)
  137. if authorization:
  138. auth = authorization.lstrip("Basic").strip().encode("ascii")
  139. user, password = self.decode(
  140. base64.b64decode(auth), environ).split(":")
  141. else:
  142. user = password = None
  143. if self.acl.has_right(calendar.owner, user, password):
  144. log.LOGGER.info("%s allowed" % calendar.owner)
  145. status, headers, answer = function(environ, calendar, content)
  146. else:
  147. log.LOGGER.info("%s refused" % calendar.owner)
  148. status = client.UNAUTHORIZED
  149. headers = {
  150. "WWW-Authenticate":
  151. "Basic realm=\"Radicale Server - Password Required\""}
  152. answer = None
  153. # Set content length
  154. if answer:
  155. log.LOGGER.debug("Response content:\n%s" % answer)
  156. headers["Content-Length"] = "%i" % len(answer)
  157. # Start response
  158. status = "%i %s" % (status, client.responses.get(status, ""))
  159. start_response(status, list(headers.items()))
  160. # Return response content
  161. return [answer] if answer else []
  162. # All these functions must have the same parameters, some are useless
  163. # pylint: disable=W0612,W0613,R0201
  164. def get(self, environ, calendar, content):
  165. """Manage GET request."""
  166. item_name = xmlutils.name_from_path(environ["PATH_INFO"], calendar)
  167. if item_name:
  168. # Get calendar item
  169. item = calendar.get_item(item_name)
  170. if item:
  171. items = calendar.timezones
  172. items.append(item)
  173. answer_text = ical.serialize(
  174. headers=calendar.headers, items=items)
  175. etag = item.etag
  176. else:
  177. return client.GONE, {}, None
  178. else:
  179. # Get whole calendar
  180. answer_text = calendar.text
  181. etag = calendar.etag
  182. headers = {
  183. "Content-Type": "text/calendar",
  184. "Last-Modified": calendar.last_modified,
  185. "ETag": etag}
  186. answer = answer_text.encode(self.encoding)
  187. return client.OK, headers, answer
  188. def head(self, environ, calendar, content):
  189. """Manage HEAD request."""
  190. status, headers, answer = self.get(environ, calendar, content)
  191. return status, headers, None
  192. def delete(self, environ, calendar, content):
  193. """Manage DELETE request."""
  194. item = calendar.get_item(
  195. xmlutils.name_from_path(environ["PATH_INFO"], calendar))
  196. if item and environ.get("HTTP_IF_MATCH", item.etag) == item.etag:
  197. # No ETag precondition or precondition verified, delete item
  198. answer = xmlutils.delete(environ["PATH_INFO"], calendar)
  199. status = client.NO_CONTENT
  200. else:
  201. # No item or ETag precondition not verified, do not delete item
  202. answer = None
  203. status = client.PRECONDITION_FAILED
  204. return status, {}, answer
  205. def mkcalendar(self, environ, calendar, content):
  206. """Manage MKCALENDAR request."""
  207. return client.CREATED, {}, None
  208. def options(self, environ, calendar, content):
  209. """Manage OPTIONS request."""
  210. headers = {
  211. "Allow": "DELETE, HEAD, GET, MKCALENDAR, " \
  212. "OPTIONS, PROPFIND, PROPPATCH, PUT, REPORT",
  213. "DAV": "1, calendar-access"}
  214. return client.OK, headers, None
  215. def propfind(self, environ, calendar, content):
  216. """Manage PROPFIND request."""
  217. headers = {
  218. "DAV": "1, calendar-access",
  219. "Content-Type": "text/xml"}
  220. answer = xmlutils.propfind(
  221. environ["PATH_INFO"], content, calendar,
  222. environ.get("HTTP_DEPTH", "infinity"))
  223. return client.MULTI_STATUS, headers, answer
  224. def proppatch(self, environ, calendar, content):
  225. """Manage PROPPATCH request."""
  226. xmlutils.proppatch(environ["PATH_INFO"], content, calendar)
  227. headers = {
  228. "DAV": "1, calendar-access",
  229. "Content-Type": "text/xml"}
  230. return client.MULTI_STATUS, headers, None
  231. def put(self, environ, calendar, content):
  232. """Manage PUT request."""
  233. headers = {}
  234. item_name = xmlutils.name_from_path(environ["PATH_INFO"], calendar)
  235. item = calendar.get_item(item_name)
  236. if (not item and not environ.get("HTTP_IF_MATCH")) or (
  237. item and environ.get("HTTP_IF_MATCH", item.etag) == item.etag):
  238. # PUT allowed in 3 cases
  239. # Case 1: No item and no ETag precondition: Add new item
  240. # Case 2: Item and ETag precondition verified: Modify item
  241. # Case 3: Item and no Etag precondition: Force modifying item
  242. xmlutils.put(environ["PATH_INFO"], content, calendar)
  243. status = client.CREATED
  244. headers["ETag"] = calendar.get_item(item_name).etag
  245. else:
  246. # PUT rejected in all other cases
  247. status = client.PRECONDITION_FAILED
  248. return status, headers, None
  249. def report(self, environ, calendar, content):
  250. """Manage REPORT request."""
  251. answer = xmlutils.report(environ["PATH_INFO"], content, calendar)
  252. return client.MULTI_STATUS, {}, answer
  253. # pylint: enable=W0612,W0613,R0201