def load_plugin(self, config, plugin_name, default=Sentinel): if plugin_name in self.plugins: return self.plugins[plugin_name] # Make sure plugin exists mod_path = os.path.join( os.path.dirname(__file__), 'plugins', plugin_name + '.py') if not os.path.exists(mod_path): msg = f"Plugin ({plugin_name}) does not exist" logging.info(msg) if default == Sentinel: raise ServerError(msg) return default module = importlib.import_module("plugins." + plugin_name) try: if plugin_name not in CORE_PLUGINS: config = config[plugin_name] load_func = getattr(module, "load_plugin") plugin = load_func(config) except Exception: msg = f"Unable to load plugin ({plugin_name})" logging.exception(msg) if default == Sentinel: raise ServerError(msg) return default self.plugins[plugin_name] = plugin logging.info(f"Plugin ({plugin_name}) loaded") return plugin
def load_component(self, config, component_name, default=Sentinel): if component_name in self.components: return self.components[component_name] # Make sure component exists mod_path = os.path.join(os.path.dirname(__file__), 'components', component_name + '.py') if not os.path.exists(mod_path): msg = f"Component ({component_name}) does not exist" logging.info(msg) self.failed_components.append(component_name) if default == Sentinel: raise ServerError(msg) return default try: module = importlib.import_module("components." + component_name) func_name = "load_component" if hasattr(module, "load_component_multi"): func_name = "load_component_multi" if component_name not in CORE_COMPONENTS and \ func_name == "load_component": config = config[component_name] load_func = getattr(module, func_name) component = load_func(config) except Exception: msg = f"Unable to load component: ({component_name})" logging.exception(msg) self.failed_components.append(component_name) if default == Sentinel: raise ServerError(msg) return default self.components[component_name] = component logging.info(f"Component ({component_name}) loaded") return component
async def send_request(self, request): if self.iostream is None: request.notify(ServerError("Klippy Host not connected", 503)) return data = json.dumps(request.to_dict()).encode() + b"\x03" try: await self.iostream.write(data) except iostream.StreamClosedError: request.notify(ServerError("Klippy Host not connected", 503))
async def send_request(self, request: BaseRequest) -> None: if self.writer is None: request.notify(ServerError("Klippy Host not connected", 503)) return data = json.dumps(request.to_dict()).encode() + b"\x03" try: self.writer.write(data) await self.writer.drain() except Exception: request.notify(ServerError("Klippy Host not connected", 503)) await self.close()
def get(self, key: str, default: Union[SentinelClass, _T] = SENTINEL) -> Union[_T, Any]: val = self.args.get(key, default) if isinstance(val, SentinelClass): raise ServerError(f"No data for argument: {key}") return val
def process_command(self, cmd): method = cmd.get('method', None) if method is not None: # This is a remote method called from klippy cb = self.remote_methods.get(method, None) if cb is not None: params = cmd.get('params', {}) cb(**params) else: logging.info(f"Unknown method received: {method}") return # This is a response to a request, process req_id = cmd.get('id', None) request = self.pending_requests.pop(req_id, None) if request is None: logging.info( f"No request matching request ID: {req_id}, " f"response: {cmd}") return if 'result' in cmd: result = cmd['result'] if not result: result = "ok" else: err = cmd.get('error', "Malformed Klippy Response") result = ServerError(err, 400) request.notify(result)
def load_component( self, config: confighelper.ConfigHelper, component_name: str, default: Union[SentinelClass, _T] = SENTINEL) -> Union[_T, Any]: if component_name in self.components: return self.components[component_name] try: module = importlib.import_module("components." + component_name) func_name = "load_component" if hasattr(module, "load_component_multi"): func_name = "load_component_multi" if component_name not in CORE_COMPONENTS and \ func_name == "load_component": config = config[component_name] load_func = getattr(module, func_name) component = load_func(config) except Exception: msg = f"Unable to load component: ({component_name})" logging.exception(msg) self.failed_components.append(component_name) if isinstance(default, SentinelClass): raise ServerError(msg) return default self.components[component_name] = component logging.info(f"Component ({component_name}) loaded") return component
def load_component( self, config: confighelper.ConfigHelper, component_name: str, default: Union[SentinelClass, _T] = SENTINEL) -> Union[_T, Any]: if component_name in self.components: return self.components[component_name] try: module = importlib.import_module("components." + component_name) is_core = component_name in CORE_COMPONENTS fallback: Optional[str] = "server" if is_core else None config = config.getsection(component_name, fallback) load_func = getattr(module, "load_component") component = load_func(config) except Exception: msg = f"Unable to load component: ({component_name})" logging.exception(msg) if component_name not in self.failed_components: self.failed_components.append(component_name) if isinstance(default, SentinelClass): raise ServerError(msg) return default self.components[component_name] = component logging.info(f"Component ({component_name}) loaded") return component
def process_command(self, cmd: Dict[str, Any]) -> None: method = cmd.get('method', None) if method is not None: # This is a remote method called from klippy if method in self.remote_methods: params = cmd.get('params', {}) self.event_loop.register_callback( self._execute_method, method, **params) else: logging.info(f"Unknown method received: {method}") return # This is a response to a request, process req_id = cmd.get('id', None) request: Optional[BaseRequest] request = self.pending_requests.pop(req_id, None) if request is None: logging.info( f"No request matching request ID: {req_id}, " f"response: {cmd}") return if 'result' in cmd: result = cmd['result'] if not result: result = "ok" else: err = cmd.get('error', "Malformed Klippy Response") result = ServerError(err, 400) request.notify(result)
def lookup_component(self, component_name: str, default: Union[SentinelClass, _T] = SENTINEL ) -> Union[_T, Any]: component = self.components.get(component_name, default) if isinstance(component, SentinelClass): raise ServerError(f"Component ({component_name}) not found") return component
def _handle_klippy_response(self, request_id, response): req = self.pending_requests.pop(request_id, None) if req is not None: if isinstance(response, dict) and 'error' in response: response = ServerError(response['message'], 400) req.notify(response) else: logging.info("No request matching response: " + str(response))
def __init__(self, config): api_key_file = config.get('api_key_file', "~/.moonraker_api_key") self.api_key_file = os.path.expanduser(api_key_file) self.api_key = self._read_api_key() self.auth_enabled = config.getboolean('enabled', True) self.trusted_connections = {} self.access_tokens = {} # Get allowed cors domains self.cors_domains = [] cors_cfg = config.get('cors_domains', "").strip() cds = [d.strip() for d in cors_cfg.split('\n')if d.strip()] for domain in cds: bad_match = re.search(r"^.+\.[^:]*\*", domain) if bad_match is not None: raise config.error( f"Unsafe CORS Domain '{domain}'. Wildcards are not" " permitted in the top level domain.") self.cors_domains.append( domain.replace(".", "\\.").replace("*", ".*")) # Get Trusted Clients self.trusted_ips = [] self.trusted_ranges = [] trusted_clients = config.get('trusted_clients', "") trusted_clients = [c.strip() for c in trusted_clients.split('\n') if c.strip()] for ip in trusted_clients: # Check IP address try: tc = ipaddress.ip_address(ip) except ValueError: tc = None if tc is None: # Check ip network try: tc = ipaddress.ip_network(ip) except ValueError: raise ServerError( f"Invalid option in trusted_clients: {ip}") self.trusted_ranges.append(tc) else: self.trusted_ips.append(tc) t_clients = "\n".join( [str(ip) for ip in self.trusted_ips] + [str(rng) for rng in self.trusted_ranges]) c_domains = "\n".join(self.cors_domains) logging.info( f"Authorization Configuration Loaded\n" f"Auth Enabled: {self.auth_enabled}\n" f"Trusted Clients:\n{t_clients}\n" f"CORS Domains:\n{c_domains}") self.prune_handler = PeriodicCallback( self._prune_conn_handler, PRUNE_CHECK_TIME) self.prune_handler.start()
async def _write_request(self, request: KlippyRequest) -> None: if self.writer is None or self.closing: self.pending_requests.pop(request.id, None) request.notify(ServerError("Klippy Host not connected", 503)) return data = json.dumps(request.to_dict()).encode() + b"\x03" try: self.writer.write(data) await self.writer.drain() except asyncio.CancelledError: self.pending_requests.pop(request.id, None) request.notify(ServerError("Klippy Write Request Cancelled", 503)) raise except Exception: self.pending_requests.pop(request.id, None) request.notify(ServerError("Klippy Write Request Error", 503)) if not self.closing: logging.debug("Klippy Disconnection From _write_request()") await self.close()
async def wait(self): # Wait for klippy to process the request or until the timeout # has been reached. try: await self._event.wait(timeout=self._timeout) except TimeoutError: logging.info("Request '%s' Timed Out" % (self.method + " " + self.path)) return ServerError("Klippy Request Timed Out", 500) return self.response
async def readuntil(self, stop: bytes) -> bytes: if self.action == "wait": evt = asyncio.Event() await evt.wait() return b"" elif self.action == "raise_error": raise ServerError("TestError") else: self.eof = True return b"NotJsonDecodable"
def raise_for_status(self, message: Optional[str] = None) -> None: if self._error is not None: code = 500 msg = f"HTTP Request Error: {self.url}" if isinstance(self._error, HTTPError): code = self._code if self._error.message is not None: msg = self._error.message if message is not None: msg = message raise ServerError(msg, code) from self._error
def _get_converted_arg(self, key, default=Sentinel, dtype=str): if key not in self.args: if default == Sentinel: raise ServerError(f"No data for argument: {key}") return default val = self.args[key] try: if dtype != bool: return dtype(val) else: if isinstance(val, str): val = val.lower() if val in ["true", "false"]: return True if val == "true" else False elif isinstance(val, bool): return val raise TypeError except Exception: raise ServerError( f"Unable to convert argument [{key}] to {dtype}: " f"value recieved: {val}")
def on_connection_closed(self): self.init_list = [] self.klippy_state = "disconnected" for request in self.pending_requests.values(): request.notify(ServerError("Klippy Disconnected", 503)) self.pending_requests = {} logging.info("Klippy Connection Removed") self.send_event("server:klippy_disconnect") if self.init_handle is not None: self.ioloop.remove_timeout(self.init_handle) if self.server_running: self.ioloop.call_later(.25, self._connect_klippy)
async def request(self, web_request: WebRequest) -> Any: if not self.is_connected(): raise ServerError("Klippy Host not connected", 503) rpc_method = web_request.get_endpoint() if rpc_method == "objects/subscribe": return await self._request_subscripton(web_request) else: if rpc_method == "gcode/script": script = web_request.get_str('script', "") if script: self.server.send_event( "klippy_connection:gcode_received", script) return await self._request_standard(web_request)
def _get_converted_arg(self, key: str, default: Union[SentinelClass, _T], dtype: Type[_C]) -> Union[_C, _T]: if key not in self.args: if isinstance(default, SentinelClass): raise ServerError(f"No data for argument: {key}") return default val = self.args[key] try: if dtype is not bool: return dtype(val) else: if isinstance(val, str): val = val.lower() if val in ["true", "false"]: return True if val == "true" else False # type: ignore elif isinstance(val, bool): return val # type: ignore raise TypeError except Exception: raise ServerError( f"Unable to convert argument [{key}] to {dtype}: " f"value recieved: {val}")
async def _on_connection_closed(self) -> None: self.init_list = [] self._state = "disconnected" for request in self.pending_requests.values(): request.notify(ServerError("Klippy Disconnected", 503)) self.pending_requests = {} self.subscriptions = {} self._peer_cred = {} self._missing_reqs.clear() logging.info("Klippy Connection Removed") await self.server.send_event("server:klippy_disconnect") if self.server.is_running(): # Reconnect if server is running loop = self.event_loop self.connection_task = loop.create_task(self._do_connect())
def on_connection_closed(self) -> None: self.init_list = [] self.klippy_state = "disconnected" for request in self.pending_requests.values(): request.notify(ServerError("Klippy Disconnected", 503)) self.pending_requests = {} self.subscriptions = {} logging.info("Klippy Connection Removed") self.send_event("server:klippy_disconnect") if self.init_handle is not None: self.init_handle.cancel() self.init_handle = None if self.server_running: self.event_loop.delay_callback(.25, self._connect_klippy) if self.klippy_disconnect_evt is not None: self.klippy_disconnect_evt.set()
def parse_args(self) -> Dict[str, Any]: try: args = self._parse_query() except Exception: raise ServerError("Error Parsing Request Arguments. " "Is the Content-Type correct?") content_type = self.request.headers.get('Content-Type', "").strip() if content_type.startswith("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
def make_request(self, path, method, args): timeout = self.long_running_requests.get(path, self.request_timeout) if path == "gcode/script": script = args.get('script', "") base_gc = script.strip().split()[0].upper() timeout = self.long_running_gcodes.get(base_gc, timeout) base_request = BaseRequest(path, method, args, timeout) self.pending_requests[base_request.id] = base_request ret = self.klippy_send(base_request.to_dict()) if not ret: self.pending_requests.pop(base_request.id, None) base_request.notify( ServerError("Klippy Host not connected", 503)) return base_request
def on_close(self) -> None: self.is_closed = True self.message_buf = [] now = self.event_loop.get_loop_time() pong_elapsed = now - self.last_pong_time for resp in self.pending_responses.values(): resp.set_exception(ServerError("Client Socket Disconnected", 500)) self.pending_responses = {} logging.info(f"Websocket Closed: ID: {self.uid} " f"Close Code: {self.close_code}, " f"Close Reason: {self.close_reason}, " f"Pong Time Elapsed: {pong_elapsed:.2f}") if self._client_data["type"] == "agent": extensions: ExtensionManager extensions = self.server.lookup_component("extensions") extensions.remove_agent(self) self.wsm.remove_websocket(self)
def __init__(self, config): api_key_file = config.get('api_key_file', "~/.moonraker_api_key") self.api_key_file = os.path.expanduser(api_key_file) self.api_key = self._read_api_key() self.auth_enabled = config.getboolean('enabled', True) self.trusted_connections = {} self.access_tokens = {} # Get Trusted Clients self.trusted_ips = [] self.trusted_ranges = [] trusted_clients = config.get('trusted_clients', "") trusted_clients = [ c.strip() for c in trusted_clients.split('\n') if c.strip() ] for ip in trusted_clients: # Check IP address try: tc = ipaddress.ip_address(ip) except ValueError: tc = None if tc is None: # Check ip network try: tc = ipaddress.ip_network(ip) except ValueError: raise ServerError( f"Invalid option in trusted_clients: {ip}") self.trusted_ranges.append(tc) else: self.trusted_ips.append(tc) t_clients = "\n".join([str(ip) for ip in self.trusted_ips] + [str(rng) for rng in self.trusted_ranges]) logging.info(f"Authorization Configuration Loaded\n" f"Auth Enabled: {self.auth_enabled}\n" f"Trusted Clients:\n{t_clients}") self.prune_handler = PeriodicCallback(self._prune_conn_handler, PRUNE_CHECK_TIME) self.prune_handler.start()
def load_plugin(self, plugin_name, default=Sentinel): if plugin_name in self.plugins: return self.plugins[plugin_name] # Make sure plugin exists mod_path = os.path.join( os.path.dirname(__file__), 'plugins', plugin_name + '.py') if not os.path.exists(mod_path): logging.info("Plugin (%s) does not exist" % (plugin_name)) return None module = importlib.import_module("plugins." + plugin_name) try: load_func = getattr(module, "load_plugin") plugin = load_func(self) except Exception: msg = "Unable to load plugin (%s)" % (plugin_name) if default == Sentinel: raise ServerError(msg) return default self.plugins[plugin_name] = plugin logging.info("Plugin (%s) loaded" % (plugin_name)) return plugin
def process_response(self, obj: Dict[str, Any], conn: Optional[WebSocket]) -> None: if conn is None: logging.debug(f"RPC Response to non-socket request: {obj}") return response_id = obj.get("id") if response_id is None: logging.debug(f"RPC Response with null ID: {obj}") return result = obj.get("result") if result is None: name = conn.client_data["name"] error = obj.get("error") msg = f"Invalid Response: {obj}" code = -32600 if isinstance(error, dict): msg = error.get("message", msg) code = error.get("code", code) msg = f"{name} rpc error: {code} {msg}" ret = ServerError(msg, 418) else: ret = result conn.resolve_pending_response(response_id, ret)
def lookup_plugin(self, plugin_name, default=Sentinel): plugin = self.plugins.get(plugin_name, default) if plugin == Sentinel: raise ServerError(f"Plugin ({plugin_name}) not found") return plugin
def get(self, key, default=Sentinel): val = self.args.get(key, default) if val == Sentinel: raise ServerError(f"No data for argument: {key}") return val