server.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  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 signal
  27. import socket
  28. import socketserver
  29. import ssl
  30. import sys
  31. import wsgiref.simple_server
  32. from configparser import ConfigParser
  33. from urllib.parse import unquote
  34. from radicale import Application
  35. from radicale.log import logger
  36. if hasattr(socketserver, "ForkingMixIn"):
  37. ParallelizationMixIn = socketserver.ForkingMixIn
  38. else:
  39. ParallelizationMixIn = socketserver.ThreadingMixIn
  40. class ParallelHTTPServer(ParallelizationMixIn,
  41. wsgiref.simple_server.WSGIServer):
  42. # These class attributes must be set before creating instance
  43. client_timeout = None
  44. max_connections = None
  45. def __init__(self, address, handler, bind_and_activate=True):
  46. """Create server."""
  47. ipv6 = ":" in address[0]
  48. if ipv6:
  49. self.address_family = socket.AF_INET6
  50. # Do not bind and activate, as we might change socket options
  51. super().__init__(address, handler, False)
  52. if ipv6:
  53. # Only allow IPv6 connections to the IPv6 socket
  54. self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
  55. if self.max_connections:
  56. self.connections_guard = multiprocessing.BoundedSemaphore(
  57. self.max_connections)
  58. else:
  59. # use dummy context manager
  60. self.connections_guard = contextlib.ExitStack()
  61. if bind_and_activate:
  62. try:
  63. self.server_bind()
  64. self.server_activate()
  65. except BaseException:
  66. self.server_close()
  67. raise
  68. def get_request(self):
  69. # Set timeout for client
  70. socket_, address = super().get_request()
  71. if self.client_timeout:
  72. socket_.settimeout(self.client_timeout)
  73. return socket_, address
  74. def finish_request(self, request, client_address):
  75. with self.connections_guard:
  76. return super().finish_request(request, client_address)
  77. def handle_error(self, request, client_address):
  78. if issubclass(sys.exc_info()[0], socket.timeout):
  79. logger.info("client timed out", exc_info=True)
  80. else:
  81. logger.error("An exception occurred during request: %s",
  82. sys.exc_info()[1], exc_info=True)
  83. class ParallelHTTPSServer(ParallelHTTPServer):
  84. # These class attributes must be set before creating instance
  85. certificate = None
  86. key = None
  87. protocol = None
  88. ciphers = None
  89. certificate_authority = None
  90. def __init__(self, address, handler, bind_and_activate=True):
  91. """Create server by wrapping HTTP socket in an SSL socket."""
  92. # Do not bind and activate, as we change the socket
  93. super().__init__(address, handler, False)
  94. self.socket = ssl.wrap_socket(
  95. self.socket, self.key, self.certificate, server_side=True,
  96. cert_reqs=ssl.CERT_REQUIRED if self.certificate_authority else
  97. ssl.CERT_NONE,
  98. ca_certs=self.certificate_authority or None,
  99. ssl_version=self.protocol, ciphers=self.ciphers,
  100. do_handshake_on_connect=False)
  101. if bind_and_activate:
  102. try:
  103. self.server_bind()
  104. self.server_activate()
  105. except BaseException:
  106. self.server_close()
  107. raise
  108. def finish_request(self, request, client_address):
  109. try:
  110. try:
  111. request.do_handshake()
  112. except socket.timeout:
  113. raise
  114. except Exception as e:
  115. raise RuntimeError("SSL handshake failed: %s" % e) from e
  116. except Exception:
  117. try:
  118. self.handle_error(request, client_address)
  119. finally:
  120. self.shutdown_request(request)
  121. return
  122. return super().finish_request(request, client_address)
  123. class ServerHandler(wsgiref.simple_server.ServerHandler):
  124. # Don't pollute WSGI environ with OS environment
  125. os_environ = {}
  126. def log_exception(self, exc_info):
  127. logger.error("An exception occurred during request: %s",
  128. exc_info[1], exc_info=exc_info)
  129. class RequestHandler(wsgiref.simple_server.WSGIRequestHandler):
  130. """HTTP requests handler."""
  131. def log_request(self, code="-", size="-"):
  132. """Disable request logging."""
  133. def log_error(self, format, *args):
  134. msg = format % args
  135. logger.error("An error occurred during request: %s" % msg)
  136. def get_environ(self):
  137. env = super().get_environ()
  138. if hasattr(self.connection, "getpeercert"):
  139. # The certificate can be evaluated by the auth module
  140. env["REMOTE_CERTIFICATE"] = self.connection.getpeercert()
  141. # Parent class only tries latin1 encoding
  142. env["PATH_INFO"] = unquote(self.path.split("?", 1)[0])
  143. return env
  144. def handle(self):
  145. """Copy of WSGIRequestHandler.handle with different ServerHandler"""
  146. self.raw_requestline = self.rfile.readline(65537)
  147. if len(self.raw_requestline) > 65536:
  148. self.requestline = ""
  149. self.request_version = ""
  150. self.command = ""
  151. self.send_error(414)
  152. return
  153. if not self.parse_request():
  154. return
  155. handler = ServerHandler(
  156. self.rfile, self.wfile, self.get_stderr(), self.get_environ()
  157. )
  158. handler.request_handler = self
  159. handler.run(self.server.get_app())
  160. def serve(configuration):
  161. """Serve radicale from configuration."""
  162. logger.info("Starting Radicale")
  163. # Copy configuration before modifying
  164. config_copy = ConfigParser()
  165. config_copy.read_dict(configuration)
  166. configuration = config_copy
  167. configuration["internal"]["internal_server"] = "True"
  168. # Create collection servers
  169. servers = {}
  170. if configuration.getboolean("server", "ssl"):
  171. server_class = ParallelHTTPSServer
  172. else:
  173. server_class = ParallelHTTPServer
  174. class ServerCopy(server_class):
  175. """Copy, avoids overriding the original class attributes."""
  176. ServerCopy.client_timeout = configuration.getint("server", "timeout")
  177. ServerCopy.max_connections = configuration.getint(
  178. "server", "max_connections")
  179. if configuration.getboolean("server", "ssl"):
  180. ServerCopy.certificate = configuration.get("server", "certificate")
  181. ServerCopy.key = configuration.get("server", "key")
  182. ServerCopy.certificate_authority = configuration.get(
  183. "server", "certificate_authority")
  184. ServerCopy.ciphers = configuration.get("server", "ciphers")
  185. ServerCopy.protocol = getattr(
  186. ssl, configuration.get("server", "protocol"), ssl.PROTOCOL_SSLv23)
  187. # Test if the SSL files can be read
  188. for name in ["certificate", "key"] + (
  189. ["certificate_authority"]
  190. if ServerCopy.certificate_authority else []):
  191. filename = getattr(ServerCopy, name)
  192. try:
  193. open(filename, "r").close()
  194. except OSError as e:
  195. raise RuntimeError("Failed to read SSL %s %r: %s" %
  196. (name, filename, e)) from e
  197. class RequestHandlerCopy(RequestHandler):
  198. """Copy, avoids overriding the original class attributes."""
  199. if not configuration.getboolean("server", "dns_lookup"):
  200. RequestHandlerCopy.address_string = lambda self: self.client_address[0]
  201. shutdown_program = False
  202. for host in configuration.get("server", "hosts").split(","):
  203. try:
  204. address, port = host.strip().rsplit(":", 1)
  205. address, port = address.strip("[] "), int(port)
  206. except ValueError as e:
  207. raise RuntimeError(
  208. "Failed to parse address %r: %s" % (host, e)) from e
  209. application = Application(configuration)
  210. try:
  211. server = wsgiref.simple_server.make_server(
  212. address, port, application, ServerCopy, RequestHandlerCopy)
  213. except OSError as e:
  214. raise RuntimeError(
  215. "Failed to start server %r: %s" % (host, e)) from e
  216. servers[server.socket] = server
  217. logger.info("Listening to %r on port %d%s",
  218. server.server_name, server.server_port, " using SSL"
  219. if configuration.getboolean("server", "ssl") else "")
  220. # Create a socket pair to notify the select syscall of program shutdown
  221. shutdown_program_socket_in, shutdown_program_socket_out = (
  222. socket.socketpair())
  223. # SIGTERM and SIGINT (aka KeyboardInterrupt) should just mark this for
  224. # shutdown
  225. def shutdown(*args):
  226. nonlocal shutdown_program
  227. if shutdown_program:
  228. # Ignore following signals
  229. return
  230. logger.info("Stopping Radicale")
  231. shutdown_program = True
  232. shutdown_program_socket_in.sendall(b" ")
  233. signal.signal(signal.SIGTERM, shutdown)
  234. signal.signal(signal.SIGINT, shutdown)
  235. # Main loop: wait for requests on any of the servers or program shutdown
  236. sockets = list(servers.keys())
  237. # Use socket pair to get notified of program shutdown
  238. sockets.append(shutdown_program_socket_out)
  239. select_timeout = None
  240. if os.name == "nt":
  241. # Fallback to busy waiting. (select.select blocks SIGINT on Windows.)
  242. select_timeout = 1.0
  243. logger.info("Radicale server ready")
  244. while not shutdown_program:
  245. try:
  246. rlist, _, xlist = select.select(
  247. sockets, [], sockets, select_timeout)
  248. except (KeyboardInterrupt, select.error):
  249. # SIGINT is handled by signal handler above
  250. rlist, xlist = [], []
  251. if xlist:
  252. raise RuntimeError("unhandled socket error")
  253. if rlist:
  254. server = servers.get(rlist[0])
  255. if server:
  256. server.handle_request()