async def _handle_item_request(self, web_request: WebRequest) -> Dict[str, Any]: action = web_request.get_action() namespace = web_request.get_str("namespace") if namespace in self.forbidden_namespaces: raise self.server.error( f"Read/Write access to namespace '{namespace}'" " is forbidden", 403) key: Any valid_types: Tuple[type, ...] if action != "GET": if namespace in self.protected_namespaces: raise self.server.error( f"Write access to namespace '{namespace}'" " is forbidden", 403) key = web_request.get("key") valid_types = (list, str) else: key = web_request.get("key", None) valid_types = (list, str, type(None)) if not isinstance(key, valid_types): raise self.server.error( "Value for argument 'key' is an invalid type: " f"{type(key).__name__}") if action == "GET": val = await self.get_item(namespace, key) elif action == "POST": val = web_request.get("value") await self.insert_item(namespace, key, val) elif action == "DELETE": val = await self.delete_item(namespace, key, drop_empty_db=True) return {'namespace': namespace, 'key': key, 'value': val}
async def _handle_job_request(self, web_request: WebRequest) -> Dict[str, Any]: action = web_request.get_action() if action == "POST": files: Union[List[str], str] = web_request.get('filenames') if isinstance(files, str): files = [f.strip() for f in files.split(',') if f.strip()] # Validate that all files exist before queueing await self.queue_job(files) elif action == "DELETE": if web_request.get_boolean("all", False): await self.delete_job([], all=True) else: job_ids: Union[List[str], str] = web_request.get('job_ids') if isinstance(job_ids, str): job_ids = [ f.strip() for f in job_ids.split(',') if f.strip() ] await self.delete_job(job_ids) else: raise self.server.error(f"Invalid action: {action}") return { 'queued_jobs': self._job_map_to_list(), 'queue_state': self.queue_state }
async def _handle_job_request(self, web_request: WebRequest ) -> Dict[str, Any]: action = web_request.get_action() if action == "GET": job_id = web_request.get_str("uid") if job_id not in self.cached_job_ids: raise self.server.error(f"Invalid job uid: {job_id}", 404) job = self.history_ns[job_id] return {"job": self._prep_requested_job(job, job_id)} if action == "DELETE": all = web_request.get_boolean("all", False) if all: deljobs = self.cached_job_ids self.history_ns.clear() self.cached_job_ids = [] self.next_job_id = 0 return {'deleted_jobs': deljobs} job_id = web_request.get_str("uid") if job_id not in self.cached_job_ids: raise self.server.error(f"Invalid job uid: {job_id}", 404) self.delete_job(job_id) return {'deleted_jobs': [job_id]} raise self.server.error("Invalid Request Method")
async def _handle_subscription_request( self, web_request: WebRequest) -> Dict[str, Any]: topic: str = web_request.get_str("topic") qos: int = web_request.get_int("qos", self.qos) timeout: Optional[float] = web_request.get_float('timeout', None) resp: asyncio.Future = asyncio.Future() hdl: Optional[SubscriptionHandle] = None try: hdl = self.subscribe_topic(topic, resp.set_result, qos) self.pending_responses.append(resp) await asyncio.wait_for(resp, timeout) ret: bytes = resp.result() except asyncio.TimeoutError: raise self.server.error("MQTT Subscribe Timed Out", 504) finally: try: self.pending_responses.remove(resp) except Exception: pass if hdl is not None: self.unsubscribe(hdl) try: payload = json.loads(ret) except json.JSONDecodeError: payload = ret.decode() return {'topic': topic, 'payload': payload}
async def _handle_update_request(self, web_request: WebRequest ) -> str: await self.initialized_lock.wait() if await self._check_klippy_printing(): raise self.server.error("Update Refused: Klippy is printing") app: str = web_request.get_endpoint().split("/")[-1] if app == "client": app = web_request.get('name') if self.cmd_helper.is_app_updating(app): return f"Object {app} is currently being updated" updater = self.updaters.get(app, None) if updater is None: raise self.server.error(f"Updater {app} not available", 404) async with self.cmd_request_lock: self.cmd_helper.set_update_info(app, id(web_request)) try: if not await self._check_need_reinstall(app): await updater.update() except Exception as e: self.cmd_helper.notify_update_response( f"Error updating {app}") self.cmd_helper.notify_update_response( str(e), is_complete=True) raise finally: self.cmd_helper.clear_update_info() return "ok"
async def _handle_webcam_request( self, web_request: WebRequest) -> Dict[str, Any]: action = web_request.get_action() name = web_request.get_str("name") webcam_data: Dict[str, Any] = {} if action == "GET": if name not in self.webcams: raise self.server.error(f"Webcam {name} not found", 404) webcam_data = self.webcams[name].as_dict() elif action == "POST": if (name in self.webcams and self.webcams[name].source == "config"): raise self.server.error( f"Cannot overwrite webcam '{name}' sourced from " "Moonraker configuration") webcam = WebCam.from_web_request(self.server, web_request) self.webcams[name] = webcam webcam_data = webcam.as_dict() await self._save_cam(webcam) elif action == "DELETE": if name not in self.webcams: raise self.server.error(f"Webcam {name} not found", 404) elif self.webcams[name].source == "config": raise self.server.error( f"Cannot delete webcam '{name}' sourced from " "Moonraker configuration") webcam = self.webcams.pop(name) webcam_data = webcam.as_dict() await self._delete_cam(webcam) if action != "GET": self.server.send_event("webcam:webcams_changed", {"webcams": self._list_webcams()}) return {"webcam": webcam_data}
async def _handle_repo_recovery(self, web_request: WebRequest ) -> str: await self.initialized_lock.wait() if await self._check_klippy_printing(): raise self.server.error( "Recovery Attempt Refused: Klippy is printing") app: str = web_request.get_str('name') hard = web_request.get_boolean("hard", False) update_deps = web_request.get_boolean("update_deps", False) updater = self.updaters.get(app, None) if updater is None: raise self.server.error(f"Updater {app} not available", 404) elif not isinstance(updater, GitDeploy): raise self.server.error(f"Upater {app} is not a Git Repo Type") async with self.cmd_request_lock: self.cmd_helper.set_update_info(f"recover_{app}", id(web_request)) try: await updater.recover(hard, update_deps) except Exception as e: self.cmd_helper.notify_update_response( f"Error Recovering {app}") self.cmd_helper.notify_update_response( str(e), is_complete=True) raise finally: self.cmd_helper.clear_update_info() return "ok"
async def _request_standard(self, web_request: WebRequest) -> Any: rpc_method = web_request.get_endpoint() args = web_request.get_args() # Create a base klippy request base_request = KlippyRequest(rpc_method, args) self.pending_requests[base_request.id] = base_request self.event_loop.register_callback(self._write_request, base_request) return await base_request.wait()
async def _handle_jobs_list(self, web_request: WebRequest ) -> Dict[str, Any]: async with self.request_lock: i = 0 count = 0 end_num = len(self.cached_job_ids) jobs: List[Dict[str, Any]] = [] start_num = 0 before = web_request.get_float("before", -1) since = web_request.get_float("since", -1) limit = web_request.get_int("limit", 50) start = web_request.get_int("start", 0) order = web_request.get_str("order", "desc") if order not in ["asc", "desc"]: raise self.server.error(f"Invalid `order` value: {order}", 400) reverse_order = (order == "desc") # cached jobs is asc order, find lower and upper boundary if since != -1: while start_num < end_num: job_id = self.cached_job_ids[start_num] job: Dict[str, Any] = await self.history_ns[job_id] if job['start_time'] > since: break start_num += 1 if before != -1: while end_num > 0: job_id = self.cached_job_ids[end_num-1] job = await self.history_ns[job_id] if job['end_time'] < before: break end_num -= 1 if start_num >= end_num or end_num == 0: return {"count": 0, "jobs": []} i = start count = end_num - start_num if limit == 0: limit = MAX_JOBS while i < count and len(jobs) < limit: if reverse_order: job_id = self.cached_job_ids[end_num - i - 1] else: job_id = self.cached_job_ids[start_num + i] job = await self.history_ns[job_id] jobs.append(self._prep_requested_job(job, job_id)) i += 1 return {"count": count, "jobs": jobs}
async def _request_standard(self, web_request: WebRequest) -> Any: rpc_method = web_request.get_endpoint() args = web_request.get_args() # Create a base klippy request base_request = BaseRequest(rpc_method, args) self.pending_requests[base_request.id] = base_request self.ioloop.spawn_callback(self.klippy_connection.send_request, base_request) return await base_request.wait()
async def _handle_dismiss_request( self, web_request: WebRequest ) -> Dict[str, Any]: async with self.request_lock: entry_id: str = web_request.get_str("entry_id") wake_time: Optional[int] = web_request.get_int("wake_time", None) await self.entry_mgr.dismiss_entry(entry_id, wake_time) return { "entry_id": entry_id }
async def make_request(self, web_request: WebRequest) -> Any: 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', "") data_store: DataStore = self.lookup_component('data_store') data_store.store_gcode_command(script) return await self._request_standard(web_request)
async def _handle_call_agent(self, web_request: WebRequest) -> Any: agent = web_request.get_str("agent") method: str = web_request.get_str("method") args: Optional[Union[List, Dict[str, Any]]] args = web_request.get("arguments", None) if args is not None and not isinstance(args, (list, dict)): raise self.server.error( "The 'arguments' field must contain an object or a list") if agent not in self.agents: raise self.server.error(f"Agent {agent} not connected") conn = self.agents[agent] return await conn.call_method(method, args)
async def _handle_publish_request( self, web_request: WebRequest) -> Dict[str, Any]: topic: str = web_request.get_str("topic") payload: Any = web_request.get("payload", None) qos: int = web_request.get_int("qos", self.qos) retain: bool = web_request.get_boolean("retain", False) timeout: Optional[float] = web_request.get_float('timeout', None) try: await asyncio.wait_for( self.publish_topic(topic, payload, qos, retain), timeout) except asyncio.TimeoutError: raise self.server.error("MQTT Publish Timed Out", 504) return {"topic": topic}
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)
async def _handle_batch_power_request( self, web_request: WebRequest) -> Dict[str, Any]: args = web_request.get_args() ep = web_request.get_endpoint() if not args: raise self.server.error("No arguments provided") requested_devs = {k: self.devices.get(k, None) for k in args} result = {} req = ep.split("/")[-1] for name, device in requested_devs.items(): if device is not None: result[name] = await self._process_request(device, req) else: result[name] = "device_not_found" return result
async def _handle_single_power_request( self, web_request: WebRequest) -> Dict[str, Any]: dev_name: str = web_request.get_str('device') req_action = web_request.get_action() if dev_name not in self.devices: raise self.server.error(f"No valid device named {dev_name}") dev = self.devices[dev_name] if req_action == 'GET': action = "status" elif req_action == "POST": action = web_request.get_str('action').lower() if action not in ["on", "off", "toggle"]: raise self.server.error(f"Invalid requested action '{action}'") result = await self._process_request(dev, action) return {dev_name: result}
async def _handle_batch_wled_request( self: WLED, web_request: WebRequest) -> Dict[str, Any]: args = web_request.get_args() ep = web_request.get_endpoint() if not args: raise self.server.error("No arguments provided") requested_strips = {k: self.strips.get(k, None) for k in args} result = {} req = ep.split("/")[-1] for name, strip in requested_strips.items(): if strip is not None: result[name] = await self._process_request(strip, req, -1) else: result[name] = {"error": "strip_not_found"} return result
async def _handle_feed_request( self, web_request: WebRequest ) -> Dict[str, Any]: action = web_request.get_action() name: str = web_request.get("name") name = name.lower() changed: bool = False db: MoonrakerDatabase = self.server.lookup_component("database") result = "skipped" if action == "POST": if name not in self.subscriptions: feed = RssFeed(name, self.entry_mgr, self.dev_mode) self.subscriptions[name] = feed await feed.initialize() changed = await feed.update_entries() self.stored_feeds.append(name) db.insert_item( "moonraker", "announcements.stored_feeds", self.stored_feeds ) result = "added" elif action == "DELETE": if name not in self.stored_feeds: raise self.server.error(f"Feed '{name}' not stored") if name in self.configured_feeds: raise self.server.error( f"Feed '{name}' exists in the configuration, cannot remove" ) self.stored_feeds.remove(name) db.insert_item( "moonraker", "announcements.stored_feeds", self.stored_feeds ) if name in self.subscriptions: del self.subscriptions[name] changed = await self.entry_mgr.prune_by_feed(name) logging.info(f"Removed Announcement Feed: {name}") result = "removed" else: raise self.server.error(f"Feed does not exist: {name}") if changed: entries = await self.entry_mgr.list_entries() self.eventloop.delay_callback( .05, self.server.send_event, "announcements:entries_updated", {"entries": entries} ) return { "feed": name, "action": result }
def _login_jwt_user(self, web_request: WebRequest, create: bool = False) -> Dict[str, Any]: username: str = web_request.get_str('username') password: str = web_request.get_str('password') user_info: Dict[str, Any] if username in RESERVED_USERS: raise self.server.error(f"Invalid Request for user {username}") if create: if username in self.users: raise self.server.error(f"User {username} already exists") salt = secrets.token_bytes(32) hashed_pass = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, HASH_ITER).hex() user_info = { 'username': username, 'password': hashed_pass, 'salt': salt.hex(), 'created_on': time.time() } self.users[username] = user_info action = "user_created" else: if username not in self.users: raise self.server.error(f"Unregistered User: {username}") user_info = self.users[username] salt = bytes.fromhex(user_info['salt']) hashed_pass = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, HASH_ITER).hex() action = "user_logged_in" if hashed_pass != user_info['password']: raise self.server.error("Invalid Password") jwt_secret_hex: Optional[str] = user_info.get('jwt_secret', None) if jwt_secret_hex is None: private_key = Signer() jwk_id = base64url_encode(secrets.token_bytes()).decode() user_info['jwt_secret'] = private_key.hex_seed().decode() user_info['jwk_id'] = jwk_id self.users[username] = user_info self.public_jwks[jwk_id] = self._generate_public_jwk(private_key) else: private_key = self._load_private_key(jwt_secret_hex) jwk_id = user_info['jwk_id'] token = self._generate_jwt(username, jwk_id, private_key) refresh_token = self._generate_jwt( username, jwk_id, private_key, token_type="refresh", exp_time=datetime.timedelta(days=self.login_timeout)) if create: IOLoop.current().call_later(.005, self.server.send_event, "authorization:user_created", {'username': username}) return { 'username': username, 'token': token, 'refresh_token': refresh_token, 'action': action }
async def _do_remote_request(self, args, conn): return await self.server.make_request( WebRequest(self.callback, args, conn=conn, ip_addr=self.request.remote_ip, user=self.current_user))
async def _handle_apikey_request(self, web_request: WebRequest) -> str: action = web_request.get_action() if action.upper() == 'POST': self.api_key = uuid.uuid4().hex self.users[API_USER]['api_key'] = self.api_key self._sync_user(API_USER) return self.api_key
async def _handle_service_request(self, web_request: WebRequest) -> str: name: str = web_request.get('service') action = web_request.get_endpoint().split('/')[-1] if name == "moonraker": if action != "restart": raise self.server.error( f"Service action '{action}' not available for moonraker") event_loop = self.server.get_event_loop() event_loop.register_callback(self.do_service_action, action, name) elif self.sys_provider.is_service_available(name): await self.do_service_action(action, name) else: if name in ALLOWED_SERVICES: raise self.server.error(f"Service '{name}' not installed") raise self.server.error(f"Service '{name}' not allowed") return "ok"
async def _handle_update_request( self, web_request: WebRequest ) -> Dict[str, Any]: subs: Optional[Union[str, List[str]]] subs = web_request.get("subscriptions", None) if isinstance(subs, str): subs = [sub.strip() for sub in subs.split(",") if sub.strip()] elif subs is None: subs = list(self.subscriptions.keys()) for sub in subs: if sub not in self.subscriptions: raise self.server.error(f"No subscription for {sub}") async with self.request_lock: changed = False for sub in subs: ret = await self.subscriptions[sub].update_entries() changed |= ret entries = await self.entry_mgr.list_entries() if changed: self.eventloop.delay_callback( .05, self.server.send_event, "announcements:entries_updated", {"entries": entries}) return { "entries": entries, "modified": changed }
async def _select_file(self, web_request: WebRequest ) -> None: command: str = web_request.get('command') rel_path: str = web_request.get('relative_path') root, filename = rel_path.strip("/").split("/", 1) fmgr: FileManager = self.server.lookup_component('file_manager') if command == "select": start_print: bool = web_request.get('print', False) if not start_print: # No-op, selecting a file has no meaning in Moonraker return if root != "gcodes": raise self.server.error( "File must be located in the 'gcodes' root", 400) if not fmgr.check_file_exists(root, filename): raise self.server.error("File does not exist") try: ret = await self.klippy_apis.query_objects( {'print_stats': None}) pstate: str = ret['print_stats']['state'] except self.server.error: pstate = "not_avail" started: bool = False if pstate not in ["printing", "paused", "not_avail"]: try: await self.klippy_apis.start_print(filename) except self.server.error: started = False else: logging.debug(f"Job '{filename}' started via Octoprint API") started = True if not started: if fmgr.upload_queue_enabled(): job_queue: JobQueue = self.server.lookup_component( 'job_queue') await job_queue.queue_job(filename, check_exists=False) # Fire the file_manager's upload_queued event for # compatibility. We assume that this endpoint is # requests by Cura after a file has been uploaded. self.server.send_event("file_manager:upload_queued", filename) logging.debug(f"Job '{filename}' queued via Octoprint API") else: raise self.server.error("Conflict", 409) else: raise self.server.error(f"Unsupported Command: {command}")
async def _handle_single_wled_request( self: WLED, web_request: WebRequest) -> Dict[str, Any]: strip_name: str = web_request.get_str('strip') preset: int = web_request.get_int('preset', -1) req_action = web_request.get_action() if strip_name not in self.strips: raise self.server.error(f"No valid strip named {strip_name}") strip = self.strips[strip_name] if req_action == 'GET': return {strip_name: strip.get_strip_info()} elif req_action == "POST": action = web_request.get_str('action').lower() if action not in ["on", "off", "toggle"]: raise self.server.error(f"Invalid requested action '{action}'") result = await self._process_request(strip, action, preset) return {strip_name: result}
async def _handle_klippy_identified(self) -> None: if self.status_objs: args = {'objects': self.status_objs} try: await self.server.make_request( WebRequest("objects/subscribe", args, conn=self)) except self.server.error: pass
async def _handle_gcode_store_request( self, web_request: WebRequest) -> Dict[str, List[Dict[str, Any]]]: count = web_request.get_int("count", None) if count is not None: gc_responses = list(self.gcode_queue)[-count:] else: gc_responses = list(self.gcode_queue) return {'gcode_store': gc_responses}
async def _handle_service_request(self, web_request: WebRequest) -> str: name: str = web_request.get('service') action = web_request.get_endpoint().split('/')[-1] if name == "moonraker": if action != "restart": raise self.server.error( f"Service action '{action}' not available for moonraker") IOLoop.current().spawn_callback(self.do_service_action, action, name) elif name in self.available_services: await self.do_service_action(action, name) else: if name in ALLOWED_SERVICES and \ name not in self.available_services: raise self.server.error(f"Service '{name}' not installed") raise self.server.error(f"Service '{name}' not allowed") return "ok"
async def _request_subscripton(self, web_request: WebRequest) -> Dict[str, Any]: args = web_request.get_args() conn = web_request.get_connection() # Build the subscription request from a superset of all client # subscriptions sub = args.get('objects', {}) if conn is None: raise self.error( "No connection associated with subscription request") self.subscriptions[conn] = sub all_subs: Dict[str, Any] = {} # request superset of all client subscriptions for sub in self.subscriptions.values(): for obj, items in sub.items(): if obj in all_subs: pi = all_subs[obj] if items is None or pi is None: all_subs[obj] = None else: uitems = list(set(pi) | set(items)) all_subs[obj] = uitems else: all_subs[obj] = items args['objects'] = all_subs args['response_template'] = {'method': "process_status_update"} result = await self._request_standard(web_request) # prune the status response pruned_status = {} all_status = result['status'] sub = self.subscriptions.get(conn, {}) for obj, fields in all_status.items(): if obj in sub: valid_fields = sub[obj] if valid_fields is None: pruned_status[obj] = fields else: pruned_status[obj] = { k: v for k, v in fields.items() if k in valid_fields } result['status'] = pruned_status return result