app: expand support for HTTP arguments

Request arguments may now be parsed from the path, body, and query string.

Signed-off-by: Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
Arksine 2021-02-28 17:17:20 -05:00
parent 62324650e5
commit fb24917f1a
1 changed files with 90 additions and 73 deletions

View File

@ -27,56 +27,6 @@ RESERVED_ENDPOINTS = [
EXCLUDED_ARGS = ["_", "token", "connection_id"] EXCLUDED_ARGS = ["_", "token", "connection_id"]
DEFAULT_KLIPPY_LOG_PATH = "/tmp/klippy.log" DEFAULT_KLIPPY_LOG_PATH = "/tmp/klippy.log"
# Converts query string values with type hints
def _convert_type(value, hint):
type_funcs = {
"int": int, "float": float,
"bool": lambda x: x.lower() == "true",
"json": json.loads}
if hint not in type_funcs:
logging.info(f"No conversion method for type hint {hint}")
return value
func = type_funcs[hint]
try:
converted = func(value)
except Exception:
logging.exception("Argument conversion error: Hint: "
f"{hint}, Arg: {value}")
return value
return converted
# Status objects require special parsing
def _status_parser(request_handler):
request = request_handler.request
arg_list = request.arguments.keys()
args = {}
for key in arg_list:
if key in EXCLUDED_ARGS:
continue
val = request_handler.get_argument(key)
if not val:
args[key] = None
else:
args[key] = val.split(',')
logging.debug(f"Parsed Arguments: {args}")
return {'objects': args}
# Built-in Query String Parser
def _default_parser(request_handler):
request = request_handler.request
arg_list = request.arguments.keys()
args = {}
for key in arg_list:
if key in EXCLUDED_ARGS:
continue
key_parts = key.rsplit(":", 1)
val = request_handler.get_argument(key)
if len(key_parts) == 1:
args[key] = val
else:
args[key_parts[0]] = _convert_type(val, key_parts[1])
return args
class MutableRouter(tornado.web.ReversibleRuleRouter): class MutableRouter(tornado.web.ReversibleRuleRouter):
def __init__(self, application): def __init__(self, application):
self.application = application self.application = application
@ -196,14 +146,15 @@ class MoonrakerApp:
f"Websocket: {', '.join(api_def.ws_methods)}") f"Websocket: {', '.join(api_def.ws_methods)}")
self.wsm.register_remote_handler(api_def) self.wsm.register_remote_handler(api_def)
params = {} params = {}
params['arg_parser'] = api_def.parser params['query_parser'] = api_def.parser
params['remote_callback'] = api_def.endpoint params['remote_callback'] = api_def.endpoint
self.mutable_router.add_handler( self.mutable_router.add_handler(
api_def.uri, RemoteRequestHandler, params) api_def.uri, RemoteRequestHandler, params)
self.registered_base_handlers.append(api_def.uri) self.registered_base_handlers.append(api_def.uri)
def register_local_handler(self, uri, request_methods, def register_local_handler(self, uri, request_methods,
callback, protocol=["http", "websocket"]): callback, protocol=["http", "websocket"],
wrap_result=True):
if uri in self.registered_base_handlers: if uri in self.registered_base_handlers:
return return
api_def = self._create_api_definition( api_def = self._create_api_definition(
@ -213,8 +164,9 @@ class MoonrakerApp:
msg += f" - HTTP: ({' '.join(request_methods)}) {uri}" msg += f" - HTTP: ({' '.join(request_methods)}) {uri}"
params = {} params = {}
params['methods'] = request_methods params['methods'] = request_methods
params['arg_parser'] = api_def.parser params['query_parser'] = api_def.parser
params['callback'] = callback params['callback'] = callback
params['wrap_result'] = wrap_result
self.mutable_router.add_handler(uri, LocalRequestHandler, params) self.mutable_router.add_handler(uri, LocalRequestHandler, params)
self.registered_base_handlers.append(uri) self.registered_base_handlers.append(uri)
if "websocket" in protocol: if "websocket" in protocol:
@ -277,9 +229,9 @@ class MoonrakerApp:
"Invalid API definition. Number of websocket methods must " "Invalid API definition. Number of websocket methods must "
"match the number of request methods") "match the number of request methods")
if endpoint.startswith("objects/"): if endpoint.startswith("objects/"):
parser = _status_parser parser = "_status_parser"
else: else:
parser = _default_parser parser = "_default_parser"
api_def = APIDefinition(endpoint, uri, ws_methods, api_def = APIDefinition(endpoint, uri, ws_methods,
request_methods, parser) request_methods, parser)
@ -287,21 +239,84 @@ class MoonrakerApp:
return api_def return api_def
# ***** Dynamic Handlers***** # ***** Dynamic Handlers*****
class RemoteRequestHandler(AuthorizedRequestHandler): class DynamicRequestBase(AuthorizedRequestHandler):
def initialize(self, remote_callback, arg_parser): def initialize(self, query_parser):
super(RemoteRequestHandler, self).initialize() super(DynamicRequestBase, self).initialize()
self.remote_callback = remote_callback try:
self.query_parser = arg_parser self.query_parser = getattr(self, query_parser)
except Exception:
self.query_parser = lambda: {}
async def get(self): # Converts query string values with type hints
def _convert_type(value, hint):
type_funcs = {
"int": int, "float": float,
"bool": lambda x: x.lower() == "true",
"json": json.loads}
if hint not in type_funcs:
logging.info(f"No conversion method for type hint {hint}")
return value
func = type_funcs[hint]
try:
converted = func(value)
except Exception:
logging.exception("Argument conversion error: Hint: "
f"{hint}, Arg: {value}")
return value
return converted
def _default_parser(self):
args = {}
for key in self.request.arguments.keys():
if key in EXCLUDED_ARGS:
continue
key_parts = key.rsplit(":", 1)
val = self.get_argument(key)
if len(key_parts) == 1:
args[key] = val
else:
args[key_parts[0]] = self._convert_type(val, key_parts[1])
return args
def _status_parser(self):
args = {}
for key in self.request.arguments.keys():
if key in EXCLUDED_ARGS:
continue
val = self.get_argument(key)
if not val:
args[key] = None
else:
args[key] = val.split(',')
logging.debug(f"Parsed Arguments: {args}")
return {'objects': args}
def parse_args(self):
args = self.query_parser()
if self.request.headers.get('Content-Type', "") == "application/json":
try:
args.update(json.loads(self.request.body))
except json.JSONDecodeError:
pass
for key, value in self.path_kwargs.items():
if value is not None:
args[key] = value
return args
class RemoteRequestHandler(DynamicRequestBase):
def initialize(self, remote_callback, query_parser):
super(RemoteRequestHandler, self).initialize(query_parser)
self.remote_callback = remote_callback
async def get(self, *args, **kwargs):
await self._process_http_request() await self._process_http_request()
async def post(self): async def post(self, *args, **kwargs):
await self._process_http_request() await self._process_http_request()
async def _process_http_request(self): async def _process_http_request(self):
conn = self.get_associated_websocket() conn = self.get_associated_websocket()
args = self.query_parser(self) args = self.parse_args()
try: try:
result = await self.server.make_request( result = await self.server.make_request(
WebRequest(self.remote_callback, args, conn=conn)) WebRequest(self.remote_callback, args, conn=conn))
@ -310,26 +325,26 @@ class RemoteRequestHandler(AuthorizedRequestHandler):
e.status_code, str(e)) from e e.status_code, str(e)) from e
self.finish({'result': result}) self.finish({'result': result})
class LocalRequestHandler(AuthorizedRequestHandler): class LocalRequestHandler(DynamicRequestBase):
def initialize(self, callback, methods, arg_parser): def initialize(self, callback, methods, query_parser, wrap_result):
super(LocalRequestHandler, self).initialize() super(LocalRequestHandler, self).initialize(query_parser)
self.callback = callback self.callback = callback
self.methods = methods self.methods = methods
self.query_parser = arg_parser self.wrap_result = wrap_result
async def get(self): async def get(self, *args, **kwargs):
if 'GET' in self.methods: if 'GET' in self.methods:
await self._process_http_request('GET') await self._process_http_request('GET')
else: else:
raise tornado.web.HTTPError(405) raise tornado.web.HTTPError(405)
async def post(self): async def post(self, *args, **kwargs):
if 'POST' in self.methods: if 'POST' in self.methods:
await self._process_http_request('POST') await self._process_http_request('POST')
else: else:
raise tornado.web.HTTPError(405) raise tornado.web.HTTPError(405)
async def delete(self): async def delete(self, *args, **kwargs):
if 'DELETE' in self.methods: if 'DELETE' in self.methods:
await self._process_http_request('DELETE') await self._process_http_request('DELETE')
else: else:
@ -337,15 +352,17 @@ class LocalRequestHandler(AuthorizedRequestHandler):
async def _process_http_request(self, method): async def _process_http_request(self, method):
conn = self.get_associated_websocket() conn = self.get_associated_websocket()
args = self.query_parser(self) args = self.parse_args()
try: try:
result = await self.callback( result = await self.callback(
WebRequest(self.request.path, args, method, conn=conn)) WebRequest(self.request.path, args, method, conn=conn))
except ServerError as e: except ServerError as e:
raise tornado.web.HTTPError( raise tornado.web.HTTPError(
e.status_code, str(e)) from e e.status_code, str(e)) from e
self.finish({'result': result}) if self.wrap_result:
self.finish({'result': result})
else:
self.finish(result)
class FileRequestHandler(AuthorizedFileHandler): class FileRequestHandler(AuthorizedFileHandler):
def set_extra_headers(self, path): def set_extra_headers(self, path):