Skip to content

Commit

Permalink
app: add jsonrpc HTTP endpoint
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
Arksine committed Dec 15, 2023
1 parent 574c8d2 commit 388def9
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 27 deletions.
57 changes: 53 additions & 4 deletions moonraker/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from tornado.routing import Rule, PathMatches, AnyMatches
from tornado.http1connection import HTTP1Connection
from tornado.log import access_log
from .utils import ServerError, source_info
from .utils import ServerError, source_info, parse_ip_address
from .common import (
JsonRPC,
WebRequest,
Expand Down Expand Up @@ -59,6 +59,7 @@
from .eventloop import EventLoop
from .confighelper import ConfigHelper
from .klippy_connection import KlippyConnection as Klippy
from .utils import IPAddress
from .components.file_manager.file_manager import FileManager
from .components.announcements import Announcements
from .components.machine import Machine
Expand Down Expand Up @@ -144,7 +145,10 @@ def __init__(self, config: ConfigHelper) -> None:
self.http_server: Optional[HTTPServer] = None
self.secure_server: Optional[HTTPServer] = None
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 *= 1024 * 1024
max_ws_conns = config.getint(
Expand Down Expand Up @@ -197,7 +201,8 @@ def __init__(self, config: ConfigHelper) -> None:
(home_pattern, WelcomeHandler),
(f"{self._route_prefix}/websocket", WebSocket),
(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.get_handler_delegate = self.app.get_handler_delegate
Expand Down Expand Up @@ -632,7 +637,7 @@ async def _process_http_request(self, req_type: RequestType) -> None:
req = f"{self.request.method} {self.request.path}"
self._log_debug(f"HTTP Request::{req}", args)
try:
ip = self.request.remote_ip or ""
ip = parse_ip_address(self.request.remote_ip or "")
result = await self.api_defintion.request(
args, req_type, transport, ip, self.current_user
)
Expand All @@ -651,6 +656,50 @@ async def _process_http_request(self, req_type: RequestType) -> None:
self.set_header("Content-Type", self.content_type)
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):
def set_extra_headers(self, path: str) -> None:
# The call below shold never return an empty string,
Expand Down
34 changes: 16 additions & 18 deletions moonraker/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

from __future__ import annotations
import sys
import ipaddress
import logging
import copy
import re
Expand Down Expand Up @@ -36,11 +35,11 @@
from .server import Server
from .websockets import WebsocketManager
from .components.authorization import Authorization
from .utils import IPAddress
from asyncio import Future
_T = TypeVar("_T")
_C = TypeVar("_C", str, bool, float, int)
_F = TypeVar("_F", bound="ExtendedFlag")
IPUnion = Union[ipaddress.IPv4Address, ipaddress.IPv6Address]
ConvType = Union[str, bool, float, int]
ArgVal = Union[None, int, float, bool, str]
RPCCallback = Callable[..., Coroutine]
Expand Down Expand Up @@ -188,7 +187,7 @@ def request(
args: Dict[str, Any],
request_type: RequestType,
transport: Optional[APITransport] = None,
ip_addr: str = "",
ip_addr: Optional[IPAddress] = None,
user: Optional[Dict[str, Any]] = None
) -> Coroutine:
return self.callback(
Expand Down Expand Up @@ -271,6 +270,14 @@ class APITransport:
def transport_type(self) -> TransportType:
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(
self, api_def: APIDefinition, req_type: RequestType, args: Dict[str, Any]
) -> None:
Expand All @@ -288,7 +295,6 @@ def on_create(self, server: Server) -> None:
self.wsm: WebsocketManager = self.server.lookup_component("websockets")
self.rpc: JsonRPC = self.server.lookup_component("jsonrpc")
self._uid = id(self)
self.ip_addr = ""
self.is_closed: bool = False
self.queue_busy: bool = False
self.pending_responses: Dict[int, Future] = {}
Expand Down Expand Up @@ -480,18 +486,14 @@ def __init__(
args: Dict[str, Any],
request_type: RequestType = RequestType(0),
transport: Optional[APITransport] = None,
ip_addr: str = "",
ip_addr: Optional[IPAddress] = None,
user: Optional[Dict[str, Any]] = None
) -> None:
self.endpoint = endpoint
self.args = args
self.transport = transport
self.request_type = request_type
self.ip_addr: Optional[IPUnion] = None
try:
self.ip_addr = ipaddress.ip_address(ip_addr)
except Exception:
self.ip_addr = None
self.ip_addr: Optional[IPAddress] = ip_addr
self.current_user = user

def get_endpoint(self) -> str:
Expand All @@ -514,7 +516,7 @@ def get_client_connection(self) -> Optional[BaseRemoteConnection]:
return self.transport
return None

def get_ip_address(self) -> Optional[IPUnion]:
def get_ip_address(self) -> Optional[IPAddress]:
return self.ip_addr

def get_current_user(self) -> Optional[Dict[str, Any]]:
Expand Down Expand Up @@ -797,13 +799,9 @@ async def execute_method(
) -> Optional[Dict[str, Any]]:
try:
transport.screen_rpc_request(api_definition, request_type, params)
if isinstance(transport, BaseRemoteConnection):
result = await api_definition.request(
params, request_type, transport, transport.ip_addr,
transport.user_info
)
else:
result = await api_definition.request(params, request_type, transport)
result = await api_definition.request(
params, request_type, transport, transport.ip_addr, transport.user_info
)
except TypeError as e:
return self.build_error(
-32602, f"Invalid params:\n{e}", req_id, True, method_name
Expand Down
8 changes: 8 additions & 0 deletions moonraker/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import struct
import socket
import enum
import ipaddress
from . import source_info
from . import json_wrapper

Expand All @@ -39,6 +40,7 @@

SYS_MOD_PATHS = glob.glob("/usr/lib/python3*/dist-packages")
SYS_MOD_PATHS += glob.glob("/usr/lib/python3*/site-packages")
IPAddress = Union[ipaddress.IPv4Address, ipaddress.IPv6Address]

class ServerError(Exception):
def __init__(self, message: str, status_code: int = 400) -> None:
Expand Down Expand Up @@ -264,3 +266,9 @@ def pretty_print_time(seconds: int) -> str:
continue
fmt_list.append(f"{val} {ident}" if val == 1 else f"{val} {ident}s")
return ", ".join(fmt_list)

def parse_ip_address(address: str) -> Optional[IPAddress]:
try:
return ipaddress.ip_address(address)
except Exception:
return None
17 changes: 12 additions & 5 deletions moonraker/websockets.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

from __future__ import annotations
import logging
import ipaddress
import asyncio
from tornado.websocket import WebSocketHandler, WebSocketClosedError
from tornado.web import HTTPError
Expand All @@ -16,7 +15,7 @@
BaseRemoteConnection,
TransportType,
)
from .utils import ServerError
from .utils import ServerError, parse_ip_address

# Annotation imports
from typing import (
Expand All @@ -37,7 +36,7 @@
from .confighelper import ConfigHelper
from .components.extensions import ExtensionManager
from .components.authorization import Authorization
IPUnion = Union[ipaddress.IPv4Address, ipaddress.IPv6Address]
from .utils import IPAddress
ConvType = Union[str, bool, float, int]
ArgVal = Union[None, int, float, bool, str]
RPCCallback = Callable[..., Coroutine]
Expand Down Expand Up @@ -231,9 +230,13 @@ class WebSocket(WebSocketHandler, BaseRemoteConnection):

def initialize(self) -> None:
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()

@property
def ip_addr(self) -> Optional[IPAddress]:
return self._ip_addr

@property
def hostname(self) -> str:
return self.request.host_name
Expand Down Expand Up @@ -336,13 +339,17 @@ def initialize(self) -> None:
self.wsm: WebsocketManager = self.server.lookup_component("websockets")
self.eventloop = self.server.get_event_loop()
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.is_closed = False
self.klippy_writer: Optional[asyncio.StreamWriter] = None
self.klippy_write_buf: List[bytes] = []
self.klippy_queue_busy: bool = False

@property
def ip_addr(self) -> Optional[IPAddress]:
return self._ip_addr

@property
def hostname(self) -> str:
return self.request.host_name
Expand Down

0 comments on commit 388def9

Please sign in to comment.