예제 #1
0
 def test_get_list_deprecate(self, test_config: ConfigHelper):
     server = test_config.get_server()
     test_config.getlist("test_list", deprecate=True)
     expected = (
         f"[test_options]: Option 'test_list' is "
         "deprecated, see the configuration documention "
         "at https://moonraker.readthedocs.io/en/latest/configuration")
     assert expected in server.warnings
예제 #2
0
 def __init__(self, config: ConfigHelper,
              cmd_helper: CommandHelper) -> None:
     super().__init__(config, cmd_helper, prefix="Web Client")
     self.repo = config.get('repo').strip().strip("/")
     self.owner = self.repo.split("/", 1)[0]
     self.path = pathlib.Path(config.get("path")).expanduser().resolve()
     self.type = config.get('type')
     self.channel = "stable" if self.type == "web" else "beta"
     self.persistent_files: List[str] = []
     pfiles = config.getlist('persistent_files', None)
     if pfiles is not None:
         self.persistent_files = [pf.strip("/") for pf in pfiles]
         if ".version" in self.persistent_files:
             raise config.error(
                 "Invalid value for option 'persistent_files': "
                 "'.version' can not be persistent")
     storage = self._load_storage()
     self.version: str = storage.get('version', "?")
     self.remote_version: str = storage.get('remote_version', "?")
     dl_info: List[Any] = storage.get('dl_info', ["?", "?", 0])
     self.dl_info: Tuple[str, str, int] = cast(Tuple[str, str, int],
                                               tuple(dl_info))
     self.refresh_evt: Optional[asyncio.Event] = None
     self.mutex: asyncio.Lock = asyncio.Lock()
     logging.info(f"\nInitializing Client Updater: '{self.name}',"
                  f"\nChannel: {self.channel}"
                  f"\npath: {self.path}")
예제 #3
0
    def __init__(self, config: ConfigHelper) -> None:

        self.config = config
        name_parts = config.get_name().split(maxsplit=1)
        if len(name_parts) != 2:
            raise config.error(f"Invalid Section Name: {config.get_name()}")
        self.server = config.get_server()
        self.name = name_parts[1]
        self.apprise = apprise.Apprise()
        self.warned = False

        self.attach_requires_file_system_check = True
        self.attach = config.get("attach", None)
        if self.attach is None or \
            (self.attach.startswith("http://") or
             self.attach.startswith("https://")):
            self.attach_requires_file_system_check = False

        url_template = config.gettemplate('url')
        self.url = url_template.render()

        if len(self.url) < 2:
            raise config.error(f"Invalid url for: {config.get_name()}")

        self.title = config.gettemplate('title', None)
        self.body = config.gettemplate("body", None)

        self.events: List[str] = config.getlist("events", separator=",")

        self.apprise.add(self.url)
예제 #4
0
    def __init__(self, config: ConfigHelper) -> None:
        self.server = config.get_server()
        self.entry_mgr = EntryManager(config)
        self.eventloop = self.server.get_event_loop()
        self.update_timer = self.eventloop.register_timer(
            self._handle_update_timer
        )
        self.request_lock = asyncio.Lock()
        self.dev_mode = config.getboolean("dev_mode", False)
        self.subscriptions: Dict[str, RssFeed] = {
            "moonraker": RssFeed("moonraker", self.entry_mgr, self.dev_mode),
            "klipper": RssFeed("klipper", self.entry_mgr, self.dev_mode)
        }
        self.stored_feeds: List[str] = []
        sub_list: List[str] = config.getlist("subscriptions", [])
        self.configured_feeds: List[str] = ["moonraker", "klipper"]
        for sub in sub_list:
            sub = sub.lower()
            if sub in self.subscriptions:
                continue
            self.configured_feeds.append(sub)
            self.subscriptions[sub] = RssFeed(
                sub, self.entry_mgr, self.dev_mode
            )

        self.server.register_endpoint(
            "/server/announcements/list", ["GET"],
            self._list_announcements
        )
        self.server.register_endpoint(
            "/server/announcements/dismiss", ["POST"],
            self._handle_dismiss_request
        )
        self.server.register_endpoint(
            "/server/announcements/update", ["POST"],
            self._handle_update_request
        )
        self.server.register_endpoint(
            "/server/announcements/feed", ["POST", "DELETE"],
            self._handle_feed_request
        )
        self.server.register_endpoint(
            "/server/announcements/feeds", ["GET"],
            self._handle_list_feeds
        )
        self.server.register_notification(
            "announcements:dismissed", "announcement_dismissed"
        )
        self.server.register_notification(
            "announcements:entries_updated", "announcement_update"
        )
        self.server.register_notification(
            "announcements:dismiss_wake", "announcement_wake"
        )
예제 #5
0
    def __init__(self, config: ConfigHelper) -> None:
        self.server = config.get_server()
        self.event_loop = self.server.get_event_loop()
        self.file_manager: FMComp = \
            self.server.lookup_component('file_manager')
        self.klippy_apis: APIComp = \
            self.server.lookup_component('klippy_apis')
        self.kinematics: str = "none"
        self.machine_name = config.get('machine_name', "Klipper")
        self.firmware_name: str = "Repetier | Klipper"
        self.last_message: Optional[str] = None
        self.last_gcode_response: Optional[str] = None
        self.current_file: str = ""
        self.file_metadata: Dict[str, Any] = {}
        self.enable_checksum = config.getboolean('enable_checksum', True)
        self.debug_queue: Deque[str] = deque(maxlen=100)

        # Initialize tracked state.
        self.printer_state: Dict[str, Dict[str, Any]] = {
            'gcode_move': {},
            'toolhead': {},
            'virtual_sdcard': {},
            'fan': {},
            'display_status': {},
            'print_stats': {},
            'idle_timeout': {},
            'gcode_macro PANELDUE_BEEP': {}
        }
        self.extruder_count: int = 0
        self.heaters: List[str] = []
        self.is_ready: bool = False
        self.is_shutdown: bool = False
        self.initialized: bool = False
        self.cq_busy: bool = False
        self.gq_busy: bool = False
        self.command_queue: List[Tuple[FlexCallback, Any, Any]] = []
        self.gc_queue: List[str] = []
        self.last_printer_state: str = 'O'
        self.last_update_time: float = 0.

        # Set up macros
        self.confirmed_gcode: str = ""
        self.mbox_sequence: int = 0
        self.available_macros: Dict[str, str] = {}
        self.confirmed_macros = {
            "RESTART": "RESTART",
            "FIRMWARE_RESTART": "FIRMWARE_RESTART"
        }
        macros = config.getlist('macros', None)
        if macros is not None:
            # The macro's configuration name is the key, whereas the full
            # command is the value
            self.available_macros = {m.split()[0]: m for m in macros}
        conf_macros = config.getlist('confirmed_macros', None)
        if conf_macros is not None:
            # The macro's configuration name is the key, whereas the full
            # command is the value
            self.confirmed_macros = {m.split()[0]: m for m in conf_macros}
        self.available_macros.update(self.confirmed_macros)

        self.non_trivial_keys = config.getlist('non_trivial_keys',
                                               ["Klipper state"])
        self.ser_conn = SerialConnection(config, self)
        logging.info("PanelDue Configured")

        # Register server events
        self.server.register_event_handler("server:klippy_ready",
                                           self._process_klippy_ready)
        self.server.register_event_handler("server:klippy_shutdown",
                                           self._process_klippy_shutdown)
        self.server.register_event_handler("server:klippy_disconnect",
                                           self._process_klippy_disconnect)
        self.server.register_event_handler("server:status_update",
                                           self.handle_status_update)
        self.server.register_event_handler("server:gcode_response",
                                           self.handle_gcode_response)

        self.server.register_remote_method("paneldue_beep", self.paneldue_beep)

        # These commands are directly executued on the server and do not to
        # make a request to Klippy
        self.direct_gcodes: Dict[str, FlexCallback] = {
            'M20': self._run_paneldue_M20,
            'M30': self._run_paneldue_M30,
            'M36': self._run_paneldue_M36,
            'M408': self._run_paneldue_M408
        }

        # These gcodes require special parsing or handling prior to being
        # sent via Klippy's "gcode/script" api command.
        self.special_gcodes: Dict[str, Callable[[List[str]], str]] = {
            'M0': lambda args: "CANCEL_PRINT",
            'M23': self._prepare_M23,
            'M24': lambda args: "RESUME",
            'M25': lambda args: "PAUSE",
            'M32': self._prepare_M32,
            'M98': self._prepare_M98,
            'M120': lambda args: "SAVE_GCODE_STATE STATE=PANELDUE",
            'M121': lambda args: "RESTORE_GCODE_STATE STATE=PANELDUE",
            'M290': self._prepare_M290,
            'M292': self._prepare_M292,
            'M999': lambda args: "FIRMWARE_RESTART"
        }
예제 #6
0
 def test_get_list_default(self, test_config: ConfigHelper):
     assert test_config.getlist("invalid_option", None) is None
예제 #7
0
 def test_get_list_fail(self, test_config: ConfigHelper):
     with pytest.raises(ConfigError):
         test_config.getlist("invalid_option")
예제 #8
0
 def test_get_list_exists(self, test_config: ConfigHelper):
     val = test_config.getlist("test_list")
     assert val == ["one", "two", "three"]
