1""" Distribution specific override class for CentOS family (RHEL, Fedora) """ 2import logging 3from typing import cast 4from typing import List 5 6from certbot import errors 7from certbot import util 8from certbot.errors import MisconfigurationError 9from certbot_apache._internal import apache_util 10from certbot_apache._internal import configurator 11from certbot_apache._internal import parser 12from certbot_apache._internal.configurator import OsOptions 13 14logger = logging.getLogger(__name__) 15 16 17class CentOSConfigurator(configurator.ApacheConfigurator): 18 """CentOS specific ApacheConfigurator override class""" 19 20 OS_DEFAULTS = OsOptions( 21 server_root="/etc/httpd", 22 vhost_root="/etc/httpd/conf.d", 23 vhost_files="*.conf", 24 logs_root="/var/log/httpd", 25 ctl="apachectl", 26 version_cmd=['apachectl', '-v'], 27 restart_cmd=['apachectl', 'graceful'], 28 restart_cmd_alt=['apachectl', 'restart'], 29 conftest_cmd=['apachectl', 'configtest'], 30 challenge_location="/etc/httpd/conf.d", 31 ) 32 33 def config_test(self): 34 """ 35 Override config_test to mitigate configtest error in vanilla installation 36 of mod_ssl in Fedora. The error is caused by non-existent self-signed 37 certificates referenced by the configuration, that would be autogenerated 38 during the first (re)start of httpd. 39 """ 40 41 os_info = util.get_os_info() 42 fedora = os_info[0].lower() == "fedora" 43 44 try: 45 super().config_test() 46 except errors.MisconfigurationError: 47 if fedora: 48 self._try_restart_fedora() 49 else: 50 raise 51 52 def _try_restart_fedora(self): 53 """ 54 Tries to restart httpd using systemctl to generate the self signed key pair. 55 """ 56 57 try: 58 util.run_script(['systemctl', 'restart', 'httpd']) 59 except errors.SubprocessError as err: 60 raise errors.MisconfigurationError(str(err)) 61 62 # Finish with actual config check to see if systemctl restart helped 63 super().config_test() 64 65 def _prepare_options(self): 66 """ 67 Override the options dictionary initialization in order to support 68 alternative restart cmd used in CentOS. 69 """ 70 super()._prepare_options() 71 if not self.options.restart_cmd_alt: # pragma: no cover 72 raise ValueError("OS option restart_cmd_alt must be set for CentOS.") 73 self.options.restart_cmd_alt[0] = self.options.ctl 74 75 def get_parser(self): 76 """Initializes the ApacheParser""" 77 return CentOSParser( 78 self.options.server_root, self.options.vhost_root, 79 self.version, configurator=self) 80 81 def _deploy_cert(self, *args, **kwargs): # pylint: disable=arguments-differ 82 """ 83 Override _deploy_cert in order to ensure that the Apache configuration 84 has "LoadModule ssl_module..." before parsing the VirtualHost configuration 85 that was created by Certbot 86 """ 87 super()._deploy_cert(*args, **kwargs) 88 if self.version < (2, 4, 0): 89 self._deploy_loadmodule_ssl_if_needed() 90 91 def _deploy_loadmodule_ssl_if_needed(self): 92 """ 93 Add "LoadModule ssl_module <pre-existing path>" to main httpd.conf if 94 it doesn't exist there already. 95 """ 96 97 loadmods = self.parser.find_dir("LoadModule", "ssl_module", exclude=False) 98 99 correct_ifmods: List[str] = [] 100 loadmod_args: List[str] = [] 101 loadmod_paths: List[str] = [] 102 for m in loadmods: 103 noarg_path = m.rpartition("/")[0] 104 path_args = self.parser.get_all_args(noarg_path) 105 if loadmod_args: 106 if loadmod_args != path_args: 107 msg = ("Certbot encountered multiple LoadModule directives " 108 "for LoadModule ssl_module with differing library paths. " 109 "Please remove or comment out the one(s) that are not in " 110 "use, and run Certbot again.") 111 raise MisconfigurationError(msg) 112 else: 113 loadmod_args = path_args 114 115 centos_parser: CentOSParser = cast(CentOSParser, self.parser) 116 if centos_parser.not_modssl_ifmodule(noarg_path): 117 if centos_parser.loc["default"] in noarg_path: 118 # LoadModule already in the main configuration file 119 if ("ifmodule/" in noarg_path.lower() or 120 "ifmodule[1]" in noarg_path.lower()): 121 # It's the first or only IfModule in the file 122 return 123 # Populate the list of known !mod_ssl.c IfModules 124 nodir_path = noarg_path.rpartition("/directive")[0] 125 correct_ifmods.append(nodir_path) 126 else: 127 loadmod_paths.append(noarg_path) 128 129 if not loadmod_args: 130 # Do not try to enable mod_ssl 131 return 132 133 # Force creation as the directive wasn't found from the beginning of 134 # httpd.conf 135 rootconf_ifmod = self.parser.create_ifmod( 136 parser.get_aug_path(self.parser.loc["default"]), 137 "!mod_ssl.c", beginning=True) 138 # parser.get_ifmod returns a path postfixed with "/", remove that 139 self.parser.add_dir(rootconf_ifmod[:-1], "LoadModule", loadmod_args) 140 correct_ifmods.append(rootconf_ifmod[:-1]) 141 self.save_notes += "Added LoadModule ssl_module to main configuration.\n" 142 143 # Wrap LoadModule mod_ssl inside of <IfModule !mod_ssl.c> if it's not 144 # configured like this already. 145 for loadmod_path in loadmod_paths: 146 nodir_path = loadmod_path.split("/directive")[0] 147 # Remove the old LoadModule directive 148 self.parser.aug.remove(loadmod_path) 149 150 # Create a new IfModule !mod_ssl.c if not already found on path 151 ssl_ifmod = self.parser.get_ifmod(nodir_path, "!mod_ssl.c", 152 beginning=True)[:-1] 153 if ssl_ifmod not in correct_ifmods: 154 self.parser.add_dir(ssl_ifmod, "LoadModule", loadmod_args) 155 correct_ifmods.append(ssl_ifmod) 156 self.save_notes += ("Wrapped pre-existing LoadModule ssl_module " 157 "inside of <IfModule !mod_ssl> block.\n") 158 159 160class CentOSParser(parser.ApacheParser): 161 """CentOS specific ApacheParser override class""" 162 def __init__(self, *args, **kwargs): 163 # CentOS specific configuration file for Apache 164 self.sysconfig_filep = "/etc/sysconfig/httpd" 165 super().__init__(*args, **kwargs) 166 167 def update_runtime_variables(self): 168 """ Override for update_runtime_variables for custom parsing """ 169 # Opportunistic, works if SELinux not enforced 170 super().update_runtime_variables() 171 self.parse_sysconfig_var() 172 173 def parse_sysconfig_var(self): 174 """ Parses Apache CLI options from CentOS configuration file """ 175 defines = apache_util.parse_define_file(self.sysconfig_filep, "OPTIONS") 176 for k, v in defines.items(): 177 self.variables[k] = v 178 179 def not_modssl_ifmodule(self, path): 180 """Checks if the provided Augeas path has argument !mod_ssl""" 181 182 if "ifmodule" not in path.lower(): 183 return False 184 185 # Trim the path to the last ifmodule 186 workpath = path.lower() 187 while workpath: 188 # Get path to the last IfModule (ignore the tail) 189 parts = workpath.rpartition("ifmodule") 190 191 if not parts[0]: 192 # IfModule not found 193 break 194 ifmod_path = parts[0] + parts[1] 195 # Check if ifmodule had an index 196 if parts[2].startswith("["): 197 # Append the index from tail 198 ifmod_path += parts[2].partition("/")[0] 199 # Get the original path trimmed to correct length 200 # This is required to preserve cases 201 ifmod_real_path = path[0:len(ifmod_path)] 202 if "!mod_ssl.c" in self.get_all_args(ifmod_real_path): 203 return True 204 # Set the workpath to the heading part 205 workpath = parts[0] 206 207 return False 208