server.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. # This file is part of Radicale - CalDAV and CardDAV server
  2. # Copyright © 2008 Nicolas Kandel
  3. # Copyright © 2008 Pascal Halter
  4. # Copyright © 2008-2017 Guillaume Ayoub
  5. # Copyright © 2017-2023 Unrud <unrud@outlook.com>
  6. # Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de>
  7. #
  8. # This library is free software: you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation, either version 3 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This library is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License
  19. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
  20. """
  21. Built-in WSGI server.
  22. """
  23. import http
  24. import os
  25. import platform
  26. import select
  27. import socket
  28. import socketserver
  29. import ssl
  30. import sys
  31. import wsgiref.simple_server
  32. from typing import (Any, Callable, Dict, List, MutableMapping, Optional, Set,
  33. Tuple, Union)
  34. from urllib.parse import unquote
  35. from radicale import Application, config, utils
  36. from radicale.log import logger
  37. COMPAT_EAI_ADDRFAMILY: int
  38. if hasattr(socket, "EAI_ADDRFAMILY"):
  39. COMPAT_EAI_ADDRFAMILY = socket.EAI_ADDRFAMILY # type:ignore[attr-defined]
  40. elif hasattr(socket, "EAI_NONAME"):
  41. # Windows and BSD don't have a special error code for this
  42. COMPAT_EAI_ADDRFAMILY = socket.EAI_NONAME
  43. COMPAT_EAI_NODATA: int
  44. if hasattr(socket, "EAI_NODATA"):
  45. COMPAT_EAI_NODATA = socket.EAI_NODATA
  46. elif hasattr(socket, "EAI_NONAME"):
  47. # Windows and BSD don't have a special error code for this
  48. COMPAT_EAI_NODATA = socket.EAI_NONAME
  49. COMPAT_IPPROTO_IPV6: int
  50. if hasattr(socket, "IPPROTO_IPV6"):
  51. COMPAT_IPPROTO_IPV6 = socket.IPPROTO_IPV6
  52. elif sys.platform == "win32":
  53. # HACK: https://bugs.python.org/issue29515
  54. COMPAT_IPPROTO_IPV6 = 41
  55. # IPv4 (host, port) and IPv6 (host, port, flowinfo, scopeid)
  56. ADDRESS_TYPE = utils.ADDRESS_TYPE
  57. class ParallelHTTPServer(socketserver.ThreadingMixIn,
  58. wsgiref.simple_server.WSGIServer):
  59. configuration: config.Configuration
  60. worker_sockets: Set[socket.socket]
  61. _timeout: float
  62. # We wait for child threads ourself (ThreadingMixIn)
  63. block_on_close: bool = False
  64. daemon_threads: bool = True
  65. def __init__(self, configuration: config.Configuration, family: int,
  66. address: Tuple[str, int], RequestHandlerClass:
  67. Callable[..., http.server.BaseHTTPRequestHandler]) -> None:
  68. self.configuration = configuration
  69. self.address_family = family
  70. super().__init__(address, RequestHandlerClass)
  71. self.worker_sockets = set()
  72. self._timeout = configuration.get("server", "timeout")
  73. def server_bind(self) -> None:
  74. if self.address_family == socket.AF_INET6:
  75. # Only allow IPv6 connections to the IPv6 socket
  76. self.socket.setsockopt(COMPAT_IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
  77. super().server_bind()
  78. def get_request( # type:ignore[override]
  79. self) -> Tuple[socket.socket, Tuple[ADDRESS_TYPE, socket.socket]]:
  80. # Set timeout for client
  81. request: socket.socket
  82. client_address: ADDRESS_TYPE
  83. request, client_address = super().get_request() # type:ignore[misc]
  84. if self._timeout > 0:
  85. request.settimeout(self._timeout)
  86. worker_socket, worker_socket_out = socket.socketpair()
  87. self.worker_sockets.add(worker_socket_out)
  88. # HACK: Forward `worker_socket` via `client_address` return value
  89. # to worker thread.
  90. # The super class calls `verify_request`, `process_request` and
  91. # `handle_error` with modified `client_address` value.
  92. return request, (client_address, worker_socket)
  93. def verify_request( # type:ignore[override]
  94. self, request: socket.socket, client_address_and_socket:
  95. Tuple[ADDRESS_TYPE, socket.socket]) -> bool:
  96. return True
  97. def process_request( # type:ignore[override]
  98. self, request: socket.socket, client_address_and_socket:
  99. Tuple[ADDRESS_TYPE, socket.socket]) -> None:
  100. # HACK: Super class calls `finish_request` in new thread with
  101. # `client_address_and_socket`
  102. return super().process_request(
  103. request, client_address_and_socket) # type:ignore[arg-type]
  104. def finish_request( # type:ignore[override]
  105. self, request: socket.socket, client_address_and_socket:
  106. Tuple[ADDRESS_TYPE, socket.socket]) -> None:
  107. # HACK: Unpack `client_address_and_socket` and call super class
  108. # `finish_request` with original `client_address`
  109. client_address, worker_socket = client_address_and_socket
  110. try:
  111. return self.finish_request_locked(request, client_address)
  112. finally:
  113. worker_socket.close()
  114. def finish_request_locked(self, request: socket.socket,
  115. client_address: ADDRESS_TYPE) -> None:
  116. return super().finish_request(
  117. request, client_address) # type:ignore[arg-type]
  118. def handle_error( # type:ignore[override]
  119. self, request: socket.socket,
  120. client_address_or_client_address_and_socket:
  121. Union[ADDRESS_TYPE, Tuple[ADDRESS_TYPE, socket.socket]]) -> None:
  122. # HACK: This method can be called with the modified
  123. # `client_address_and_socket` or the original `client_address` value
  124. e = sys.exc_info()[1]
  125. assert e is not None
  126. if isinstance(e, socket.timeout):
  127. logger.info("Client timed out", exc_info=True)
  128. else:
  129. logger.error("An exception occurred during request: %s",
  130. sys.exc_info()[1], exc_info=True)
  131. class ParallelHTTPSServer(ParallelHTTPServer):
  132. def server_bind(self) -> None:
  133. super().server_bind()
  134. # Wrap the TCP socket in an SSL socket
  135. certfile: str = self.configuration.get("server", "certificate")
  136. keyfile: str = self.configuration.get("server", "key")
  137. cafile: str = self.configuration.get("server", "certificate_authority")
  138. protocol: str = self.configuration.get("server", "protocol")
  139. ciphersuite: str = self.configuration.get("server", "ciphersuite")
  140. # Test if the files can be read
  141. for name, filename in [("certificate", certfile), ("key", keyfile),
  142. ("certificate_authority", cafile)]:
  143. type_name = config.DEFAULT_CONFIG_SCHEMA["server"][name][
  144. "type"].__name__
  145. source = self.configuration.get_source("server", name)
  146. if name == "certificate_authority" and not filename:
  147. continue
  148. try:
  149. open(filename).close()
  150. except OSError as e:
  151. raise RuntimeError(
  152. "Invalid %s value for option %r in section %r in %s: %r "
  153. "(%s)" % (type_name, name, "server", source, filename,
  154. e)) from e
  155. context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
  156. logger.info("SSL load files certificate='%s' key='%s'", certfile, keyfile)
  157. context.load_cert_chain(certfile=certfile, keyfile=keyfile)
  158. if protocol:
  159. logger.info("SSL set explicit protocols (maybe not all supported by underlying OpenSSL): '%s'", protocol)
  160. context.options = utils.ssl_context_options_by_protocol(protocol, context.options)
  161. context.minimum_version = utils.ssl_context_minimum_version_by_options(context.options)
  162. if (context.minimum_version == 0):
  163. raise RuntimeError("No SSL minimum protocol active")
  164. context.maximum_version = utils.ssl_context_maximum_version_by_options(context.options)
  165. if (context.maximum_version == 0):
  166. raise RuntimeError("No SSL maximum protocol active")
  167. else:
  168. logger.info("SSL active protocols: (system-default)")
  169. logger.debug("SSL minimum acceptable protocol: %s", context.minimum_version)
  170. logger.debug("SSL maximum acceptable protocol: %s", context.maximum_version)
  171. logger.info("SSL accepted protocols: %s", ' '.join(utils.ssl_get_protocols(context)))
  172. if ciphersuite:
  173. logger.info("SSL set explicit ciphersuite (maybe not all supported by underlying OpenSSL): '%s'", ciphersuite)
  174. context.set_ciphers(ciphersuite)
  175. else:
  176. logger.info("SSL active ciphersuite: (system-default)")
  177. cipherlist = []
  178. for entry in context.get_ciphers():
  179. cipherlist.append(entry["name"])
  180. logger.info("SSL accepted ciphers: %s", ' '.join(cipherlist))
  181. if cafile:
  182. logger.info("SSL enable mandatory client certificate verification using CA file='%s'", cafile)
  183. context.load_verify_locations(cafile=cafile)
  184. context.verify_mode = ssl.CERT_REQUIRED
  185. self.socket = context.wrap_socket(
  186. self.socket, server_side=True, do_handshake_on_connect=False)
  187. def finish_request_locked( # type:ignore[override]
  188. self, request: ssl.SSLSocket, client_address: ADDRESS_TYPE
  189. ) -> None:
  190. try:
  191. try:
  192. request.do_handshake()
  193. except socket.timeout:
  194. raise
  195. except Exception as e:
  196. raise RuntimeError("SSL handshake failed: %s client %s" % (e, str(client_address[0]))) from e
  197. except Exception:
  198. try:
  199. self.handle_error(request, client_address)
  200. finally:
  201. self.shutdown_request(request) # type:ignore[attr-defined]
  202. return
  203. return super().finish_request_locked(request, client_address)
  204. class ServerHandler(wsgiref.simple_server.ServerHandler):
  205. # Don't pollute WSGI environ with OS environment
  206. os_environ: MutableMapping[str, str] = {}
  207. def log_exception(self, exc_info) -> None:
  208. logger.error("An exception occurred during request: %s",
  209. exc_info[1], exc_info=exc_info) # type:ignore[arg-type]
  210. class RequestHandler(wsgiref.simple_server.WSGIRequestHandler):
  211. """HTTP requests handler."""
  212. # HACK: Assigned in `socketserver.StreamRequestHandler`
  213. connection: socket.socket
  214. def log_request(self, code: Union[int, str] = "-",
  215. size: Union[int, str] = "-") -> None:
  216. pass # Disable request logging.
  217. def log_error(self, format_: str, *args: Any) -> None:
  218. logger.error("An error occurred during request: %s", format_ % args)
  219. def get_environ(self) -> Dict[str, Any]:
  220. env = super().get_environ()
  221. if isinstance(self.connection, ssl.SSLSocket):
  222. env["HTTPS"] = "on"
  223. env["SSL_CIPHER"] = self.request.cipher()[0]
  224. env["SSL_PROTOCOL"] = self.request.version()
  225. # The certificate can be evaluated by the auth module
  226. env["REMOTE_CERTIFICATE"] = self.connection.getpeercert()
  227. # Parent class only tries latin1 encoding
  228. env["PATH_INFO"] = unquote(self.path.split("?", 1)[0])
  229. return env
  230. def handle(self) -> None:
  231. """Copy of WSGIRequestHandler.handle with different ServerHandler"""
  232. self.raw_requestline = self.rfile.readline(65537)
  233. if len(self.raw_requestline) > 65536:
  234. self.requestline = ""
  235. self.request_version = ""
  236. self.command = ""
  237. self.send_error(414)
  238. return
  239. if not self.parse_request():
  240. return
  241. handler = ServerHandler(
  242. self.rfile, self.wfile, self.get_stderr(), self.get_environ()
  243. )
  244. handler.request_handler = self # type:ignore[attr-defined]
  245. app = self.server.get_app() # type:ignore[attr-defined]
  246. handler.run(app)
  247. def serve(configuration: config.Configuration,
  248. shutdown_socket: Optional[socket.socket] = None) -> None:
  249. """Serve radicale from configuration.
  250. `shutdown_socket` can be used to gracefully shutdown the server.
  251. The socket can be created with `socket.socketpair()`, when the other socket
  252. gets closed the server stops accepting new requests by clients and the
  253. function returns after all active requests are finished.
  254. """
  255. if os.environ.get("PYTHONPATH"):
  256. info = "with PYTHONPATH=%r " % os.environ.get("PYTHONPATH")
  257. else:
  258. info = ""
  259. logger.info("Starting Radicale %s(%s) as %s on %s", info, utils.packages_version(), utils.user_groups_as_string(), platform.platform())
  260. # Copy configuration before modifying
  261. configuration = configuration.copy()
  262. configuration.update({"server": {"_internal_server": "True"}}, "server",
  263. privileged=True)
  264. use_ssl: bool = configuration.get("server", "ssl")
  265. server_class = ParallelHTTPSServer if use_ssl else ParallelHTTPServer
  266. application = Application(configuration)
  267. servers = {}
  268. try:
  269. hosts: List[Tuple[str, int]] = configuration.get("server", "hosts")
  270. for address_port in hosts:
  271. # retrieve IPv4/IPv6 address of address
  272. try:
  273. getaddrinfo = socket.getaddrinfo(address_port[0], address_port[1], 0, socket.SOCK_STREAM, socket.IPPROTO_TCP)
  274. except OSError as e:
  275. logger.warning("cannot retrieve IPv4 or IPv6 address of '%s': %s" % (utils.format_address(address_port), e))
  276. continue
  277. logger.debug("getaddrinfo of '%s': %s" % (utils.format_address(address_port), getaddrinfo))
  278. for (address_family, socket_kind, socket_proto, socket_flags, socket_address) in getaddrinfo:
  279. logger.debug("try to create server socket on '%s'" % (utils.format_address(socket_address)))
  280. try:
  281. server = server_class(configuration, address_family, (socket_address[0], socket_address[1]), RequestHandler)
  282. except OSError as e:
  283. logger.warning("cannot create server socket on '%s': %s" % (utils.format_address(socket_address), e))
  284. continue
  285. servers[server.socket] = server
  286. server.set_app(application)
  287. logger.info("Listening on %r%s",
  288. utils.format_address(server.server_address),
  289. " with SSL" if use_ssl else "")
  290. if not servers:
  291. raise RuntimeError("No servers started")
  292. # Mainloop
  293. select_timeout = None
  294. if sys.platform == "win32":
  295. # Fallback to busy waiting. (select(...) blocks SIGINT on Windows.)
  296. select_timeout = 1.0
  297. max_connections: int = configuration.get("server", "max_connections")
  298. logger.info("Radicale server ready")
  299. logger.debug("TRACE: Radicale server ready ('logging/trace_on_debug' is active)")
  300. logger.debug("TRACE/SERVER: Radicale server ready ('logging/trace_on_debug' is active - either with 'SERVER' or empty filter)")
  301. while True:
  302. rlist: List[socket.socket] = []
  303. # Wait for finished clients
  304. for server in servers.values():
  305. rlist.extend(server.worker_sockets)
  306. # Accept new connections if max_connections is not reached
  307. if max_connections <= 0 or len(rlist) < max_connections:
  308. rlist.extend(servers)
  309. # Use socket to get notified of program shutdown
  310. if shutdown_socket is not None:
  311. rlist.append(shutdown_socket)
  312. rlist, _, _ = select.select(rlist, [], [], select_timeout)
  313. rset = set(rlist)
  314. if shutdown_socket in rset:
  315. logger.info("Stopping Radicale")
  316. break
  317. for server in servers.values():
  318. finished_sockets = server.worker_sockets.intersection(rset)
  319. for s in finished_sockets:
  320. s.close()
  321. server.worker_sockets.remove(s)
  322. rset.remove(s)
  323. if finished_sockets:
  324. server.service_actions()
  325. if rset:
  326. active_server = servers.get(rset.pop())
  327. if active_server:
  328. active_server.handle_request()
  329. finally:
  330. # Wait for clients to finish and close servers
  331. for server in servers.values():
  332. for s in server.worker_sockets:
  333. s.recv(1)
  334. s.close()
  335. server.server_close()