application: refactor HTTP routing

Moonraker dynamically registers its routes, so we cannot easily
use the routers provided by tornado.Application.  Previously
all routes went through tornado.Application, then went to
our mutable router.  This refactor avoids that by having our
mutable router contain the tornadoapp instance, only using
it to provide the application delegate.

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
Eric Callahan 2024-01-14 11:17:20 -05:00
parent 35785be5dc
commit 7beca7a1a3
No known key found for this signature in database
GPG Key ID: 5A1EB336DFB4C71B
1 changed files with 73 additions and 57 deletions

View File

@ -19,8 +19,9 @@ import tornado.web
from asyncio import Lock from asyncio import Lock
from inspect import isclass from inspect import isclass
from tornado.escape import url_unescape, url_escape from tornado.escape import url_unescape, url_escape
from tornado.routing import Rule, PathMatches, AnyMatches from tornado.routing import Rule, PathMatches, RuleRouter
from tornado.http1connection import HTTP1Connection from tornado.http1connection import HTTP1Connection
from tornado.httpserver import HTTPServer
from tornado.log import access_log from tornado.log import access_log
from ..utils import ServerError, source_info, parse_ip_address from ..utils import ServerError, source_info, parse_ip_address
from ..common import ( from ..common import (
@ -50,8 +51,8 @@ from typing import (
Type Type
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from tornado.httpserver import HTTPServer
from tornado.websocket import WebSocketHandler from tornado.websocket import WebSocketHandler
from tornado.httputil import HTTPMessageDelegate, HTTPServerRequest
from ..server import Server from ..server import Server
from ..eventloop import EventLoop from ..eventloop import EventLoop
from ..confighelper import ConfigHelper from ..confighelper import ConfigHelper
@ -76,8 +77,8 @@ EXCLUDED_ARGS = ["_", "token", "access_token", "connection_id"]
AUTHORIZED_EXTS = [".png", ".jpg"] AUTHORIZED_EXTS = [".png", ".jpg"]
DEFAULT_KLIPPY_LOG_PATH = "/tmp/klippy.log" DEFAULT_KLIPPY_LOG_PATH = "/tmp/klippy.log"
class MutableRouter(tornado.web.ReversibleRuleRouter): class MutableRouter(RuleRouter):
def __init__(self, application: MoonrakerApp) -> None: def __init__(self, application: tornado.web.Application) -> None:
self.application = application self.application = application
self.pattern_to_rule: Dict[str, Rule] = {} self.pattern_to_rule: Dict[str, Rule] = {}
super(MutableRouter, self).__init__(None) super(MutableRouter, self).__init__(None)
@ -89,8 +90,8 @@ class MutableRouter(tornado.web.ReversibleRuleRouter):
) -> MessageDelgate: ) -> MessageDelgate:
if isclass(target) and issubclass(target, tornado.web.RequestHandler): if isclass(target) and issubclass(target, tornado.web.RequestHandler):
return self.application.get_handler_delegate( return self.application.get_handler_delegate(
request, target, **target_params) request, target, **target_params
)
return super(MutableRouter, self).get_target_delegate( return super(MutableRouter, self).get_target_delegate(
target, request, **target_params) target, request, **target_params)
@ -100,7 +101,7 @@ class MutableRouter(tornado.web.ReversibleRuleRouter):
def add_handler(self, def add_handler(self,
pattern: str, pattern: str,
target: Any, target: Any,
target_params: Optional[Dict[str, Any]] target_params: Optional[Dict[str, Any]] = None
) -> None: ) -> None:
if pattern in self.pattern_to_rule: if pattern in self.pattern_to_rule:
self.remove_handler(pattern) self.remove_handler(pattern)
@ -116,6 +117,56 @@ class MutableRouter(tornado.web.ReversibleRuleRouter):
except Exception: except Exception:
logging.exception(f"Unable to remove rule: {pattern}") logging.exception(f"Unable to remove rule: {pattern}")
class PrimaryRouter(MutableRouter):
def __init__(self, config: ConfigHelper) -> None:
server = config.get_server()
max_ws_conns = config.getint('max_websocket_connections', MAX_WS_CONNS_DEFAULT)
self.verbose_logging = server.is_verbose_enabled()
app_args: Dict[str, Any] = {
'serve_traceback': self.verbose_logging,
'websocket_ping_interval': 10,
'websocket_ping_timeout': 30,
'server': server,
'max_websocket_connections': max_ws_conns,
'log_function': self.log_request
}
super().__init__(tornado.web.Application(**app_args))
@property
def tornado_app(self) -> tornado.web.Application:
return self.application
def find_handler(
self, request: HTTPServerRequest, **kwargs: Any
) -> Optional[HTTPMessageDelegate]:
hdlr = super().find_handler(request, **kwargs)
if hdlr is not None:
return hdlr
return self.application.get_handler_delegate(request, AuthorizedErrorHandler)
def log_request(self, handler: tornado.web.RequestHandler) -> None:
status_code = handler.get_status()
if (
not self.verbose_logging and
status_code in [200, 204, 206, 304]
):
# don't log successful requests in release mode
return
if status_code < 400:
log_method = access_log.info
elif status_code < 500:
log_method = access_log.warning
else:
log_method = access_log.error
request_time = 1000.0 * handler.request.request_time()
user = handler.current_user
username = "No User"
if user is not None and 'username' in user:
username = user['username']
log_method(
f"{status_code} {handler._request_summary()} "
f"[{username}] {request_time:.2f}ms"
)
class InternalTransport(APITransport): class InternalTransport(APITransport):
def __init__(self, server: Server) -> None: def __init__(self, server: Server) -> None:
@ -149,9 +200,6 @@ class MoonrakerApp:
] ]
self.max_upload_size = config.getint('max_upload_size', 1024) self.max_upload_size = config.getint('max_upload_size', 1024)
self.max_upload_size *= 1024 * 1024 self.max_upload_size *= 1024 * 1024
max_ws_conns = config.getint(
'max_websocket_connections', MAX_WS_CONNS_DEFAULT
)
# SSL config # SSL config
self.cert_path: pathlib.Path = self._get_path_option( self.cert_path: pathlib.Path = self._get_path_option(
@ -180,28 +228,14 @@ class MoonrakerApp:
mimetypes.add_type('text/plain', '.gcode') mimetypes.add_type('text/plain', '.gcode')
mimetypes.add_type('text/plain', '.cfg') mimetypes.add_type('text/plain', '.cfg')
app_args: Dict[str, Any] = { # Set up HTTP routing. Our "mutable_router" wraps a Tornado Application
'serve_traceback': self.server.is_verbose_enabled(), self.mutable_router = PrimaryRouter(config)
'websocket_ping_interval': 10, for (ptrn, hdlr) in (
'websocket_ping_timeout': 30,
'server': self.server,
'max_websocket_connections': max_ws_conns,
'default_handler_class': AuthorizedErrorHandler,
'default_handler_args': {},
'log_function': self.log_request,
'compiled_template_cache': False,
}
# Set up HTTP only requests
self.mutable_router = MutableRouter(self)
app_handlers: List[Any] = [
(AnyMatches(), self.mutable_router),
(home_pattern, WelcomeHandler), (home_pattern, WelcomeHandler),
(f"{self._route_prefix}/server/redirect", RedirectHandler), (f"{self._route_prefix}/server/redirect", RedirectHandler),
(f"{self._route_prefix}/server/jsonrpc", RPCHandler) (f"{self._route_prefix}/server/jsonrpc", RPCHandler)
] ):
self.app = tornado.web.Application(app_handlers, **app_args) self.mutable_router.add_handler(ptrn, hdlr, None)
self.get_handler_delegate = self.app.get_handler_delegate
# Register handlers # Register handlers
logfile = self.server.get_app_args().get('log_file') logfile = self.server.get_app_args().get('log_file')
@ -252,42 +286,24 @@ class MoonrakerApp:
def listen(self, host: str, port: int, ssl_port: int) -> None: def listen(self, host: str, port: int, ssl_port: int) -> None:
if host.lower() == "all": if host.lower() == "all":
host = "" host = ""
self.http_server = self.app.listen( self.http_server = self._create_http_server(port, host)
port, address=host, max_body_size=MAX_BODY_SIZE,
xheaders=True)
if self.https_enabled(): if self.https_enabled():
logging.info(f"Starting secure server on port {ssl_port}") logging.info(f"Starting secure server on port {ssl_port}")
ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_ctx.load_cert_chain(self.cert_path, self.key_path) ssl_ctx.load_cert_chain(self.cert_path, self.key_path)
self.secure_server = self.app.listen( self.secure_server = self._create_http_server(
ssl_port, address=host, max_body_size=MAX_BODY_SIZE, ssl_port, host, ssl_options=ssl_ctx
xheaders=True, ssl_options=ssl_ctx) )
else: else:
logging.info("SSL Certificate/Key not configured, " logging.info("SSL Certificate/Key not configured, "
"aborting HTTPS Server startup") "aborting HTTPS Server startup")
def log_request(self, handler: tornado.web.RequestHandler) -> None: def _create_http_server(self, port: int, address: str, **kwargs) -> HTTPServer:
status_code = handler.get_status() args: Dict[str, Any] = dict(max_body_size=MAX_BODY_SIZE, xheaders=True)
if ( args.update(kwargs)
not self.server.is_verbose_enabled() svr = HTTPServer(self.mutable_router, **args)
and status_code in [200, 204, 206, 304] svr.listen(port, address)
): return svr
# don't log successful requests in release mode
return
if status_code < 400:
log_method = access_log.info
elif status_code < 500:
log_method = access_log.warning
else:
log_method = access_log.error
request_time = 1000.0 * handler.request.request_time()
user = handler.current_user
username = "No User"
if user is not None and 'username' in user:
username = user['username']
log_method(
f"{status_code} {handler._request_summary()} "
f"[{username}] {request_time:.2f}ms")
def get_server(self) -> Server: def get_server(self) -> Server:
return self.server return self.server