1"""Apache Configurator."""
2# pylint: disable=too-many-lines
3from collections import defaultdict
4import copy
5import fnmatch
6import logging
7import re
8import socket
9import time
10from typing import DefaultDict
11from typing import Dict
12from typing import List
13from typing import Optional
14from typing import Set
15from typing import Union
16
17from acme import challenges
18from certbot import errors
19from certbot import util
20from certbot.achallenges import KeyAuthorizationAnnotatedChallenge
21from certbot.compat import filesystem
22from certbot.compat import os
23from certbot.display import util as display_util
24from certbot.plugins import common
25from certbot.plugins.enhancements import AutoHSTSEnhancement
26from certbot.plugins.util import path_surgery
27from certbot_apache._internal import apache_util
28from certbot_apache._internal import assertions
29from certbot_apache._internal import constants
30from certbot_apache._internal import display_ops
31from certbot_apache._internal import dualparser
32from certbot_apache._internal import http_01
33from certbot_apache._internal import obj
34from certbot_apache._internal import parser
35from certbot_apache._internal.dualparser import DualBlockNode
36from certbot_apache._internal.obj import VirtualHost
37from certbot_apache._internal.parser import ApacheParser
38
39try:
40    import apacheconfig
41    HAS_APACHECONFIG = True
42except ImportError:  # pragma: no cover
43    HAS_APACHECONFIG = False
44
45
46logger = logging.getLogger(__name__)
47
48
49class OsOptions:
50    """
51    Dedicated class to describe the OS specificities (eg. paths, binary names)
52    that the Apache configurator needs to be aware to operate properly.
53    """
54    def __init__(self,
55                 server_root="/usr/local/etc/apache24",
56                 vhost_root="/usr/local/etc/apache24/sites-available",
57                 vhost_files="*",
58                 logs_root="/var/log/apache2",
59                 ctl="apachectl",
60                 version_cmd: Optional[List[str]] = None,
61                 restart_cmd: Optional[List[str]] = None,
62                 restart_cmd_alt: Optional[List[str]] = None,
63                 conftest_cmd: Optional[List[str]] = None,
64                 enmod: Optional[str] = None,
65                 dismod: Optional[str] = None,
66                 le_vhost_ext="-le-ssl.conf",
67                 handle_modules=False,
68                 handle_sites=False,
69                 challenge_location="/usr/local/etc/apache24",
70                 apache_bin: Optional[str] = None,
71                 ):
72        self.server_root = server_root
73        self.vhost_root = vhost_root
74        self.vhost_files = vhost_files
75        self.logs_root = logs_root
76        self.ctl = ctl
77        self.version_cmd = ['apachectl', '-v'] if not version_cmd else version_cmd
78        self.restart_cmd = ['apachectl', 'graceful'] if not restart_cmd else restart_cmd
79        self.restart_cmd_alt = restart_cmd_alt
80        self.conftest_cmd = ['apachectl', 'configtest'] if not conftest_cmd else conftest_cmd
81        self.enmod = enmod
82        self.dismod = dismod
83        self.le_vhost_ext = le_vhost_ext
84        self.handle_modules = handle_modules
85        self.handle_sites = handle_sites
86        self.challenge_location = challenge_location
87        self.bin = apache_bin
88
89
90# TODO: Augeas sections ie. <VirtualHost>, <IfModule> beginning and closing
91# tags need to be the same case, otherwise Augeas doesn't recognize them.
92# This is not able to be completely remedied by regular expressions because
93# Augeas views <VirtualHost> </Virtualhost> as an error. This will just
94# require another check_parsing_errors() after all files are included...
95# (after a find_directive search is executed currently). It can be a one
96# time check however because all of LE's transactions will ensure
97# only properly formed sections are added.
98
99# Note: This protocol works for filenames with spaces in it, the sites are
100# properly set up and directives are changed appropriately, but Apache won't
101# recognize names in sites-enabled that have spaces. These are not added to the
102# Apache configuration. It may be wise to warn the user if they are trying
103# to use vhost filenames that contain spaces and offer to change ' ' to '_'
104
105# Note: FILEPATHS and changes to files are transactional.  They are copied
106# over before the updates are made to the existing files. NEW_FILES is
107# transactional due to the use of register_file_creation()
108
109
110# TODO: Verify permissions on configuration root... it is easier than
111#     checking permissions on each of the relative directories and less error
112#     prone.
113# TODO: Write a server protocol finder. Listen <port> <protocol> or
114#     Protocol <protocol>.  This can verify partial setups are correct
115# TODO: Add directives to sites-enabled... not sites-available.
116#     sites-available doesn't allow immediate find_dir search even with save()
117#     and load()
118class ApacheConfigurator(common.Configurator):
119    """Apache configurator.
120
121    :ivar config: Configuration.
122    :type config: certbot.configuration.NamespaceConfig
123
124    :ivar parser: Handles low level parsing
125    :type parser: :class:`~certbot_apache._internal.parser`
126
127    :ivar tup version: version of Apache
128    :ivar list vhosts: All vhosts found in the configuration
129        (:class:`list` of :class:`~certbot_apache._internal.obj.VirtualHost`)
130
131    :ivar dict assoc: Mapping between domains and vhosts
132
133    """
134
135    description = "Apache Web Server plugin"
136    if os.environ.get("CERTBOT_DOCS") == "1":
137        description += (  # pragma: no cover
138            " (Please note that the default values of the Apache plugin options"
139            " change depending on the operating system Certbot is run on.)"
140        )
141
142    OS_DEFAULTS = OsOptions()
143
144    def pick_apache_config(self, warn_on_no_mod_ssl=True):
145        """
146        Pick the appropriate TLS Apache configuration file for current version of Apache and OS.
147
148        :param bool warn_on_no_mod_ssl: True if we should warn if mod_ssl is not found.
149
150        :return: the path to the TLS Apache configuration file to use
151        :rtype: str
152        """
153        # Disabling TLS session tickets is supported by Apache 2.4.11+ and OpenSSL 1.0.2l+.
154        # So for old versions of Apache we pick a configuration without this option.
155        min_openssl_version = util.parse_loose_version('1.0.2l')
156        openssl_version = self.openssl_version(warn_on_no_mod_ssl)
157        if self.version < (2, 4, 11) or not openssl_version or\
158            util.parse_loose_version(openssl_version) < min_openssl_version:
159            return apache_util.find_ssl_apache_conf("old")
160        return apache_util.find_ssl_apache_conf("current")
161
162    def _prepare_options(self):
163        """
164        Set the values possibly changed by command line parameters to
165        OS_DEFAULTS constant dictionary
166        """
167        opts = ["enmod", "dismod", "le_vhost_ext", "server_root", "vhost_root",
168                "logs_root", "challenge_location", "handle_modules", "handle_sites",
169                "ctl", "bin"]
170        for o in opts:
171            # Config options use dashes instead of underscores
172            if self.conf(o.replace("_", "-")) is not None:
173                setattr(self.options, o, self.conf(o.replace("_", "-")))
174            else:
175                setattr(self.options, o, getattr(self.OS_DEFAULTS, o))
176
177        # Special cases
178        self.options.version_cmd[0] = self.options.ctl
179        self.options.restart_cmd[0] = self.options.ctl
180        self.options.conftest_cmd[0] = self.options.ctl
181
182    @classmethod
183    def add_parser_arguments(cls, add):
184        # When adding, modifying or deleting command line arguments, be sure to
185        # include the changes in the list used in method _prepare_options() to
186        # ensure consistent behavior.
187
188        # Respect CERTBOT_DOCS environment variable and use default values from
189        # base class regardless of the underlying distribution (overrides).
190        if os.environ.get("CERTBOT_DOCS") == "1":
191            DEFAULTS = ApacheConfigurator.OS_DEFAULTS
192        else:
193            # cls.OS_DEFAULTS can be distribution specific, see override classes
194            DEFAULTS = cls.OS_DEFAULTS
195        add("enmod", default=DEFAULTS.enmod,
196            help="Path to the Apache 'a2enmod' binary")
197        add("dismod", default=DEFAULTS.dismod,
198            help="Path to the Apache 'a2dismod' binary")
199        add("le-vhost-ext", default=DEFAULTS.le_vhost_ext,
200            help="SSL vhost configuration extension")
201        add("server-root", default=DEFAULTS.server_root,
202            help="Apache server root directory")
203        add("vhost-root", default=None,
204            help="Apache server VirtualHost configuration root")
205        add("logs-root", default=DEFAULTS.logs_root,
206            help="Apache server logs directory")
207        add("challenge-location",
208            default=DEFAULTS.challenge_location,
209            help="Directory path for challenge configuration")
210        add("handle-modules", default=DEFAULTS.handle_modules,
211            help="Let installer handle enabling required modules for you " +
212                 "(Only Ubuntu/Debian currently)")
213        add("handle-sites", default=DEFAULTS.handle_sites,
214            help="Let installer handle enabling sites for you " +
215                 "(Only Ubuntu/Debian currently)")
216        add("ctl", default=DEFAULTS.ctl,
217            help="Full path to Apache control script")
218        add("bin", default=DEFAULTS.bin,
219            help="Full path to apache2/httpd binary")
220
221    def __init__(self, *args, **kwargs):
222        """Initialize an Apache Configurator.
223
224        :param tup version: version of Apache as a tuple (2, 4, 7)
225            (used mostly for unittesting)
226
227        """
228        version = kwargs.pop("version", None)
229        use_parsernode = kwargs.pop("use_parsernode", False)
230        openssl_version = kwargs.pop("openssl_version", None)
231        super().__init__(*args, **kwargs)
232
233        # Add name_server association dict
234        self.assoc: Dict[str, obj.VirtualHost] = {}
235        # Outstanding challenges
236        self._chall_out: Set[KeyAuthorizationAnnotatedChallenge] = set()
237        # List of vhosts configured per wildcard domain on this run.
238        # used by deploy_cert() and enhance()
239        self._wildcard_vhosts: Dict[str, List[obj.VirtualHost]] = {}
240        # Maps enhancements to vhosts we've enabled the enhancement for
241        self._enhanced_vhosts: DefaultDict[str, Set[obj.VirtualHost]] = defaultdict(set)
242        # Temporary state for AutoHSTS enhancement
243        self._autohsts: Dict[str, Dict[str, Union[int, float]]] = {}
244        # Reverter save notes
245        self.save_notes = ""
246        # Should we use ParserNode implementation instead of the old behavior
247        self.USE_PARSERNODE = use_parsernode
248        # Saves the list of file paths that were parsed initially, and
249        # not added to parser tree by self.conf("vhost-root") for example.
250        self.parsed_paths: List[str] = []
251        # These will be set in the prepare function
252        self._prepared = False
253        self.parser: ApacheParser
254        self.parser_root: Optional[DualBlockNode] = None
255        self.version = version
256        self._openssl_version = openssl_version
257        self.vhosts: List[VirtualHost]
258        self.options = copy.deepcopy(self.OS_DEFAULTS)
259        self._enhance_func = {"redirect": self._enable_redirect,
260                              "ensure-http-header": self._set_http_header,
261                              "staple-ocsp": self._enable_ocsp_stapling}
262
263    @property
264    def mod_ssl_conf(self):
265        """Full absolute path to SSL configuration file."""
266        return os.path.join(self.config.config_dir, constants.MOD_SSL_CONF_DEST)
267
268    @property
269    def updated_mod_ssl_conf_digest(self):
270        """Full absolute path to digest of updated SSL configuration file."""
271        return os.path.join(self.config.config_dir, constants.UPDATED_MOD_SSL_CONF_DIGEST)
272
273    def _open_module_file(self, ssl_module_location):
274        """Extract the open lines of openssl_version for testing purposes"""
275        try:
276            with open(ssl_module_location, mode="rb") as f:
277                contents = f.read()
278        except IOError as error:
279            logger.debug(str(error), exc_info=True)
280            return None
281        return contents
282
283    def openssl_version(self, warn_on_no_mod_ssl=True):
284        """Lazily retrieve openssl version
285
286        :param bool warn_on_no_mod_ssl: `True` if we should warn if mod_ssl is not found. Set to
287            `False` when we know we'll try to enable mod_ssl later. This is currently debian/ubuntu,
288            when called from `prepare`.
289
290        :return: the OpenSSL version as a string, or None.
291        :rtype: str or None
292        """
293        if self._openssl_version:
294            return self._openssl_version
295        # Step 1. Determine the location of ssl_module
296        try:
297            ssl_module_location = self.parser.modules['ssl_module']
298        except KeyError:
299            if warn_on_no_mod_ssl:
300                logger.warning("Could not find ssl_module; not disabling session tickets.")
301            return None
302        if ssl_module_location:
303            # Possibility A: ssl_module is a DSO
304            ssl_module_location = self.parser.standard_path_from_server_root(ssl_module_location)
305        else:
306            # Possibility B: ssl_module is statically linked into Apache
307            if self.options.bin:
308                ssl_module_location = self.options.bin
309            else:
310                logger.warning("ssl_module is statically linked but --apache-bin is "
311                               "missing; not disabling session tickets.")
312                return None
313        # Step 2. Grep in the binary for openssl version
314        contents = self._open_module_file(ssl_module_location)
315        if not contents:
316            logger.warning("Unable to read ssl_module file; not disabling session tickets.")
317            return None
318        # looks like: OpenSSL 1.0.2s  28 May 2019
319        matches = re.findall(br"OpenSSL ([0-9]\.[^ ]+) ", contents)
320        if not matches:
321            logger.warning("Could not find OpenSSL version; not disabling session tickets.")
322            return None
323        self._openssl_version = matches[0].decode('UTF-8')
324        return self._openssl_version
325
326    def prepare(self):
327        """Prepare the authenticator/installer.
328
329        :raises .errors.NoInstallationError: If Apache configs cannot be found
330        :raises .errors.MisconfigurationError: If Apache is misconfigured
331        :raises .errors.NotSupportedError: If Apache version is not supported
332        :raises .errors.PluginError: If there is any other error
333
334        """
335        self._prepare_options()
336
337        # Verify Apache is installed
338        self._verify_exe_availability(self.options.ctl)
339
340        # Make sure configuration is valid
341        self.config_test()
342
343        # Set Version
344        if self.version is None:
345            self.version = self.get_version()
346            logger.debug('Apache version is %s',
347                         '.'.join(str(i) for i in self.version))
348        if self.version < (2, 2):
349            raise errors.NotSupportedError(
350                "Apache Version {0} not supported.".format(str(self.version)))
351        elif self.version < (2, 4):
352            logger.warning('Support for Apache 2.2 is deprecated and will be removed in a '
353                           'future release.')
354
355        # Recover from previous crash before Augeas initialization to have the
356        # correct parse tree from the get go.
357        self.recovery_routine()
358        # Perform the actual Augeas initialization to be able to react
359        self.parser = self.get_parser()
360
361        # Set up ParserNode root
362        pn_meta = {"augeasparser": self.parser,
363                   "augeaspath": self.parser.get_root_augpath(),
364                   "ac_ast": None}
365        if self.USE_PARSERNODE:
366            parser_root = self.get_parsernode_root(pn_meta)
367            self.parser_root = parser_root
368            self.parsed_paths = parser_root.parsed_paths()
369
370        # Check for errors in parsing files with Augeas
371        self.parser.check_parsing_errors("httpd.aug")
372
373        # Get all of the available vhosts
374        self.vhosts = self.get_virtual_hosts()
375
376        # We may try to enable mod_ssl later. If so, we shouldn't warn if we can't find it now.
377        # This is currently only true for debian/ubuntu.
378        warn_on_no_mod_ssl = not self.options.handle_modules
379        self.install_ssl_options_conf(self.mod_ssl_conf,
380                                      self.updated_mod_ssl_conf_digest,
381                                      warn_on_no_mod_ssl)
382
383        # Prevent two Apache plugins from modifying a config at once
384        try:
385            util.lock_dir_until_exit(self.options.server_root)
386        except (OSError, errors.LockError):
387            logger.debug("Encountered error:", exc_info=True)
388            raise errors.PluginError(
389                "Unable to create a lock file in {0}. Are you running"
390                " Certbot with sufficient privileges to modify your"
391                " Apache configuration?".format(self.options.server_root))
392        self._prepared = True
393
394    def save(self, title=None, temporary=False):
395        """Saves all changes to the configuration files.
396
397        This function first checks for save errors, if none are found,
398        all configuration changes made will be saved. According to the
399        function parameters. If an exception is raised, a new checkpoint
400        was not created.
401
402        :param str title: The title of the save. If a title is given, the
403            configuration will be saved as a new checkpoint and put in a
404            timestamped directory.
405
406        :param bool temporary: Indicates whether the changes made will
407            be quickly reversed in the future (ie. challenges)
408
409        """
410        save_files = self.parser.unsaved_files()
411        if save_files:
412            self.add_to_checkpoint(save_files,
413                                   self.save_notes, temporary=temporary)
414        # Handle the parser specific tasks
415        self.parser.save(save_files)
416        if title and not temporary:
417            self.finalize_checkpoint(title)
418
419    def recovery_routine(self):
420        """Revert all previously modified files.
421
422        Reverts all modified files that have not been saved as a checkpoint
423
424        :raises .errors.PluginError: If unable to recover the configuration
425
426        """
427        super().recovery_routine()
428        # Reload configuration after these changes take effect if needed
429        # ie. ApacheParser has been initialized.
430        if hasattr(self, "parser"):
431            # TODO: wrap into non-implementation specific  parser interface
432            self.parser.aug.load()
433
434    def revert_challenge_config(self):
435        """Used to cleanup challenge configurations.
436
437        :raises .errors.PluginError: If unable to revert the challenge config.
438
439        """
440        self.revert_temporary_config()
441        self.parser.aug.load()
442
443    def rollback_checkpoints(self, rollback=1):
444        """Rollback saved checkpoints.
445
446        :param int rollback: Number of checkpoints to revert
447
448        :raises .errors.PluginError: If there is a problem with the input or
449            the function is unable to correctly revert the configuration
450
451        """
452        super().rollback_checkpoints(rollback)
453        self.parser.aug.load()
454
455    def _verify_exe_availability(self, exe):
456        """Checks availability of Apache executable"""
457        if not util.exe_exists(exe):
458            if not path_surgery(exe):
459                raise errors.NoInstallationError(
460                    'Cannot find Apache executable {0}'.format(exe))
461
462    def get_parser(self):
463        """Initializes the ApacheParser"""
464        # If user provided vhost_root value in command line, use it
465        return parser.ApacheParser(
466            self.options.server_root, self.conf("vhost-root"),
467            self.version, configurator=self)
468
469    def get_parsernode_root(self, metadata):
470        """Initializes the ParserNode parser root instance."""
471
472        if HAS_APACHECONFIG:
473            apache_vars = {}
474            apache_vars["defines"] = apache_util.parse_defines(self.options.ctl)
475            apache_vars["includes"] = apache_util.parse_includes(self.options.ctl)
476            apache_vars["modules"] = apache_util.parse_modules(self.options.ctl)
477            metadata["apache_vars"] = apache_vars
478
479            with open(self.parser.loc["root"]) as f:
480                with apacheconfig.make_loader(writable=True,
481                      **apacheconfig.flavors.NATIVE_APACHE) as loader:
482                    metadata["ac_ast"] = loader.loads(f.read())
483
484        return dualparser.DualBlockNode(
485            name=assertions.PASS,
486            ancestor=None,
487            filepath=self.parser.loc["root"],
488            metadata=metadata,
489        )
490
491    def deploy_cert(self, domain, cert_path, key_path,
492                    chain_path=None, fullchain_path=None):
493        """Deploys certificate to specified virtual host.
494
495        Currently tries to find the last directives to deploy the certificate
496        in the VHost associated with the given domain. If it can't find the
497        directives, it searches the "included" confs. The function verifies
498        that it has located the three directives and finally modifies them
499        to point to the correct destination. After the certificate is
500        installed, the VirtualHost is enabled if it isn't already.
501
502        .. todo:: Might be nice to remove chain directive if none exists
503                  This shouldn't happen within certbot though
504
505        :raises errors.PluginError: When unable to deploy certificate due to
506            a lack of directives
507
508        """
509        vhosts = self.choose_vhosts(domain)
510        for vhost in vhosts:
511            self._deploy_cert(vhost, cert_path, key_path, chain_path, fullchain_path)
512            display_util.notify("Successfully deployed certificate for {} to {}"
513                                .format(domain, vhost.filep))
514
515    def choose_vhosts(self, domain, create_if_no_ssl=True):
516        """
517        Finds VirtualHosts that can be used with the provided domain
518
519        :param str domain: Domain name to match VirtualHosts to
520        :param bool create_if_no_ssl: If found VirtualHost doesn't have a HTTPS
521            counterpart, should one get created
522
523        :returns: List of VirtualHosts or None
524        :rtype: `list` of :class:`~certbot_apache._internal.obj.VirtualHost`
525        """
526
527        if util.is_wildcard_domain(domain):
528            if domain in self._wildcard_vhosts:
529                # Vhosts for a wildcard domain were already selected
530                return self._wildcard_vhosts[domain]
531            # Ask user which VHosts to support.
532            # Returned objects are guaranteed to be ssl vhosts
533            return self._choose_vhosts_wildcard(domain, create_if_no_ssl)
534        else:
535            return [self.choose_vhost(domain, create_if_no_ssl)]
536
537    def _vhosts_for_wildcard(self, domain):
538        """
539        Get VHost objects for every VirtualHost that the user wants to handle
540        with the wildcard certificate.
541        """
542
543        # Collect all vhosts that match the name
544        matched = set()
545        for vhost in self.vhosts:
546            for name in vhost.get_names():
547                if self._in_wildcard_scope(name, domain):
548                    matched.add(vhost)
549
550        return list(matched)
551
552    def _raise_no_suitable_vhost_error(self, target_name: str):
553        """
554        Notifies the user that Certbot could not find a vhost to secure
555        and raises an error.
556        :param str target_name: The server name that could not be mapped
557        :raises errors.PluginError: Raised unconditionally
558        """
559        raise errors.PluginError(
560            "Certbot could not find a VirtualHost for {0} in the Apache "
561            "configuration. Please create a VirtualHost with a ServerName "
562            "matching {0} and try again.".format(target_name)
563        )
564
565    def _in_wildcard_scope(self, name, domain):
566        """
567        Helper method for _vhosts_for_wildcard() that makes sure that the domain
568        is in the scope of wildcard domain.
569
570        eg. in scope: domain = *.wild.card, name = 1.wild.card
571        not in scope: domain = *.wild.card, name = 1.2.wild.card
572        """
573        if len(name.split(".")) == len(domain.split(".")):
574            return fnmatch.fnmatch(name, domain)
575        return None
576
577    def _choose_vhosts_wildcard(self, domain, create_ssl=True):
578        """Prompts user to choose vhosts to install a wildcard certificate for"""
579
580        # Get all vhosts that are covered by the wildcard domain
581        vhosts = self._vhosts_for_wildcard(domain)
582
583        # Go through the vhosts, making sure that we cover all the names
584        # present, but preferring the SSL vhosts
585        filtered_vhosts = {}
586        for vhost in vhosts:
587            for name in vhost.get_names():
588                if vhost.ssl:
589                    # Always prefer SSL vhosts
590                    filtered_vhosts[name] = vhost
591                elif name not in filtered_vhosts and create_ssl:
592                    # Add if not in list previously
593                    filtered_vhosts[name] = vhost
594
595        # Only unique VHost objects
596        dialog_input = set(filtered_vhosts.values())
597
598        # Ask the user which of names to enable, expect list of names back
599        dialog_output = display_ops.select_vhost_multiple(list(dialog_input))
600
601        if not dialog_output:
602            self._raise_no_suitable_vhost_error(domain)
603
604        # Make sure we create SSL vhosts for the ones that are HTTP only
605        # if requested.
606        return_vhosts = []
607        for vhost in dialog_output:
608            if not vhost.ssl:
609                return_vhosts.append(self.make_vhost_ssl(vhost))
610            else:
611                return_vhosts.append(vhost)
612
613        self._wildcard_vhosts[domain] = return_vhosts
614        return return_vhosts
615
616    def _deploy_cert(self, vhost, cert_path, key_path, chain_path, fullchain_path):
617        """
618        Helper function for deploy_cert() that handles the actual deployment
619        this exists because we might want to do multiple deployments per
620        domain originally passed for deploy_cert(). This is especially true
621        with wildcard certificates
622        """
623        # This is done first so that ssl module is enabled and cert_path,
624        # cert_key... can all be parsed appropriately
625        self.prepare_server_https("443")
626
627        # If we haven't managed to enable mod_ssl by this point, error out
628        if "ssl_module" not in self.parser.modules:
629            raise errors.MisconfigurationError("Could not find ssl_module; "
630                "not installing certificate.")
631
632        # Add directives and remove duplicates
633        self._add_dummy_ssl_directives(vhost.path)
634        self._clean_vhost(vhost)
635
636        path = {"cert_path": self.parser.find_dir("SSLCertificateFile",
637                                                  None, vhost.path),
638                "cert_key": self.parser.find_dir("SSLCertificateKeyFile",
639                                                 None, vhost.path)}
640
641        # Only include if a certificate chain is specified
642        if chain_path is not None:
643            path["chain_path"] = self.parser.find_dir(
644                "SSLCertificateChainFile", None, vhost.path)
645
646        logger.info("Deploying Certificate to VirtualHost %s", vhost.filep)
647
648        if self.version < (2, 4, 8) or (chain_path and not fullchain_path):
649            # install SSLCertificateFile, SSLCertificateKeyFile,
650            # and SSLCertificateChainFile directives
651            set_cert_path = cert_path
652            self.parser.aug.set(path["cert_path"][-1], cert_path)
653            self.parser.aug.set(path["cert_key"][-1], key_path)
654            if chain_path is not None:
655                self.parser.add_dir(vhost.path,
656                                    "SSLCertificateChainFile", chain_path)
657            else:
658                raise errors.PluginError("--chain-path is required for your "
659                                         "version of Apache")
660        else:
661            if not fullchain_path:
662                raise errors.PluginError("Please provide the --fullchain-path "
663                                         "option pointing to your full chain file")
664            set_cert_path = fullchain_path
665            self.parser.aug.set(path["cert_path"][-1], fullchain_path)
666            self.parser.aug.set(path["cert_key"][-1], key_path)
667
668        # Enable the new vhost if needed
669        if not vhost.enabled:
670            self.enable_site(vhost)
671
672        # Save notes about the transaction that took place
673        self.save_notes += ("Changed vhost at %s with addresses of %s\n"
674                            "\tSSLCertificateFile %s\n"
675                            "\tSSLCertificateKeyFile %s\n" %
676                            (vhost.filep,
677                             ", ".join(str(addr) for addr in vhost.addrs),
678                             set_cert_path, key_path))
679        if chain_path is not None:
680            self.save_notes += "\tSSLCertificateChainFile %s\n" % chain_path
681
682    def choose_vhost(self, target_name, create_if_no_ssl=True):
683        """Chooses a virtual host based on the given domain name.
684
685        If there is no clear virtual host to be selected, the user is prompted
686        with all available choices.
687
688        The returned vhost is guaranteed to have TLS enabled unless
689        create_if_no_ssl is set to False, in which case there is no such guarantee
690        and the result is not cached.
691
692        :param str target_name: domain name
693        :param bool create_if_no_ssl: If found VirtualHost doesn't have a HTTPS
694            counterpart, should one get created
695
696        :returns: vhost associated with name
697        :rtype: :class:`~certbot_apache._internal.obj.VirtualHost`
698
699        :raises .errors.PluginError: If no vhost is available or chosen
700
701        """
702        # Allows for domain names to be associated with a virtual host
703        if target_name in self.assoc:
704            return self.assoc[target_name]
705
706        # Try to find a reasonable vhost
707        vhost = self._find_best_vhost(target_name)
708        if vhost is not None:
709            if not create_if_no_ssl:
710                return vhost
711            if not vhost.ssl:
712                vhost = self.make_vhost_ssl(vhost)
713
714            self._add_servername_alias(target_name, vhost)
715            self.assoc[target_name] = vhost
716            return vhost
717
718        # Negate create_if_no_ssl value to indicate if we want a SSL vhost
719        # to get created if a non-ssl vhost is selected.
720        return self._choose_vhost_from_list(target_name, temp=not create_if_no_ssl)
721
722    def _choose_vhost_from_list(self, target_name, temp=False):
723        # Select a vhost from a list
724        vhost = display_ops.select_vhost(target_name, self.vhosts)
725        if vhost is None:
726            self._raise_no_suitable_vhost_error(target_name)
727        if temp:
728            return vhost
729        if not vhost.ssl:
730            addrs = self._get_proposed_addrs(vhost, "443")
731            # TODO: Conflicts is too conservative
732            if not any(vhost.enabled and vhost.conflicts(addrs) for
733                       vhost in self.vhosts):
734                vhost = self.make_vhost_ssl(vhost)
735            else:
736                logger.error(
737                    "The selected vhost would conflict with other HTTPS "
738                    "VirtualHosts within Apache. Please select another "
739                    "vhost or add ServerNames to your configuration.")
740                raise errors.PluginError(
741                    "VirtualHost not able to be selected.")
742
743        self._add_servername_alias(target_name, vhost)
744        self.assoc[target_name] = vhost
745        return vhost
746
747    def domain_in_names(self, names, target_name):
748        """Checks if target domain is covered by one or more of the provided
749        names. The target name is matched by wildcard as well as exact match.
750
751        :param names: server aliases
752        :type names: `collections.Iterable` of `str`
753        :param str target_name: name to compare with wildcards
754
755        :returns: True if target_name is covered by a wildcard,
756            otherwise, False
757        :rtype: bool
758
759        """
760        # use lowercase strings because fnmatch can be case sensitive
761        target_name = target_name.lower()
762        for name in names:
763            name = name.lower()
764            # fnmatch treats "[seq]" specially and [ or ] characters aren't
765            # valid in Apache but Apache doesn't error out if they are present
766            if "[" not in name and fnmatch.fnmatch(target_name, name):
767                return True
768        return False
769
770    def find_best_http_vhost(self, target, filter_defaults, port="80"):
771        """Returns non-HTTPS vhost objects found from the Apache config
772
773        :param str target: Domain name of the desired VirtualHost
774        :param bool filter_defaults: whether _default_ vhosts should be
775            included if it is the best match
776        :param str port: port number the vhost should be listening on
777
778        :returns: VirtualHost object that's the best match for target name
779        :rtype: `obj.VirtualHost` or None
780        """
781        filtered_vhosts = []
782        for vhost in self.vhosts:
783            if any(a.is_wildcard() or a.get_port() == port for a in vhost.addrs) and not vhost.ssl:
784                filtered_vhosts.append(vhost)
785        return self._find_best_vhost(target, filtered_vhosts, filter_defaults)
786
787    def _find_best_vhost(self, target_name, vhosts=None, filter_defaults=True):
788        """Finds the best vhost for a target_name.
789
790        This does not upgrade a vhost to HTTPS... it only finds the most
791        appropriate vhost for the given target_name.
792
793        :param str target_name: domain handled by the desired vhost
794        :param vhosts: vhosts to consider
795        :type vhosts: `collections.Iterable` of :class:`~certbot_apache._internal.obj.VirtualHost`
796        :param bool filter_defaults: whether a vhost with a _default_
797            addr is acceptable
798
799        :returns: VHost or None
800
801        """
802        # Points 6 - Servername SSL
803        # Points 5 - Wildcard SSL
804        # Points 4 - Address name with SSL
805        # Points 3 - Servername no SSL
806        # Points 2 - Wildcard no SSL
807        # Points 1 - Address name with no SSL
808        best_candidate = None
809        best_points = 0
810
811        if vhosts is None:
812            vhosts = self.vhosts
813
814        for vhost in vhosts:
815            if vhost.modmacro is True:
816                continue
817            names = vhost.get_names()
818            if target_name in names:
819                points = 3
820            elif self.domain_in_names(names, target_name):
821                points = 2
822            elif any(addr.get_addr() == target_name for addr in vhost.addrs):
823                points = 1
824            else:
825                # No points given if names can't be found.
826                # This gets hit but doesn't register
827                continue  # pragma: no cover
828
829            if vhost.ssl:
830                points += 3
831
832            if points > best_points:
833                best_points = points
834                best_candidate = vhost
835
836        # No winners here... is there only one reasonable vhost?
837        if best_candidate is None:
838            if filter_defaults:
839                vhosts = self._non_default_vhosts(vhosts)
840            # remove mod_macro hosts from reasonable vhosts
841            reasonable_vhosts = [vh for vh
842                                 in vhosts if vh.modmacro is False]
843            if len(reasonable_vhosts) == 1:
844                best_candidate = reasonable_vhosts[0]
845
846        return best_candidate
847
848    def _non_default_vhosts(self, vhosts):
849        """Return all non _default_ only vhosts."""
850        return [vh for vh in vhosts if not all(
851            addr.get_addr() == "_default_" for addr in vh.addrs
852        )]
853
854    def get_all_names(self):
855        """Returns all names found in the Apache Configuration.
856
857        :returns: All ServerNames, ServerAliases, and reverse DNS entries for
858                  virtual host addresses
859        :rtype: set
860
861        """
862        all_names: Set[str] = set()
863
864        vhost_macro = []
865
866        for vhost in self.vhosts:
867            all_names.update(vhost.get_names())
868            if vhost.modmacro:
869                vhost_macro.append(vhost.filep)
870
871            for addr in vhost.addrs:
872                if common.hostname_regex.match(addr.get_addr()):
873                    all_names.add(addr.get_addr())
874                else:
875                    name = self.get_name_from_ip(addr)
876                    if name:
877                        all_names.add(name)
878
879        if vhost_macro:
880            display_util.notification(
881                "Apache mod_macro seems to be in use in file(s):\n{0}"
882                "\n\nUnfortunately mod_macro is not yet supported".format(
883                    "\n  ".join(vhost_macro)), force_interactive=True)
884
885        return util.get_filtered_names(all_names)
886
887    def get_name_from_ip(self, addr):
888        """Returns a reverse dns name if available.
889
890        :param addr: IP Address
891        :type addr: ~.common.Addr
892
893        :returns: name or empty string if name cannot be determined
894        :rtype: str
895
896        """
897        # If it isn't a private IP, do a reverse DNS lookup
898        if not common.private_ips_regex.match(addr.get_addr()):
899            try:
900                socket.inet_aton(addr.get_addr())
901                return socket.gethostbyaddr(addr.get_addr())[0]
902            except (socket.error, socket.herror, socket.timeout):
903                pass
904
905        return ""
906
907    def _get_vhost_names(self, path):
908        """Helper method for getting the ServerName and
909        ServerAlias values from vhost in path
910
911        :param path: Path to read ServerName and ServerAliases from
912
913        :returns: Tuple including ServerName and `list` of ServerAlias strings
914        """
915
916        servername_match = self.parser.find_dir(
917            "ServerName", None, start=path, exclude=False)
918        serveralias_match = self.parser.find_dir(
919            "ServerAlias", None, start=path, exclude=False)
920
921        serveraliases = []
922        for alias in serveralias_match:
923            serveralias = self.parser.get_arg(alias)
924            serveraliases.append(serveralias)
925
926        servername = None
927        if servername_match:
928            # Get last ServerName as each overwrites the previous
929            servername = self.parser.get_arg(servername_match[-1])
930
931        return (servername, serveraliases)
932
933    def _add_servernames(self, host):
934        """Helper function for get_virtual_hosts().
935
936        :param host: In progress vhost whose names will be added
937        :type host: :class:`~certbot_apache._internal.obj.VirtualHost`
938
939        """
940
941        servername, serveraliases = self._get_vhost_names(host.path)
942
943        for alias in serveraliases:
944            if not host.modmacro:
945                host.aliases.add(alias)
946
947        if not host.modmacro:
948            host.name = servername
949
950    def _create_vhost(self, path):
951        """Used by get_virtual_hosts to create vhost objects
952
953        :param str path: Augeas path to virtual host
954
955        :returns: newly created vhost
956        :rtype: :class:`~certbot_apache._internal.obj.VirtualHost`
957
958        """
959        addrs = set()
960        try:
961            args = self.parser.aug.match(path + "/arg")
962        except RuntimeError:
963            logger.warning("Encountered a problem while parsing file: %s, skipping", path)
964            return None
965        for arg in args:
966            addrs.add(obj.Addr.fromstring(self.parser.get_arg(arg)))
967        is_ssl = False
968
969        if self.parser.find_dir("SSLEngine", "on", start=path, exclude=False):
970            is_ssl = True
971
972        # "SSLEngine on" might be set outside of <VirtualHost>
973        # Treat vhosts with port 443 as ssl vhosts
974        for addr in addrs:
975            if addr.get_port() == "443":
976                is_ssl = True
977
978        filename = apache_util.get_file_path(
979            self.parser.aug.get("/augeas/files%s/path" % apache_util.get_file_path(path)))
980        if filename is None:
981            return None
982
983        macro = False
984        if "/macro/" in path.lower():
985            macro = True
986
987        vhost_enabled = self.parser.parsed_in_original(filename)
988
989        vhost = obj.VirtualHost(filename, path, addrs, is_ssl,
990                                vhost_enabled, modmacro=macro)
991        self._add_servernames(vhost)
992        return vhost
993
994    def get_virtual_hosts(self):
995        """
996        Temporary wrapper for legacy and ParserNode version for
997        get_virtual_hosts. This should be replaced with the ParserNode
998        implementation when ready.
999        """
1000
1001        v1_vhosts = self.get_virtual_hosts_v1()
1002        if self.USE_PARSERNODE and HAS_APACHECONFIG:
1003            v2_vhosts = self.get_virtual_hosts_v2()
1004
1005            for v1_vh in v1_vhosts:
1006                found = False
1007                for v2_vh in v2_vhosts:
1008                    if assertions.isEqualVirtualHost(v1_vh, v2_vh):
1009                        found = True
1010                        break
1011                if not found:
1012                    raise AssertionError("Equivalent for {} was not found".format(v1_vh.path))
1013
1014            return v2_vhosts
1015        return v1_vhosts
1016
1017    def get_virtual_hosts_v1(self):
1018        """Returns list of virtual hosts found in the Apache configuration.
1019
1020        :returns: List of :class:`~certbot_apache._internal.obj.VirtualHost`
1021            objects found in configuration
1022        :rtype: list
1023
1024        """
1025        # Search base config, and all included paths for VirtualHosts
1026        file_paths: Dict[str, str] = {}
1027        internal_paths: DefaultDict[str, Set[str]] = defaultdict(set)
1028        vhs = []
1029        # Make a list of parser paths because the parser_paths
1030        # dictionary may be modified during the loop.
1031        for vhost_path in list(self.parser.parser_paths):
1032            paths = self.parser.aug.match(
1033                ("/files%s//*[label()=~regexp('%s')]" %
1034                    (vhost_path, parser.case_i("VirtualHost"))))
1035            paths = [path for path in paths if
1036                     "virtualhost" in os.path.basename(path).lower()]
1037            for path in paths:
1038                new_vhost = self._create_vhost(path)
1039                if not new_vhost:
1040                    continue
1041                internal_path = apache_util.get_internal_aug_path(new_vhost.path)
1042                realpath = filesystem.realpath(new_vhost.filep)
1043                if realpath not in file_paths:
1044                    file_paths[realpath] = new_vhost.filep
1045                    internal_paths[realpath].add(internal_path)
1046                    vhs.append(new_vhost)
1047                elif (realpath == new_vhost.filep and
1048                      realpath != file_paths[realpath]):
1049                    # Prefer "real" vhost paths instead of symlinked ones
1050                    # ex: sites-enabled/vh.conf -> sites-available/vh.conf
1051
1052                    # remove old (most likely) symlinked one
1053                    new_vhs = []
1054                    for v in vhs:
1055                        if v.filep == file_paths[realpath]:
1056                            internal_paths[realpath].remove(
1057                                apache_util.get_internal_aug_path(v.path))
1058                        else:
1059                            new_vhs.append(v)
1060                    vhs = new_vhs
1061
1062                    file_paths[realpath] = realpath
1063                    internal_paths[realpath].add(internal_path)
1064                    vhs.append(new_vhost)
1065                elif internal_path not in internal_paths[realpath]:
1066                    internal_paths[realpath].add(internal_path)
1067                    vhs.append(new_vhost)
1068        return vhs
1069
1070    def get_virtual_hosts_v2(self):
1071        """Returns list of virtual hosts found in the Apache configuration using
1072        ParserNode interface.
1073        :returns: List of :class:`~certbot_apache.obj.VirtualHost`
1074            objects found in configuration
1075        :rtype: list
1076        """
1077
1078        if not self.parser_root:
1079            raise errors.Error("This ApacheConfigurator instance is not"  # pragma: no cover
1080                               " configured to use a node parser.")
1081        vhs = []
1082        vhosts = self.parser_root.find_blocks("VirtualHost", exclude=False)
1083        for vhblock in vhosts:
1084            vhs.append(self._create_vhost_v2(vhblock))
1085        return vhs
1086
1087    def _create_vhost_v2(self, node):
1088        """Used by get_virtual_hosts_v2 to create vhost objects using ParserNode
1089        interfaces.
1090        :param interfaces.BlockNode node: The BlockNode object of VirtualHost block
1091        :returns: newly created vhost
1092        :rtype: :class:`~certbot_apache.obj.VirtualHost`
1093        """
1094        addrs = set()
1095        for param in node.parameters:
1096            addrs.add(obj.Addr.fromstring(param))
1097
1098        is_ssl = False
1099        # Exclusion to match the behavior in get_virtual_hosts_v2
1100        sslengine = node.find_directives("SSLEngine", exclude=False)
1101        if sslengine:
1102            for directive in sslengine:
1103                if directive.parameters[0].lower() == "on":
1104                    is_ssl = True
1105                    break
1106
1107        # "SSLEngine on" might be set outside of <VirtualHost>
1108        # Treat vhosts with port 443 as ssl vhosts
1109        for addr in addrs:
1110            if addr.get_port() == "443":
1111                is_ssl = True
1112
1113        enabled = apache_util.included_in_paths(node.filepath, self.parsed_paths)
1114
1115        macro = False
1116        # Check if the VirtualHost is contained in a mod_macro block
1117        if node.find_ancestors("Macro"):
1118            macro = True
1119        vhost = obj.VirtualHost(
1120            node.filepath, None, addrs, is_ssl, enabled, modmacro=macro, node=node
1121        )
1122        self._populate_vhost_names_v2(vhost)
1123        return vhost
1124
1125    def _populate_vhost_names_v2(self, vhost):
1126        """Helper function that populates the VirtualHost names.
1127        :param host: In progress vhost whose names will be added
1128        :type host: :class:`~certbot_apache.obj.VirtualHost`
1129        """
1130
1131        servername_match = vhost.node.find_directives("ServerName",
1132                                                      exclude=False)
1133        serveralias_match = vhost.node.find_directives("ServerAlias",
1134                                                       exclude=False)
1135
1136        servername = None
1137        if servername_match:
1138            servername = servername_match[-1].parameters[-1]
1139
1140        if not vhost.modmacro:
1141            for alias in serveralias_match:
1142                for serveralias in alias.parameters:
1143                    vhost.aliases.add(serveralias)
1144            vhost.name = servername
1145
1146
1147    def is_name_vhost(self, target_addr):
1148        """Returns if vhost is a name based vhost
1149
1150        NameVirtualHost was deprecated in Apache 2.4 as all VirtualHosts are
1151        now NameVirtualHosts. If version is earlier than 2.4, check if addr
1152        has a NameVirtualHost directive in the Apache config
1153
1154        :param certbot_apache._internal.obj.Addr target_addr: vhost address
1155
1156        :returns: Success
1157        :rtype: bool
1158
1159        """
1160        # Mixed and matched wildcard NameVirtualHost with VirtualHost
1161        # behavior is undefined. Make sure that an exact match exists
1162
1163        # search for NameVirtualHost directive for ip_addr
1164        # note ip_addr can be FQDN although Apache does not recommend it
1165        return (self.version >= (2, 4) or
1166                self.parser.find_dir("NameVirtualHost", str(target_addr)))
1167
1168    def add_name_vhost(self, addr):
1169        """Adds NameVirtualHost directive for given address.
1170
1171        :param addr: Address that will be added as NameVirtualHost directive
1172        :type addr: :class:`~certbot_apache._internal.obj.Addr`
1173
1174        """
1175
1176        loc = parser.get_aug_path(self.parser.loc["name"])
1177        if addr.get_port() == "443":
1178            self.parser.add_dir_to_ifmodssl(
1179                loc, "NameVirtualHost", [str(addr)])
1180        else:
1181            self.parser.add_dir(loc, "NameVirtualHost", [str(addr)])
1182
1183        msg = "Setting {0} to be NameBasedVirtualHost\n".format(addr)
1184        logger.debug(msg)
1185        self.save_notes += msg
1186
1187    def prepare_server_https(self, port, temp=False):
1188        """Prepare the server for HTTPS.
1189
1190        Make sure that the ssl_module is loaded and that the server
1191        is appropriately listening on port.
1192
1193        :param str port: Port to listen on
1194
1195        """
1196
1197        self.prepare_https_modules(temp)
1198        self.ensure_listen(port, https=True)
1199
1200    def ensure_listen(self, port, https=False):
1201        """Make sure that Apache is listening on the port. Checks if the
1202        Listen statement for the port already exists, and adds it to the
1203        configuration if necessary.
1204
1205        :param str port: Port number to check and add Listen for if not in
1206            place already
1207        :param bool https: If the port will be used for HTTPS
1208
1209        """
1210
1211        # If HTTPS requested for nonstandard port, add service definition
1212        if https and port != "443":
1213            port_service = "%s %s" % (port, "https")
1214        else:
1215            port_service = port
1216
1217        # Check for Listen <port>
1218        # Note: This could be made to also look for ip:443 combo
1219        listens = [self.parser.get_arg(x).split()[0] for
1220                   x in self.parser.find_dir("Listen")]
1221
1222        # Listen already in place
1223        if self._has_port_already(listens, port):
1224            return
1225
1226        listen_dirs = set(listens)
1227
1228        if not listens:
1229            listen_dirs.add(port_service)
1230
1231        for listen in listens:
1232            # For any listen statement, check if the machine also listens on
1233            # the given port. If not, add such a listen statement.
1234            if len(listen.split(":")) == 1:
1235                # Its listening to all interfaces
1236                if port not in listen_dirs and port_service not in listen_dirs:
1237                    listen_dirs.add(port_service)
1238            else:
1239                # The Listen statement specifies an ip
1240                _, ip = listen[::-1].split(":", 1)
1241                ip = ip[::-1]
1242                if "%s:%s" % (ip, port_service) not in listen_dirs and (
1243                   "%s:%s" % (ip, port_service) not in listen_dirs):
1244                    listen_dirs.add("%s:%s" % (ip, port_service))
1245        if https:
1246            self._add_listens_https(listen_dirs, listens, port)
1247        else:
1248            self._add_listens_http(listen_dirs, listens, port)
1249
1250    def _add_listens_http(self, listens, listens_orig, port):
1251        """Helper method for ensure_listen to figure out which new
1252        listen statements need adding for listening HTTP on port
1253
1254        :param set listens: Set of all needed Listen statements
1255        :param list listens_orig: List of existing listen statements
1256        :param string port: Port number we're adding
1257        """
1258
1259        new_listens = listens.difference(listens_orig)
1260
1261        if port in new_listens:
1262            # We have wildcard, skip the rest
1263            self.parser.add_dir(parser.get_aug_path(self.parser.loc["listen"]),
1264                                "Listen", port)
1265            self.save_notes += "Added Listen %s directive to %s\n" % (
1266                port, self.parser.loc["listen"])
1267        else:
1268            for listen in new_listens:
1269                self.parser.add_dir(parser.get_aug_path(
1270                    self.parser.loc["listen"]), "Listen", listen.split(" "))
1271                self.save_notes += ("Added Listen %s directive to "
1272                                    "%s\n") % (listen,
1273                                               self.parser.loc["listen"])
1274
1275    def _add_listens_https(self, listens, listens_orig, port):
1276        """Helper method for ensure_listen to figure out which new
1277        listen statements need adding for listening HTTPS on port
1278
1279        :param set listens: Set of all needed Listen statements
1280        :param list listens_orig: List of existing listen statements
1281        :param string port: Port number we're adding
1282        """
1283
1284        # Add service definition for non-standard ports
1285        if port != "443":
1286            port_service = "%s %s" % (port, "https")
1287        else:
1288            port_service = port
1289
1290        new_listens = listens.difference(listens_orig)
1291
1292        if port in new_listens or port_service in new_listens:
1293            # We have wildcard, skip the rest
1294            self.parser.add_dir_to_ifmodssl(
1295                parser.get_aug_path(self.parser.loc["listen"]),
1296                "Listen", port_service.split(" "))
1297            self.save_notes += "Added Listen %s directive to %s\n" % (
1298                port_service, self.parser.loc["listen"])
1299        else:
1300            for listen in new_listens:
1301                self.parser.add_dir_to_ifmodssl(
1302                    parser.get_aug_path(self.parser.loc["listen"]),
1303                    "Listen", listen.split(" "))
1304                self.save_notes += ("Added Listen %s directive to "
1305                                    "%s\n") % (listen,
1306                                               self.parser.loc["listen"])
1307
1308    def _has_port_already(self, listens, port):
1309        """Helper method for prepare_server_https to find out if user
1310        already has an active Listen statement for the port we need
1311
1312        :param list listens: List of listen variables
1313        :param string port: Port in question
1314        """
1315
1316        if port in listens:
1317            return True
1318        # Check if Apache is already listening on a specific IP
1319        for listen in listens:
1320            if len(listen.split(":")) > 1:
1321                # Ugly but takes care of protocol def, eg: 1.1.1.1:443 https
1322                if listen.split(":")[-1].split(" ")[0] == port:
1323                    return True
1324        return None
1325
1326    def prepare_https_modules(self, temp):
1327        """Helper method for prepare_server_https, taking care of enabling
1328        needed modules
1329
1330        :param boolean temp: If the change is temporary
1331        """
1332
1333        if self.options.handle_modules:
1334            if self.version >= (2, 4) and ("socache_shmcb_module" not in
1335                                           self.parser.modules):
1336                self.enable_mod("socache_shmcb", temp=temp)
1337            if "ssl_module" not in self.parser.modules:
1338                self.enable_mod("ssl", temp=temp)
1339                # Make sure we're not throwing away any unwritten changes to the config
1340                self.parser.ensure_augeas_state()
1341                self.parser.aug.load()
1342                self.parser.reset_modules() # Reset to load the new ssl_module path
1343                # Call again because now we can gate on openssl version
1344                self.install_ssl_options_conf(self.mod_ssl_conf,
1345                                              self.updated_mod_ssl_conf_digest,
1346                                              warn_on_no_mod_ssl=True)
1347
1348    def make_vhost_ssl(self, nonssl_vhost):
1349        """Makes an ssl_vhost version of a nonssl_vhost.
1350
1351        Duplicates vhost and adds default ssl options
1352        New vhost will reside as (nonssl_vhost.path) +
1353        ``self.options.le_vhost_ext``
1354
1355        .. note:: This function saves the configuration
1356
1357        :param nonssl_vhost: Valid VH that doesn't have SSLEngine on
1358        :type nonssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
1359
1360        :returns: SSL vhost
1361        :rtype: :class:`~certbot_apache._internal.obj.VirtualHost`
1362
1363        :raises .errors.PluginError: If more than one virtual host is in
1364            the file or if plugin is unable to write/read vhost files.
1365
1366        """
1367        avail_fp = nonssl_vhost.filep
1368        ssl_fp = self._get_ssl_vhost_path(avail_fp)
1369
1370        orig_matches = self.parser.aug.match("/files%s//* [label()=~regexp('%s')]" %
1371                                      (self._escape(ssl_fp),
1372                                       parser.case_i("VirtualHost")))
1373
1374        self._copy_create_ssl_vhost_skeleton(nonssl_vhost, ssl_fp)
1375
1376        # Reload augeas to take into account the new vhost
1377        self.parser.aug.load()
1378        # Get Vhost augeas path for new vhost
1379        new_matches = self.parser.aug.match("/files%s//* [label()=~regexp('%s')]" %
1380                                     (self._escape(ssl_fp),
1381                                      parser.case_i("VirtualHost")))
1382
1383        vh_p = self._get_new_vh_path(orig_matches, new_matches)
1384
1385        if not vh_p:
1386            # The vhost was not found on the currently parsed paths
1387            # Make Augeas aware of the new vhost
1388            self.parser.parse_file(ssl_fp)
1389            # Try to search again
1390            new_matches = self.parser.aug.match(
1391                "/files%s//* [label()=~regexp('%s')]" %
1392                (self._escape(ssl_fp),
1393                 parser.case_i("VirtualHost")))
1394            vh_p = self._get_new_vh_path(orig_matches, new_matches)
1395            if not vh_p:
1396                raise errors.PluginError(
1397                    "Could not reverse map the HTTPS VirtualHost to the original")
1398
1399
1400        # Update Addresses
1401        self._update_ssl_vhosts_addrs(vh_p)
1402
1403        # Log actions and create save notes
1404        logger.info("Created an SSL vhost at %s", ssl_fp)
1405        self.save_notes += "Created ssl vhost at %s\n" % ssl_fp
1406        self.save()
1407
1408        # We know the length is one because of the assertion above
1409        # Create the Vhost object
1410        ssl_vhost = self._create_vhost(vh_p)
1411        ssl_vhost.ancestor = nonssl_vhost
1412
1413        self.vhosts.append(ssl_vhost)
1414
1415        # NOTE: Searches through Augeas seem to ruin changes to directives
1416        #       The configuration must also be saved before being searched
1417        #       for the new directives; For these reasons... this is tacked
1418        #       on after fully creating the new vhost
1419
1420        # Now check if addresses need to be added as NameBasedVhost addrs
1421        # This is for compliance with versions of Apache < 2.4
1422        self._add_name_vhost_if_necessary(ssl_vhost)
1423
1424        return ssl_vhost
1425
1426    def _get_new_vh_path(self, orig_matches, new_matches):
1427        """ Helper method for make_vhost_ssl for matching augeas paths. Returns
1428        VirtualHost path from new_matches that's not present in orig_matches.
1429
1430        Paths are normalized, because augeas leaves indices out for paths
1431        with only single directive with a similar key """
1432
1433        orig_matches = [i.replace("[1]", "") for i in orig_matches]
1434        for match in new_matches:
1435            if match.replace("[1]", "") not in orig_matches:
1436                # Return the unmodified path
1437                return match
1438        return None
1439
1440    def _get_ssl_vhost_path(self, non_ssl_vh_fp):
1441        """ Get a file path for SSL vhost, uses user defined path as priority,
1442        but if the value is invalid or not defined, will fall back to non-ssl
1443        vhost filepath.
1444
1445        :param str non_ssl_vh_fp: Filepath of non-SSL vhost
1446
1447        :returns: Filepath for SSL vhost
1448        :rtype: str
1449        """
1450
1451        if self.conf("vhost-root") and os.path.exists(self.conf("vhost-root")):
1452            fp = os.path.join(filesystem.realpath(self.options.vhost_root),
1453                              os.path.basename(non_ssl_vh_fp))
1454        else:
1455            # Use non-ssl filepath
1456            fp = filesystem.realpath(non_ssl_vh_fp)
1457
1458        if fp.endswith(".conf"):
1459            return fp[:-(len(".conf"))] + self.options.le_vhost_ext
1460        return fp + self.options.le_vhost_ext
1461
1462    def _sift_rewrite_rule(self, line):
1463        """Decides whether a line should be copied to a SSL vhost.
1464
1465        A canonical example of when sifting a line is required:
1466        When the http vhost contains a RewriteRule that unconditionally
1467        redirects any request to the https version of the same site.
1468        e.g:
1469        RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [L,QSA,R=permanent]
1470        Copying the above line to the ssl vhost would cause a
1471        redirection loop.
1472
1473        :param str line: a line extracted from the http vhost.
1474
1475        :returns: True - don't copy line from http vhost to SSL vhost.
1476        :rtype: bool
1477
1478        """
1479        if not line.lower().lstrip().startswith("rewriterule"):
1480            return False
1481
1482        # According to: https://httpd.apache.org/docs/2.4/rewrite/flags.html
1483        # The syntax of a RewriteRule is:
1484        # RewriteRule pattern target [Flag1,Flag2,Flag3]
1485        # i.e. target is required, so it must exist.
1486        target = line.split()[2].strip()
1487
1488        # target may be surrounded with quotes
1489        if target[0] in ("'", '"') and target[0] == target[-1]:
1490            target = target[1:-1]
1491
1492        # Sift line if it redirects the request to a HTTPS site
1493        return target.startswith("https://")
1494
1495    def _copy_create_ssl_vhost_skeleton(self, vhost, ssl_fp):
1496        """Copies over existing Vhost with IfModule mod_ssl.c> skeleton.
1497
1498        :param obj.VirtualHost vhost: Original VirtualHost object
1499        :param str ssl_fp: Full path where the new ssl_vhost will reside.
1500
1501        A new file is created on the filesystem.
1502
1503        """
1504        # First register the creation so that it is properly removed if
1505        # configuration is rolled back
1506        if os.path.exists(ssl_fp):
1507            notes = "Appended new VirtualHost directive to file %s" % ssl_fp
1508            files = set()
1509            files.add(ssl_fp)
1510            self.reverter.add_to_checkpoint(files, notes)
1511        else:
1512            self.reverter.register_file_creation(False, ssl_fp)
1513        sift = False
1514
1515        try:
1516            orig_contents = self._get_vhost_block(vhost)
1517            ssl_vh_contents, sift = self._sift_rewrite_rules(orig_contents)
1518
1519            with open(ssl_fp, "a") as new_file:
1520                new_file.write("<IfModule mod_ssl.c>\n")
1521                new_file.write("\n".join(ssl_vh_contents))
1522                # The content does not include the closing tag, so add it
1523                new_file.write("</VirtualHost>\n")
1524                new_file.write("</IfModule>\n")
1525            # Add new file to augeas paths if we're supposed to handle
1526            # activation (it's not included as default)
1527            if not self.parser.parsed_in_current(ssl_fp):
1528                self.parser.parse_file(ssl_fp)
1529        except IOError:
1530            logger.critical("Error writing/reading to file in make_vhost_ssl", exc_info=True)
1531            raise errors.PluginError("Unable to write/read in make_vhost_ssl")
1532
1533        if sift:
1534            display_util.notify(
1535                f"Some rewrite rules copied from {vhost.filep} were disabled in the "
1536                f"vhost for your HTTPS site located at {ssl_fp} because they have "
1537                "the potential to create redirection loops."
1538            )
1539        self.parser.aug.set("/augeas/files%s/mtime" % (self._escape(ssl_fp)), "0")
1540        self.parser.aug.set("/augeas/files%s/mtime" % (self._escape(vhost.filep)), "0")
1541
1542    def _sift_rewrite_rules(self, contents):
1543        """ Helper function for _copy_create_ssl_vhost_skeleton to prepare the
1544        new HTTPS VirtualHost contents. Currently disabling the rewrites """
1545
1546        result = []
1547        sift = False
1548        contents = iter(contents)
1549
1550        comment = ("# Some rewrite rules in this file were "
1551                   "disabled on your HTTPS site,\n"
1552                   "# because they have the potential to create "
1553                   "redirection loops.\n")
1554
1555        for line in contents:
1556            A = line.lower().lstrip().startswith("rewritecond")
1557            B = line.lower().lstrip().startswith("rewriterule")
1558
1559            if not (A or B):
1560                result.append(line)
1561                continue
1562
1563            # A RewriteRule that doesn't need filtering
1564            if B and not self._sift_rewrite_rule(line):
1565                result.append(line)
1566                continue
1567
1568            # A RewriteRule that does need filtering
1569            if B and self._sift_rewrite_rule(line):
1570                if not sift:
1571                    result.append(comment)
1572                    sift = True
1573                result.append("# " + line)
1574                continue
1575
1576            # We save RewriteCond(s) and their corresponding
1577            # RewriteRule in 'chunk'.
1578            # We then decide whether we comment out the entire
1579            # chunk based on its RewriteRule.
1580            chunk = []
1581            if A:
1582                chunk.append(line)
1583                line = next(contents)
1584
1585                # RewriteCond(s) must be followed by one RewriteRule
1586                while not line.lower().lstrip().startswith("rewriterule"):
1587                    chunk.append(line)
1588                    line = next(contents)
1589
1590                # Now, current line must start with a RewriteRule
1591                chunk.append(line)
1592
1593                if self._sift_rewrite_rule(line):
1594                    if not sift:
1595                        result.append(comment)
1596                        sift = True
1597
1598                    result.append('\n'.join('# ' + l for l in chunk))
1599                else:
1600                    result.append('\n'.join(chunk))
1601        return result, sift
1602
1603    def _get_vhost_block(self, vhost):
1604        """ Helper method to get VirtualHost contents from the original file.
1605        This is done with help of augeas span, which returns the span start and
1606        end positions
1607
1608        :returns: `list` of VirtualHost block content lines without closing tag
1609        """
1610
1611        try:
1612            span_val = self.parser.aug.span(vhost.path)
1613        except ValueError:
1614            logger.critical("Error while reading the VirtualHost %s from "
1615                         "file %s", vhost.name, vhost.filep, exc_info=True)
1616            raise errors.PluginError("Unable to read VirtualHost from file")
1617        span_filep = span_val[0]
1618        span_start = span_val[5]
1619        span_end = span_val[6]
1620        with open(span_filep, 'r') as fh:
1621            fh.seek(span_start)
1622            vh_contents = fh.read(span_end-span_start).split("\n")
1623        self._remove_closing_vhost_tag(vh_contents)
1624        return vh_contents
1625
1626    def _remove_closing_vhost_tag(self, vh_contents):
1627        """Removes the closing VirtualHost tag if it exists.
1628
1629        This method modifies vh_contents directly to remove the closing
1630        tag. If the closing vhost tag is found, everything on the line
1631        after it is also removed. Whether or not this tag is included
1632        in the result of span depends on the Augeas version.
1633
1634        :param list vh_contents: VirtualHost block contents to check
1635
1636        """
1637        for offset, line in enumerate(reversed(vh_contents)):
1638            if line:
1639                line_index = line.lower().find("</virtualhost>")
1640                if line_index != -1:
1641                    content_index = len(vh_contents) - offset - 1
1642                    vh_contents[content_index] = line[:line_index]
1643                break
1644
1645    def _update_ssl_vhosts_addrs(self, vh_path):
1646        ssl_addrs = set()
1647        ssl_addr_p = self.parser.aug.match(vh_path + "/arg")
1648
1649        for addr in ssl_addr_p:
1650            old_addr = obj.Addr.fromstring(
1651                str(self.parser.get_arg(addr)))
1652            ssl_addr = old_addr.get_addr_obj("443")
1653            self.parser.aug.set(addr, str(ssl_addr))
1654            ssl_addrs.add(ssl_addr)
1655
1656        return ssl_addrs
1657
1658    def _clean_vhost(self, vhost):
1659        # remove duplicated or conflicting ssl directives
1660        self._deduplicate_directives(vhost.path,
1661                                     ["SSLCertificateFile",
1662                                      "SSLCertificateKeyFile"])
1663        # remove all problematic directives
1664        self._remove_directives(vhost.path, ["SSLCertificateChainFile"])
1665
1666    def _deduplicate_directives(self, vh_path, directives):
1667        for directive in directives:
1668            while len(self.parser.find_dir(directive, None,
1669                                           vh_path, False)) > 1:
1670                directive_path = self.parser.find_dir(directive, None,
1671                                                      vh_path, False)
1672                self.parser.aug.remove(re.sub(r"/\w*$", "", directive_path[0]))
1673
1674    def _remove_directives(self, vh_path, directives):
1675        for directive in directives:
1676            while self.parser.find_dir(directive, None, vh_path, False):
1677                directive_path = self.parser.find_dir(directive, None,
1678                                                      vh_path, False)
1679                self.parser.aug.remove(re.sub(r"/\w*$", "", directive_path[0]))
1680
1681    def _add_dummy_ssl_directives(self, vh_path):
1682        self.parser.add_dir(vh_path, "SSLCertificateFile",
1683                            "insert_cert_file_path")
1684        self.parser.add_dir(vh_path, "SSLCertificateKeyFile",
1685                            "insert_key_file_path")
1686        # Only include the TLS configuration if not already included
1687        existing_inc = self.parser.find_dir("Include", self.mod_ssl_conf, vh_path)
1688        if not existing_inc:
1689            self.parser.add_dir(vh_path, "Include", self.mod_ssl_conf)
1690
1691    def _add_servername_alias(self, target_name, vhost):
1692        vh_path = vhost.path
1693        sname, saliases = self._get_vhost_names(vh_path)
1694        if target_name == sname or target_name in saliases:
1695            return
1696        if self._has_matching_wildcard(vh_path, target_name):
1697            return
1698        if not self.parser.find_dir("ServerName", None,
1699                                    start=vh_path, exclude=False):
1700            self.parser.add_dir(vh_path, "ServerName", target_name)
1701        else:
1702            self.parser.add_dir(vh_path, "ServerAlias", target_name)
1703        self._add_servernames(vhost)
1704
1705    def _has_matching_wildcard(self, vh_path, target_name):
1706        """Is target_name already included in a wildcard in the vhost?
1707
1708        :param str vh_path: Augeas path to the vhost
1709        :param str target_name: name to compare with wildcards
1710
1711        :returns: True if there is a wildcard covering target_name in
1712            the vhost in vhost_path, otherwise, False
1713        :rtype: bool
1714
1715        """
1716        matches = self.parser.find_dir(
1717            "ServerAlias", start=vh_path, exclude=False)
1718        aliases = (self.parser.aug.get(match) for match in matches)
1719        return self.domain_in_names(aliases, target_name)
1720
1721    def _add_name_vhost_if_necessary(self, vhost):
1722        """Add NameVirtualHost Directives if necessary for new vhost.
1723
1724        NameVirtualHosts was a directive in Apache < 2.4
1725        https://httpd.apache.org/docs/2.2/mod/core.html#namevirtualhost
1726
1727        :param vhost: New virtual host that was recently created.
1728        :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
1729
1730        """
1731        need_to_save = False
1732
1733        # See if the exact address appears in any other vhost
1734        # Remember 1.1.1.1:* == 1.1.1.1 -> hence any()
1735        for addr in vhost.addrs:
1736            # In Apache 2.2, when a NameVirtualHost directive is not
1737            # set, "*" and "_default_" will conflict when sharing a port
1738            addrs = {addr,}
1739            if addr.get_addr() in ("*", "_default_"):
1740                addrs.update(obj.Addr((a, addr.get_port(),))
1741                             for a in ("*", "_default_"))
1742
1743            for test_vh in self.vhosts:
1744                if (vhost.filep != test_vh.filep and
1745                        any(test_addr in addrs for
1746                            test_addr in test_vh.addrs) and
1747                        not self.is_name_vhost(addr)):
1748                    self.add_name_vhost(addr)
1749                    logger.info("Enabling NameVirtualHosts on %s", addr)
1750                    need_to_save = True
1751                    break
1752
1753        if need_to_save:
1754            self.save()
1755
1756    def find_vhost_by_id(self, id_str):
1757        """
1758        Searches through VirtualHosts and tries to match the id in a comment
1759
1760        :param str id_str: Id string for matching
1761
1762        :returns: The matched VirtualHost or None
1763        :rtype: :class:`~certbot_apache._internal.obj.VirtualHost` or None
1764
1765        :raises .errors.PluginError: If no VirtualHost is found
1766        """
1767
1768        for vh in self.vhosts:
1769            if self._find_vhost_id(vh) == id_str:
1770                return vh
1771        msg = "No VirtualHost with ID {} was found.".format(id_str)
1772        logger.warning(msg)
1773        raise errors.PluginError(msg)
1774
1775    def _find_vhost_id(self, vhost):
1776        """Tries to find the unique ID from the VirtualHost comments. This is
1777        used for keeping track of VirtualHost directive over time.
1778
1779        :param vhost: Virtual host to add the id
1780        :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
1781
1782        :returns: The unique ID or None
1783        :rtype: str or None
1784        """
1785
1786        # Strip the {} off from the format string
1787        search_comment = constants.MANAGED_COMMENT_ID.format("")
1788
1789        id_comment = self.parser.find_comments(search_comment, vhost.path)
1790        if id_comment:
1791            # Use the first value, multiple ones shouldn't exist
1792            comment = self.parser.get_arg(id_comment[0])
1793            return comment.split(" ")[-1]
1794        return None
1795
1796    def add_vhost_id(self, vhost):
1797        """Adds an unique ID to the VirtualHost as a comment for mapping back
1798        to it on later invocations, as the config file order might have changed.
1799        If ID already exists, returns that instead.
1800
1801        :param vhost: Virtual host to add or find the id
1802        :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
1803
1804        :returns: The unique ID for vhost
1805        :rtype: str or None
1806        """
1807
1808        vh_id = self._find_vhost_id(vhost)
1809        if vh_id:
1810            return vh_id
1811
1812        id_string = apache_util.unique_id()
1813        comment = constants.MANAGED_COMMENT_ID.format(id_string)
1814        self.parser.add_comment(vhost.path, comment)
1815        return id_string
1816
1817    def _escape(self, fp):
1818        fp = fp.replace(",", "\\,")
1819        fp = fp.replace("[", "\\[")
1820        fp = fp.replace("]", "\\]")
1821        fp = fp.replace("|", "\\|")
1822        fp = fp.replace("=", "\\=")
1823        fp = fp.replace("(", "\\(")
1824        fp = fp.replace(")", "\\)")
1825        fp = fp.replace("!", "\\!")
1826        return fp
1827
1828    ######################################################################
1829    # Enhancements
1830    ######################################################################
1831    def supported_enhancements(self):
1832        """Returns currently supported enhancements."""
1833        return ["redirect", "ensure-http-header", "staple-ocsp"]
1834
1835    def enhance(self, domain, enhancement, options=None):
1836        """Enhance configuration.
1837
1838        :param str domain: domain to enhance
1839        :param str enhancement: enhancement type defined in
1840            :const:`~certbot.plugins.enhancements.ENHANCEMENTS`
1841        :param options: options for the enhancement
1842            See :const:`~certbot.plugins.enhancements.ENHANCEMENTS`
1843            documentation for appropriate parameter.
1844
1845        :raises .errors.PluginError: If Enhancement is not supported, or if
1846            there is any other problem with the enhancement.
1847
1848        """
1849        try:
1850            func = self._enhance_func[enhancement]
1851        except KeyError:
1852            raise errors.PluginError(
1853                "Unsupported enhancement: {0}".format(enhancement))
1854
1855        matched_vhosts = self.choose_vhosts(domain, create_if_no_ssl=False)
1856        # We should be handling only SSL vhosts for enhancements
1857        vhosts = [vhost for vhost in matched_vhosts if vhost.ssl]
1858
1859        if not vhosts:
1860            msg_tmpl = ("Certbot was not able to find SSL VirtualHost for a "
1861                        "domain {0} for enabling enhancement \"{1}\". The requested "
1862                        "enhancement was not configured.")
1863            msg_enhancement = enhancement
1864            if options:
1865                msg_enhancement += ": " + options
1866            msg = msg_tmpl.format(domain, msg_enhancement)
1867            logger.error(msg)
1868            raise errors.PluginError(msg)
1869        try:
1870            for vhost in vhosts:
1871                func(vhost, options)
1872        except errors.PluginError:
1873            logger.error("Failed %s for %s", enhancement, domain)
1874            raise
1875
1876    def _autohsts_increase(self, vhost, id_str, nextstep):
1877        """Increase the AutoHSTS max-age value
1878
1879        :param vhost: Virtual host object to modify
1880        :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
1881
1882        :param str id_str: The unique ID string of VirtualHost
1883
1884        :param int nextstep: Next AutoHSTS max-age value index
1885
1886        """
1887        nextstep_value = constants.AUTOHSTS_STEPS[nextstep]
1888        self._autohsts_write(vhost, nextstep_value)
1889        self._autohsts[id_str] = {"laststep": nextstep, "timestamp": time.time()}
1890
1891    def _autohsts_write(self, vhost, nextstep_value):
1892        """
1893        Write the new HSTS max-age value to the VirtualHost file
1894        """
1895
1896        hsts_dirpath = None
1897        header_path = self.parser.find_dir("Header", None, vhost.path)
1898        if header_path:
1899            pat = '(?:[ "]|^)(strict-transport-security)(?:[ "]|$)'
1900            for match in header_path:
1901                if re.search(pat, self.parser.aug.get(match).lower()):
1902                    hsts_dirpath = match
1903        if not hsts_dirpath:
1904            err_msg = ("Certbot was unable to find the existing HSTS header "
1905                       "from the VirtualHost at path {0}.").format(vhost.filep)
1906            raise errors.PluginError(err_msg)
1907
1908        # Prepare the HSTS header value
1909        hsts_maxage = "\"max-age={0}\"".format(nextstep_value)
1910
1911        # Update the header
1912        # Our match statement was for string strict-transport-security, but
1913        # we need to update the value instead. The next index is for the value
1914        hsts_dirpath = hsts_dirpath.replace("arg[3]", "arg[4]")
1915        self.parser.aug.set(hsts_dirpath, hsts_maxage)
1916        note_msg = ("Increasing HSTS max-age value to {0} for VirtualHost "
1917                    "in {1}\n".format(nextstep_value, vhost.filep))
1918        logger.debug(note_msg)
1919        self.save_notes += note_msg
1920        self.save(note_msg)
1921
1922    def _autohsts_fetch_state(self):
1923        """
1924        Populates the AutoHSTS state from the pluginstorage
1925        """
1926        try:
1927            self._autohsts = self.storage.fetch("autohsts")
1928        except KeyError:
1929            self._autohsts = {}
1930
1931    def _autohsts_save_state(self):
1932        """
1933        Saves the state of AutoHSTS object to pluginstorage
1934        """
1935        self.storage.put("autohsts", self._autohsts)
1936        self.storage.save()
1937
1938    def _autohsts_vhost_in_lineage(self, vhost, lineage):
1939        """
1940        Searches AutoHSTS managed VirtualHosts that belong to the lineage.
1941        Matches the private key path.
1942        """
1943
1944        return bool(
1945            self.parser.find_dir("SSLCertificateKeyFile",
1946                                 lineage.key_path, vhost.path))
1947
1948    def _enable_ocsp_stapling(self, ssl_vhost, unused_options):
1949        """Enables OCSP Stapling
1950
1951        In OCSP, each client (e.g. browser) would have to query the
1952        OCSP Responder to validate that the site certificate was not revoked.
1953
1954        Enabling OCSP Stapling, would allow the web-server to query the OCSP
1955        Responder, and staple its response to the offered certificate during
1956        TLS. i.e. clients would not have to query the OCSP responder.
1957
1958        OCSP Stapling enablement on Apache implicitly depends on
1959        SSLCertificateChainFile being set by other code.
1960
1961        .. note:: This function saves the configuration
1962
1963        :param ssl_vhost: Destination of traffic, an ssl enabled vhost
1964        :type ssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
1965
1966        :param unused_options: Not currently used
1967        :type unused_options: Not Available
1968
1969        :returns: Success, general_vhost (HTTP vhost)
1970        :rtype: (bool, :class:`~certbot_apache._internal.obj.VirtualHost`)
1971
1972        """
1973        min_apache_ver = (2, 3, 3)
1974        if self.get_version() < min_apache_ver:
1975            raise errors.PluginError(
1976                "Unable to set OCSP directives.\n"
1977                "Apache version is below 2.3.3.")
1978
1979        if "socache_shmcb_module" not in self.parser.modules:
1980            self.enable_mod("socache_shmcb")
1981
1982        # Check if there's an existing SSLUseStapling directive on.
1983        use_stapling_aug_path = self.parser.find_dir("SSLUseStapling",
1984                "on", start=ssl_vhost.path)
1985        if not use_stapling_aug_path:
1986            self.parser.add_dir(ssl_vhost.path, "SSLUseStapling", "on")
1987
1988        ssl_vhost_aug_path = self._escape(parser.get_aug_path(ssl_vhost.filep))
1989
1990        # Check if there's an existing SSLStaplingCache directive.
1991        stapling_cache_aug_path = self.parser.find_dir('SSLStaplingCache',
1992                None, ssl_vhost_aug_path)
1993
1994        # We'll simply delete the directive, so that we'll have a
1995        # consistent OCSP cache path.
1996        if stapling_cache_aug_path:
1997            self.parser.aug.remove(
1998                    re.sub(r"/\w*$", "", stapling_cache_aug_path[0]))
1999
2000        self.parser.add_dir_to_ifmodssl(ssl_vhost_aug_path,
2001                "SSLStaplingCache",
2002                ["shmcb:/var/run/apache2/stapling_cache(128000)"])
2003
2004        msg = "OCSP Stapling was enabled on SSL Vhost: %s.\n"%(
2005                ssl_vhost.filep)
2006        self.save_notes += msg
2007        self.save()
2008        logger.info(msg)
2009
2010    def _set_http_header(self, ssl_vhost, header_substring):
2011        """Enables header that is identified by header_substring on ssl_vhost.
2012
2013        If the header identified by header_substring is not already set,
2014        a new Header directive is placed in ssl_vhost's configuration with
2015        arguments from: constants.HTTP_HEADER[header_substring]
2016
2017        .. note:: This function saves the configuration
2018
2019        :param ssl_vhost: Destination of traffic, an ssl enabled vhost
2020        :type ssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
2021
2022        :param header_substring: string that uniquely identifies a header.
2023                e.g: Strict-Transport-Security, Upgrade-Insecure-Requests.
2024        :type str
2025
2026        :returns: Success, general_vhost (HTTP vhost)
2027        :rtype: (bool, :class:`~certbot_apache._internal.obj.VirtualHost`)
2028
2029        :raises .errors.PluginError: If no viable HTTP host can be created or
2030            set with header header_substring.
2031
2032        """
2033        if "headers_module" not in self.parser.modules:
2034            self.enable_mod("headers")
2035
2036        # Check if selected header is already set
2037        self._verify_no_matching_http_header(ssl_vhost, header_substring)
2038
2039        # Add directives to server
2040        self.parser.add_dir(ssl_vhost.path, "Header",
2041                            constants.HEADER_ARGS[header_substring])
2042
2043        self.save_notes += ("Adding %s header to ssl vhost in %s\n" %
2044                            (header_substring, ssl_vhost.filep))
2045
2046        self.save()
2047        logger.info("Adding %s header to ssl vhost in %s", header_substring,
2048                    ssl_vhost.filep)
2049
2050    def _verify_no_matching_http_header(self, ssl_vhost, header_substring):
2051        """Checks to see if there is an existing Header directive that
2052        contains the string header_substring.
2053
2054        :param ssl_vhost: vhost to check
2055        :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
2056
2057        :param header_substring: string that uniquely identifies a header.
2058                e.g: Strict-Transport-Security, Upgrade-Insecure-Requests.
2059        :type str
2060
2061        :returns: boolean
2062        :rtype: (bool)
2063
2064        :raises errors.PluginEnhancementAlreadyPresent When header
2065                header_substring exists
2066
2067        """
2068        header_path = self.parser.find_dir("Header", None,
2069                                           start=ssl_vhost.path)
2070        if header_path:
2071            # "Existing Header directive for virtualhost"
2072            pat = '(?:[ "]|^)(%s)(?:[ "]|$)' % (header_substring.lower())
2073            for match in header_path:
2074                if re.search(pat, self.parser.aug.get(match).lower()):
2075                    raise errors.PluginEnhancementAlreadyPresent(
2076                        "Existing %s header" % (header_substring))
2077
2078    def _enable_redirect(self, ssl_vhost, unused_options):
2079        """Redirect all equivalent HTTP traffic to ssl_vhost.
2080
2081        .. todo:: This enhancement should be rewritten and will
2082           unfortunately require lots of debugging by hand.
2083
2084        Adds Redirect directive to the port 80 equivalent of ssl_vhost
2085        First the function attempts to find the vhost with equivalent
2086        ip addresses that serves on non-ssl ports
2087        The function then adds the directive
2088
2089        .. note:: This function saves the configuration
2090
2091        :param ssl_vhost: Destination of traffic, an ssl enabled vhost
2092        :type ssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
2093
2094        :param unused_options: Not currently used
2095        :type unused_options: Not Available
2096
2097        :raises .errors.PluginError: If no viable HTTP host can be created or
2098            used for the redirect.
2099
2100        """
2101        if "rewrite_module" not in self.parser.modules:
2102            self.enable_mod("rewrite")
2103        general_vh = self._get_http_vhost(ssl_vhost)
2104
2105        if general_vh is None:
2106            # Add virtual_server with redirect
2107            logger.debug("Did not find http version of ssl virtual host "
2108                         "attempting to create")
2109            redirect_addrs = self._get_proposed_addrs(ssl_vhost)
2110            for vhost in self.vhosts:
2111                if vhost.enabled and vhost.conflicts(redirect_addrs):
2112                    raise errors.PluginError(
2113                        "Unable to find corresponding HTTP vhost; "
2114                        "Unable to create one as intended addresses conflict; "
2115                        "Current configuration does not support automated "
2116                        "redirection")
2117            self._create_redirect_vhost(ssl_vhost)
2118        else:
2119            if general_vh in self._enhanced_vhosts["redirect"]:
2120                logger.debug("Already enabled redirect for this vhost")
2121                return
2122
2123            # Check if Certbot redirection already exists
2124            self._verify_no_certbot_redirect(general_vh)
2125
2126            # Note: if code flow gets here it means we didn't find the exact
2127            # certbot RewriteRule config for redirection. Finding
2128            # another RewriteRule is likely to be fine in most or all cases,
2129            # but redirect loops are possible in very obscure cases; see #1620
2130            # for reasoning.
2131            if self._is_rewrite_exists(general_vh):
2132                logger.warning("Added an HTTP->HTTPS rewrite in addition to "
2133                               "other RewriteRules; you may wish to check for "
2134                               "overall consistency.")
2135
2136            # Add directives to server
2137            # Note: These are not immediately searchable in sites-enabled
2138            #     even with save() and load()
2139            if not self._is_rewrite_engine_on(general_vh):
2140                self.parser.add_dir(general_vh.path, "RewriteEngine", "on")
2141
2142            names = ssl_vhost.get_names()
2143            for idx, name in enumerate(names):
2144                args = ["%{SERVER_NAME}", "={0}".format(name), "[OR]"]
2145                if idx == len(names) - 1:
2146                    args.pop()
2147                self.parser.add_dir(general_vh.path, "RewriteCond", args)
2148
2149            self._set_https_redirection_rewrite_rule(general_vh)
2150
2151            self.save_notes += ("Redirecting host in %s to ssl vhost in %s\n" %
2152                                (general_vh.filep, ssl_vhost.filep))
2153            self.save()
2154
2155            self._enhanced_vhosts["redirect"].add(general_vh)
2156            logger.info("Redirecting vhost in %s to ssl vhost in %s",
2157                        general_vh.filep, ssl_vhost.filep)
2158
2159    def _set_https_redirection_rewrite_rule(self, vhost):
2160        if self.get_version() >= (2, 3, 9):
2161            self.parser.add_dir(vhost.path, "RewriteRule",
2162                    constants.REWRITE_HTTPS_ARGS_WITH_END)
2163        else:
2164            self.parser.add_dir(vhost.path, "RewriteRule",
2165                    constants.REWRITE_HTTPS_ARGS)
2166
2167    def _verify_no_certbot_redirect(self, vhost):
2168        """Checks to see if a redirect was already installed by certbot.
2169
2170        Checks to see if virtualhost already contains a rewrite rule that is
2171        identical to Certbot's redirection rewrite rule.
2172
2173        For graceful transition to new rewrite rules for HTTPS redireciton we
2174        delete certbot's old rewrite rules and set the new one instead.
2175
2176        :param vhost: vhost to check
2177        :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
2178
2179        :raises errors.PluginEnhancementAlreadyPresent: When the exact
2180                certbot redirection WriteRule exists in virtual host.
2181        """
2182        rewrite_path = self.parser.find_dir(
2183            "RewriteRule", None, start=vhost.path)
2184
2185        # There can be other RewriteRule directive lines in vhost config.
2186        # rewrite_args_dict keys are directive ids and the corresponding value
2187        # for each is a list of arguments to that directive.
2188        rewrite_args_dict: DefaultDict[str, List[str]] = defaultdict(list)
2189        pat = r'(.*directive\[\d+\]).*'
2190        for match in rewrite_path:
2191            m = re.match(pat, match)
2192            if m:
2193                dir_path = m.group(1)
2194                rewrite_args_dict[dir_path].append(match)
2195
2196        if rewrite_args_dict:
2197            redirect_args = [constants.REWRITE_HTTPS_ARGS,
2198                             constants.REWRITE_HTTPS_ARGS_WITH_END]
2199
2200            for dir_path, args_paths in rewrite_args_dict.items():
2201                arg_vals = [self.parser.aug.get(x) for x in args_paths]
2202
2203                # Search for past redirection rule, delete it, set the new one
2204                if arg_vals in constants.OLD_REWRITE_HTTPS_ARGS:
2205                    self.parser.aug.remove(dir_path)
2206                    self._set_https_redirection_rewrite_rule(vhost)
2207                    self.save()
2208                    raise errors.PluginEnhancementAlreadyPresent(
2209                        "Certbot has already enabled redirection")
2210
2211                if arg_vals in redirect_args:
2212                    raise errors.PluginEnhancementAlreadyPresent(
2213                        "Certbot has already enabled redirection")
2214
2215    def _is_rewrite_exists(self, vhost):
2216        """Checks if there exists a RewriteRule directive in vhost
2217
2218        :param vhost: vhost to check
2219        :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
2220
2221        :returns: True if a RewriteRule directive exists.
2222        :rtype: bool
2223
2224        """
2225        rewrite_path = self.parser.find_dir(
2226            "RewriteRule", None, start=vhost.path)
2227        return bool(rewrite_path)
2228
2229    def _is_rewrite_engine_on(self, vhost):
2230        """Checks if a RewriteEngine directive is on
2231
2232        :param vhost: vhost to check
2233        :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
2234
2235        """
2236        rewrite_engine_path_list = self.parser.find_dir("RewriteEngine", "on",
2237                                                   start=vhost.path)
2238        if rewrite_engine_path_list:
2239            for re_path in rewrite_engine_path_list:
2240                # A RewriteEngine directive may also be included in per
2241                # directory .htaccess files. We only care about the VirtualHost.
2242                if 'virtualhost' in re_path.lower():
2243                    return self.parser.get_arg(re_path)
2244        return False
2245
2246    def _create_redirect_vhost(self, ssl_vhost):
2247        """Creates an http_vhost specifically to redirect for the ssl_vhost.
2248
2249        :param ssl_vhost: ssl vhost
2250        :type ssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
2251
2252        :returns: tuple of the form
2253            (`success`, :class:`~certbot_apache._internal.obj.VirtualHost`)
2254        :rtype: tuple
2255
2256        """
2257        text = self._get_redirect_config_str(ssl_vhost)
2258
2259        redirect_filepath = self._write_out_redirect(ssl_vhost, text)
2260
2261        self.parser.aug.load()
2262        # Make a new vhost data structure and add it to the lists
2263        new_vhost = self._create_vhost(parser.get_aug_path(self._escape(redirect_filepath)))
2264        self.vhosts.append(new_vhost)
2265        self._enhanced_vhosts["redirect"].add(new_vhost)
2266
2267        # Finally create documentation for the change
2268        self.save_notes += ("Created a port 80 vhost, %s, for redirection to "
2269                            "ssl vhost %s\n" %
2270                            (new_vhost.filep, ssl_vhost.filep))
2271
2272    def _get_redirect_config_str(self, ssl_vhost):
2273        # get servernames and serveraliases
2274        serveralias = ""
2275        servername = ""
2276
2277        if ssl_vhost.name is not None:
2278            servername = "ServerName " + ssl_vhost.name
2279        if ssl_vhost.aliases:
2280            serveralias = "ServerAlias " + " ".join(ssl_vhost.aliases)
2281
2282        rewrite_rule_args: List[str] = []
2283        if self.get_version() >= (2, 3, 9):
2284            rewrite_rule_args = constants.REWRITE_HTTPS_ARGS_WITH_END
2285        else:
2286            rewrite_rule_args = constants.REWRITE_HTTPS_ARGS
2287
2288        return ("<VirtualHost %s>\n"
2289                "%s \n"
2290                "%s \n"
2291                "ServerSignature Off\n"
2292                "\n"
2293                "RewriteEngine On\n"
2294                "RewriteRule %s\n"
2295                "\n"
2296                "ErrorLog %s/redirect.error.log\n"
2297                "LogLevel warn\n"
2298                "</VirtualHost>\n"
2299                % (" ".join(str(addr) for
2300                            addr in self._get_proposed_addrs(ssl_vhost)),
2301                   servername, serveralias,
2302                   " ".join(rewrite_rule_args),
2303                   self.options.logs_root))
2304
2305    def _write_out_redirect(self, ssl_vhost, text):
2306        # This is the default name
2307        redirect_filename = "le-redirect.conf"
2308
2309        # See if a more appropriate name can be applied
2310        if ssl_vhost.name is not None:
2311            # make sure servername doesn't exceed filename length restriction
2312            if len(ssl_vhost.name) < (255 - (len(redirect_filename) + 1)):
2313                redirect_filename = "le-redirect-%s.conf" % ssl_vhost.name
2314
2315        redirect_filepath = os.path.join(self.options.vhost_root,
2316                                         redirect_filename)
2317
2318        # Register the new file that will be created
2319        # Note: always register the creation before writing to ensure file will
2320        # be removed in case of unexpected program exit
2321        self.reverter.register_file_creation(False, redirect_filepath)
2322
2323        # Write out file
2324        with open(redirect_filepath, "w") as redirect_file:
2325            redirect_file.write(text)
2326
2327        # Add new include to configuration if it doesn't exist yet
2328        if not self.parser.parsed_in_current(redirect_filepath):
2329            self.parser.parse_file(redirect_filepath)
2330
2331        logger.info("Created redirect file: %s", redirect_filename)
2332
2333        return redirect_filepath
2334
2335    def _get_http_vhost(self, ssl_vhost):
2336        """Find appropriate HTTP vhost for ssl_vhost."""
2337        # First candidate vhosts filter
2338        if ssl_vhost.ancestor:
2339            return ssl_vhost.ancestor
2340        candidate_http_vhs = [
2341            vhost for vhost in self.vhosts if not vhost.ssl
2342        ]
2343
2344        # Second filter - check addresses
2345        for http_vh in candidate_http_vhs:
2346            if http_vh.same_server(ssl_vhost):
2347                return http_vh
2348        # Third filter - if none with same names, return generic
2349        for http_vh in candidate_http_vhs:
2350            if http_vh.same_server(ssl_vhost, generic=True):
2351                return http_vh
2352
2353        return None
2354
2355    def _get_proposed_addrs(self, vhost, port="80"):
2356        """Return all addrs of vhost with the port replaced with the specified.
2357
2358        :param obj.VirtualHost ssl_vhost: Original Vhost
2359        :param str port: Desired port for new addresses
2360
2361        :returns: `set` of :class:`~obj.Addr`
2362
2363        """
2364        redirects = set()
2365        for addr in vhost.addrs:
2366            redirects.add(addr.get_addr_obj(port))
2367
2368        return redirects
2369
2370    def enable_site(self, vhost):
2371        """Enables an available site, Apache reload required.
2372
2373        .. note:: Does not make sure that the site correctly works or that all
2374                  modules are enabled appropriately.
2375        .. note:: The distribution specific override replaces functionality
2376                  of this method where available.
2377
2378        :param vhost: vhost to enable
2379        :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
2380
2381        :raises .errors.NotSupportedError: If filesystem layout is not
2382            supported.
2383
2384        """
2385        if vhost.enabled:
2386            return
2387
2388        if not self.parser.parsed_in_original(vhost.filep):
2389            # Add direct include to root conf
2390            logger.info("Enabling site %s by adding Include to root configuration",
2391                        vhost.filep)
2392            self.save_notes += "Enabled site %s\n" % vhost.filep
2393            self.parser.add_include(self.parser.loc["default"], vhost.filep)
2394            vhost.enabled = True
2395        return
2396
2397    def enable_mod(self, mod_name, temp=False):  # pylint: disable=unused-argument
2398        """Enables module in Apache.
2399
2400        Both enables and reloads Apache so module is active.
2401
2402        :param str mod_name: Name of the module to enable. (e.g. 'ssl')
2403        :param bool temp: Whether or not this is a temporary action.
2404
2405        .. note:: The distribution specific override replaces functionality
2406                  of this method where available.
2407
2408        :raises .errors.MisconfigurationError: We cannot enable modules in
2409            generic fashion.
2410
2411        """
2412        mod_message = ("Apache needs to have module  \"{0}\" active for the " +
2413            "requested installation options. Unfortunately Certbot is unable " +
2414            "to install or enable it for you. Please install the module, and " +
2415            "run Certbot again.")
2416        raise errors.MisconfigurationError(mod_message.format(mod_name))
2417
2418    def restart(self):
2419        """Runs a config test and reloads the Apache server.
2420
2421        :raises .errors.MisconfigurationError: If either the config test
2422            or reload fails.
2423
2424        """
2425        self.config_test()
2426        self._reload()
2427
2428    def _reload(self):
2429        """Reloads the Apache server.
2430
2431        :raises .errors.MisconfigurationError: If reload fails
2432
2433        """
2434        try:
2435            util.run_script(self.options.restart_cmd)
2436        except errors.SubprocessError as err:
2437            logger.warning("Unable to restart apache using %s",
2438                        self.options.restart_cmd)
2439            if self.options.restart_cmd_alt:
2440                logger.debug("Trying alternative restart command: %s",
2441                             self.options.restart_cmd_alt)
2442                # There is an alternative restart command available
2443                # This usually is "restart" verb while original is "graceful"
2444                try:
2445                    util.run_script(self.options.restart_cmd_alt)
2446                    return
2447                except errors.SubprocessError as secerr:
2448                    error = str(secerr)
2449            else:
2450                error = str(err)
2451            raise errors.MisconfigurationError(error)
2452
2453    def config_test(self):
2454        """Check the configuration of Apache for errors.
2455
2456        :raises .errors.MisconfigurationError: If config_test fails
2457
2458        """
2459        try:
2460            util.run_script(self.options.conftest_cmd)
2461        except errors.SubprocessError as err:
2462            raise errors.MisconfigurationError(str(err))
2463
2464    def get_version(self):
2465        """Return version of Apache Server.
2466
2467        Version is returned as tuple. (ie. 2.4.7 = (2, 4, 7))
2468
2469        :returns: version
2470        :rtype: tuple
2471
2472        :raises .PluginError: if unable to find Apache version
2473
2474        """
2475        try:
2476            stdout, _ = util.run_script(self.options.version_cmd)
2477        except errors.SubprocessError:
2478            raise errors.PluginError(
2479                "Unable to run %s -v" %
2480                self.options.version_cmd)
2481
2482        regex = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE)
2483        matches = regex.findall(stdout)
2484
2485        if len(matches) != 1:
2486            raise errors.PluginError("Unable to find Apache version")
2487
2488        return tuple(int(i) for i in matches[0].split("."))
2489
2490    def more_info(self):
2491        """Human-readable string to help understand the module"""
2492        return (
2493            "Configures Apache to authenticate and install HTTPS.{0}"
2494            "Server root: {root}{0}"
2495            "Version: {version}".format(
2496                os.linesep, root=self.parser.loc["root"],
2497                version=".".join(str(i) for i in self.version))
2498        )
2499
2500    def auth_hint(self, failed_achalls): # pragma: no cover
2501        return ("The Certificate Authority failed to verify the temporary Apache configuration "
2502                "changes made by Certbot. Ensure that the listed domains point to this Apache "
2503                "server and that it is accessible from the internet.")
2504
2505    ###########################################################################
2506    # Challenges Section
2507    ###########################################################################
2508    def get_chall_pref(self, unused_domain):
2509        """Return list of challenge preferences."""
2510        return [challenges.HTTP01]
2511
2512    def perform(self, achalls):
2513        """Perform the configuration related challenge.
2514
2515        This function currently assumes all challenges will be fulfilled.
2516        If this turns out not to be the case in the future. Cleanup and
2517        outstanding challenges will have to be designed better.
2518
2519        """
2520        self._chall_out.update(achalls)
2521        responses = [None] * len(achalls)
2522        http_doer = http_01.ApacheHttp01(self)
2523
2524        for i, achall in enumerate(achalls):
2525            # Currently also have chall_doer hold associated index of the
2526            # challenge. This helps to put all of the responses back together
2527            # when they are all complete.
2528            http_doer.add_chall(achall, i)
2529
2530        http_response = http_doer.perform()
2531        if http_response:
2532            # Must reload in order to activate the challenges.
2533            # Handled here because we may be able to load up other challenge
2534            # types
2535            self.restart()
2536
2537            # TODO: Remove this dirty hack. We need to determine a reliable way
2538            # of identifying when the new configuration is being used.
2539            time.sleep(3)
2540
2541            self._update_responses(responses, http_response, http_doer)
2542
2543        return responses
2544
2545    def _update_responses(self, responses, chall_response, chall_doer):
2546        # Go through all of the challenges and assign them to the proper
2547        # place in the responses return value. All responses must be in the
2548        # same order as the original challenges.
2549        for i, resp in enumerate(chall_response):
2550            responses[chall_doer.indices[i]] = resp
2551
2552    def cleanup(self, achalls):
2553        """Revert all challenges."""
2554        self._chall_out.difference_update(achalls)
2555
2556        # If all of the challenges have been finished, clean up everything
2557        if not self._chall_out:
2558            self.revert_challenge_config()
2559            self.restart()
2560            self.parser.reset_modules()
2561
2562    def install_ssl_options_conf(self, options_ssl, options_ssl_digest, warn_on_no_mod_ssl=True):
2563        """Copy Certbot's SSL options file into the system's config dir if required.
2564
2565        :param bool warn_on_no_mod_ssl: True if we should warn if mod_ssl is not found.
2566        """
2567
2568        # XXX if we ever try to enforce a local privilege boundary (eg, running
2569        # certbot for unprivileged users via setuid), this function will need
2570        # to be modified.
2571        apache_config_path = self.pick_apache_config(warn_on_no_mod_ssl)
2572
2573        return common.install_version_controlled_file(
2574            options_ssl, options_ssl_digest, apache_config_path, constants.ALL_SSL_OPTIONS_HASHES)
2575
2576    def enable_autohsts(self, _unused_lineage, domains):
2577        """
2578        Enable the AutoHSTS enhancement for defined domains
2579
2580        :param _unused_lineage: Certificate lineage object, unused
2581        :type _unused_lineage: certbot._internal.storage.RenewableCert
2582
2583        :param domains: List of domains in certificate to enhance
2584        :type domains: `list` of `str`
2585        """
2586
2587        self._autohsts_fetch_state()
2588        _enhanced_vhosts = []
2589        for d in domains:
2590            matched_vhosts = self.choose_vhosts(d, create_if_no_ssl=False)
2591            # We should be handling only SSL vhosts for AutoHSTS
2592            vhosts = [vhost for vhost in matched_vhosts if vhost.ssl]
2593
2594            if not vhosts:
2595                msg_tmpl = ("Certbot was not able to find SSL VirtualHost for a "
2596                            "domain {0} for enabling AutoHSTS enhancement.")
2597                msg = msg_tmpl.format(d)
2598                logger.error(msg)
2599                raise errors.PluginError(msg)
2600            for vh in vhosts:
2601                try:
2602                    self._enable_autohsts_domain(vh)
2603                    _enhanced_vhosts.append(vh)
2604                except errors.PluginEnhancementAlreadyPresent:
2605                    if vh in _enhanced_vhosts:
2606                        continue
2607                    msg = ("VirtualHost for domain {0} in file {1} has a " +
2608                           "String-Transport-Security header present, exiting.")
2609                    raise errors.PluginEnhancementAlreadyPresent(
2610                        msg.format(d, vh.filep))
2611        if _enhanced_vhosts:
2612            note_msg = "Enabling AutoHSTS"
2613            self.save(note_msg)
2614            logger.info(note_msg)
2615            self.restart()
2616
2617        # Save the current state to pluginstorage
2618        self._autohsts_save_state()
2619
2620    def _enable_autohsts_domain(self, ssl_vhost):
2621        """Do the initial AutoHSTS deployment to a vhost
2622
2623        :param ssl_vhost: The VirtualHost object to deploy the AutoHSTS
2624        :type ssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost` or None
2625
2626        :raises errors.PluginEnhancementAlreadyPresent: When already enhanced
2627
2628        """
2629        # This raises the exception
2630        self._verify_no_matching_http_header(ssl_vhost,
2631                                             "Strict-Transport-Security")
2632
2633        if "headers_module" not in self.parser.modules:
2634            self.enable_mod("headers")
2635        # Prepare the HSTS header value
2636        hsts_header = constants.HEADER_ARGS["Strict-Transport-Security"][:-1]
2637        initial_maxage = constants.AUTOHSTS_STEPS[0]
2638        hsts_header.append("\"max-age={0}\"".format(initial_maxage))
2639
2640        # Add ID to the VirtualHost for mapping back to it later
2641        uniq_id = self.add_vhost_id(ssl_vhost)
2642        self.save_notes += "Adding unique ID {0} to VirtualHost in {1}\n".format(
2643            uniq_id, ssl_vhost.filep)
2644        # Add the actual HSTS header
2645        self.parser.add_dir(ssl_vhost.path, "Header", hsts_header)
2646        note_msg = ("Adding gradually increasing HSTS header with initial value "
2647                    "of {0} to VirtualHost in {1}\n".format(
2648                        initial_maxage, ssl_vhost.filep))
2649        self.save_notes += note_msg
2650
2651        # Save the current state to pluginstorage
2652        self._autohsts[uniq_id] = {"laststep": 0, "timestamp": time.time()}
2653
2654    def update_autohsts(self, _unused_domain):
2655        """
2656        Increase the AutoHSTS values of VirtualHosts that the user has enabled
2657        this enhancement for.
2658
2659        :param _unused_domain: Not currently used
2660        :type _unused_domain: Not Available
2661
2662        """
2663        self._autohsts_fetch_state()
2664        if not self._autohsts:
2665            # No AutoHSTS enabled for any domain
2666            return
2667        curtime = time.time()
2668        save_and_restart = False
2669        for id_str, config in list(self._autohsts.items()):
2670            if config["timestamp"] + constants.AUTOHSTS_FREQ > curtime:
2671                # Skip if last increase was < AUTOHSTS_FREQ ago
2672                continue
2673            nextstep = config["laststep"] + 1
2674            if nextstep < len(constants.AUTOHSTS_STEPS):
2675                # If installer hasn't been prepared yet, do it now
2676                if not self._prepared:
2677                    self.prepare()
2678                # Have not reached the max value yet
2679                try:
2680                    vhost = self.find_vhost_by_id(id_str)
2681                except errors.PluginError:
2682                    msg = ("Could not find VirtualHost with ID {0}, disabling "
2683                           "AutoHSTS for this VirtualHost").format(id_str)
2684                    logger.error(msg)
2685                    # Remove the orphaned AutoHSTS entry from pluginstorage
2686                    self._autohsts.pop(id_str)
2687                    continue
2688                self._autohsts_increase(vhost, id_str, nextstep)
2689                msg = ("Increasing HSTS max-age value for VirtualHost with id "
2690                       "{0}").format(id_str)
2691                self.save_notes += msg
2692                save_and_restart = True
2693
2694        if save_and_restart:
2695            self.save("Increased HSTS max-age values")
2696            self.restart()
2697
2698        self._autohsts_save_state()
2699
2700    def deploy_autohsts(self, lineage):
2701        """
2702        Checks if autohsts vhost has reached maximum auto-increased value
2703        and changes the HSTS max-age to a high value.
2704
2705        :param lineage: Certificate lineage object
2706        :type lineage: certbot._internal.storage.RenewableCert
2707        """
2708        self._autohsts_fetch_state()
2709        if not self._autohsts:
2710            # No autohsts enabled for any vhost
2711            return
2712
2713        vhosts = []
2714        affected_ids = []
2715        # Copy, as we are removing from the dict inside the loop
2716        for id_str, config in list(self._autohsts.items()):
2717            if config["laststep"]+1 >= len(constants.AUTOHSTS_STEPS):
2718                # max value reached, try to make permanent
2719                try:
2720                    vhost = self.find_vhost_by_id(id_str)
2721                except errors.PluginError:
2722                    msg = ("VirtualHost with id {} was not found, unable to "
2723                           "make HSTS max-age permanent.").format(id_str)
2724                    logger.error(msg)
2725                    self._autohsts.pop(id_str)
2726                    continue
2727                if self._autohsts_vhost_in_lineage(vhost, lineage):
2728                    vhosts.append(vhost)
2729                    affected_ids.append(id_str)
2730
2731        save_and_restart = False
2732        for vhost in vhosts:
2733            self._autohsts_write(vhost, constants.AUTOHSTS_PERMANENT)
2734            msg = ("Strict-Transport-Security max-age value for "
2735                   "VirtualHost in {0} was made permanent.").format(vhost.filep)
2736            logger.debug(msg)
2737            self.save_notes += msg+"\n"
2738            save_and_restart = True
2739
2740        if save_and_restart:
2741            self.save("Made HSTS max-age permanent")
2742            self.restart()
2743
2744        for id_str in affected_ids:
2745            self._autohsts.pop(id_str)
2746
2747        # Update AutoHSTS storage (We potentially removed vhosts from managed)
2748        self._autohsts_save_state()
2749
2750
2751AutoHSTSEnhancement.register(ApacheConfigurator)
2752