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_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_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_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_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_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}
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 _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 _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_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 _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_password_reset( self, web_request: WebRequest) -> Dict[str, str]: password: str = web_request.get_str('password') new_pass: str = web_request.get_str('new_password') user_info = web_request.get_current_user() if user_info is None: raise self.server.error("No Current User") username = user_info['username'] if username in RESERVED_USERS: raise self.server.error( f"Invalid Reset Request for user {username}") salt = bytes.fromhex(user_info['salt']) hashed_pass = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, HASH_ITER).hex() if hashed_pass != user_info['password']: raise self.server.error("Invalid Password") new_hashed_pass = hashlib.pbkdf2_hmac('sha256', new_pass.encode(), salt, HASH_ITER).hex() self.users[f'{username}.password'] = new_hashed_pass return {'username': username, 'action': "user_password_reset"}
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_agent_event(self, web_request: WebRequest) -> str: conn = web_request.get_connection() if not isinstance(conn, WebSocket): raise self.server.error("No connection detected") if conn.client_data["type"] != "agent": raise self.server.error( "Only connections of the 'agent' type can send events") name = conn.client_data["name"] evt_name = web_request.get_str("event") if evt_name in ["connected", "disconnected"]: raise self.server.error(f"Event '{evt_name}' is reserved") data: Optional[Union[List, Dict[str, Any]]] data = web_request.get("data", None) evt: Dict[str, Any] = {"agent": name, "event": evt_name} if data is not None: evt["data"] = data conn.send_notification("agent_event", [evt]) return "ok"
async def _handle_refresh_jwt(self, web_request: WebRequest) -> Dict[str, str]: refresh_token: str = web_request.get_str('refresh_token') try: user_info = self._decode_jwt(refresh_token, token_type="refresh") except Exception: raise self.server.error("Invalid Refresh Token", 401) username: str = user_info['username'] if 'jwt_secret' not in user_info or "jwk_id" not in user_info: raise self.server.error("User not logged in", 401) private_key = self._load_private_key(user_info['jwt_secret']) jwk_id: str = user_info['jwk_id'] token = self._generate_jwt(username, jwk_id, private_key) return { 'username': username, 'token': token, 'action': 'user_jwt_refresh' }
async def _handle_webcam_test(self, web_request: WebRequest) -> Dict[str, Any]: name = web_request.get_str("name") if name not in self.webcams: raise self.server.error(f"Webcam '{name}' not found", 404) client: HttpClient = self.server.lookup_component("http_client") cam = self.webcams[name] result: Dict[str, Any] = {"name": name, "snapshot_reachable": False} for img_type in ["snapshot", "stream"]: try: func = getattr(cam, f"get_{img_type}_url") result[f"{img_type}_url"] = await func(True) except Exception: logging.exception(f"Error Processing {img_type} url") result[f"{img_type}_url"] = "" if result.get("snapshot_url", "").startswith("http"): url = client.escape_url(result["snapshot_url"]) ret = await client.get(url, connect_timeout=1., request_timeout=1.) result["snapshot_reachable"] = not ret.has_error() return result
def _delete_jwt_user(self, web_request: WebRequest) -> Dict[str, str]: username: str = web_request.get_str('username') current_user = web_request.get_current_user() if current_user is not None: curname = current_user.get('username', None) if curname is not None and curname == username: raise self.server.error( f"Cannot delete logged in user {curname}") if username in RESERVED_USERS: raise self.server.error( f"Invalid Request for reserved user {username}") user_info: Optional[Dict[str, Any]] = self.users.get(username) if user_info is None: raise self.server.error(f"No registered user: {username}") self.public_jwks.pop(self.users.get(f"{username}.jwk_id"), None) del self.users[username] IOLoop.current().call_later(.005, self.server.send_event, "authorization:user_deleted", {'username': username}) return {"username": username, "action": "user_deleted"}
def from_web_request(cls, server: Server, web_request: WebRequest) -> WebCam: webcam: Dict[str, Any] = {} webcam["name"] = web_request.get_str("name") webcam["location"] = web_request.get_str("location", "printer") webcam["service"] = web_request.get_str("service", "mjpegstreamer") webcam["target_fps"] = web_request.get_int("target_fps", 15) webcam["stream_url"] = web_request.get_str("stream_url") webcam["snapshot_url"] = web_request.get_str("snapshot_url") webcam["flip_horizontal"] = web_request.get_boolean( "flip_horizontal", False) webcam["flip_vertical"] = web_request.get_boolean( "flip_vertical", False) webcam["rotation"] = web_request.get_str("rotation", 0) if webcam["rotation"] not in [0, 90, 180, 270]: raise server.error("Invalid value for parameter 'rotate'") webcam["source"] = "database" return cls(server, **webcam)
async def _gcode_start_print(self, web_request: WebRequest) -> str: filename: str = web_request.get_str('filename') return await self.start_print(filename)
async 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') source: str = web_request.get_str('source', self.default_source).lower() if source not in AUTH_SOURCES: raise self.server.error(f"Invalid 'source': {source}") user_info: Dict[str, Any] if username in RESERVED_USERS: raise self.server.error(f"Invalid Request for user {username}") if source == "ldap": if create: raise self.server.error("Cannot Create LDAP User") if self.ldap is None: raise self.server.error("LDAP authentication not available", 401) await self.ldap.authenticate_ldap_user(username, password) if username not in self.users: create = True 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(), 'source': source, 'created_on': time.time() } self.users[username] = user_info self._sync_user(username) action = "user_created" if source == "ldap": # Dont notify user created action = "user_logged_in" create = False else: if username not in self.users: raise self.server.error(f"Unregistered User: {username}") user_info = self.users[username] auth_src = user_info.get("source", "moonraker") if auth_src != source: raise self.server.error( f"Moonraker cannot authenticate user '{username}', must " f"specify source '{auth_src}'", 401) 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._sync_user(username) 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: event_loop = self.server.get_event_loop() event_loop.delay_callback(.005, self.server.send_event, "authorization:user_created", {'username': username}) return { 'username': username, 'token': token, 'source': user_info.get("source", "moonraker"), 'refresh_token': refresh_token, 'action': action }