Ejemplo n.º 1
0
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)
Ejemplo n.º 2
0
    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))
Ejemplo n.º 3
0
    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)
Ejemplo n.º 4
0
    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")
Ejemplo n.º 5
0
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
Ejemplo n.º 6
0
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
Ejemplo n.º 7
0
    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")
Ejemplo n.º 8
0
    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")
Ejemplo n.º 9
0
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
Ejemplo n.º 10
0
    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")
Ejemplo n.º 11
0
    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
Ejemplo n.º 12
0
    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")
Ejemplo n.º 13
0
 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)
Ejemplo n.º 14
0
    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")
Ejemplo n.º 15
0
    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
Ejemplo n.º 16
0
    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()
Ejemplo n.º 17
0
    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
Ejemplo n.º 18
0
    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)
Ejemplo n.º 19
0
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)
Ejemplo n.º 20
0
 def rollback_checkpoints(unused_rollback=1):
     """Revert deployer state to the previous."""
     raise errors.NotSupportedError()
Ejemplo n.º 21
0
 def enhance(unused_domain, unused_enhancement, unused_options=None):
     """No enhancements are supported."""
     raise errors.NotSupportedError('No enhancements are supported.')
Ejemplo n.º 22
0
 def view_config_changes():
     """No ability to preview configs"""
     raise errors.NotSupportedError(
         'No ability to preview configs')
Ejemplo n.º 23
0
 def view_config_changes():
     """No ability to preview configs generated by Plesk."""
     raise errors.NotSupportedError(
         'No ability to preview configs generated by Plesk')