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