server.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  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-2019 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. Built-in WSGI server.
  21. """
  22. import errno
  23. import os
  24. import select
  25. import socket
  26. import socketserver
  27. import ssl
  28. import sys
  29. import wsgiref.simple_server
  30. from urllib.parse import unquote
  31. from radicale import Application, config
  32. from radicale.log import logger
  33. if hasattr(socket, "EAI_ADDRFAMILY"):
  34. COMPAT_EAI_ADDRFAMILY = socket.EAI_ADDRFAMILY
  35. elif hasattr(socket, "EAI_NONAME"):
  36. # Windows and BSD don't have a special error code for this
  37. COMPAT_EAI_ADDRFAMILY = socket.EAI_NONAME
  38. if hasattr(socket, "EAI_NODATA"):
  39. COMPAT_EAI_NODATA = socket.EAI_NODATA
  40. elif hasattr(socket, "EAI_NONAME"):
  41. # Windows and BSD don't have a special error code for this
  42. COMPAT_EAI_NODATA = socket.EAI_NONAME
  43. if hasattr(socket, "IPPROTO_IPV6"):
  44. COMPAT_IPPROTO_IPV6 = socket.IPPROTO_IPV6
  45. elif os.name == "nt":
  46. # Workaround: https://bugs.python.org/issue29515
  47. COMPAT_IPPROTO_IPV6 = 41
  48. def format_address(address):
  49. return "[%s]:%d" % address[:2]
  50. class ParallelHTTPServer(socketserver.ThreadingMixIn,
  51. wsgiref.simple_server.WSGIServer):
  52. # We wait for child threads ourself
  53. block_on_close = False
  54. def __init__(self, configuration, family, address, RequestHandlerClass):
  55. self.configuration = configuration
  56. self.address_family = family
  57. super().__init__(address, RequestHandlerClass)
  58. self.client_sockets = set()
  59. def server_bind(self):
  60. if self.address_family == socket.AF_INET6:
  61. # Only allow IPv6 connections to the IPv6 socket
  62. self.socket.setsockopt(COMPAT_IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
  63. super().server_bind()
  64. def get_request(self):
  65. # Set timeout for client
  66. request, client_address = super().get_request()
  67. timeout = self.configuration.get("server", "timeout")
  68. if timeout:
  69. request.settimeout(timeout)
  70. client_socket, client_socket_out = socket.socketpair()
  71. self.client_sockets.add(client_socket_out)
  72. return request, (*client_address, client_socket)
  73. def finish_request_locked(self, request, client_address):
  74. return super().finish_request(request, client_address)
  75. def finish_request(self, request, client_address):
  76. *client_address, client_socket = client_address
  77. client_address = tuple(client_address)
  78. try:
  79. return self.finish_request_locked(request, client_address)
  80. finally:
  81. client_socket.close()
  82. def handle_error(self, request, client_address):
  83. if issubclass(sys.exc_info()[0], socket.timeout):
  84. logger.info("client timed out", exc_info=True)
  85. else:
  86. logger.error("An exception occurred during request: %s",
  87. sys.exc_info()[1], exc_info=True)
  88. class ParallelHTTPSServer(ParallelHTTPServer):
  89. def server_bind(self):
  90. super().server_bind()
  91. # Wrap the TCP socket in an SSL socket
  92. certfile = self.configuration.get("server", "certificate")
  93. keyfile = self.configuration.get("server", "key")
  94. cafile = self.configuration.get("server", "certificate_authority")
  95. # Test if the files can be read
  96. for name, filename in [("certificate", certfile), ("key", keyfile),
  97. ("certificate_authority", cafile)]:
  98. type_name = config.DEFAULT_CONFIG_SCHEMA["server"][name][
  99. "type"].__name__
  100. source = self.configuration.get_source("server", name)
  101. if name == "certificate_authority" and not filename:
  102. continue
  103. try:
  104. open(filename, "r").close()
  105. except OSError as e:
  106. raise RuntimeError(
  107. "Invalid %s value for option %r in section %r in %s: %r "
  108. "(%s)" % (type_name, name, "server", source, filename,
  109. e)) from e
  110. context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
  111. context.load_cert_chain(certfile=certfile, keyfile=keyfile)
  112. if cafile:
  113. context.load_verify_locations(cafile=cafile)
  114. context.verify_mode = ssl.CERT_REQUIRED
  115. self.socket = context.wrap_socket(
  116. self.socket, server_side=True, do_handshake_on_connect=False)
  117. def finish_request_locked(self, request, client_address):
  118. try:
  119. try:
  120. request.do_handshake()
  121. except socket.timeout:
  122. raise
  123. except Exception as e:
  124. raise RuntimeError("SSL handshake failed: %s" % e) from e
  125. except Exception:
  126. try:
  127. self.handle_error(request, client_address)
  128. finally:
  129. self.shutdown_request(request)
  130. return
  131. return super().finish_request_locked(request, client_address)
  132. class ServerHandler(wsgiref.simple_server.ServerHandler):
  133. # Don't pollute WSGI environ with OS environment
  134. os_environ = {}
  135. def log_exception(self, exc_info):
  136. logger.error("An exception occurred during request: %s",
  137. exc_info[1], exc_info=exc_info)
  138. class RequestHandler(wsgiref.simple_server.WSGIRequestHandler):
  139. """HTTP requests handler."""
  140. def log_request(self, code="-", size="-"):
  141. pass # Disable request logging.
  142. def log_error(self, format_, *args):
  143. logger.error("An error occurred during request: %s", format_ % args)
  144. def get_environ(self):
  145. env = super().get_environ()
  146. if hasattr(self.connection, "getpeercert"):
  147. # The certificate can be evaluated by the auth module
  148. env["REMOTE_CERTIFICATE"] = self.connection.getpeercert()
  149. # Parent class only tries latin1 encoding
  150. env["PATH_INFO"] = unquote(self.path.split("?", 1)[0])
  151. return env
  152. def handle(self):
  153. """Copy of WSGIRequestHandler.handle with different ServerHandler"""
  154. self.raw_requestline = self.rfile.readline(65537)
  155. if len(self.raw_requestline) > 65536:
  156. self.requestline = ""
  157. self.request_version = ""
  158. self.command = ""
  159. self.send_error(414)
  160. return
  161. if not self.parse_request():
  162. return
  163. handler = ServerHandler(
  164. self.rfile, self.wfile, self.get_stderr(), self.get_environ()
  165. )
  166. handler.request_handler = self
  167. handler.run(self.server.get_app())
  168. def serve(configuration, shutdown_socket):
  169. """Serve radicale from configuration."""
  170. logger.info("Starting Radicale")
  171. # Copy configuration before modifying
  172. configuration = configuration.copy()
  173. configuration.update({"server": {"_internal_server": "True"}}, "server",
  174. privileged=True)
  175. use_ssl = configuration.get("server", "ssl")
  176. server_class = ParallelHTTPSServer if use_ssl else ParallelHTTPServer
  177. application = Application(configuration)
  178. servers = {}
  179. try:
  180. for address in configuration.get("server", "hosts"):
  181. # Try to bind sockets for IPv4 and IPv6
  182. possible_families = (socket.AF_INET, socket.AF_INET6)
  183. bind_ok = False
  184. for i, family in enumerate(possible_families):
  185. is_last = i == len(possible_families) - 1
  186. try:
  187. server = server_class(configuration, family, address,
  188. RequestHandler)
  189. except OSError as e:
  190. # Ignore unsupported families (only one must work)
  191. if ((bind_ok or not is_last) and (
  192. isinstance(e, socket.gaierror) and (
  193. # Hostname does not exist or doesn't have
  194. # address for address family
  195. # macOS: IPv6 address for INET address family
  196. e.errno == socket.EAI_NONAME or
  197. # Address not for address family
  198. e.errno == COMPAT_EAI_ADDRFAMILY or
  199. e.errno == COMPAT_EAI_NODATA) or
  200. # Workaround for PyPy
  201. str(e) == "address family mismatched" or
  202. # Address family not available (e.g. IPv6 disabled)
  203. # macOS: IPv4 address for INET6 address family with
  204. # IPV6_V6ONLY set
  205. e.errno == errno.EADDRNOTAVAIL)):
  206. continue
  207. raise RuntimeError("Failed to start server %r: %s" % (
  208. format_address(address), e)) from e
  209. servers[server.socket] = server
  210. bind_ok = True
  211. server.set_app(application)
  212. logger.info("Listening on %r%s",
  213. format_address(server.server_address),
  214. " with SSL" if use_ssl else "")
  215. assert servers, "no servers started"
  216. # Mainloop
  217. select_timeout = None
  218. if os.name == "nt":
  219. # Fallback to busy waiting. (select(...) blocks SIGINT on Windows.)
  220. select_timeout = 1.0
  221. max_connections = configuration.get("server", "max_connections")
  222. logger.info("Radicale server ready")
  223. while True:
  224. rlist = xlist = []
  225. # Wait for finished clients
  226. for server in servers.values():
  227. rlist.extend(server.client_sockets)
  228. # Accept new connections if max_connections is not reached
  229. if max_connections <= 0 or len(rlist) < max_connections:
  230. rlist.extend(servers)
  231. # Use socket to get notified of program shutdown
  232. rlist.append(shutdown_socket)
  233. rlist, _, xlist = select.select(rlist, [], xlist, select_timeout)
  234. if xlist:
  235. raise RuntimeError("unhandled socket error")
  236. rlist = set(rlist)
  237. if shutdown_socket in rlist:
  238. logger.info("Stopping Radicale")
  239. break
  240. for server in servers.values():
  241. finished_sockets = server.client_sockets.intersection(rlist)
  242. for s in finished_sockets:
  243. s.close()
  244. server.client_sockets.remove(s)
  245. rlist.remove(s)
  246. if finished_sockets:
  247. server.service_actions()
  248. if rlist:
  249. server = servers.get(rlist.pop())
  250. if server:
  251. server.handle_request()
  252. finally:
  253. # Wait for clients to finish and close servers
  254. for server in servers.values():
  255. for s in server.client_sockets:
  256. s.recv(1)
  257. s.close()
  258. server.server_close()