server.py 10 KB

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