def os_analyse(): """ Returns tuple containing the OS distro and version corresponding with supported versions and caches the result. Output is cached. :returns: (distro, version_nr) :rtype: tuple """ os_info = util.get_os_info() distro = os_info[0].lower() version = os_info[1] if distro not in CLI_DEFAULTS: raise errors.NotSupportedError( "We're sorry, your OS %s %s is currently not supported :(" " you may be able to get this plugin working by defining a list of" " CLI_DEFAULTS in our `constants` module. Please consider making " " a pull-request if you do!") if version not in CLI_DEFAULTS[distro]: min_version = CLI_DEFAULTS[distro]['_min_version'] max_version = CLI_DEFAULTS[distro]['_max_version'] if LooseVersion(version) < LooseVersion(min_version): raise errors.NotSupportedError( "The OS you are using (%s %s) is not supported by this" " plugin, minimum supported version is %s %s", distro, version, distro, version) elif LooseVersion(version) > LooseVersion(max_version): logger.warn( "Your OS version \"%s %s\" is not officially supported by" " this plugin yet. Will try to run with the most recent" " set of constants (%s %s), your mileage may vary.", distro, version, distro, max_version) version = max_version else: # Version within range but not occurring in CLI_DEFAULTS versions = CLI_DEFAULTS[distro] # Only items whose contents stripped of "." are digits, e.g.: 16.04 versions = [v for v in versions if v.replace(".", "").isdigit()] compare = LooseVersion(version) for index, versionno in enumerate(sorted(versions)): # Find the highest supported version number _under_ the # detected version number. In other words: the detected version # number should be smaller than next one in the loop, but # bigger than the current one. # Next version number is? peek = versions[index + 1] if LooseVersion(peek) > compare > LooseVersion(versionno): logger.warn( "Your OS version \"%s %s\" is not officially supported" " by this plugin yet. Will try to run with the most" " recent set of constants of a version before your" " os's (%s %s), your mileage may vary.", distro, version, distro, versionno) version = versionno break return (distro, version)
def prepare(): """ Check if we can restart HAProxy when we are done. :raises .errors.NoInstallationError when no haproxy executable can be found :raises .errors.NoInstallationError when the default service manager executable can't be found :raises .errors.NotSupportedError when the installed haproxy version is incompatible with this plugin """ service_mgr = constants.os_constant("service_manager") if not util.exe_exists(service_mgr): raise errors.NoInstallationError( "Can't find the default service manager for your system:" "{0}, please install it first or configure different OS" " constants".format(service_mgr)) # Check that a supported version of HAProxy is installed. version_cmd = constants.os_constant("version_cmd") output = subprocess.check_output(version_cmd).decode('utf8') matches = re.match( r'HA-Proxy version' r' (?P<version>[0-9]{1,4}\.[0-9]{1,4}\.[0-9a-z]{1,10}).*', output) if matches is None: raise errors.NoInstallationError( "It looks like HAProxy is not installed or the version might" " be incompatible.") else: version = matches.group('version') if StrictVersion(version) < StrictVersion(HAPROXY_MIN_VERSION): raise errors.NotSupportedError( "Version {} of HAProxy is not supported by this plugin," " you need to install {} or higher to be" " incompatible.".format(version, HAPROXY_MIN_VERSION))
def _check_version(self): """Verifies that the installed Postfix version is supported. :raises errors.NotSupportedError: if the version is unsupported """ if self._get_version() < constants.MINIMUM_VERSION: version_string = '.'.join([str(n) for n in constants.MINIMUM_VERSION]) raise errors.NotSupportedError('Postfix version must be at least %s' % version_string)
def config_test(self): # pylint: disable=no-self-use """Check the configuration of Nginx for errors. :raises .errors.MisconfigurationError: If config_test fails """ logger.warning("config_test not implemented") raise errors.NotSupportedError("N/A")
def run(config, plugins): """Obtain a certificate and install. :param config: Configuration object :type config: interfaces.IConfig :param plugins: List of plugins :type plugins: `list` of `str` :returns: `None` :rtype: None """ # TODO: Make run as close to auth + install as possible # Possible difficulties: config.csr was hacked into auth try: installer, authenticator = plug_sel.choose_configurator_plugins( config, plugins, "run") except errors.PluginSelectionError as e: return str(e) # Preflight check for enhancement support by the selected installer if not enhancements.are_supported(config, installer): raise errors.NotSupportedError( "One ore more of the requested enhancements " "are not supported by the selected installer") # TODO: Handle errors from _init_le_client? le_client = _init_le_client(config, authenticator, installer) domains, certname = _find_domains_or_certname(config, installer) should_get_cert, lineage = _find_cert(config, domains, certname) new_lineage = lineage if should_get_cert: new_lineage = _get_and_save_cert(le_client, config, domains, certname, lineage) cert_path = new_lineage.cert_path if new_lineage else None fullchain_path = new_lineage.fullchain_path if new_lineage else None key_path = new_lineage.key_path if new_lineage else None if should_get_cert: _report_new_cert(config, cert_path, fullchain_path, key_path) _install_cert(config, le_client, domains, new_lineage) if enhancements.are_requested(config) and new_lineage: enhancements.enable(new_lineage, domains, installer, config) if lineage is None or not should_get_cert: display_ops.success_installation(domains) else: display_ops.success_renewal(domains) _suggest_donation_if_appropriate(config) eff.handle_subscription(config, le_client.account) return None
def install(config, plugins): """Install a previously obtained cert in a server. :param config: Configuration object :type config: interfaces.IConfig :param plugins: List of plugins :type plugins: `list` of `str` :returns: `None` :rtype: None """ # XXX: Update for renewer/RenewableCert # FIXME: be consistent about whether errors are raised or returned from # this function ... try: installer, _ = plug_sel.choose_configurator_plugins(config, plugins, "install") except errors.PluginSelectionError as e: return str(e) custom_cert = (config.key_path and config.cert_path) if not config.certname and not custom_cert: certname_question = "Which certificate would you like to install?" config.certname = cert_manager.get_certnames( config, "install", allow_multiple=False, custom_prompt=certname_question)[0] if not enhancements.are_supported(config, installer): raise errors.NotSupportedError("One ore more of the requested enhancements " "are not supported by the selected installer") # If cert-path is defined, populate missing (ie. not overridden) values. # Unfortunately this can't be done in argument parser, as certificate # manager needs the access to renewal directory paths if config.certname: config = _populate_from_certname(config) elif enhancements.are_requested(config): # Preflight config check raise errors.ConfigurationError("One or more of the requested enhancements " "require --cert-name to be provided") if config.key_path and config.cert_path: _check_certificate_and_key(config) domains, _ = _find_domains_or_certname(config, installer) le_client = _init_le_client(config, authenticator=None, installer=installer) _install_cert(config, le_client, domains) else: raise errors.ConfigurationError("Path to certificate or key was not defined. " "If your certificate is managed by Certbot, please use --cert-name " "to define which certificate you would like to install.") if enhancements.are_requested(config): # In the case where we don't have certname, we have errored out already lineage = cert_manager.lineage_for_certname(config, config.certname) enhancements.enable(lineage, domains, installer, config) return None
def view_config_changes(self): """Show all of the configuration changes that have taken place. :raises .errors.PluginError: If there is a problem while processing the checkpoints directories. """ logger.warning("view_config_changes not implemented") raise errors.NotSupportedError("N/A")
def __init__(self, root, vhostroot=None, version=(2, 4), configurator=None): # Note: Order is important here. # Needed for calling save() with reverter functionality that resides in # AugeasConfigurator superclass of ApacheConfigurator. This resolves # issues with aug.load() after adding new files / defines to parse tree self.configurator = configurator # Initialize augeas self.aug = None self.init_augeas() if not self.check_aug_version(): raise errors.NotSupportedError( "Apache plugin support requires libaugeas0 and augeas-lenses " "version 1.2.0 or higher, please make sure you have you have " "those installed.") self.modules = {} # type: Dict[str, str] self.parser_paths = {} # type: Dict[str, List[str]] self.variables = {} # type: Dict[str, str] # Find configuration root and make sure augeas can parse it. self.root = os.path.abspath(root) self.loc = {"root": self._find_config_root()} self.parse_file(self.loc["root"]) if version >= (2, 4): # Look up variables from httpd and add to DOM if not already parsed self.update_runtime_variables() # This problem has been fixed in Augeas 1.0 self.standardize_excl() # Parse LoadModule directives from configuration files self.parse_modules() # Set up rest of locations self.loc.update(self._set_locations()) # list of the active include paths, before modifications self.existing_paths = copy.deepcopy(self.parser_paths) # Must also attempt to parse additional virtual host root if vhostroot: self.parse_file( os.path.abspath(vhostroot) + "/" + self.configurator.option("vhost_files")) # check to see if there were unparsed define statements if version < (2, 4): if self.find_dir("Define", exclude=False): raise errors.PluginError("Error parsing runtime variables")
def enhance(config, plugins): """Add security enhancements to existing configuration :param config: Configuration object :type config: interfaces.IConfig :param plugins: List of plugins :type plugins: `list` of `str` :returns: `None` :rtype: None """ supported_enhancements = ["hsts", "redirect", "uir", "staple"] # Check that at least one enhancement was requested on command line oldstyle_enh = any(getattr(config, enh) for enh in supported_enhancements) if not enhancements.are_requested(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]) raise errors.MisconfigurationError("No enhancements requested, exiting.") try: installer, _ = plug_sel.choose_configurator_plugins(config, plugins, "enhance") except errors.PluginSelectionError as e: return str(e) if not enhancements.are_supported(config, installer): raise errors.NotSupportedError("One ore more of the requested enhancements " "are not supported by the selected installer") certname_question = ("Which certificate would you like to use to enhance " "your configuration?") config.certname = cert_manager.get_certnames( config, "enhance", allow_multiple=False, custom_prompt=certname_question)[0] cert_domains = cert_manager.domains_for_certname(config, config.certname) if config.noninteractive_mode: domains = cert_domains else: domain_question = ("Which domain names would you like to enable the " "selected enhancements for?") domains = display_ops.choose_values(cert_domains, domain_question) if not domains: raise errors.Error("User cancelled the domain selection. No domains " "defined, exiting.") lineage = cert_manager.lineage_for_certname(config, config.certname) if not config.chain_path: config.chain_path = lineage.chain_path if oldstyle_enh: le_client = _init_le_client(config, authenticator=None, installer=installer) le_client.enhance_config(domains, config.chain_path, redirect_default=False) if enhancements.are_requested(config): enhancements.enable(lineage, domains, installer, config) return None
def recovery_routine(self): """Revert all previously modified files. Reverts all modified files that have not been saved as a checkpoint :raises .errors.PluginError: If unable to recover the configuration """ logger.warning("recovery_routine not implemented") raise errors.NotSupportedError("N/A")
def get_version(self): """Return version of Nginx Server. Version is returned as tuple. (ie. 2.4.7 = (2, 4, 7)) :returns: version :rtype: tuple :raises .PluginError: Unable to find Nginx version or version is unsupported """ try: proc = subprocess.Popen( [self.conf('ctl'), "-c", self.nginx_conf, "-V"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) text = proc.communicate()[1] # nginx prints output to stderr except (OSError, ValueError) as error: logger.debug(str(error), exc_info=True) raise errors.PluginError("Unable to run %s -V" % self.conf('ctl')) version_regex = re.compile(r"nginx version: ([^/]+)/([0-9\.]*)", re.IGNORECASE) version_matches = version_regex.findall(text) sni_regex = re.compile(r"TLS SNI support enabled", re.IGNORECASE) sni_matches = sni_regex.findall(text) ssl_regex = re.compile(r" --with-http_ssl_module") ssl_matches = ssl_regex.findall(text) if not version_matches: raise errors.PluginError("Unable to find Nginx version") if not ssl_matches: raise errors.PluginError( "Nginx build is missing SSL module (--with-http_ssl_module).") if not sni_matches: raise errors.PluginError("Nginx build doesn't support SNI") product_name, product_version = version_matches[0] if product_name != 'nginx': logger.warning( "NGINX derivative %s is not officially supported by" " certbot", product_name) nginx_version = tuple([int(i) for i in product_version.split(".")]) # nginx < 0.8.48 uses machine hostname as default server_name instead of # the empty string if nginx_version < (0, 8, 48): raise errors.NotSupportedError("Nginx version must be 0.8.48+") return nginx_version
def rollback_checkpoints(self, rollback=1): """Rollback saved checkpoints. :param int rollback: Number of checkpoints to revert :raises .errors.PluginError: If there is a problem with the input or the function is unable to correctly revert the configuration """ logger.warning("rollback_checkpoints not implemented") raise errors.NotSupportedError("N/A")
def check_version(self): """Check Plesk installed and version is supported""" if self.secret_key: return version = os.path.join(self.PSA_PATH, "version") if not os.path.exists(version): raise errors.NoInstallationError('Plesk is not installed') with open(version, 'r') as f: version_data = f.read() major, _ = version_data.split('.', 1) if int(major) < 12: raise errors.NotSupportedError( 'Plesk version is not supported: %s' % version_data)
def get_all_certs_keys(self): """Find all existing keys, certs from configuration. :returns: list of tuples with form [(cert, key, path)] cert - str path to certificate file key - str path to associated key file path - File path to configuration file. :rtype: set """ logger.warning("get_all_cert_keys not implemented") raise errors.NotSupportedError( "Can't get existing keys, since they're not on the file system")
def get_version(self): """Return version of Nginx Server. Version is returned as tuple. (ie. 2.4.7 = (2, 4, 7)) :returns: version :rtype: tuple :raises .PluginError: Unable to find Nginx version or version is unsupported """ text = self._nginx_version() version_regex = re.compile(r"nginx version: ([^/]+)/([0-9\.]*)", re.IGNORECASE) version_matches = version_regex.findall(text) sni_regex = re.compile(r"TLS SNI support enabled", re.IGNORECASE) sni_matches = sni_regex.findall(text) ssl_regex = re.compile(r" --with-http_ssl_module") ssl_matches = ssl_regex.findall(text) if not version_matches: raise errors.PluginError("Unable to find Nginx version") if not ssl_matches: raise errors.PluginError( "Nginx build is missing SSL module (--with-http_ssl_module).") if not sni_matches: raise errors.PluginError("Nginx build doesn't support SNI") product_name, product_version = version_matches[0] if product_name != 'nginx': logger.warning( "NGINX derivative %s is not officially supported by" " certbot", product_name) nginx_version = tuple([int(i) for i in product_version.split(".")]) # nginx < 0.8.48 uses machine hostname as default server_name instead of # the empty string if nginx_version < (0, 8, 48): raise errors.NotSupportedError("Nginx version must be 0.8.48+") return nginx_version
def enable_mod(self, mod_name, temp=False): # pylint: disable=unused-argument """Enables module in Apache. Both enables and reloads Apache so module is active. :param str mod_name: Name of the module to enable. (e.g. 'ssl') :param bool temp: Whether or not this is a temporary action. :raises .errors.NotSupportedError: If the filesystem layout is not supported. :raises .errors.MisconfigurationError: If a2enmod or a2dismod cannot be run. """ avail_path = os.path.join(self.parser.root, "mods-available") enabled_path = os.path.join(self.parser.root, "mods-enabled") if not os.path.isdir(avail_path) or not os.path.isdir(enabled_path): raise errors.NotSupportedError( "Unsupported directory layout. You may try to enable mod %s " "and try again." % mod_name) deps = apache_util.get_mod_deps(mod_name) # Enable all dependencies for dep in deps: if (dep + "_module") not in self.parser.modules: self._enable_mod_debian(dep, temp) self.parser.add_mod(dep) note = "Enabled dependency of %s module - %s" % (mod_name, dep) if not temp: self.save_notes += note + os.linesep logger.debug(note) # Enable actual module self._enable_mod_debian(mod_name, temp) self.parser.add_mod(mod_name) if not temp: self.save_notes += "Enabled %s module in Apache\n" % mod_name logger.info("Enabled Apache %s module", mod_name) # Modules can enable additional config files. Variables may be defined # within these new configuration sections. # Reload is not necessary as DUMP_RUN_CFG uses latest config. self.parser.update_runtime_variables()
def enable_site(self, vhost: VirtualHost) -> None: """Enables an available site, Apache reload required. .. note:: Does not make sure that the site correctly works or that all modules are enabled appropriately. :param vhost: vhost to enable :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost` :raises .errors.NotSupportedError: If filesystem layout is not supported. """ if vhost.enabled: return None enabled_path = ("%s/sites-enabled/%s" % (self.parser.root, os.path.basename(vhost.filep))) if not os.path.isdir(os.path.dirname(enabled_path)): # For some reason, sites-enabled / sites-available do not exist # Call the parent method return super().enable_site(vhost) self.reverter.register_file_creation(False, enabled_path) try: os.symlink(vhost.filep, enabled_path) except OSError as err: if os.path.islink(enabled_path) and filesystem.realpath( enabled_path) == vhost.filep: # Already in shape vhost.enabled = True return None logger.error( "Could not symlink %s to %s, got error: %s", enabled_path, vhost.filep, err.strerror) errstring = ("Encountered error while trying to enable a " + "newly created VirtualHost located at {0} by " + "linking to it from {1}") raise errors.NotSupportedError(errstring.format(vhost.filep, enabled_path)) vhost.enabled = True logger.info("Enabling available site: %s", vhost.filep) self.save_notes += "Enabled site %s\n" % vhost.filep return None
def save(self, title=None, temporary=False): """Saves all changes to the configuration files. :param str title: The title of the save. If a title is given, the configuration will be saved as a new checkpoint and put in a timestamped directory. :param bool temporary: Indicates whether the changes made will be quickly reversed in the future (ie. challenges) :raises .errors.PluginError: If there was an error in an attempt to save the configuration, or an error creating a checkpoint """ if temporary: raise errors.NotSupportedError( "Heroku does not support temporary configuration changes; title=" + title) if self._new_certificate is not None: logger.warning("Installing new certificate...") self._heroku_app().update_certificate(**self._new_certificate)
def acme_from_config_key( config: configuration.NamespaceConfig, key: jose.JWK, regr: Optional[messages.RegistrationResource] = None ) -> acme_client.ClientV2: """Wrangle ACME client construction""" if key.typ == 'EC': public_key = key.key if public_key.key_size == 256: alg = ES256 elif public_key.key_size == 384: alg = ES384 elif public_key.key_size == 521: alg = ES512 else: raise errors.NotSupportedError( "No matching signing algorithm can be found for the key") else: alg = RS256 net = acme_client.ClientNetwork(key, alg=alg, account=regr, verify_ssl=(not config.no_verify_ssl), user_agent=determine_user_agent(config)) with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) client = acme_client.BackwardsCompatibleClientV2( net, key, config.server) if client.acme_version == 1: logger.warning( "Certbot is configured to use an ACMEv1 server (%s). ACMEv1 support is deprecated" " and will soon be removed. See https://community.letsencrypt.org/t/143839 for " "more information.", config.server) return cast(acme_client.ClientV2, client)
def rollback_checkpoints(unused_rollback=1): """Revert deployer state to the previous.""" raise errors.NotSupportedError()
def enhance(unused_domain, unused_enhancement, unused_options=None): """No enhancements are supported.""" raise errors.NotSupportedError('No enhancements are supported.')
def view_config_changes(): """No ability to preview configs""" raise errors.NotSupportedError( 'No ability to preview configs')
def view_config_changes(): """No ability to preview configs generated by Plesk.""" raise errors.NotSupportedError( 'No ability to preview configs generated by Plesk')