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:
parent
35785be5dc
commit
7beca7a1a3
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue