From 97bf33130789bc443f91579b6f2dcdf8d91f0fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Fri, 18 Nov 2016 13:01:14 +0100 Subject: [PATCH] Add safe mode that disables all third party plugins Can be enabled either through new --safe command line parameter or through server.startOnceInSafeMode in config.yaml When running in safe mode the plugin manager will only allow to disable or uninstall third party plugins. Enabling third party plugins or installing new plugins is disabled. That will hopefully allow for more straightforward recovery in case of a misbehaving plugin. --- src/octoprint/__init__.py | 59 +++++++++++++++---- src/octoprint/cli/__init__.py | 5 +- src/octoprint/cli/plugins.py | 2 +- src/octoprint/cli/server.py | 36 ++++++----- src/octoprint/plugin/__init__.py | 32 ++++------ src/octoprint/plugin/core.py | 19 +++++- .../plugins/pluginmanager/__init__.py | 23 +++++--- .../pluginmanager/static/js/pluginmanager.js | 28 +++++++-- .../templates/pluginmanager_settings.jinja2 | 11 +++- src/octoprint/server/__init__.py | 11 +++- src/octoprint/server/util/sockjs.py | 4 +- src/octoprint/settings.py | 1 + src/octoprint/static/js/app/dataupdater.js | 15 +++++ 13 files changed, 179 insertions(+), 67 deletions(-) diff --git a/src/octoprint/__init__.py b/src/octoprint/__init__.py index b27e14b2..ebf1ffbd 100644 --- a/src/octoprint/__init__.py +++ b/src/octoprint/__init__.py @@ -57,16 +57,21 @@ class FatalStartupError(BaseException): def init_platform(basedir, configfile, use_logging_file=True, logging_file=None, logging_config=None, debug=False, verbosity=0, uncaught_logger=None, - uncaught_handler=None, after_preinit_logging=None, - after_settings=None, after_logging=None): + uncaught_handler=None, safe_mode=False, after_preinit_logging=None, + after_settings=None, after_logging=None, after_safe_mode=None): + kwargs = dict() + logger, recorder = preinit_logging(debug, verbosity, uncaught_logger, uncaught_handler) + kwargs["logger"] = logger + kwargs["recorder"] = recorder if callable(after_preinit_logging): - after_preinit_logging(logger, recorder) + after_preinit_logging(**kwargs) settings = init_settings(basedir, configfile) + kwargs["settings"] = settings if callable(after_settings): - after_settings(settings) + after_settings(**kwargs) logger = init_logging(settings, use_logging_file=use_logging_file, @@ -76,12 +81,20 @@ def init_platform(basedir, configfile, use_logging_file=True, logging_file=None, verbosity=verbosity, uncaught_logger=uncaught_logger, uncaught_handler=uncaught_handler) + kwargs["logger"] = logger if callable(after_logging): - after_logging(logger, recorder) + after_logging(**kwargs) - plugin_manager = init_pluginsystem(settings) - return settings, logger, plugin_manager + settings_safe_mode = settings.getBoolean(["server", "startOnceInSafeMode"]) + safe_mode = safe_mode or settings_safe_mode + kwargs["safe_mode"] = safe_mode + + if callable(after_safe_mode): + after_safe_mode(**kwargs) + + plugin_manager = init_pluginsystem(settings, safe_mode=safe_mode) + return settings, logger, safe_mode, plugin_manager def init_settings(basedir, configfile): @@ -254,19 +267,45 @@ def set_logging_config(config, debug, verbosity, uncaught_logger, uncaught_handl if uncaught_handler is None: def exception_logger(exc_type, exc_value, exc_tb): logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_tb)) + uncaught_handler = exception_logger sys.excepthook = uncaught_handler return logger -def init_pluginsystem(settings): +def init_pluginsystem(settings, safe_mode=False): """Initializes the plugin manager based on the settings.""" - from octoprint.plugin import plugin_manager - pm = plugin_manager(init=True, settings=settings) + import os logger = log.getLogger(__name__) + + plugin_folders = [(os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "plugins")), True), + settings.getBaseFolder("plugins")] + plugin_entry_points = ["octoprint.plugin"] + plugin_disabled_list = settings.get(["plugins", "_disabled"]) + + plugin_validators = [] + if safe_mode: + def validator(phase, plugin_info): + if phase == "after_load": + setattr(plugin_info, "safe_mode_victim", not plugin_info.bundled) + setattr(plugin_info, "safe_mode_enabled", False) + elif phase == "before_enable": + if not plugin_info.bundled: + setattr(plugin_info, "safe_mode_enabled", True) + return False + return True + plugin_validators.append(validator) + + from octoprint.plugin import plugin_manager + pm = plugin_manager(init=True, + plugin_folders=plugin_folders, + plugin_entry_points=plugin_entry_points, + plugin_disabled_list=plugin_disabled_list, + plugin_validators=plugin_validators) + settings_overlays = dict() disabled_from_overlays = dict() diff --git a/src/octoprint/cli/__init__.py b/src/octoprint/cli/__init__.py index 43923eff..635ffc79 100644 --- a/src/octoprint/cli/__init__.py +++ b/src/octoprint/cli/__init__.py @@ -13,10 +13,11 @@ import octoprint class OctoPrintContext(object): """Custom context wrapping the standard options.""" - def __init__(self, configfile=None, basedir=None, verbosity=0): + def __init__(self, configfile=None, basedir=None, verbosity=0, safe_mode=False): self.configfile = configfile self.basedir = basedir self.verbosity = verbosity + self.safe_mode = safe_mode pass_octoprint_ctx = click.make_pass_decorator(OctoPrintContext, ensure=True) """Decorator to pass in the :class:`OctoPrintContext` instance.""" @@ -95,6 +96,8 @@ def standard_options(hidden=False): help="Specify the config file to use."), factory("--verbose", "-v", "verbosity", count=True, callback=set_ctx_obj_option, is_eager=True, expose_value=False, help="Increase logging verbosity"), + factory("--safe", "safe_mode", is_flag=True, callback=set_ctx_obj_option, is_eager=True, expose_value=False, + help="Enable safe mode; disables all third party plugins") ] return bulk_options(options) diff --git a/src/octoprint/cli/plugins.py b/src/octoprint/cli/plugins.py index e28c3ee6..44d60952 100644 --- a/src/octoprint/cli/plugins.py +++ b/src/octoprint/cli/plugins.py @@ -51,7 +51,7 @@ class OctoPrintPluginCommands(click.MultiCommand): from octoprint import init_settings, init_pluginsystem, FatalStartupError try: self.settings = init_settings(ctx.obj.basedir, ctx.obj.configfile) - self.plugin_manager = init_pluginsystem(self.settings) + self.plugin_manager = init_pluginsystem(self.settings, safe_mode=ctx.obj.safe_mode) except FatalStartupError as e: click.echo(e.message, err=True) click.echo("There was a fatal error initializing the settings or the plugin system.", err=True) diff --git a/src/octoprint/cli/server.py b/src/octoprint/cli/server.py index 0b187bd6..2ea6fc4b 100644 --- a/src/octoprint/cli/server.py +++ b/src/octoprint/cli/server.py @@ -11,14 +11,16 @@ import sys from octoprint.cli import pass_octoprint_ctx, bulk_options, standard_options -def run_server(basedir, configfile, host, port, debug, allow_root, logging_config, verbosity, octoprint_daemon = None): +def run_server(basedir, configfile, host, port, debug, allow_root, logging_config, verbosity, safe_mode, octoprint_daemon=None): """Initializes the environment and starts up the server.""" from octoprint import init_platform, __display_version__, FatalStartupError - def log_startup(_, recorder): + def log_startup(recorder=None, safe_mode=None, **kwargs): logger = logging.getLogger("octoprint.server") logger.info("Starting OctoPrint {}".format(__display_version__)) + if safe_mode: + logger.info("Starting in SAFE MODE. Third party plugins will be disabled!") if recorder and len(recorder): logger.info("--- Logged during platform initialization: ---") @@ -41,13 +43,14 @@ def run_server(basedir, configfile, host, port, debug, allow_root, logging_confi "https://urllib3.readthedocs.org/en/latest/security.html#openssl-pyopenssl") try: - settings, _, plugin_manager = init_platform(basedir, - configfile, - logging_file=logging_config, - debug=debug, - verbosity=verbosity, - uncaught_logger=__name__, - after_logging=log_startup) + settings, _, safe_mode, plugin_manager = init_platform(basedir, + configfile, + logging_file=logging_config, + debug=debug, + verbosity=verbosity, + uncaught_logger=__name__, + safe_mode=safe_mode, + after_safe_mode=log_startup) except FatalStartupError as e: click.echo(e.message, err=True) click.echo("There was a fatal error starting up OctoPrint.", err=True) @@ -58,6 +61,7 @@ def run_server(basedir, configfile, host, port, debug, allow_root, logging_confi host=host, port=port, debug=debug, + safe_mode=safe_mode, allow_root=allow_root, octoprint_daemon=octoprint_daemon) octoprint_server.run() @@ -73,7 +77,7 @@ server_options = bulk_options([ help="Specify the config file to use for configuring logging."), click.option("--iknowwhatimdoing", "allow_root", is_flag=True, help="Allow OctoPrint to run as user root."), - click.option("--debug", is_flag=True, help="Enable debug mode"), + click.option("--debug", is_flag=True, help="Enable debug mode.") ]) """Decorator to add the options shared among the server commands: ``--host``, ``--port``, ``--logging``, ``--iknowwhatimdoing`` and ``--debug``.""" @@ -93,7 +97,7 @@ def server_commands(obj): def serve_command(obj, host, port, logging, allow_root, debug): """Starts the OctoPrint server.""" run_server(obj.basedir, obj.configfile, host, port, debug, - allow_root, logging, obj.verbosity) + allow_root, logging, obj.verbosity, obj.safe_mode) @server_commands.command(name="daemon") @@ -122,7 +126,7 @@ def daemon_command(octoprint_ctx, pid, host, port, logging, allow_root, debug, c from octoprint.daemon import Daemon class OctoPrintDaemon(Daemon): - def __init__(self, pidfile, basedir, configfile, host, port, debug, allow_root, logging_config, verbosity): + def __init__(self, pidfile, basedir, configfile, host, port, debug, allow_root, logging_config, verbosity, safe_mode): Daemon.__init__(self, pidfile) self._basedir = basedir @@ -133,12 +137,16 @@ def daemon_command(octoprint_ctx, pid, host, port, logging, allow_root, debug, c self._allow_root = allow_root self._logging_config = logging_config self._verbosity = verbosity + self._safe_mode = safe_mode def run(self): - run_server(self._basedir, self._configfile, self._host, self._port, self._debug, self._allow_root, self._logging_config, self._verbosity, self) + run_server(self._basedir, self._configfile, self._host, self._port, self._debug, + self._allow_root, self._logging_config, self._verbosity, self._safe_mode, + octoprint_daemon=self) octoprint_daemon = OctoPrintDaemon(pid, octoprint_ctx.basedir, octoprint_ctx.configfile, - host, port, debug, allow_root, logging, octoprint_ctx.verbosity) + host, port, debug, allow_root, logging, octoprint_ctx.verbosity, + octoprint_ctx.safe_mode) if command == "start": octoprint_daemon.start() diff --git a/src/octoprint/plugin/__init__.py b/src/octoprint/plugin/__init__.py index bab1350a..31367e54 100644 --- a/src/octoprint/plugin/__init__.py +++ b/src/octoprint/plugin/__init__.py @@ -42,9 +42,10 @@ def _validate_plugin(phase, plugin_info): if not "octoprint.accesscontrol.appkey" in hooks: hooks["octoprint.accesscontrol.appkey"] = plugin_info.implementation.get_additional_apps setattr(plugin_info.instance, PluginInfo.attr_hooks, hooks) + return True def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_entry_points=None, plugin_disabled_list=None, - plugin_restart_needing_hooks=None, plugin_obsolete_hooks=None, plugin_validators=None, settings=None): + plugin_restart_needing_hooks=None, plugin_obsolete_hooks=None, plugin_validators=None): """ Factory method for initially constructing and consecutively retrieving the :class:`~octoprint.plugin.core.PluginManager` singleton. @@ -87,14 +88,6 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en else: if init: - if settings is None: - settings = s() - - if plugin_folders is None: - plugin_folders = ( - settings.getBaseFolder("plugins"), - (os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "plugins")), True) - ) if plugin_types is None: plugin_types = [StartupPlugin, ShutdownPlugin, @@ -109,22 +102,17 @@ def plugin_manager(init=False, plugin_folders=None, plugin_types=None, plugin_en ProgressPlugin, WizardPlugin, UiPlugin] - if plugin_entry_points is None: - plugin_entry_points = "octoprint.plugin" - if plugin_disabled_list is None: - plugin_disabled_list = settings.get(["plugins", "_disabled"]) + if plugin_restart_needing_hooks is None: - plugin_restart_needing_hooks = [ - "octoprint.server.http" - ] + plugin_restart_needing_hooks = ["octoprint.server.http"] + if plugin_obsolete_hooks is None: - plugin_obsolete_hooks = [ - "octoprint.comm.protocol.gcode" - ] + plugin_obsolete_hooks = ["octoprint.comm.protocol.gcode"] + if plugin_validators is None: - plugin_validators = [ - _validate_plugin - ] + plugin_validators = [_validate_plugin] + else: + plugin_validators.append(_validate_plugin) _instance = PluginManager(plugin_folders, plugin_types, diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index c95f1326..61ba0ff7 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -149,6 +149,8 @@ class PluginInfo(object): self._license = license def validate(self, phase, additional_validators=None): + result = True + if phase == "before_load": # if the plugin still uses __plugin_init__, log a deprecation warning and move it to __plugin_load__ if hasattr(self.instance, self.__class__.attr_init): @@ -183,7 +185,9 @@ class PluginInfo(object): if additional_validators is not None: for validator in additional_validators: - validator(phase, self) + result = result and validator(phase, self) + + return result def __str__(self): if self.version: @@ -448,6 +452,12 @@ class PluginManager(object): if logging_prefix is None: logging_prefix = "" + if plugin_folders is None: + plugin_folders = [] + if plugin_types is None: + plugin_types = [] + if plugin_entry_points is None: + plugin_entry_points = [] if plugin_disabled_list is None: plugin_disabled_list = [] @@ -740,7 +750,9 @@ class PluginManager(object): plugin = self.plugins[name] try: - plugin.validate("before_load", additional_validators=self.plugin_validators) + if not plugin.validate("before_load", additional_validators=self.plugin_validators): + return + plugin.load() plugin.validate("after_load", additional_validators=self.plugin_validators) self.on_plugin_loaded(name, plugin) @@ -802,6 +814,9 @@ class PluginManager(object): raise PluginCantEnable(name, "Dependency on obsolete hooks detected, full functionality cannot be guaranteed") try: + if not plugin.validate("before_enable", additional_validators=self.plugin_validators): + return False + plugin.enable() self._activate_plugin(name, plugin) except PluginLifecycleException as e: diff --git a/src/octoprint/plugins/pluginmanager/__init__.py b/src/octoprint/plugins/pluginmanager/__init__.py index e1f36c99..97dfc456 100644 --- a/src/octoprint/plugins/pluginmanager/__init__.py +++ b/src/octoprint/plugins/pluginmanager/__init__.py @@ -180,6 +180,8 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, if not admin_permission.can(): return make_response("Insufficient rights", 403) + from octoprint.server import safe_mode + refresh_repository = request.values.get("refresh_repository", "false") in valid_boolean_trues if refresh_repository: self._repository_available = self._refresh_repository() @@ -200,7 +202,8 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, virtual_env=self._pip_caller.virtual_env, additional_args=self._settings.get(["pip_args"]), python=sys.executable - )) + ), + safe_mode=safe_mode) def etag(): import hashlib @@ -208,6 +211,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, hash.update(repr(self._get_plugins())) hash.update(str(self._repository_available)) hash.update(repr(self._repository_plugins)) + hash.update(repr(safe_mode)) return hash.hexdigest() def condition(): @@ -450,7 +454,8 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, needs_refresh = plugin.implementation and isinstance(plugin.implementation, octoprint.plugin.ReloadNeedingPlugin) pending = ((command == "disable" and plugin.key in self._pending_enable) or (command == "enable" and plugin.key in self._pending_disable)) - needs_restart_api = needs_restart and not pending + safe_mode_victim = getattr(plugin, "safe_mode_victim", False) + needs_restart_api = (needs_restart or safe_mode_victim) and not pending needs_refresh_api = needs_refresh and not pending try: @@ -514,12 +519,12 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, self._settings.global_set(["plugins", "_disabled"], disabled_list) self._settings.save(force=True) - if not needs_restart: + if not needs_restart and not getattr(plugin, "safe_mode_victim", False): self._plugin_manager.enable_plugin(plugin.key) else: if plugin.key in self._pending_disable: self._pending_disable.remove(plugin.key) - elif not plugin.enabled and plugin.key not in self._pending_enable: + elif (not plugin.enabled and not getattr(plugin, "safe_mode_enabled", False)) and plugin.key not in self._pending_enable: self._pending_enable.add(plugin.key) def _mark_plugin_disabled(self, plugin, needs_restart=False): @@ -529,12 +534,12 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, self._settings.global_set(["plugins", "_disabled"], disabled_list) self._settings.save(force=True) - if not needs_restart: + if not needs_restart and not getattr(plugin, "safe_mode_victim", False): self._plugin_manager.disable_plugin(plugin.key) else: if plugin.key in self._pending_enable: self._pending_enable.remove(plugin.key) - elif plugin.enabled and plugin.key not in self._pending_disable: + elif (plugin.enabled or getattr(plugin, "safe_mode_enabled", False)) and plugin.key not in self._pending_disable: self._pending_disable.add(plugin.key) def _fetch_repository_from_disk(self): @@ -688,8 +693,10 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin, bundled=plugin.bundled, managable=plugin.managable, enabled=plugin.enabled, - pending_enable=(not plugin.enabled and plugin.key in self._pending_enable), - pending_disable=(plugin.enabled and plugin.key in self._pending_disable), + safe_mode_victim=getattr(plugin, "safe_mode_victim", False), + safe_mode_enabled=getattr(plugin, "safe_mode_enabled", False), + pending_enable=(not plugin.enabled and not getattr(plugin, "safe_mode_enabled", False) and plugin.key in self._pending_enable), + pending_disable=((plugin.enabled or getattr(plugin, "safe_mode_enabled", False)) and plugin.key in self._pending_disable), pending_install=(self._plugin_manager.is_plugin_marked(plugin.key, "installed")), pending_uninstall=(self._plugin_manager.is_plugin_marked(plugin.key, "uninstalled")), origin=plugin.origin.type diff --git a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js index 7b6ac520..c634dfe3 100644 --- a/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js +++ b/src/octoprint/plugins/pluginmanager/static/js/pluginmanager.js @@ -155,6 +155,8 @@ $(function() { self.pipAdditionalArgs = ko.observable(); self.pipPython = ko.observable(); + self.safeMode = ko.observable(); + self.pipUseUserString = ko.pureComputed(function() { return self.pipUseUser() ? "yes" : "no"; }); @@ -186,7 +188,8 @@ $(function() { }); self.enableToggle = function(data) { - return self.enableManagement() && data.key != 'pluginmanager'; + var command = self._getToggleCommand(data); + return self.enableManagement() && (command == "disable" || !data.safe_mode_victim || data.safe_mode_enabled) && data.key != 'pluginmanager'; }; self.enableUninstall = function(data) { @@ -199,7 +202,7 @@ $(function() { }; self.enableRepoInstall = function(data) { - return self.enableManagement() && self.pipAvailable() && self.isCompatible(data); + return self.enableManagement() && self.pipAvailable() && !self.safeMode() && self.isCompatible(data); }; self.invalidUrl = ko.pureComputed(function() { @@ -209,7 +212,7 @@ $(function() { self.enableUrlInstall = ko.pureComputed(function() { var url = self.installUrl(); - return self.enableManagement() && self.pipAvailable() && url !== undefined && url.trim() != "" && !self.invalidUrl(); + return self.enableManagement() && self.pipAvailable() && !self.safeMode() && url !== undefined && url.trim() != "" && !self.invalidUrl(); }); self.invalidArchive = ko.pureComputed(function() { @@ -219,7 +222,7 @@ $(function() { self.enableArchiveInstall = ko.pureComputed(function() { var name = self.uploadFilename(); - return self.enableManagement() && self.pipAvailable() && name !== undefined && name.trim() != "" && !self.invalidArchive(); + return self.enableManagement() && self.pipAvailable() && !self.safeMode() && name !== undefined && name.trim() != "" && !self.invalidArchive(); }); self.uploadElement.fileupload({ @@ -278,6 +281,8 @@ $(function() { self._fromPluginsResponse(data.plugins); self._fromRepositoryResponse(data.repository); self._fromPipResponse(data.pip); + + self.safeMode(data.safe_mode || false); }; self._fromPluginsResponse = function(data) { @@ -347,6 +352,7 @@ $(function() { }; if (self._getToggleCommand(data) == "enable") { + if (data.safe_mode_victim && !data.safe_mode_enabled) return; OctoPrint.plugins.pluginmanager.enable(data.key) .done(onSuccess) .fail(onError); @@ -655,7 +661,8 @@ $(function() { }; self._getToggleCommand = function(data) { - return ((!data.enabled || data.pending_disable) && !data.pending_enable) ? "enable" : "disable"; + var disable = (data.enabled || data.pending_enable || (data.safe_mode_victim && data.safe_mode_enabled)) && !data.pending_disable; + return disable ? "disable" : "enable"; }; self.toggleButtonCss = function(data) { @@ -666,7 +673,16 @@ $(function() { }; self.toggleButtonTitle = function(data) { - return self._getToggleCommand(data) == "enable" ? gettext("Enable Plugin") : gettext("Disable Plugin"); + var command = self._getToggleCommand(data); + if (command == "enable") { + if (data.safe_mode_victim && !data.safe_mode_enabled) { + return gettext("Disabled due to active safe mode"); + } else { + return gettext("Enable Plugin"); + } + } else { + return gettext("Disable Plugin"); + } }; self.onBeforeBinding = function() { diff --git a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 index 0c9c721d..bd717be5 100644 --- a/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 +++ b/src/octoprint/plugins/pluginmanager/templates/pluginmanager_settings.jinja2 @@ -13,8 +13,16 @@ {% endtrans %} {% endmacro %} +{% macro pluginmanager_safemode() %} +
{% trans %} + Safe mode is currently active. All third party plugins are disabled and cannot be + enabled. Installation of plugin packages is disabled. +{% endtrans %}
+{% endmacro %} + {{ pluginmanager_printing() }} {{ pluginmanager_nopip() }} +{{ pluginmanager_safemode() }}
@@ -32,7 +40,7 @@ -
()
+
()
 
{{ _('Homepage') }} @@ -99,6 +107,7 @@