server.py 12 KB

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