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 inspect import isclass
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.httpserver import HTTPServer
from tornado.log import access_log
from ..utils import ServerError, source_info, parse_ip_address
from ..common import (
@ -50,8 +51,8 @@ from typing import (
Type
)
if TYPE_CHECKING:
from tornado.httpserver import HTTPServer
from tornado.websocket import WebSocketHandler
from tornado.httputil import HTTPMessageDelegate, HTTPServerRequest
from ..server import Server
from ..eventloop import EventLoop
from ..confighelper import ConfigHelper
@ -76,8 +77,8 @@ EXCLUDED_ARGS = ["_", "token", "access_token", "connection_id"]
AUTHORIZED_EXTS = [".png", ".jpg"]
DEFAULT_KLIPPY_LOG_PATH = "/tmp/klippy.log"
class MutableRouter(tornado.web.ReversibleRuleRouter):
def __init__(self, application: MoonrakerApp) -> None:
class MutableRouter(RuleRouter):
def __init__(self, application: tornado.web.Application) -> None:
self.application = application
self.pattern_to_rule: Dict[str, Rule] = {}
super(MutableRouter, self).__init__(None)
@ -89,8 +90,8 @@ class MutableRouter(tornado.web.ReversibleRuleRouter):
) -> MessageDelgate:
if isclass(target) and issubclass(target, tornado.web.RequestHandler):
return self.application.get_handler_delegate(
request, target, **target_params)
request, target, **target_params
)
return super(MutableRouter, self).get_target_delegate(
target, request, **target_params)
@ -100,7 +101,7 @@ class MutableRouter(tornado.web.ReversibleRuleRouter):
def add_handler(self,
pattern: str,
target: Any,
target_params: Optional[Dict[str, Any]]
target_params: Optional[Dict[str, Any]] = None
) -> None:
if pattern in self.pattern_to_rule:
self.remove_handler(pattern)
@ -116,6 +117,56 @@ class MutableRouter(tornado.web.ReversibleRuleRouter):
except Exception:
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):
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 *= 1024 * 1024
max_ws_conns = config.getint(
'max_websocket_connections', MAX_WS_CONNS_DEFAULT
)
# SSL config
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', '.cfg')
app_args: Dict[str, Any] = {
'serve_traceback': self.server.is_verbose_enabled(),
'websocket_ping_interval': 10,
'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),
# Set up HTTP routing. Our "mutable_router" wraps a Tornado Application
self.mutable_router = PrimaryRouter(config)
for (ptrn, hdlr) in (
(home_pattern, WelcomeHandler),
(f"{self._route_prefix}/server/redirect", RedirectHandler),
(f"{self._route_prefix}/server/jsonrpc", RPCHandler)
]
self.app = tornado.web.Application(app_handlers, **app_args)
self.get_handler_delegate = self.app.get_handler_delegate
):
self.mutable_router.add_handler(ptrn, hdlr, None)
# Register handlers
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:
if host.lower() == "all":
host = ""
self.http_server = self.app.listen(
port, address=host, max_body_size=MAX_BODY_SIZE,
xheaders=True)
self.http_server = self._create_http_server(port, host)
if self.https_enabled():
logging.info(f"Starting secure server on port {ssl_port}")
ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_ctx.load_cert_chain(self.cert_path, self.key_path)
self.secure_server = self.app.listen(
ssl_port, address=host, max_body_size=MAX_BODY_SIZE,
xheaders=True, ssl_options=ssl_ctx)
self.secure_server = self._create_http_server(
ssl_port, host, ssl_options=ssl_ctx
)
else:
logging.info("SSL Certificate/Key not configured, "
"aborting HTTPS Server startup")
def log_request(self, handler: tornado.web.RequestHandler) -> None:
status_code = handler.get_status()
if (
not self.server.is_verbose_enabled()
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")
def _create_http_server(self, port: int, address: str, **kwargs) -> HTTPServer:
args: Dict[str, Any] = dict(max_body_size=MAX_BODY_SIZE, xheaders=True)
args.update(kwargs)
svr = HTTPServer(self.mutable_router, **args)
svr.listen(port, address)
return svr
def get_server(self) -> Server:
return self.server