__init__.py 11 KB

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