app: add jsonrpc HTTP endpoint
Add a POST /server/jsonrpc endpoint that processes jsonrpc requests from the body. This allows developers familiar with the JSON-RPC API to use it in places where a websocket is not desiriable. Signed-off-by: Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
parent
6dbcdf537f
commit
aa16dd671b
@ -22,7 +22,7 @@ from tornado.escape import url_unescape, url_escape
|
|||||||
from tornado.routing import Rule, PathMatches, AnyMatches
|
from tornado.routing import Rule, PathMatches, AnyMatches
|
||||||
from tornado.http1connection import HTTP1Connection
|
from tornado.http1connection import HTTP1Connection
|
||||||
from tornado.log import access_log
|
from tornado.log import access_log
|
||||||
from .utils import ServerError, source_info
|
from .utils import ServerError, source_info, parse_ip_address
|
||||||
from .common import (
|
from .common import (
|
||||||
JsonRPC,
|
JsonRPC,
|
||||||
WebRequest,
|
WebRequest,
|
||||||
@ -59,6 +59,7 @@ if TYPE_CHECKING:
|
|||||||
from .eventloop import EventLoop
|
from .eventloop import EventLoop
|
||||||
from .confighelper import ConfigHelper
|
from .confighelper import ConfigHelper
|
||||||
from .klippy_connection import KlippyConnection as Klippy
|
from .klippy_connection import KlippyConnection as Klippy
|
||||||
|
from .utils import IPAddress
|
||||||
from .components.file_manager.file_manager import FileManager
|
from .components.file_manager.file_manager import FileManager
|
||||||
from .components.announcements import Announcements
|
from .components.announcements import Announcements
|
||||||
from .components.machine import Machine
|
from .components.machine import Machine
|
||||||
@ -144,7 +145,10 @@ class MoonrakerApp:
|
|||||||
self.http_server: Optional[HTTPServer] = None
|
self.http_server: Optional[HTTPServer] = None
|
||||||
self.secure_server: Optional[HTTPServer] = None
|
self.secure_server: Optional[HTTPServer] = None
|
||||||
self.template_cache: Dict[str, JinjaTemplate] = {}
|
self.template_cache: Dict[str, JinjaTemplate] = {}
|
||||||
self.registered_base_handlers: List[str] = []
|
self.registered_base_handlers: List[str] = [
|
||||||
|
"/server/redirect",
|
||||||
|
"/server/jsonrpc"
|
||||||
|
]
|
||||||
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_ws_conns = config.getint(
|
||||||
@ -197,7 +201,8 @@ class MoonrakerApp:
|
|||||||
(home_pattern, WelcomeHandler),
|
(home_pattern, WelcomeHandler),
|
||||||
(f"{self._route_prefix}/websocket", WebSocket),
|
(f"{self._route_prefix}/websocket", WebSocket),
|
||||||
(f"{self._route_prefix}/klippysocket", BridgeSocket),
|
(f"{self._route_prefix}/klippysocket", BridgeSocket),
|
||||||
(f"{self._route_prefix}/server/redirect", RedirectHandler)
|
(f"{self._route_prefix}/server/redirect", RedirectHandler),
|
||||||
|
(f"{self._route_prefix}/server/jsonrpc", RPCHandler)
|
||||||
]
|
]
|
||||||
self.app = tornado.web.Application(app_handlers, **app_args)
|
self.app = tornado.web.Application(app_handlers, **app_args)
|
||||||
self.get_handler_delegate = self.app.get_handler_delegate
|
self.get_handler_delegate = self.app.get_handler_delegate
|
||||||
@ -632,7 +637,7 @@ class DynamicRequestHandler(AuthorizedRequestHandler):
|
|||||||
req = f"{self.request.method} {self.request.path}"
|
req = f"{self.request.method} {self.request.path}"
|
||||||
self._log_debug(f"HTTP Request::{req}", args)
|
self._log_debug(f"HTTP Request::{req}", args)
|
||||||
try:
|
try:
|
||||||
ip = self.request.remote_ip or ""
|
ip = parse_ip_address(self.request.remote_ip or "")
|
||||||
result = await self.api_defintion.request(
|
result = await self.api_defintion.request(
|
||||||
args, req_type, transport, ip, self.current_user
|
args, req_type, transport, ip, self.current_user
|
||||||
)
|
)
|
||||||
@ -651,6 +656,50 @@ class DynamicRequestHandler(AuthorizedRequestHandler):
|
|||||||
self.set_header("Content-Type", self.content_type)
|
self.set_header("Content-Type", self.content_type)
|
||||||
self.finish(result)
|
self.finish(result)
|
||||||
|
|
||||||
|
class RPCHandler(AuthorizedRequestHandler, APITransport):
|
||||||
|
def initialize(self) -> None:
|
||||||
|
super(RPCHandler, self).initialize()
|
||||||
|
self.auth_required = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def transport_type(self) -> TransportType:
|
||||||
|
return TransportType.HTTP
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_info(self) -> Optional[Dict[str, Any]]:
|
||||||
|
return self.current_user
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ip_addr(self) -> Optional[IPAddress]:
|
||||||
|
return parse_ip_address(self.request.remote_ip or "")
|
||||||
|
|
||||||
|
def screen_rpc_request(
|
||||||
|
self, api_def: APIDefinition, req_type: RequestType, args: Dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
if self.current_user is None and api_def.auth_required:
|
||||||
|
raise self.server.error("Unauthorized", 401)
|
||||||
|
if api_def.endpoint == "objects/subscribe":
|
||||||
|
raise self.server.error(
|
||||||
|
"Subscriptions not available for HTTP transport", 404
|
||||||
|
)
|
||||||
|
|
||||||
|
def send_status(self, status: Dict[str, Any], eventtime: float) -> None:
|
||||||
|
# Can't handle status updates. This should not be called, but
|
||||||
|
# we don't want to raise an exception if it is
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def post(self, *args, **kwargs) -> None:
|
||||||
|
content_type = self.request.headers.get('Content-Type', "").strip()
|
||||||
|
if not content_type.startswith("application/json"):
|
||||||
|
raise tornado.web.HTTPError(
|
||||||
|
400, "Invalid content type, application/json required"
|
||||||
|
)
|
||||||
|
rpc: JsonRPC = self.server.lookup_component("jsonrpc")
|
||||||
|
result = await rpc.dispatch(self.request.body, self)
|
||||||
|
if result is not None:
|
||||||
|
self.set_header("Content-Type", "application/json; charset=UTF-8")
|
||||||
|
self.finish(result)
|
||||||
|
|
||||||
class FileRequestHandler(AuthorizedFileHandler):
|
class FileRequestHandler(AuthorizedFileHandler):
|
||||||
def set_extra_headers(self, path: str) -> None:
|
def set_extra_headers(self, path: str) -> None:
|
||||||
# The call below shold never return an empty string,
|
# The call below shold never return an empty string,
|
||||||
|
@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import sys
|
import sys
|
||||||
import ipaddress
|
|
||||||
import logging
|
import logging
|
||||||
import copy
|
import copy
|
||||||
import re
|
import re
|
||||||
@ -36,11 +35,11 @@ if TYPE_CHECKING:
|
|||||||
from .server import Server
|
from .server import Server
|
||||||
from .websockets import WebsocketManager
|
from .websockets import WebsocketManager
|
||||||
from .components.authorization import Authorization
|
from .components.authorization import Authorization
|
||||||
|
from .utils import IPAddress
|
||||||
from asyncio import Future
|
from asyncio import Future
|
||||||
_T = TypeVar("_T")
|
_T = TypeVar("_T")
|
||||||
_C = TypeVar("_C", str, bool, float, int)
|
_C = TypeVar("_C", str, bool, float, int)
|
||||||
_F = TypeVar("_F", bound="ExtendedFlag")
|
_F = TypeVar("_F", bound="ExtendedFlag")
|
||||||
IPUnion = Union[ipaddress.IPv4Address, ipaddress.IPv6Address]
|
|
||||||
ConvType = Union[str, bool, float, int]
|
ConvType = Union[str, bool, float, int]
|
||||||
ArgVal = Union[None, int, float, bool, str]
|
ArgVal = Union[None, int, float, bool, str]
|
||||||
RPCCallback = Callable[..., Coroutine]
|
RPCCallback = Callable[..., Coroutine]
|
||||||
@ -188,7 +187,7 @@ class APIDefinition:
|
|||||||
args: Dict[str, Any],
|
args: Dict[str, Any],
|
||||||
request_type: RequestType,
|
request_type: RequestType,
|
||||||
transport: Optional[APITransport] = None,
|
transport: Optional[APITransport] = None,
|
||||||
ip_addr: str = "",
|
ip_addr: Optional[IPAddress] = None,
|
||||||
user: Optional[Dict[str, Any]] = None
|
user: Optional[Dict[str, Any]] = None
|
||||||
) -> Coroutine:
|
) -> Coroutine:
|
||||||
return self.callback(
|
return self.callback(
|
||||||
@ -271,6 +270,14 @@ class APITransport:
|
|||||||
def transport_type(self) -> TransportType:
|
def transport_type(self) -> TransportType:
|
||||||
return TransportType.INTERNAL
|
return TransportType.INTERNAL
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_info(self) -> Optional[Dict[str, Any]]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ip_addr(self) -> Optional[IPAddress]:
|
||||||
|
return None
|
||||||
|
|
||||||
def screen_rpc_request(
|
def screen_rpc_request(
|
||||||
self, api_def: APIDefinition, req_type: RequestType, args: Dict[str, Any]
|
self, api_def: APIDefinition, req_type: RequestType, args: Dict[str, Any]
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -288,7 +295,6 @@ class BaseRemoteConnection(APITransport):
|
|||||||
self.wsm: WebsocketManager = self.server.lookup_component("websockets")
|
self.wsm: WebsocketManager = self.server.lookup_component("websockets")
|
||||||
self.rpc: JsonRPC = self.server.lookup_component("jsonrpc")
|
self.rpc: JsonRPC = self.server.lookup_component("jsonrpc")
|
||||||
self._uid = id(self)
|
self._uid = id(self)
|
||||||
self.ip_addr = ""
|
|
||||||
self.is_closed: bool = False
|
self.is_closed: bool = False
|
||||||
self.queue_busy: bool = False
|
self.queue_busy: bool = False
|
||||||
self.pending_responses: Dict[int, Future] = {}
|
self.pending_responses: Dict[int, Future] = {}
|
||||||
@ -480,18 +486,14 @@ class WebRequest:
|
|||||||
args: Dict[str, Any],
|
args: Dict[str, Any],
|
||||||
request_type: RequestType = RequestType(0),
|
request_type: RequestType = RequestType(0),
|
||||||
transport: Optional[APITransport] = None,
|
transport: Optional[APITransport] = None,
|
||||||
ip_addr: str = "",
|
ip_addr: Optional[IPAddress] = None,
|
||||||
user: Optional[Dict[str, Any]] = None
|
user: Optional[Dict[str, Any]] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
self.endpoint = endpoint
|
self.endpoint = endpoint
|
||||||
self.args = args
|
self.args = args
|
||||||
self.transport = transport
|
self.transport = transport
|
||||||
self.request_type = request_type
|
self.request_type = request_type
|
||||||
self.ip_addr: Optional[IPUnion] = None
|
self.ip_addr: Optional[IPAddress] = ip_addr
|
||||||
try:
|
|
||||||
self.ip_addr = ipaddress.ip_address(ip_addr)
|
|
||||||
except Exception:
|
|
||||||
self.ip_addr = None
|
|
||||||
self.current_user = user
|
self.current_user = user
|
||||||
|
|
||||||
def get_endpoint(self) -> str:
|
def get_endpoint(self) -> str:
|
||||||
@ -514,7 +516,7 @@ class WebRequest:
|
|||||||
return self.transport
|
return self.transport
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_ip_address(self) -> Optional[IPUnion]:
|
def get_ip_address(self) -> Optional[IPAddress]:
|
||||||
return self.ip_addr
|
return self.ip_addr
|
||||||
|
|
||||||
def get_current_user(self) -> Optional[Dict[str, Any]]:
|
def get_current_user(self) -> Optional[Dict[str, Any]]:
|
||||||
@ -797,13 +799,9 @@ class JsonRPC:
|
|||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
try:
|
try:
|
||||||
transport.screen_rpc_request(api_definition, request_type, params)
|
transport.screen_rpc_request(api_definition, request_type, params)
|
||||||
if isinstance(transport, BaseRemoteConnection):
|
result = await api_definition.request(
|
||||||
result = await api_definition.request(
|
params, request_type, transport, transport.ip_addr, transport.user_info
|
||||||
params, request_type, transport, transport.ip_addr,
|
)
|
||||||
transport.user_info
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
result = await api_definition.request(params, request_type, transport)
|
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
return self.build_error(
|
return self.build_error(
|
||||||
-32602, f"Invalid params:\n{e}", req_id, True, method_name
|
-32602, f"Invalid params:\n{e}", req_id, True, method_name
|
||||||
|
@ -19,6 +19,7 @@ import re
|
|||||||
import struct
|
import struct
|
||||||
import socket
|
import socket
|
||||||
import enum
|
import enum
|
||||||
|
import ipaddress
|
||||||
from . import source_info
|
from . import source_info
|
||||||
from . import json_wrapper
|
from . import json_wrapper
|
||||||
|
|
||||||
@ -39,6 +40,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
SYS_MOD_PATHS = glob.glob("/usr/lib/python3*/dist-packages")
|
SYS_MOD_PATHS = glob.glob("/usr/lib/python3*/dist-packages")
|
||||||
SYS_MOD_PATHS += glob.glob("/usr/lib/python3*/site-packages")
|
SYS_MOD_PATHS += glob.glob("/usr/lib/python3*/site-packages")
|
||||||
|
IPAddress = Union[ipaddress.IPv4Address, ipaddress.IPv6Address]
|
||||||
|
|
||||||
class ServerError(Exception):
|
class ServerError(Exception):
|
||||||
def __init__(self, message: str, status_code: int = 400) -> None:
|
def __init__(self, message: str, status_code: int = 400) -> None:
|
||||||
@ -264,3 +266,9 @@ def pretty_print_time(seconds: int) -> str:
|
|||||||
continue
|
continue
|
||||||
fmt_list.append(f"{val} {ident}" if val == 1 else f"{val} {ident}s")
|
fmt_list.append(f"{val} {ident}" if val == 1 else f"{val} {ident}s")
|
||||||
return ", ".join(fmt_list)
|
return ", ".join(fmt_list)
|
||||||
|
|
||||||
|
def parse_ip_address(address: str) -> Optional[IPAddress]:
|
||||||
|
try:
|
||||||
|
return ipaddress.ip_address(address)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import logging
|
import logging
|
||||||
import ipaddress
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from tornado.websocket import WebSocketHandler, WebSocketClosedError
|
from tornado.websocket import WebSocketHandler, WebSocketClosedError
|
||||||
from tornado.web import HTTPError
|
from tornado.web import HTTPError
|
||||||
@ -16,7 +15,7 @@ from .common import (
|
|||||||
BaseRemoteConnection,
|
BaseRemoteConnection,
|
||||||
TransportType,
|
TransportType,
|
||||||
)
|
)
|
||||||
from .utils import ServerError
|
from .utils import ServerError, parse_ip_address
|
||||||
|
|
||||||
# Annotation imports
|
# Annotation imports
|
||||||
from typing import (
|
from typing import (
|
||||||
@ -37,7 +36,7 @@ if TYPE_CHECKING:
|
|||||||
from .confighelper import ConfigHelper
|
from .confighelper import ConfigHelper
|
||||||
from .components.extensions import ExtensionManager
|
from .components.extensions import ExtensionManager
|
||||||
from .components.authorization import Authorization
|
from .components.authorization import Authorization
|
||||||
IPUnion = Union[ipaddress.IPv4Address, ipaddress.IPv6Address]
|
from .utils import IPAddress
|
||||||
ConvType = Union[str, bool, float, int]
|
ConvType = Union[str, bool, float, int]
|
||||||
ArgVal = Union[None, int, float, bool, str]
|
ArgVal = Union[None, int, float, bool, str]
|
||||||
RPCCallback = Callable[..., Coroutine]
|
RPCCallback = Callable[..., Coroutine]
|
||||||
@ -231,9 +230,13 @@ class WebSocket(WebSocketHandler, BaseRemoteConnection):
|
|||||||
|
|
||||||
def initialize(self) -> None:
|
def initialize(self) -> None:
|
||||||
self.on_create(self.settings['server'])
|
self.on_create(self.settings['server'])
|
||||||
self.ip_addr: str = self.request.remote_ip or ""
|
self._ip_addr = parse_ip_address(self.request.remote_ip or "")
|
||||||
self.last_pong_time: float = self.eventloop.get_loop_time()
|
self.last_pong_time: float = self.eventloop.get_loop_time()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ip_addr(self) -> Optional[IPAddress]:
|
||||||
|
return self._ip_addr
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hostname(self) -> str:
|
def hostname(self) -> str:
|
||||||
return self.request.host_name
|
return self.request.host_name
|
||||||
@ -336,13 +339,17 @@ class BridgeSocket(WebSocketHandler):
|
|||||||
self.wsm: WebsocketManager = self.server.lookup_component("websockets")
|
self.wsm: WebsocketManager = self.server.lookup_component("websockets")
|
||||||
self.eventloop = self.server.get_event_loop()
|
self.eventloop = self.server.get_event_loop()
|
||||||
self.uid = id(self)
|
self.uid = id(self)
|
||||||
self.ip_addr: str = self.request.remote_ip or ""
|
self._ip_addr = parse_ip_address(self.request.remote_ip or "")
|
||||||
self.last_pong_time: float = self.eventloop.get_loop_time()
|
self.last_pong_time: float = self.eventloop.get_loop_time()
|
||||||
self.is_closed = False
|
self.is_closed = False
|
||||||
self.klippy_writer: Optional[asyncio.StreamWriter] = None
|
self.klippy_writer: Optional[asyncio.StreamWriter] = None
|
||||||
self.klippy_write_buf: List[bytes] = []
|
self.klippy_write_buf: List[bytes] = []
|
||||||
self.klippy_queue_busy: bool = False
|
self.klippy_queue_busy: bool = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ip_addr(self) -> Optional[IPAddress]:
|
||||||
|
return self._ip_addr
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hostname(self) -> str:
|
def hostname(self) -> str:
|
||||||
return self.request.host_name
|
return self.request.host_name
|
||||||
|
Loading…
x
Reference in New Issue
Block a user