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