server.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  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. Radicale WSGI server.
  21. """
  22. import contextlib
  23. import multiprocessing
  24. import os
  25. import select
  26. import socket
  27. import socketserver
  28. import ssl
  29. import sys
  30. import threading
  31. import wsgiref.simple_server
  32. from urllib.parse import unquote
  33. from radicale import Application
  34. from radicale.log import logger
  35. try:
  36. import systemd.daemon
  37. except ImportError:
  38. systemd = None
  39. USE_FORKING = hasattr(os, "fork")
  40. try:
  41. multiprocessing.BoundedSemaphore()
  42. except Exception:
  43. # HACK: Workaround for Android
  44. USE_FORKING = False
  45. if USE_FORKING:
  46. ParallelizationMixIn = socketserver.ForkingMixIn
  47. else:
  48. ParallelizationMixIn = socketserver.ThreadingMixIn
  49. HAS_IPV6 = socket.has_ipv6
  50. if hasattr(socket, "EAI_NONAME"):
  51. EAI_NONAME = socket.EAI_NONAME
  52. else:
  53. HAS_IPV6 = False
  54. if hasattr(socket, "EAI_ADDRFAMILY"):
  55. EAI_ADDRFAMILY = socket.EAI_ADDRFAMILY
  56. elif os.name == "nt":
  57. EAI_ADDRFAMILY = None
  58. else:
  59. HAS_IPV6 = False
  60. if hasattr(socket, "IPPROTO_IPV6"):
  61. IPPROTO_IPV6 = socket.IPPROTO_IPV6
  62. elif os.name == "nt":
  63. IPPROTO_IPV6 = 41
  64. else:
  65. HAS_IPV6 = False
  66. if hasattr(socket, "IPV6_V6ONLY"):
  67. IPV6_V6ONLY = socket.IPV6_V6ONLY
  68. elif os.name == "nt":
  69. IPV6_V6ONLY = 27
  70. else:
  71. HAS_IPV6 = False
  72. class ParallelHTTPServer(ParallelizationMixIn,
  73. wsgiref.simple_server.WSGIServer):
  74. # wait for child processes/threads
  75. _block_on_close = True
  76. # These class attributes must be set before creating instance
  77. client_timeout = None
  78. max_connections = None
  79. def __init__(self, *args, **kwargs):
  80. super().__init__(*args, **kwargs)
  81. if USE_FORKING:
  82. sema_class = multiprocessing.BoundedSemaphore
  83. else:
  84. sema_class = threading.BoundedSemaphore
  85. if self.max_connections:
  86. self.connections_guard = sema_class(self.max_connections)
  87. else:
  88. # use dummy context manager
  89. self.connections_guard = contextlib.ExitStack()
  90. def server_bind(self):
  91. if isinstance(self.server_address, socket.socket):
  92. # Socket activation
  93. self.socket = self.server_address
  94. self.server_address = self.socket.getsockname()
  95. host, port = self.server_address[:2]
  96. self.server_name = socket.getfqdn(host)
  97. self.server_port = port
  98. self.setup_environ()
  99. return
  100. try:
  101. super().server_bind()
  102. except socket.gaierror as e:
  103. if (not HAS_IPV6 or self.address_family != socket.AF_INET or
  104. e.errno not in (EAI_NONAME, EAI_ADDRFAMILY)):
  105. raise
  106. # Try again with IPv6
  107. self.address_family = socket.AF_INET6
  108. self.socket = socket.socket(self.address_family, self.socket_type)
  109. # Only allow IPv6 connections to the IPv6 socket
  110. self.socket.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 1)
  111. super().server_bind()
  112. def get_request(self):
  113. # Set timeout for client
  114. socket_, address = super().get_request()
  115. if self.client_timeout:
  116. socket_.settimeout(self.client_timeout)
  117. return socket_, address
  118. def process_request(self, request, client_address):
  119. try:
  120. return super().process_request(request, client_address)
  121. finally:
  122. # Modify OpenSSL's RNG state, in case process forked
  123. # See https://docs.python.org/3.7/library/ssl.html#multi-processing
  124. ssl.RAND_add(os.urandom(8), 0.0)
  125. def finish_request_locked(self, request, client_address):
  126. return super().finish_request(request, client_address)
  127. def finish_request(self, request, client_address):
  128. """Don't overwrite this! (Modified by tests.)"""
  129. with self.connections_guard:
  130. return self.finish_request_locked(request, client_address)
  131. def handle_error(self, request, client_address):
  132. if issubclass(sys.exc_info()[0], socket.timeout):
  133. logger.info("client timed out", exc_info=True)
  134. else:
  135. logger.error("An exception occurred during request: %s",
  136. sys.exc_info()[1], exc_info=True)
  137. class ParallelHTTPSServer(ParallelHTTPServer):
  138. # These class attributes must be set before creating instance
  139. certificate = None
  140. key = None
  141. protocol = None
  142. ciphers = None
  143. certificate_authority = None
  144. def server_bind(self):
  145. super().server_bind()
  146. """Create server by wrapping HTTP socket in an SSL socket."""
  147. self.socket = ssl.wrap_socket(
  148. self.socket, self.key, self.certificate, server_side=True,
  149. cert_reqs=ssl.CERT_REQUIRED if self.certificate_authority else
  150. ssl.CERT_NONE,
  151. ca_certs=self.certificate_authority or None,
  152. ssl_version=self.protocol, ciphers=self.ciphers,
  153. do_handshake_on_connect=False)
  154. def finish_request_locked(self, request, client_address):
  155. try:
  156. try:
  157. request.do_handshake()
  158. except socket.timeout:
  159. raise
  160. except Exception as e:
  161. raise RuntimeError("SSL handshake failed: %s" % e) from e
  162. except Exception:
  163. try:
  164. self.handle_error(request, client_address)
  165. finally:
  166. self.shutdown_request(request)
  167. return
  168. return super().finish_request_locked(request, client_address)
  169. class ServerHandler(wsgiref.simple_server.ServerHandler):
  170. # Don't pollute WSGI environ with OS environment
  171. os_environ = {}
  172. def log_exception(self, exc_info):
  173. logger.error("An exception occurred during request: %s",
  174. exc_info[1], exc_info=exc_info)
  175. class RequestHandler(wsgiref.simple_server.WSGIRequestHandler):
  176. """HTTP requests handler."""
  177. def log_request(self, code="-", size="-"):
  178. """Disable request logging."""
  179. def log_error(self, format, *args):
  180. msg = format % args
  181. logger.error("An error occurred during request: %s" % msg)
  182. def get_environ(self):
  183. env = super().get_environ()
  184. if hasattr(self.connection, "getpeercert"):
  185. # The certificate can be evaluated by the auth module
  186. env["REMOTE_CERTIFICATE"] = self.connection.getpeercert()
  187. # Parent class only tries latin1 encoding
  188. env["PATH_INFO"] = unquote(self.path.split("?", 1)[0])
  189. return env
  190. def handle(self):
  191. """Copy of WSGIRequestHandler.handle with different ServerHandler"""
  192. self.raw_requestline = self.rfile.readline(65537)
  193. if len(self.raw_requestline) > 65536:
  194. self.requestline = ""
  195. self.request_version = ""
  196. self.command = ""
  197. self.send_error(414)
  198. return
  199. if not self.parse_request():
  200. return
  201. handler = ServerHandler(
  202. self.rfile, self.wfile, self.get_stderr(), self.get_environ()
  203. )
  204. handler.request_handler = self
  205. handler.run(self.server.get_app())
  206. def serve(configuration, shutdown_socket=None):
  207. """Serve radicale from configuration."""
  208. logger.info("Starting Radicale")
  209. # Copy configuration before modifying
  210. configuration = configuration.copy()
  211. configuration.update({"internal": {"internal_server": "True"}}, "server")
  212. # Create collection servers
  213. servers = {}
  214. if configuration.get("server", "ssl"):
  215. server_class = ParallelHTTPSServer
  216. else:
  217. server_class = ParallelHTTPServer
  218. class ServerCopy(server_class):
  219. """Copy, avoids overriding the original class attributes."""
  220. ServerCopy.client_timeout = configuration.get("server", "timeout")
  221. ServerCopy.max_connections = configuration.get("server", "max_connections")
  222. if configuration.get("server", "ssl"):
  223. ServerCopy.certificate = configuration.get("server", "certificate")
  224. ServerCopy.key = configuration.get("server", "key")
  225. ServerCopy.certificate_authority = configuration.get(
  226. "server", "certificate_authority")
  227. ServerCopy.ciphers = configuration.get("server", "ciphers")
  228. ServerCopy.protocol = getattr(
  229. ssl, configuration.get("server", "protocol"), ssl.PROTOCOL_SSLv23)
  230. # Test if the SSL files can be read
  231. for name in ["certificate", "key"] + (
  232. ["certificate_authority"]
  233. if ServerCopy.certificate_authority else []):
  234. filename = getattr(ServerCopy, name)
  235. try:
  236. open(filename, "r").close()
  237. except OSError as e:
  238. raise RuntimeError("Failed to read SSL %s %r: %s" %
  239. (name, filename, e)) from e
  240. class RequestHandlerCopy(RequestHandler):
  241. """Copy, avoids overriding the original class attributes."""
  242. if not configuration.get("server", "dns_lookup"):
  243. RequestHandlerCopy.address_string = lambda self: self.client_address[0]
  244. if systemd:
  245. listen_fds = systemd.daemon.listen_fds()
  246. else:
  247. listen_fds = []
  248. server_addresses = []
  249. if listen_fds:
  250. logger.info("Using socket activation")
  251. ServerCopy.address_family = socket.AF_UNIX
  252. for fd in listen_fds:
  253. server_addresses.append(socket.fromfd(
  254. fd, ServerCopy.address_family, ServerCopy.socket_type))
  255. else:
  256. for address, port in configuration.get("server", "hosts"):
  257. server_addresses.append((address, port))
  258. application = Application(configuration)
  259. for server_address in server_addresses:
  260. try:
  261. server = ServerCopy(server_address, RequestHandlerCopy)
  262. server.set_app(application)
  263. except OSError as e:
  264. raise RuntimeError(
  265. "Failed to start server %r: %s" % (server_address, e)) from e
  266. servers[server.socket] = server
  267. logger.info("Listening to %r on port %d%s",
  268. server.server_name, server.server_port, " using SSL"
  269. if configuration.get("server", "ssl") else "")
  270. # Main loop: wait for requests on any of the servers or program shutdown
  271. sockets = list(servers.keys())
  272. # Use socket pair to get notified of program shutdown
  273. if shutdown_socket:
  274. sockets.append(shutdown_socket)
  275. select_timeout = None
  276. if os.name == "nt":
  277. # Fallback to busy waiting. (select.select blocks SIGINT on Windows.)
  278. select_timeout = 1.0
  279. logger.info("Radicale server ready")
  280. with contextlib.ExitStack() as stack:
  281. for _, server in servers.items():
  282. stack.callback(server.server_close)
  283. while True:
  284. rlist, _, xlist = select.select(
  285. sockets, [], sockets, select_timeout)
  286. if xlist:
  287. raise RuntimeError("unhandled socket error")
  288. if shutdown_socket in rlist:
  289. logger.info("Stopping Radicale")
  290. break
  291. if rlist:
  292. server = servers.get(rlist[0])
  293. if server:
  294. server.handle_request()
  295. server.service_actions()