server.py 12 KB

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