""" Distribution specific override class for CentOS family (RHEL, Fedora) """ import logging import pkg_resources import zope.interface from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module from certbot import errors from certbot import interfaces from certbot import util from certbot.compat import os from certbot.errors import MisconfigurationError from certbot_apache._internal import apache_util from certbot_apache._internal import configurator from certbot_apache._internal import parser logger = logging.getLogger(__name__) @zope.interface.provider(interfaces.IPluginFactory) class CentOSConfigurator(configurator.ApacheConfigurator): """CentOS specific ApacheConfigurator override class""" OS_DEFAULTS = dict( server_root="/etc/httpd", vhost_root="/etc/httpd/conf.d", vhost_files="*.conf", logs_root="/var/log/httpd", ctl="apachectl", version_cmd=['apachectl', '-v'], restart_cmd=['apachectl', 'graceful'], restart_cmd_alt=['apachectl', 'restart'], conftest_cmd=['apachectl', 'configtest'], enmod=None, dismod=None, le_vhost_ext="-le-ssl.conf", handle_modules=False, handle_sites=False, challenge_location="/etc/httpd/conf.d", MOD_SSL_CONF_SRC=pkg_resources.resource_filename( "certbot_apache", os.path.join("_internal", "options-ssl-apache.conf")) ) def config_test(self): """ Override config_test to mitigate configtest error in vanilla installation of mod_ssl in Fedora. The error is caused by non-existent self-signed certificates referenced by the configuration, that would be autogenerated during the first (re)start of httpd. """ os_info = util.get_os_info() fedora = os_info[0].lower() == "fedora" try: super(CentOSConfigurator, self).config_test() except errors.MisconfigurationError: if fedora: self._try_restart_fedora() else: raise def _try_restart_fedora(self): """ Tries to restart httpd using systemctl to generate the self signed keypair. """ try: util.run_script(['systemctl', 'restart', 'httpd']) except errors.SubprocessError as err: raise errors.MisconfigurationError(str(err)) # Finish with actual config check to see if systemctl restart helped super(CentOSConfigurator, self).config_test() def _prepare_options(self): """ Override the options dictionary initialization in order to support alternative restart cmd used in CentOS. """ super(CentOSConfigurator, self)._prepare_options() self.options["restart_cmd_alt"][0] = self.option("ctl") def get_parser(self): """Initializes the ApacheParser""" return CentOSParser( self.option("server_root"), self.option("vhost_root"), self.version, configurator=self) def _deploy_cert(self, *args, **kwargs): # pylint: disable=arguments-differ """ Override _deploy_cert in order to ensure that the Apache configuration has "LoadModule ssl_module..." before parsing the VirtualHost configuration that was created by Certbot """ super(CentOSConfigurator, self)._deploy_cert(*args, **kwargs) if self.version < (2, 4, 0): self._deploy_loadmodule_ssl_if_needed() def _deploy_loadmodule_ssl_if_needed(self): """ Add "LoadModule ssl_module " to main httpd.conf if it doesn't exist there already. """ loadmods = self.parser.find_dir("LoadModule", "ssl_module", exclude=False) correct_ifmods = [] # type: List[str] loadmod_args = [] # type: List[str] loadmod_paths = [] # type: List[str] for m in loadmods: noarg_path = m.rpartition("/")[0] path_args = self.parser.get_all_args(noarg_path) if loadmod_args: if loadmod_args != path_args: msg = ("Certbot encountered multiple LoadModule directives " "for LoadModule ssl_module with differing library paths. " "Please remove or comment out the one(s) that are not in " "use, and run Certbot again.") raise MisconfigurationError(msg) else: loadmod_args = path_args if self.parser.not_modssl_ifmodule(noarg_path): # pylint: disable=no-member if self.parser.loc["default"] in noarg_path: # LoadModule already in the main configuration file if ("ifmodule/" in noarg_path.lower() or "ifmodule[1]" in noarg_path.lower()): # It's the first or only IfModule in the file return # Populate the list of known !mod_ssl.c IfModules nodir_path = noarg_path.rpartition("/directive")[0] correct_ifmods.append(nodir_path) else: loadmod_paths.append(noarg_path) if not loadmod_args: # Do not try to enable mod_ssl return # Force creation as the directive wasn't found from the beginning of # httpd.conf rootconf_ifmod = self.parser.create_ifmod( parser.get_aug_path(self.parser.loc["default"]), "!mod_ssl.c", beginning=True) # parser.get_ifmod returns a path postfixed with "/", remove that self.parser.add_dir(rootconf_ifmod[:-1], "LoadModule", loadmod_args) correct_ifmods.append(rootconf_ifmod[:-1]) self.save_notes += "Added LoadModule ssl_module to main configuration.\n" # Wrap LoadModule mod_ssl inside of if it's not # configured like this already. for loadmod_path in loadmod_paths: nodir_path = loadmod_path.split("/directive")[0] # Remove the old LoadModule directive self.parser.aug.remove(loadmod_path) # Create a new IfModule !mod_ssl.c if not already found on path ssl_ifmod = self.parser.get_ifmod(nodir_path, "!mod_ssl.c", beginning=True)[:-1] if ssl_ifmod not in correct_ifmods: self.parser.add_dir(ssl_ifmod, "LoadModule", loadmod_args) correct_ifmods.append(ssl_ifmod) self.save_notes += ("Wrapped pre-existing LoadModule ssl_module " "inside of block.\n") class CentOSParser(parser.ApacheParser): """CentOS specific ApacheParser override class""" def __init__(self, *args, **kwargs): # CentOS specific configuration file for Apache self.sysconfig_filep = "/etc/sysconfig/httpd" super(CentOSParser, self).__init__(*args, **kwargs) def update_runtime_variables(self): """ Override for update_runtime_variables for custom parsing """ # Opportunistic, works if SELinux not enforced super(CentOSParser, self).update_runtime_variables() self.parse_sysconfig_var() def parse_sysconfig_var(self): """ Parses Apache CLI options from CentOS configuration file """ defines = apache_util.parse_define_file(self.sysconfig_filep, "OPTIONS") for k in defines: self.variables[k] = defines[k] def not_modssl_ifmodule(self, path): """Checks if the provided Augeas path has argument !mod_ssl""" if "ifmodule" not in path.lower(): return False # Trim the path to the last ifmodule workpath = path.lower() while workpath: # Get path to the last IfModule (ignore the tail) parts = workpath.rpartition("ifmodule") if not parts[0]: # IfModule not found break ifmod_path = parts[0] + parts[1] # Check if ifmodule had an index if parts[2].startswith("["): # Append the index from tail ifmod_path += parts[2].partition("/")[0] # Get the original path trimmed to correct length # This is required to preserve cases ifmod_real_path = path[0:len(ifmod_path)] if "!mod_ssl.c" in self.get_all_args(ifmod_real_path): return True # Set the workpath to the heading part workpath = parts[0] return False