__init__.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  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 pprint
  28. import base64
  29. import socket
  30. import ssl
  31. import wsgiref.simple_server
  32. # Manage Python2/3 different modules
  33. # pylint: disable=F0401
  34. try:
  35. from http import client, server
  36. except ImportError:
  37. import httplib as client
  38. import BaseHTTPServer as server
  39. # pylint: enable=F0401
  40. from radicale import acl, config, ical, log, xmlutils
  41. VERSION = "git"
  42. class HTTPServer(wsgiref.simple_server.WSGIServer, object):
  43. """HTTP server."""
  44. def __init__(self, address, handler, bind_and_activate=True):
  45. """Create server."""
  46. ipv6 = ":" in address[0]
  47. if ipv6:
  48. self.address_family = socket.AF_INET6
  49. # Do not bind and activate, as we might change socket options
  50. super(HTTPServer, self).__init__(address, handler, False)
  51. if ipv6:
  52. # Only allow IPv6 connections to the IPv6 socket
  53. self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
  54. if bind_and_activate:
  55. self.server_bind()
  56. self.server_activate()
  57. class HTTPSServer(HTTPServer):
  58. """HTTPS server."""
  59. def __init__(self, address, handler):
  60. """Create server by wrapping HTTP socket in an SSL socket."""
  61. super(HTTPSServer, self).__init__(address, handler, False)
  62. self.socket = ssl.wrap_socket(
  63. self.socket,
  64. server_side=True,
  65. certfile=config.get("server", "certificate"),
  66. keyfile=config.get("server", "key"),
  67. ssl_version=ssl.PROTOCOL_SSLv23)
  68. self.server_bind()
  69. self.server_activate()
  70. class RequestHandler(wsgiref.simple_server.WSGIRequestHandler):
  71. """HTTP requests handler."""
  72. def log_message(self, *args, **kwargs):
  73. """Disable inner logging management."""
  74. class Application(object):
  75. """WSGI application managing calendars."""
  76. def __init__(self):
  77. """Initialize application."""
  78. super(Application, self).__init__()
  79. self.acl = acl.load()
  80. self.encoding = config.get("encoding", "request")
  81. if config.getboolean('logging', 'full_environment'):
  82. self.headers_log = lambda environ: environ
  83. # This method is overriden in __init__ if full_environment is set
  84. # pylint: disable=E0202
  85. @staticmethod
  86. def headers_log(environ):
  87. """Remove environment variables from the headers for logging purpose."""
  88. request_environ = dict(environ)
  89. for shell_variable in os.environ:
  90. del request_environ[shell_variable]
  91. return request_environ
  92. # pylint: enable=E0202
  93. def decode(self, text, environ):
  94. """Try to magically decode ``text`` according to given ``environ``."""
  95. # List of charsets to try
  96. charsets = []
  97. # First append content charset given in the request
  98. content_type = environ.get("CONTENT_TYPE")
  99. if content_type and "charset=" in content_type:
  100. charsets.append(content_type.split("charset=")[1].strip())
  101. # Then append default Radicale charset
  102. charsets.append(self.encoding)
  103. # Then append various fallbacks
  104. charsets.append("utf-8")
  105. charsets.append("iso8859-1")
  106. # Try to decode
  107. for charset in charsets:
  108. try:
  109. return text.decode(charset)
  110. except UnicodeDecodeError:
  111. pass
  112. raise UnicodeDecodeError
  113. def __call__(self, environ, start_response):
  114. """Manage a request."""
  115. log.LOGGER.info("%s request at %s received" % (
  116. environ["REQUEST_METHOD"], environ["PATH_INFO"]))
  117. headers = pprint.pformat(self.headers_log(environ))
  118. log.LOGGER.debug("Request headers:\n%s" % headers)
  119. # Get content
  120. content_length = int(environ.get("CONTENT_LENGTH") or 0)
  121. if content_length:
  122. content = self.decode(
  123. environ["wsgi.input"].read(content_length), environ)
  124. log.LOGGER.debug("Request content:\n%s" % content)
  125. else:
  126. content = None
  127. # Find calendar(s)
  128. items = ical.Calendar.from_path(environ["PATH_INFO"],
  129. environ.get("HTTP_DEPTH", "0"))
  130. # Get function corresponding to method
  131. function = getattr(self, environ["REQUEST_METHOD"].lower())
  132. # Check rights
  133. if not items or not self.acl:
  134. # No calendar or no acl, don't check rights
  135. status, headers, answer = function(environ, items, content)
  136. else:
  137. # Ask authentication backend to check rights
  138. authorization = environ.get("HTTP_AUTHORIZATION", None)
  139. if authorization:
  140. auth = authorization.lstrip("Basic").strip().encode("ascii")
  141. user, password = self.decode(
  142. base64.b64decode(auth), environ).split(":")
  143. environ['USER'] = user
  144. else:
  145. user = password = None
  146. last_allowed = False
  147. calendars = []
  148. for calendar in items:
  149. if not isinstance(calendar, ical.Calendar):
  150. if last_allowed:
  151. calendars.append(calendar)
  152. continue
  153. log.LOGGER.info(
  154. "Checking rights for calendar owned by %s" % calendar.owner)
  155. if self.acl.has_right(calendar.owner, user, password):
  156. log.LOGGER.info("%s allowed" % (user or "anonymous user"))
  157. calendars.append(calendar)
  158. last_allowed = True
  159. else:
  160. log.LOGGER.info("%s refused" % (user or "anonymous user"))
  161. last_allowed = False
  162. if calendars:
  163. status, headers, answer = function(environ, calendars, content)
  164. else:
  165. status = client.UNAUTHORIZED
  166. headers = {
  167. "WWW-Authenticate":
  168. "Basic realm=\"Radicale Server - Password Required\""}
  169. answer = None
  170. # Set content length
  171. if answer:
  172. log.LOGGER.debug(
  173. "Response content:\n%s" % self.decode(answer, environ))
  174. headers["Content-Length"] = str(len(answer))
  175. # Start response
  176. status = "%i %s" % (status, client.responses.get(status, ""))
  177. start_response(status, list(headers.items()))
  178. # Return response content
  179. return [answer] if answer else []
  180. # All these functions must have the same parameters, some are useless
  181. # pylint: disable=W0612,W0613,R0201
  182. def get(self, environ, calendars, content):
  183. """Manage GET request."""
  184. calendar = calendars[0]
  185. item_name = xmlutils.name_from_path(environ["PATH_INFO"], calendar)
  186. if item_name:
  187. # Get calendar item
  188. item = calendar.get_item(item_name)
  189. if item:
  190. items = calendar.timezones
  191. items.append(item)
  192. answer_text = ical.serialize(
  193. headers=calendar.headers, items=items)
  194. etag = item.etag
  195. else:
  196. return client.GONE, {}, None
  197. else:
  198. # Get whole calendar
  199. answer_text = calendar.text
  200. etag = calendar.etag
  201. headers = {
  202. "Content-Type": "text/calendar",
  203. "Last-Modified": calendar.last_modified,
  204. "ETag": etag}
  205. answer = answer_text.encode(self.encoding)
  206. return client.OK, headers, answer
  207. def head(self, environ, calendars, content):
  208. """Manage HEAD request."""
  209. status, headers, answer = self.get(environ, calendars, content)
  210. return status, headers, None
  211. def delete(self, environ, calendars, content):
  212. """Manage DELETE request."""
  213. calendar = calendars[0]
  214. item = calendar.get_item(
  215. xmlutils.name_from_path(environ["PATH_INFO"], calendar))
  216. if item and environ.get("HTTP_IF_MATCH", item.etag) == item.etag:
  217. # No ETag precondition or precondition verified, delete item
  218. answer = xmlutils.delete(environ["PATH_INFO"], calendar)
  219. status = client.NO_CONTENT
  220. else:
  221. # No item or ETag precondition not verified, do not delete item
  222. answer = None
  223. status = client.PRECONDITION_FAILED
  224. return status, {}, answer
  225. def mkcalendar(self, environ, calendars, content):
  226. """Manage MKCALENDAR request."""
  227. calendar = calendars[0]
  228. props = xmlutils.props_from_request(content)
  229. timezone = props.get('C:calendar-timezone')
  230. if timezone:
  231. calendar.replace('', timezone)
  232. del props['C:calendar-timezone']
  233. with calendar.props as calendar_props:
  234. for key, value in props.items():
  235. calendar_props[key] = value
  236. calendar.write()
  237. return client.CREATED, {}, None
  238. def options(self, environ, calendars, content):
  239. """Manage OPTIONS request."""
  240. headers = {
  241. "Allow": "DELETE, HEAD, GET, MKCALENDAR, " \
  242. "OPTIONS, PROPFIND, PROPPATCH, PUT, REPORT",
  243. "DAV": "1, calendar-access"}
  244. return client.OK, headers, None
  245. def propfind(self, environ, calendars, content):
  246. """Manage PROPFIND request."""
  247. headers = {
  248. "DAV": "1, calendar-access",
  249. "Content-Type": "text/xml"}
  250. answer = xmlutils.propfind(
  251. environ["PATH_INFO"], content, calendars, environ.get("USER"))
  252. return client.MULTI_STATUS, headers, answer
  253. def proppatch(self, environ, calendars, content):
  254. """Manage PROPPATCH request."""
  255. calendar = calendars[0]
  256. answer = xmlutils.proppatch(environ["PATH_INFO"], content, calendar)
  257. headers = {
  258. "DAV": "1, calendar-access",
  259. "Content-Type": "text/xml"}
  260. return client.MULTI_STATUS, headers, answer
  261. def put(self, environ, calendars, content):
  262. """Manage PUT request."""
  263. calendar = calendars[0]
  264. headers = {}
  265. item_name = xmlutils.name_from_path(environ["PATH_INFO"], calendar)
  266. item = calendar.get_item(item_name)
  267. if (not item and not environ.get("HTTP_IF_MATCH")) or (
  268. item and environ.get("HTTP_IF_MATCH", item.etag) == item.etag):
  269. # PUT allowed in 3 cases
  270. # Case 1: No item and no ETag precondition: Add new item
  271. # Case 2: Item and ETag precondition verified: Modify item
  272. # Case 3: Item and no Etag precondition: Force modifying item
  273. xmlutils.put(environ["PATH_INFO"], content, calendar)
  274. status = client.CREATED
  275. headers["ETag"] = calendar.get_item(item_name).etag
  276. else:
  277. # PUT rejected in all other cases
  278. status = client.PRECONDITION_FAILED
  279. return status, headers, None
  280. def report(self, environ, calendars, content):
  281. """Manage REPORT request."""
  282. # TODO: support multiple calendars here
  283. calendar = calendars[0]
  284. headers = {'Content-Type': 'text/xml'}
  285. answer = xmlutils.report(environ["PATH_INFO"], content, calendar)
  286. return client.MULTI_STATUS, headers, answer
  287. # pylint: enable=W0612,W0613,R0201