diff --git a/certbot-apache/certbot_apache/configurator.py b/certbot-apache/certbot_apache/configurator.py index 03ba05bb0..7f6cb3d76 100644 --- a/certbot-apache/certbot_apache/configurator.py +++ b/certbot-apache/certbot_apache/configurator.py @@ -20,6 +20,7 @@ from certbot import util from certbot.plugins import common from certbot.plugins.util import path_surgery +from certbot.plugins.enhancements import AutoHSTSEnhancement from certbot_apache import apache_util from certbot_apache import augeas_configurator @@ -2156,3 +2157,24 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator): # to be modified. return common.install_version_controlled_file(options_ssl, options_ssl_digest, self.constant("MOD_SSL_CONF_SRC"), constants.ALL_SSL_OPTIONS_HASHES) + + def update_autohsts(self, domain): + """ + The actual enhancement update method in installer plugin. + """ + logger.warning("NOOP from update") + + def deploy_autohsts(self, lineage): + """ + The actual enhancement deploy method in installer plugin. + """ + logger.warning("NOOP from deploy") + + def enable_autohsts(self, lineage): + """ + The actual enhancement enabling method in installer plugin. + """ + logger.warning("NOOP from enable") + + +AutoHSTSEnhancement.register(ApacheConfigurator) diff --git a/certbot/cli.py b/certbot/cli.py index b71d60055..d16be6247 100644 --- a/certbot/cli.py +++ b/certbot/cli.py @@ -28,6 +28,7 @@ from certbot import util from certbot.display import util as display_util from certbot.plugins import disco as plugins_disco +import certbot.plugins.enhancements as enhancements import certbot.plugins.selection as plugin_selection logger = logging.getLogger(__name__) @@ -1201,6 +1202,12 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False): # pylint: dis " is renewed. This setting does not apply to important TLS configuration" " updates.") + # Add the new style enhancements + for enh in enhancements.INDEX: + helpful.add(enh["cli_groups"], enh["cli_flag"], action=enh["cli_action"], + dest=enh["cli_dest"], default=enh["cli_flag_default"], + help=enh["cli_help"]) + helpful.add_deprecated_argument("--agree-dev-preview", 0) helpful.add_deprecated_argument("--dialog", 0) diff --git a/certbot/main.py b/certbot/main.py index a041b998f..c020b469d 100644 --- a/certbot/main.py +++ b/certbot/main.py @@ -33,6 +33,7 @@ from certbot import updater from certbot import util from certbot.display import util as display_util, ops as display_ops +from certbot.plugins import enhancements from certbot.plugins import disco as plugins_disco from certbot.plugins import selection as plug_sel @@ -877,7 +878,8 @@ def enhance(config, plugins): """ supported_enhancements = ["hsts", "redirect", "uir", "staple"] # Check that at least one enhancement was requested on command line - if not any([getattr(config, enh) for enh in supported_enhancements]): + oldstyle_enh = any([getattr(config, enh) for enh in supported_enhancements]) + if not enhancements.is_supported(config) and not oldstyle_enh: msg = ("Please specify one or more enhancement types to configure. To list " "the available enhancement types, run:\n\n%s --help enhance\n") logger.warning(msg, sys.argv[0]) @@ -906,8 +908,11 @@ def enhance(config, plugins): if not config.chain_path: lineage = cert_manager.lineage_for_certname(config, config.certname) config.chain_path = lineage.chain_path - le_client = _init_le_client(config, authenticator=None, installer=installer) - le_client.enhance_config(domains, config.chain_path, ask_redirect=False) + if oldstyle_enh: + le_client = _init_le_client(config, authenticator=None, installer=installer) + le_client.enhance_config(domains, config.chain_path, ask_redirect=False) + if enhancements.is_supported(config): + enhancements.enable(lineage, installer, config) def rollback(config, plugins): diff --git a/certbot/plugins/enhancements.py b/certbot/plugins/enhancements.py new file mode 100644 index 000000000..317265153 --- /dev/null +++ b/certbot/plugins/enhancements.py @@ -0,0 +1,109 @@ +import abc +import six + +from certbot import errors + +def is_supported(config): + """Checks if one or more of the requested enhancements are supported by + the enhancement interfaces.""" + supported = [] + for enh in INDEX: + if hasattr(config, enh["cli_dest"]): + supported.append(getattr(config, enh["cli_dest"])) + return bool(supported) + +def enable(lineage, installer, config): + """ + Run enable method for each requested enhancement that is supported. + + :param lineage: Certificate lineage object + :type lineage: storage.RenewableCert + + :param installer: Installer object + :type installer: interfaces.IInstaller + + :param config: Configuration. + :type config: :class:`certbot.interfaces.IConfig` + """ + for enh in INDEX: + if hasattr(config, enh["cli_dest"]) and getattr(config, enh["cli_dest"]): + if not isinstance(installer, enh["class"]): + msg = ("Requested enhancement {} not supported by selected " + "installer").format(enh["name"]) + raise errors.NotSupportedError(msg) + # Run the enable function + getattr(installer, enh["enable_function"])(lineage) + + +@six.add_metaclass(abc.ABCMeta) +class AutoHSTSEnhancement(object): + """Example enhancement interface class for AutoHSTS""" + + @abc.abstractmethod + def update_autohsts(self, domain, *args, **kwargs): + """As updater function, takes the same parameters as + interfaces.GenericUpdater.generic_updates + """ + + @abc.abstractmethod + def deploy_autohsts(self, lineage, *args, **kwargs): + """As renewer function, takes the same parameters as + interfaces.RenewDeployer + """ + + @abc.abstractmethod + def enable_autohsts(self, lineage, *args, **kwargs): + """Installer function, uses lineage as a parameter. + """ + + +@six.add_metaclass(abc.ABCMeta) +class OCSPPrefetchEnhancement(object): + """Example enhancement interface class for OCSP prefetch""" + + @abc.abstractmethod + def update_ocsp_prefetch(self, domain, *args, **kwargs): + """As updater function, takes the same parameters as + interfaces.GenericUpdater.generic_updates + """ + + @abc.abstractmethod + def deploy_ocsp_prefetch(self, lineage, *args, **kwargs): + """As renewer function, takes the same parameters as + interfaces.RenewDeployer + """ + + @abc.abstractmethod + def enable_ocsp_prefetch(self, lineage, *args, **kwargs): + """Installer function, uses lineage as a parameter.""" + + +INDEX = [ + { + "name": "AutoHSTS", + "cli_help": "Gradually increasing max-age value for HTTP Strict Transport "+ + "Security security header", + "cli_flag": "--autohsts", + "cli_flag_default": None, + "cli_groups": ["security", "enhance"], + "cli_dest": "auto_hsts", + "cli_action": "store_true", + "class": AutoHSTSEnhancement, + "updater_function": "update_autohsts", + "deployer_function": "deploy_autohsts", + "enable_function": "enable_autohsts" + }, + { + "name": "OCSP Prefetch", + "cli_help": "Prefetch OCSP responses within scheduled run with renew verb", + "cli_flag": "--ocspprefetch", + "cli_flag_default": None, + "cli_groups": ["security", "enhance"], + "cli_dest": "ocsp_prefetch", + "cli_action": "store_true", + "class": OCSPPrefetchEnhancement, + "updater_function": "update_ocsp_prefetch", + "deployer_function": None, + "enable_function": "enable_ocsp_prefetch" + } +] diff --git a/certbot/updater.py b/certbot/updater.py index f822c55ee..22d0b04e9 100644 --- a/certbot/updater.py +++ b/certbot/updater.py @@ -5,6 +5,7 @@ from certbot import errors from certbot import interfaces from certbot.plugins import selection as plug_sel +import certbot.plugins.enhancements as enhancements logger = logging.getLogger(__name__) @@ -30,6 +31,7 @@ def run_generic_updaters(config, plugins, lineage): logger.warning("Could not choose appropriate plugin for updaters: %s", e) return _run_updaters(lineage, installer, config) + _run_enhancement_updaters(lineage, installer, config) def run_renewal_deployer(lineage, installer, config): """Helper function to run deployer interface method if supported by the used @@ -47,6 +49,7 @@ def run_renewal_deployer(lineage, installer, config): if not config.disable_renew_updates and isinstance(installer, interfaces.RenewDeployer): installer.renew_deploy(lineage) + _run_enhancement_deployers(lineage, installer, config) def _run_updaters(lineage, installer, config): """Helper function to run the updater interface methods if supported by the @@ -65,3 +68,50 @@ def _run_updaters(lineage, installer, config): if not config.disable_renew_updates: if isinstance(installer, interfaces.GenericUpdater): installer.generic_updates(domain) + +def _run_enhancement_updaters(lineage, installer, config): + """Iterates through known enhancement interfaces. If the installer implements + an enhancement interface and the enhance interface has an updater method, the + updater method gets run. + + :param lineage: Certificate lineage object + :type lineage: storage.RenewableCert + + :param installer: Installer object + :type installer: interfaces.IInstaller + + :param config: Configuration object + :type config: interfaces.IConfig + """ + + if config.disable_renew_updates: + return + for enh in enhancements.INDEX: + if isinstance(installer, enh["class"]) and enh["updater_function"]: + upd_func = getattr(installer, enh["updater_function"]) + for domain in lineage.names(): + upd_func(domain) + else: + continue + + +def _run_enhancement_deployers(lineage, installer, config): + """Iterates through known enhancement interfaces. If the installer implements + an enhancement interface and the enhance interface has an deployer method, the + deployer method gets run. + + :param lineage: Certificate lineage object + :type lineage: storage.RenewableCert + + :param installer: Installer object + :type installer: interfaces.IInstaller + + :param config: Configuration object + :type config: interfaces.IConfig + """ + + if config.disable_renew_updates: + return + for enh in enhancements.INDEX: + if isinstance(installer, enh["class"]) and enh["deployer_function"]: + getattr(installer, enh["deployer_function"])(lineage)