From d26e60724a72083836a0717608904f1e553f8510 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sun, 29 Oct 2023 16:45:09 +0100 Subject: [PATCH] Socket listening and socket activation enabled on tornado Refactoring of server code Updates requirements Improved requirements parsing for versions containing letters --- cps/dep_check.py | 2 +- cps/server.py | 84 +++++++++++++++++++++++++++++++----------------- requirements.txt | 6 ++-- 3 files changed, 59 insertions(+), 33 deletions(-) diff --git a/cps/dep_check.py b/cps/dep_check.py index bc015756..34d0e24b 100644 --- a/cps/dep_check.py +++ b/cps/dep_check.py @@ -61,7 +61,7 @@ def dependency_check(optional=False): deps = load_dependencies(optional) for dep in deps: try: - dep_version_int = [int(x) for x in dep[0].split('.')] + dep_version_int = [int(x) if x.isnumeric() else 0 for x in dep[0].split('.')] low_check = [int(x) for x in dep[3].split('.')] high_check = [int(x) for x in dep[5].split('.')] except AttributeError: diff --git a/cps/server.py b/cps/server.py index 193be9d5..79b342d5 100644 --- a/cps/server.py +++ b/cps/server.py @@ -21,10 +21,9 @@ import os import errno import signal import socket -import subprocess # nosec try: - from gevent.pywsgi import WSGIServer + from gevwent.pywsgi import WSGIServer from .gevent_wsgi import MyWSGIHandler from gevent.pool import Pool from gevent.socket import socket as GeventSocket @@ -37,6 +36,7 @@ except ImportError: from .tornado_wsgi import MyWSGIContainer from tornado.httpserver import HTTPServer from tornado.ioloop import IOLoop + from tornado.netutil import bind_unix_socket from tornado import version as _version VERSION = 'Tornado ' + _version _GEVENT = False @@ -101,7 +101,7 @@ class WebServer(object): SD_LISTEN_FDS_START = 3 return GeventSocket(fileno=SD_LISTEN_FDS_START) - def _make_gevent_unix_socket(self, socket_file): + def _prepare_unix_socket(self, socket_file): # the socket file must not exist prior to bind() if os.path.exists(socket_file): # avoid nuking regular files and symbolic links (could be a mistype or security issue) @@ -109,32 +109,34 @@ class WebServer(object): raise OSError(errno.EEXIST, os.strerror(errno.EEXIST), socket_file) os.remove(socket_file) - unix_sock = WSGIServer.get_listener(socket_file, family=socket.AF_UNIX) self.unix_socket_file = socket_file - # ensure current user and group have r/w permissions, no permissions for other users - # this way the socket can be shared in a semi-secure manner - # between the user running calibre-web and the user running the fronting webserver - os.chmod(socket_file, 0o660) - - return unix_sock - - def _make_gevent_socket(self): + def _make_gevent_listener(self): if os.name != 'nt': socket_activated = os.environ.get("LISTEN_FDS") if socket_activated: sock = self._make_gevent_socket_activated() - return sock, sock.getsockname() + sock_info = sock.getsockname() + return sock, "systemd-socket:" + _readable_listen_address(sock_info[0], sock_info[1]) unix_socket_file = os.environ.get("CALIBRE_UNIX_SOCKET") if unix_socket_file: - return self._make_gevent_unix_socket(unix_socket_file), "unix:" + unix_socket_file + self._prepare_unix_socket(unix_socket_file) + unix_sock = WSGIServer.get_listener(unix_socket_file, family=socket.AF_UNIX) + # ensure current user and group have r/w permissions, no permissions for other users + # this way the socket can be shared in a semi-secure manner + # between the user running calibre-web and the user running the fronting webserver + os.chmod(unix_socket_file, 0o660) + + return unix_sock, "unix:" + unix_socket_file if self.listen_address: - return (self.listen_address, self.listen_port), None + return ((self.listen_address, self.listen_port), + _readable_listen_address(self.listen_address, self.listen_port)) if os.name == 'nt': self.listen_address = '0.0.0.0' - return (self.listen_address, self.listen_port), None + return ((self.listen_address, self.listen_port), + _readable_listen_address(self.listen_address, self.listen_port)) try: address = ('::', self.listen_port) @@ -211,9 +213,7 @@ class WebServer(object): ssl_args = self.ssl_args or {} try: - sock, output = self._make_gevent_socket() - if output is None: - output = _readable_listen_address(self.listen_address, self.listen_port) + sock, output = self._make_gevent_listener() log.info('Starting Gevent server on %s', output) self.wsgiserver = WSGIServer(sock, self.app, log=self.access_logger, handler_class=MyWSGIHandler, error_log=log, @@ -238,17 +238,43 @@ class WebServer(object): if os.name == 'nt' and sys.version_info > (3, 7): import asyncio asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) - log.info('Starting Tornado server on %s', _readable_listen_address(self.listen_address, self.listen_port)) + try: + # Max Buffersize set to 200MB + http_server = HTTPServer(MyWSGIContainer(self.app), + max_buffer_size=209700000, + ssl_options=self.ssl_args) - # Max Buffersize set to 200MB - http_server = HTTPServer(MyWSGIContainer(self.app), - max_buffer_size=209700000, - ssl_options=self.ssl_args) - http_server.listen(self.listen_port, self.listen_address) - self.wsgiserver = IOLoop.current() - self.wsgiserver.start() - # wait for stop signal - self.wsgiserver.close(True) + unix_socket_file = os.environ.get("CALIBRE_UNIX_SOCKET") + if os.environ.get("LISTEN_FDS") and os.name != 'nt': + SD_LISTEN_FDS_START = 3 + sock = socket.socket(fileno=SD_LISTEN_FDS_START) + http_server.add_socket(sock) + sock.setblocking(0) + socket_name =sock.getsockname() + output = "systemd-socket:" + _readable_listen_address(socket_name[0], socket_name[1]) + # log.error("Tornado server isn't supporting socket activation, normal port is used") + elif unix_socket_file and os.name != 'nt': + self._prepare_unix_socket(unix_socket_file) + output = "unix:" + unix_socket_file + unix_socket = bind_unix_socket(self.unix_socket_file) + http_server.add_socket(unix_socket) + # ensure current user and group have r/w permissions, no permissions for other users + # this way the socket can be shared in a semi-secure manner + # between the user running calibre-web and the user running the fronting webserver + os.chmod(self.unix_socket_file, 0o660) + else: + output = _readable_listen_address(self.listen_address, self.listen_port) + http_server.listen(self.listen_port, self.listen_address) + log.info('Starting Tornado server on %s', output) + + self.wsgiserver = IOLoop.current() + self.wsgiserver.start() + # wait for stop signal + self.wsgiserver.close(True) + finally: + if self.unix_socket_file: + os.remove(self.unix_socket_file) + self.unix_socket_file = None def start(self): try: diff --git a/requirements.txt b/requirements.txt index e384ed87..f1e5b712 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ Werkzeug<3.0.0 APScheduler>=3.6.3,<3.11.0 Babel>=1.3,<3.0 -Flask-Babel>=0.11.1,<3.2.0 +Flask-Babel>=0.11.1,<4.1.0 Flask-Login>=0.3.2,<0.6.3 Flask-Principal>=0.3.2,<0.5.1 Flask>=1.0.2,<2.4.0 @@ -14,7 +14,7 @@ tornado>=6.3,<6.4 Wand>=0.4.4,<0.7.0 unidecode>=0.04.19,<1.4.0 lxml>=3.8.0,<5.0.0 -flask-wtf>=0.14.2,<1.2.0 +flask-wtf>=0.14.2,<1.3.0 chardet>=3.0.0,<4.1.0 advocate>=1.0.0,<1.1.0 -Flask-Limiter>=2.3.0,<3.5.0 +Flask-Limiter>=2.3.0,<3.6.0