예제 #9
0
    def __init__(self, config: ConfigHelper) -> None:
        self.server = config.get_server()
        self.login_timeout = config.getint('login_timeout', 90)
        self.force_logins = config.getboolean('force_logins', False)
        database: DBComp = self.server.lookup_component('database')
        database.register_local_namespace('authorized_users', forbidden=True)
        self.users = database.wrap_namespace('authorized_users')
        api_user: Optional[Dict[str, Any]] = self.users.get(API_USER, None)
        if api_user is None:
            self.api_key = uuid.uuid4().hex
            self.users[API_USER] = {
                'username': API_USER,
                'api_key': self.api_key,
                'created_on': time.time()
            }
        else:
            self.api_key = api_user['api_key']
        hi = self.server.get_host_info()
        self.issuer = f"http://{hi['hostname']}:{hi['port']}"
        self.public_jwks: Dict[str, Dict[str, Any]] = {}
        for username, user_info in list(self.users.items()):
            if username == API_USER:
                # Validate the API User
                for item in ["username", "api_key", "created_on"]:
                    if item not in user_info:
                        self.users[API_USER] = {
                            'username': API_USER,
                            'api_key': self.api_key,
                            'created_on': time.time()
                        }
                        break
                continue
            else:
                # validate created users
                valid = True
                for item in ["username", "password", "salt", "created_on"]:
                    if item not in user_info:
                        logging.info(
                            f"Authorization: User {username} does not "
                            f"contain field {item}, removing")
                        del self.users[username]
                        valid = False
                        break
                if not valid:
                    continue
            # generate jwks for valid users
            if 'jwt_secret' in user_info:
                try:
                    priv_key = self._load_private_key(user_info['jwt_secret'])
                    jwk_id = user_info['jwk_id']
                except (self.server.error, KeyError):
                    logging.info("Invalid key found for user, removing")
                    user_info.pop('jwt_secret', None)
                    user_info.pop('jwk_id', None)
                    self.users[username] = user_info
                    continue
                self.public_jwks[jwk_id] = self._generate_public_jwk(priv_key)

        self.trusted_users: Dict[IPAddr, Any] = {}
        self.oneshot_tokens: Dict[str, OneshotToken] = {}
        self.permitted_paths: Set[str] = set()

        # Get allowed cors domains
        self.cors_domains: List[str] = []
        for domain in config.getlist('cors_domains', []):
            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.")
            if domain.endswith("/"):
                self.server.add_warning(
                    f"[authorization]: Invalid domain '{domain}' in option "
                    "'cors_domains'.  Domain's cannot contain a trailing "
                    "slash.")
            else:
                self.cors_domains.append(
                    domain.replace(".", "\\.").replace("*", ".*"))

        # Get Trusted Clients
        self.trusted_ips: List[IPAddr] = []
        self.trusted_ranges: List[IPNetwork] = []
        self.trusted_domains: List[str] = []
        for val in config.getlist('trusted_clients', []):
            # Check IP address
            try:
                tc = ipaddress.ip_address(val)
            except ValueError:
                pass
            else:
                self.trusted_ips.append(tc)
                continue
            # Check ip network
            try:
                tc = ipaddress.ip_network(val)
            except ValueError as e:
                if "has host bits set" in str(e):
                    self.server.add_warning(
                        f"[authorization]: Invalid CIDR expression '{val}' "
                        "in option 'trusted_clients'")
                    continue
                pass
            else:
                self.trusted_ranges.append(tc)
                continue
            # Check hostname
            match = re.match(r"([a-z0-9]+(-[a-z0-9]+)*\.?)+[a-z]{2,}$", val)
            if match is not None:
                self.trusted_domains.append(val.lower())
            else:
                self.server.add_warning(
                    f"[authorization]: Invalid domain name '{val}' "
                    "in option 'trusted_clients'")

        t_clients = "\n".join([str(ip) for ip in self.trusted_ips] +
                              [str(rng) for rng in self.trusted_ranges] +
                              self.trusted_domains)
        c_domains = "\n".join(self.cors_domains)

        logging.info(f"Authorization Configuration Loaded\n"
                     f"Trusted Clients:\n{t_clients}\n"
                     f"CORS Domains:\n{c_domains}")

        eventloop = self.server.get_event_loop()
        self.prune_timer = eventloop.register_timer(self._prune_conn_handler)

        # Register Authorization Endpoints
        self.permitted_paths.add("/server/redirect")
        self.permitted_paths.add("/access/login")
        self.permitted_paths.add("/access/refresh_jwt")
        self.server.register_endpoint("/access/login", ['POST'],
                                      self._handle_login,
                                      transports=['http'])
        self.server.register_endpoint("/access/logout", ['POST'],
                                      self._handle_logout,
                                      transports=['http'])
        self.server.register_endpoint("/access/refresh_jwt", ['POST'],
                                      self._handle_refresh_jwt,
                                      transports=['http'])
        self.server.register_endpoint("/access/user",
                                      ['GET', 'POST', 'DELETE'],
                                      self._handle_user_request,
                                      transports=['http'])
        self.server.register_endpoint("/access/users/list", ['GET'],
                                      self._handle_list_request,
                                      transports=['http'])
        self.server.register_endpoint("/access/user/password", ['POST'],
                                      self._handle_password_reset,
                                      transports=['http'])
        self.server.register_endpoint("/access/api_key", ['GET', 'POST'],
                                      self._handle_apikey_request,
                                      transports=['http'])
        self.server.register_endpoint("/access/oneshot_token", ['GET'],
                                      self._handle_oneshot_request,
                                      transports=['http'])
        self.server.register_notification("authorization:user_created")
        self.server.register_notification("authorization:user_deleted")
