server.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  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=None):
  169. """Serve radicale from configuration.
  170. `shutdown_socket` can be used to gracefully shutdown the server.
  171. The socket can be created with `socket.socketpair()`, when the other socket
  172. gets closed the server stops accepting new requests by clients and the
  173. function returns after all active requests are finished.
  174. """
  175. logger.info("Starting Radicale")
  176. # Copy configuration before modifying
  177. configuration = configuration.copy()
  178. configuration.update({"server": {"_internal_server": "True"}}, "server",
  179. privileged=True)
  180. use_ssl = configuration.get("server", "ssl")
  181. server_class = ParallelHTTPSServer if use_ssl else ParallelHTTPServer
  182. application = Application(configuration)
  183. servers = {}
  184. try:
  185. for address in configuration.get("server", "hosts"):
  186. # Try to bind sockets for IPv4 and IPv6
  187. possible_families = (socket.AF_INET, socket.AF_INET6)
  188. bind_ok = False
  189. for i, family in enumerate(possible_families):
  190. is_last = i == len(possible_families) - 1
  191. try:
  192. server = server_class(configuration, family, address,
  193. RequestHandler)
  194. except OSError as e:
  195. # Ignore unsupported families (only one must work)
  196. if ((bind_ok or not is_last) and (
  197. isinstance(e, socket.gaierror) and (
  198. # Hostname does not exist or doesn't have
  199. # address for address family
  200. # macOS: IPv6 address for INET address family
  201. e.errno == socket.EAI_NONAME or
  202. # Address not for address family
  203. e.errno == COMPAT_EAI_ADDRFAMILY or
  204. e.errno == COMPAT_EAI_NODATA) or
  205. # Workaround for PyPy
  206. str(e) == "address family mismatched" or
  207. # Address family not available (e.g. IPv6 disabled)
  208. # macOS: IPv4 address for INET6 address family with
  209. # IPV6_V6ONLY set
  210. e.errno == errno.EADDRNOTAVAIL or
  211. # Address family not supported
  212. e.errno == errno.EAFNOSUPPORT or
  213. # Protocol not supported
  214. e.errno == errno.EPROTONOSUPPORT)):
  215. continue
  216. raise RuntimeError("Failed to start server %r: %s" % (
  217. format_address(address), e)) from e
  218. servers[server.socket] = server
  219. bind_ok = True
  220. server.set_app(application)
  221. logger.info("Listening on %r%s",
  222. format_address(server.server_address),
  223. " with SSL" if use_ssl else "")
  224. assert servers, "no servers started"
  225. # Mainloop
  226. select_timeout = None
  227. if os.name == "nt":
  228. # Fallback to busy waiting. (select(...) blocks SIGINT on Windows.)
  229. select_timeout = 1.0
  230. max_connections = configuration.get("server", "max_connections")
  231. logger.info("Radicale server ready")
  232. while True:
  233. rlist = []
  234. # Wait for finished clients
  235. for server in servers.values():
  236. rlist.extend(server.client_sockets)
  237. # Accept new connections if max_connections is not reached
  238. if max_connections <= 0 or len(rlist) < max_connections:
  239. rlist.extend(servers)
  240. # Use socket to get notified of program shutdown
  241. if shutdown_socket is not None:
  242. rlist.append(shutdown_socket)
  243. rlist, _, _ = select.select(rlist, [], [], select_timeout)
  244. rlist = set(rlist)
  245. if shutdown_socket in rlist:
  246. logger.info("Stopping Radicale")
  247. break
  248. for server in servers.values():
  249. finished_sockets = server.client_sockets.intersection(rlist)
  250. for s in finished_sockets:
  251. s.close()
  252. server.client_sockets.remove(s)
  253. rlist.remove(s)
  254. if finished_sockets:
  255. server.service_actions()
  256. if rlist:
  257. server = servers.get(rlist.pop())
  258. if server:
  259. server.handle_request()
  260. finally:
  261. # Wait for clients to finish and close servers
  262. for server in servers.values():
  263. for s in server.client_sockets:
  264. s.recv(1)
  265. s.close()
  266. server.server_close()