예제 #10
0
    def __init__(self, config: ConfigHelper,
                 cmd_helper: CommandHelper) -> None:
        super().__init__(config, cmd_helper, prefix="Application")
        self.config = config
        self.debug = self.cmd_helper.is_debug_enabled()
        type_choices = list(TYPE_TO_CHANNEL.keys())
        self.type = config.get('type').lower()
        if self.type not in type_choices:
            raise config.error(
                f"Config Error: Section [{config.get_name()}], Option "
                f"'type: {self.type}': value must be one "
                f"of the following choices: {type_choices}")
        self.channel = config.get("channel", TYPE_TO_CHANNEL[self.type])
        if self.type == "zip_beta":
            self.server.add_warning(
                f"Config Section [{config.get_name()}], Option 'type: "
                "zip_beta', value 'zip_beta' is deprecated.  Set 'type' "
                "to zip and 'channel' to 'beta'")
            self.type = "zip"
        self.path = pathlib.Path(config.get('path')).expanduser().resolve()
        executable = config.get('env', None)
        if self.channel not in SUPPORTED_CHANNELS[self.type]:
            raise config.error(
                f"Invalid Channel '{self.channel}' for config "
                f"section [{config.get_name()}], type: {self.type}")
        self._verify_path(config, 'path', self.path)
        self.executable: Optional[pathlib.Path] = None
        self.pip_exe: Optional[pathlib.Path] = None
        self.venv_args: Optional[str] = None
        if executable is not None:
            self.executable = pathlib.Path(executable).expanduser()
            self.pip_exe = self.executable.parent.joinpath("pip")
            if not self.pip_exe.exists():
                self.server.add_warning(
                    f"Update Manger {self.name}: Unable to locate pip "
                    "executable")
            self._verify_path(config, 'env', self.executable)
            self.venv_args = config.get('venv_args', None)

        self.info_tags: List[str] = config.getlist("info_tags", [])
        self.managed_services: List[str] = []
        svc_default = []
        if config.getboolean("is_system_service", True):
            svc_default.append(self.name)
        svc_choices = [self.name, "klipper", "moonraker"]
        services: List[str] = config.getlist("managed_services",
                                             svc_default,
                                             separator=None)
        for svc in services:
            if svc not in svc_choices:
                raw = " ".join(services)
                self.server.add_warning(
                    f"[{config.get_name()}]: Option 'restart_action: {raw}' "
                    f"contains an invalid value '{svc}'.  All values must be "
                    f"one of the following choices: {svc_choices}")
                break
        for svc in svc_choices:
            if svc in services and svc not in self.managed_services:
                self.managed_services.append(svc)
        logging.debug(
            f"Extension {self.name} managed services: {self.managed_services}")
        # We need to fetch all potential options for an Application.  Not
        # all options apply to each subtype, however we can't limit the
        # options in children if we want to switch between channels and
        # satisfy the confighelper's requirements.
        self.moved_origin: Optional[str] = config.get('moved_origin', None)
        self.origin: str = config.get('origin')
        self.primary_branch = config.get("primary_branch", "master")
        self.npm_pkg_json: Optional[pathlib.Path] = None
        if config.getboolean("enable_node_updates", False):
            self.npm_pkg_json = self.path.joinpath("package-lock.json")
            self._verify_path(config, 'enable_node_updates', self.npm_pkg_json)
        self.python_reqs: Optional[pathlib.Path] = None
        if self.executable is not None:
            self.python_reqs = self.path.joinpath(config.get("requirements"))
            self._verify_path(config, 'requirements', self.python_reqs)
        self.install_script: Optional[pathlib.Path] = None
        install_script = config.get('install_script', None)
        if install_script is not None:
            self.install_script = self.path.joinpath(install_script).resolve()
            self._verify_path(config, 'install_script', self.install_script)