Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/certbot/certbot.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'certbot-apache/certbot_apache/_internal')
-rw-r--r--certbot-apache/certbot_apache/_internal/__init__.py1
-rw-r--r--certbot-apache/certbot_apache/_internal/apache_util.py107
-rw-r--r--certbot-apache/certbot_apache/_internal/augeas_lens/README2
-rw-r--r--certbot-apache/certbot_apache/_internal/augeas_lens/httpd.aug206
-rw-r--r--certbot-apache/certbot_apache/_internal/configurator.py2512
-rw-r--r--certbot-apache/certbot_apache/_internal/constants.py71
-rw-r--r--certbot-apache/certbot_apache/_internal/display_ops.py125
-rw-r--r--certbot-apache/certbot_apache/_internal/entrypoint.py68
-rw-r--r--certbot-apache/certbot_apache/_internal/http_01.py209
-rw-r--r--certbot-apache/certbot_apache/_internal/obj.py269
-rw-r--r--certbot-apache/certbot_apache/_internal/options-ssl-apache.conf18
-rw-r--r--certbot-apache/certbot_apache/_internal/override_arch.py31
-rw-r--r--certbot-apache/certbot_apache/_internal/override_centos.py215
-rw-r--r--certbot-apache/certbot_apache/_internal/override_darwin.py31
-rw-r--r--certbot-apache/certbot_apache/_internal/override_debian.py144
-rw-r--r--certbot-apache/certbot_apache/_internal/override_fedora.py98
-rw-r--r--certbot-apache/certbot_apache/_internal/override_gentoo.py75
-rw-r--r--certbot-apache/certbot_apache/_internal/override_suse.py31
-rw-r--r--certbot-apache/certbot_apache/_internal/parser.py1008
19 files changed, 5221 insertions, 0 deletions
diff --git a/certbot-apache/certbot_apache/_internal/__init__.py b/certbot-apache/certbot_apache/_internal/__init__.py
new file mode 100644
index 000000000..9c195ccc7
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/__init__.py
@@ -0,0 +1 @@
+"""Certbot Apache plugin."""
diff --git a/certbot-apache/certbot_apache/_internal/apache_util.py b/certbot-apache/certbot_apache/_internal/apache_util.py
new file mode 100644
index 000000000..7a2ecf49b
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/apache_util.py
@@ -0,0 +1,107 @@
+""" Utility functions for certbot-apache plugin """
+import binascii
+
+from certbot import util
+from certbot.compat import os
+
+
+def get_mod_deps(mod_name):
+ """Get known module dependencies.
+
+ .. note:: This does not need to be accurate in order for the client to
+ run. This simply keeps things clean if the user decides to revert
+ changes.
+ .. warning:: If all deps are not included, it may cause incorrect parsing
+ behavior, due to enable_mod's shortcut for updating the parser's
+ currently defined modules (`.ApacheParser.add_mod`)
+ This would only present a major problem in extremely atypical
+ configs that use ifmod for the missing deps.
+
+ """
+ deps = {
+ "ssl": ["setenvif", "mime"]
+ }
+ return deps.get(mod_name, [])
+
+
+def get_file_path(vhost_path):
+ """Get file path from augeas_vhost_path.
+
+ Takes in Augeas path and returns the file name
+
+ :param str vhost_path: Augeas virtual host path
+
+ :returns: filename of vhost
+ :rtype: str
+
+ """
+ if not vhost_path or not vhost_path.startswith("/files/"):
+ return None
+
+ return _split_aug_path(vhost_path)[0]
+
+
+def get_internal_aug_path(vhost_path):
+ """Get the Augeas path for a vhost with the file path removed.
+
+ :param str vhost_path: Augeas virtual host path
+
+ :returns: Augeas path to vhost relative to the containing file
+ :rtype: str
+
+ """
+ return _split_aug_path(vhost_path)[1]
+
+
+def _split_aug_path(vhost_path):
+ """Splits an Augeas path into a file path and an internal path.
+
+ After removing "/files", this function splits vhost_path into the
+ file path and the remaining Augeas path.
+
+ :param str vhost_path: Augeas virtual host path
+
+ :returns: file path and internal Augeas path
+ :rtype: `tuple` of `str`
+
+ """
+ # Strip off /files
+ file_path = vhost_path[6:]
+ internal_path = []
+
+ # Remove components from the end of file_path until it becomes valid
+ while not os.path.exists(file_path):
+ file_path, _, internal_path_part = file_path.rpartition("/")
+ internal_path.append(internal_path_part)
+
+ return file_path, "/".join(reversed(internal_path))
+
+
+def parse_define_file(filepath, varname):
+ """ Parses Defines from a variable in configuration file
+
+ :param str filepath: Path of file to parse
+ :param str varname: Name of the variable
+
+ :returns: Dict of Define:Value pairs
+ :rtype: `dict`
+
+ """
+ return_vars = {}
+ # Get list of words in the variable
+ a_opts = util.get_var_from_file(varname, filepath).split()
+ for i, v in enumerate(a_opts):
+ # Handle Define statements and make sure it has an argument
+ if v == "-D" and len(a_opts) >= i+2:
+ var_parts = a_opts[i+1].partition("=")
+ return_vars[var_parts[0]] = var_parts[2]
+ elif len(v) > 2 and v.startswith("-D"):
+ # Found var with no whitespace separator
+ var_parts = v[2:].partition("=")
+ return_vars[var_parts[0]] = var_parts[2]
+ return return_vars
+
+
+def unique_id():
+ """ Returns an unique id to be used as a VirtualHost identifier"""
+ return binascii.hexlify(os.urandom(16)).decode("utf-8")
diff --git a/certbot-apache/certbot_apache/_internal/augeas_lens/README b/certbot-apache/certbot_apache/_internal/augeas_lens/README
new file mode 100644
index 000000000..bf9161f93
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/augeas_lens/README
@@ -0,0 +1,2 @@
+Certbot includes the very latest Augeas lenses in order to ship bug fixes
+to Apache configuration handling bugs as quickly as possible
diff --git a/certbot-apache/certbot_apache/_internal/augeas_lens/httpd.aug b/certbot-apache/certbot_apache/_internal/augeas_lens/httpd.aug
new file mode 100644
index 000000000..5600088cf
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/augeas_lens/httpd.aug
@@ -0,0 +1,206 @@
+(* Apache HTTPD lens for Augeas
+
+Authors:
+ David Lutterkort <lutter@redhat.com>
+ Francis Giraldeau <francis.giraldeau@usherbrooke.ca>
+ Raphael Pinson <raphink@gmail.com>
+
+About: Reference
+ Online Apache configuration manual: http://httpd.apache.org/docs/trunk/
+
+About: License
+ This file is licensed under the LGPL v2+.
+
+About: Lens Usage
+ Sample usage of this lens in augtool
+
+ Apache configuration is represented by two main structures, nested sections
+ and directives. Sections are used as labels, while directives are kept as a
+ value. Sections and directives can have positional arguments inside values
+ of "arg" nodes. Arguments of sections must be the firsts child of the
+ section node.
+
+ This lens doesn't support automatic string quoting. Hence, the string must
+ be quoted when containing a space.
+
+ Create a new VirtualHost section with one directive:
+ > clear /files/etc/apache2/sites-available/foo/VirtualHost
+ > set /files/etc/apache2/sites-available/foo/VirtualHost/arg "172.16.0.1:80"
+ > set /files/etc/apache2/sites-available/foo/VirtualHost/directive "ServerAdmin"
+ > set /files/etc/apache2/sites-available/foo/VirtualHost/*[self::directive="ServerAdmin"]/arg "admin@example.com"
+
+About: Configuration files
+ This lens applies to files in /etc/httpd and /etc/apache2. See <filter>.
+
+*)
+
+
+module Httpd =
+
+autoload xfm
+
+(******************************************************************
+ * Utilities lens
+ *****************************************************************)
+let dels (s:string) = del s s
+
+(* The continuation sequence that indicates that we should consider the
+ * next line part of the current line *)
+let cont = /\\\\\r?\n/
+
+(* Whitespace within a line: space, tab, and the continuation sequence *)
+let ws = /[ \t]/ | cont
+
+(* Any possible character - '.' does not match \n *)
+let any = /(.|\n)/
+
+(* Any character preceded by a backslash *)
+let esc_any = /\\\\(.|\n)/
+
+(* Newline sequence - both for Unix and DOS newlines *)
+let nl = /\r?\n/
+
+(* Whitespace at the end of a line *)
+let eol = del (ws* . nl) "\n"
+
+(* deal with continuation lines *)
+let sep_spc = del ws+ " "
+let sep_osp = del ws* ""
+let sep_eq = del (ws* . "=" . ws*) "="
+
+let nmtoken = /[a-zA-Z:_][a-zA-Z0-9:_.-]*/
+let word = /[a-z][a-z0-9._-]*/i
+
+(* A complete line that is either just whitespace or a comment that only
+ * contains whitespace *)
+let empty = [ del (ws* . /#?/ . ws* . nl) "\n" ]
+
+let indent = Util.indent
+
+(* A comment that is not just whitespace. We define it in terms of the
+ * things that are not allowed as part of such a comment:
+ * 1) Starts with whitespace
+ * 2) Ends with whitespace, a backslash or \r
+ * 3) Unescaped newlines
+ *)
+let comment =
+ let comment_start = del (ws* . "#" . ws* ) "# " in
+ let unesc_eol = /[^\]?/ . nl in
+ let w = /[^\t\n\r \\]/ in
+ let r = /[\r\\]/ in
+ let s = /[\t\r ]/ in
+ (*
+ * we'd like to write
+ * let b = /\\\\/ in
+ * let t = /[\t\n\r ]/ in
+ * let x = b . (t? . (s|w)* ) in
+ * but the definition of b depends on commit 244c0edd in 1.9.0 and
+ * would make the lens unusable with versions before 1.9.0. So we write
+ * x out which works in older versions, too
+ *)
+ let x = /\\\\[\t\n\r ]?[^\n\\]*/ in
+ let line = ((r . s* . w|w|r) . (s|w)* . x*|(r.s* )?).w.(s*.w)* in
+ [ label "#comment" . comment_start . store line . eol ]
+
+(* borrowed from shellvars.aug *)
+let char_arg_sec = /([^\\ '"\t\r\n>]|[^ '"\t\r\n>]+[^\\ \t\r\n>])|\\\\"|\\\\'|\\\\ /
+let char_arg_wl = /([^\\ '"},\t\r\n]|[^ '"},\t\r\n]+[^\\ '"},\t\r\n])/
+
+let dquot =
+ let no_dquot = /[^"\\\r\n]/
+ in /"/ . (no_dquot|esc_any)* . /"/
+let dquot_msg =
+ let no_dquot = /([^ \t"\\\r\n]|[^"\\\r\n]+[^ \t"\\\r\n])/
+ in /"/ . (no_dquot|esc_any)* . no_dquot
+
+let squot =
+ let no_squot = /[^'\\\r\n]/
+ in /'/ . (no_squot|esc_any)* . /'/
+let comp = /[<>=]?=/
+
+(******************************************************************
+ * Attributes
+ *****************************************************************)
+
+(* The arguments for a directive come in two flavors: quoted with single or
+ * double quotes, or bare. Bare arguments may not start with a single or
+ * double quote; since we also treat "word lists" special, i.e. lists
+ * enclosed in curly braces, bare arguments may not start with those,
+ * either.
+ *
+ * Bare arguments may not contain unescaped spaces, but we allow escaping
+ * with '\\'. Quoted arguments can contain anything, though the quote must
+ * be escaped with '\\'.
+ *)
+let bare = /([^{"' \t\n\r]|\\\\.)([^ \t\n\r]|\\\\.)*[^ \t\n\r\\]|[^{"' \t\n\r\\]/
+
+let arg_quoted = [ label "arg" . store (dquot|squot) ]
+let arg_bare = [ label "arg" . store bare ]
+
+(* message argument starts with " but ends at EOL *)
+let arg_dir_msg = [ label "arg" . store dquot_msg ]
+let arg_wl = [ label "arg" . store (char_arg_wl+|dquot|squot) ]
+
+(* comma-separated wordlist as permitted in the SSLRequire directive *)
+let arg_wordlist =
+ let wl_start = dels "{" in
+ let wl_end = dels "}" in
+ let wl_sep = del /[ \t]*,[ \t]*/ ", "
+ in [ label "wordlist" . wl_start . arg_wl . (wl_sep . arg_wl)* . wl_end ]
+
+let argv (l:lens) = l . (sep_spc . l)*
+
+(* the arguments of a directive. We use this once we have parsed the name
+ * of the directive, and the space right after it. When dir_args is used,
+ * we also know that we have at least one argument. We need to be careful
+ * with the spacing between arguments: quoted arguments and word lists do
+ * not need to have space between them, but bare arguments do.
+ *
+ * Apache apparently is also happy if the last argument starts with a double
+ * quote, but has no corresponding closing duoble quote, which is what
+ * arg_dir_msg handles
+ *)
+let dir_args =
+ let arg_nospc = arg_quoted|arg_wordlist in
+ (arg_bare . sep_spc | arg_nospc . sep_osp)* . (arg_bare|arg_nospc|arg_dir_msg)
+
+let directive =
+ [ indent . label "directive" . store word . (sep_spc . dir_args)? . eol ]
+
+let arg_sec = [ label "arg" . store (char_arg_sec+|comp|dquot|squot) ]
+
+let section (body:lens) =
+ (* opt_eol includes empty lines *)
+ let opt_eol = del /([ \t]*#?[ \t]*\r?\n)*/ "\n" in
+ let inner = (sep_spc . argv arg_sec)? . sep_osp .
+ dels ">" . opt_eol . ((body|comment) . (body|empty|comment)*)? .
+ indent . dels "</" in
+ let kword = key (word - /perl/i) in
+ let dword = del (word - /perl/i) "a" in
+ [ indent . dels "<" . square kword inner dword . del />[ \t\n\r]*/ ">\n" ]
+
+let perl_section = [ indent . label "Perl" . del /<perl>/i "<Perl>"
+ . store /[^<]*/
+ . del /<\/perl>/i "</Perl>" . eol ]
+
+
+let rec content = section (content|directive)
+ | perl_section
+
+let lns = (content|directive|comment|empty)*
+
+let filter = (incl "/etc/apache2/apache2.conf") .
+ (incl "/etc/apache2/httpd.conf") .
+ (incl "/etc/apache2/ports.conf") .
+ (incl "/etc/apache2/conf.d/*") .
+ (incl "/etc/apache2/conf-available/*.conf") .
+ (incl "/etc/apache2/mods-available/*") .
+ (incl "/etc/apache2/sites-available/*") .
+ (incl "/etc/apache2/vhosts.d/*.conf") .
+ (incl "/etc/httpd/conf.d/*.conf") .
+ (incl "/etc/httpd/httpd.conf") .
+ (incl "/etc/httpd/conf/httpd.conf") .
+ (incl "/etc/httpd/conf.modules.d/*.conf") .
+ Util.stdexcl
+
+let xfm = transform lns filter
diff --git a/certbot-apache/certbot_apache/_internal/configurator.py b/certbot-apache/certbot_apache/_internal/configurator.py
new file mode 100644
index 000000000..84b59d2c7
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/configurator.py
@@ -0,0 +1,2512 @@
+"""Apache Configurator."""
+# pylint: disable=too-many-lines
+from collections import defaultdict
+import copy
+import fnmatch
+import logging
+import re
+import socket
+import time
+
+import pkg_resources
+import six
+import zope.component
+import zope.interface
+
+from acme import challenges
+from acme.magic_typing import DefaultDict # pylint: disable=unused-import, no-name-in-module
+from acme.magic_typing import Dict # pylint: disable=unused-import, no-name-in-module
+from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
+from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
+from acme.magic_typing import Union # pylint: disable=unused-import, no-name-in-module
+from certbot import errors
+from certbot import interfaces
+from certbot import util
+from certbot.achallenges import KeyAuthorizationAnnotatedChallenge # pylint: disable=unused-import
+from certbot.compat import filesystem
+from certbot.compat import os
+from certbot.plugins import common
+from certbot.plugins.enhancements import AutoHSTSEnhancement
+from certbot.plugins.util import path_surgery
+from certbot_apache._internal import apache_util
+from certbot_apache._internal import constants
+from certbot_apache._internal import display_ops
+from certbot_apache._internal import http_01
+from certbot_apache._internal import obj
+from certbot_apache._internal import parser
+
+logger = logging.getLogger(__name__)
+
+
+# TODO: Augeas sections ie. <VirtualHost>, <IfModule> beginning and closing
+# tags need to be the same case, otherwise Augeas doesn't recognize them.
+# This is not able to be completely remedied by regular expressions because
+# Augeas views <VirtualHost> </Virtualhost> as an error. This will just
+# require another check_parsing_errors() after all files are included...
+# (after a find_directive search is executed currently). It can be a one
+# time check however because all of LE's transactions will ensure
+# only properly formed sections are added.
+
+# Note: This protocol works for filenames with spaces in it, the sites are
+# properly set up and directives are changed appropriately, but Apache won't
+# recognize names in sites-enabled that have spaces. These are not added to the
+# Apache configuration. It may be wise to warn the user if they are trying
+# to use vhost filenames that contain spaces and offer to change ' ' to '_'
+
+# Note: FILEPATHS and changes to files are transactional. They are copied
+# over before the updates are made to the existing files. NEW_FILES is
+# transactional due to the use of register_file_creation()
+
+
+# TODO: Verify permissions on configuration root... it is easier than
+# checking permissions on each of the relative directories and less error
+# prone.
+# TODO: Write a server protocol finder. Listen <port> <protocol> or
+# Protocol <protocol>. This can verify partial setups are correct
+# TODO: Add directives to sites-enabled... not sites-available.
+# sites-available doesn't allow immediate find_dir search even with save()
+# and load()
+
+@zope.interface.implementer(interfaces.IAuthenticator, interfaces.IInstaller)
+@zope.interface.provider(interfaces.IPluginFactory)
+class ApacheConfigurator(common.Installer):
+ """Apache configurator.
+
+ :ivar config: Configuration.
+ :type config: :class:`~certbot.interfaces.IConfig`
+
+ :ivar parser: Handles low level parsing
+ :type parser: :class:`~certbot_apache._internal.parser`
+
+ :ivar tup version: version of Apache
+ :ivar list vhosts: All vhosts found in the configuration
+ (:class:`list` of :class:`~certbot_apache._internal.obj.VirtualHost`)
+
+ :ivar dict assoc: Mapping between domains and vhosts
+
+ """
+
+ description = "Apache Web Server plugin"
+ if os.environ.get("CERTBOT_DOCS") == "1":
+ description += ( # pragma: no cover
+ " (Please note that the default values of the Apache plugin options"
+ " change depending on the operating system Certbot is run on.)"
+ )
+
+ OS_DEFAULTS = dict(
+ server_root="/etc/apache2",
+ vhost_root="/etc/apache2/sites-available",
+ vhost_files="*",
+ logs_root="/var/log/apache2",
+ ctl="apache2ctl",
+ version_cmd=['apache2ctl', '-v'],
+ restart_cmd=['apache2ctl', 'graceful'],
+ conftest_cmd=['apache2ctl', 'configtest'],
+ enmod=None,
+ dismod=None,
+ le_vhost_ext="-le-ssl.conf",
+ handle_modules=False,
+ handle_sites=False,
+ challenge_location="/etc/apache2",
+ MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
+ "certbot_apache", os.path.join("_internal", "options-ssl-apache.conf"))
+ )
+
+ def option(self, key):
+ """Get a value from options"""
+ return self.options.get(key)
+
+ def _prepare_options(self):
+ """
+ Set the values possibly changed by command line parameters to
+ OS_DEFAULTS constant dictionary
+ """
+ opts = ["enmod", "dismod", "le_vhost_ext", "server_root", "vhost_root",
+ "logs_root", "challenge_location", "handle_modules", "handle_sites",
+ "ctl"]
+ for o in opts:
+ # Config options use dashes instead of underscores
+ if self.conf(o.replace("_", "-")) is not None:
+ self.options[o] = self.conf(o.replace("_", "-"))
+ else:
+ self.options[o] = self.OS_DEFAULTS[o]
+
+ # Special cases
+ self.options["version_cmd"][0] = self.option("ctl")
+ self.options["restart_cmd"][0] = self.option("ctl")
+ self.options["conftest_cmd"][0] = self.option("ctl")
+
+ @classmethod
+ def add_parser_arguments(cls, add):
+ # When adding, modifying or deleting command line arguments, be sure to
+ # include the changes in the list used in method _prepare_options() to
+ # ensure consistent behavior.
+
+ # Respect CERTBOT_DOCS environment variable and use default values from
+ # base class regardless of the underlying distribution (overrides).
+ if os.environ.get("CERTBOT_DOCS") == "1":
+ DEFAULTS = ApacheConfigurator.OS_DEFAULTS
+ else:
+ # cls.OS_DEFAULTS can be distribution specific, see override classes
+ DEFAULTS = cls.OS_DEFAULTS
+ add("enmod", default=DEFAULTS["enmod"],
+ help="Path to the Apache 'a2enmod' binary")
+ add("dismod", default=DEFAULTS["dismod"],
+ help="Path to the Apache 'a2dismod' binary")
+ add("le-vhost-ext", default=DEFAULTS["le_vhost_ext"],
+ help="SSL vhost configuration extension")
+ add("server-root", default=DEFAULTS["server_root"],
+ help="Apache server root directory")
+ add("vhost-root", default=None,
+ help="Apache server VirtualHost configuration root")
+ add("logs-root", default=DEFAULTS["logs_root"],
+ help="Apache server logs directory")
+ add("challenge-location",
+ default=DEFAULTS["challenge_location"],
+ help="Directory path for challenge configuration")
+ add("handle-modules", default=DEFAULTS["handle_modules"],
+ help="Let installer handle enabling required modules for you " +
+ "(Only Ubuntu/Debian currently)")
+ add("handle-sites", default=DEFAULTS["handle_sites"],
+ help="Let installer handle enabling sites for you " +
+ "(Only Ubuntu/Debian currently)")
+ add("ctl", default=DEFAULTS["ctl"],
+ help="Full path to Apache control script")
+
+ def __init__(self, *args, **kwargs):
+ """Initialize an Apache Configurator.
+
+ :param tup version: version of Apache as a tuple (2, 4, 7)
+ (used mostly for unittesting)
+
+ """
+ version = kwargs.pop("version", None)
+ super(ApacheConfigurator, self).__init__(*args, **kwargs)
+
+ # Add name_server association dict
+ self.assoc = dict() # type: Dict[str, obj.VirtualHost]
+ # Outstanding challenges
+ self._chall_out = set() # type: Set[KeyAuthorizationAnnotatedChallenge]
+ # List of vhosts configured per wildcard domain on this run.
+ # used by deploy_cert() and enhance()
+ self._wildcard_vhosts = dict() # type: Dict[str, List[obj.VirtualHost]]
+ # Maps enhancements to vhosts we've enabled the enhancement for
+ self._enhanced_vhosts = defaultdict(set) # type: DefaultDict[str, Set[obj.VirtualHost]]
+ # Temporary state for AutoHSTS enhancement
+ self._autohsts = {} # type: Dict[str, Dict[str, Union[int, float]]]
+ # Reverter save notes
+ self.save_notes = ""
+
+ # These will be set in the prepare function
+ self._prepared = False
+ self.parser = None
+ self.version = version
+ self.vhosts = None
+ self.options = copy.deepcopy(self.OS_DEFAULTS)
+ self._enhance_func = {"redirect": self._enable_redirect,
+ "ensure-http-header": self._set_http_header,
+ "staple-ocsp": self._enable_ocsp_stapling}
+
+ @property
+ def mod_ssl_conf(self):
+ """Full absolute path to SSL configuration file."""
+ return os.path.join(self.config.config_dir, constants.MOD_SSL_CONF_DEST)
+
+ @property
+ def updated_mod_ssl_conf_digest(self):
+ """Full absolute path to digest of updated SSL configuration file."""
+ return os.path.join(self.config.config_dir, constants.UPDATED_MOD_SSL_CONF_DIGEST)
+
+ def prepare(self):
+ """Prepare the authenticator/installer.
+
+ :raises .errors.NoInstallationError: If Apache configs cannot be found
+ :raises .errors.MisconfigurationError: If Apache is misconfigured
+ :raises .errors.NotSupportedError: If Apache version is not supported
+ :raises .errors.PluginError: If there is any other error
+
+ """
+ self._prepare_options()
+
+ # Verify Apache is installed
+ self._verify_exe_availability(self.option("ctl"))
+
+ # Make sure configuration is valid
+ self.config_test()
+
+ # Set Version
+ if self.version is None:
+ self.version = self.get_version()
+ logger.debug('Apache version is %s',
+ '.'.join(str(i) for i in self.version))
+ if self.version < (2, 2):
+ raise errors.NotSupportedError(
+ "Apache Version {0} not supported.".format(str(self.version)))
+
+ # Recover from previous crash before Augeas initialization to have the
+ # correct parse tree from the get go.
+ self.recovery_routine()
+ # Perform the actual Augeas initialization to be able to react
+ self.parser = self.get_parser()
+
+ # Check for errors in parsing files with Augeas
+ self.parser.check_parsing_errors("httpd.aug")
+
+ # Get all of the available vhosts
+ self.vhosts = self.get_virtual_hosts()
+
+ self.install_ssl_options_conf(self.mod_ssl_conf,
+ self.updated_mod_ssl_conf_digest)
+
+ # Prevent two Apache plugins from modifying a config at once
+ try:
+ util.lock_dir_until_exit(self.option("server_root"))
+ except (OSError, errors.LockError):
+ logger.debug("Encountered error:", exc_info=True)
+ raise errors.PluginError(
+ "Unable to create a lock file in {0}. Are you running"
+ " Certbot with sufficient privileges to modify your"
+ " Apache configuration?".format(self.option("server_root")))
+ self._prepared = True
+
+ def save(self, title=None, temporary=False):
+ """Saves all changes to the configuration files.
+
+ This function first checks for save errors, if none are found,
+ all configuration changes made will be saved. According to the
+ function parameters. If an exception is raised, a new checkpoint
+ was not created.
+
+ :param str title: The title of the save. If a title is given, the
+ configuration will be saved as a new checkpoint and put in a
+ timestamped directory.
+
+ :param bool temporary: Indicates whether the changes made will
+ be quickly reversed in the future (ie. challenges)
+
+ """
+ save_files = self.parser.unsaved_files()
+ if save_files:
+ self.add_to_checkpoint(save_files,
+ self.save_notes, temporary=temporary)
+ # Handle the parser specific tasks
+ self.parser.save(save_files)
+ if title and not temporary:
+ self.finalize_checkpoint(title)
+
+ def recovery_routine(self):
+ """Revert all previously modified files.
+
+ Reverts all modified files that have not been saved as a checkpoint
+
+ :raises .errors.PluginError: If unable to recover the configuration
+
+ """
+ super(ApacheConfigurator, self).recovery_routine()
+ # Reload configuration after these changes take effect if needed
+ # ie. ApacheParser has been initialized.
+ if self.parser:
+ # TODO: wrap into non-implementation specific parser interface
+ self.parser.aug.load()
+
+ def revert_challenge_config(self):
+ """Used to cleanup challenge configurations.
+
+ :raises .errors.PluginError: If unable to revert the challenge config.
+
+ """
+ self.revert_temporary_config()
+ self.parser.aug.load()
+
+ def rollback_checkpoints(self, rollback=1):
+ """Rollback saved checkpoints.
+
+ :param int rollback: Number of checkpoints to revert
+
+ :raises .errors.PluginError: If there is a problem with the input or
+ the function is unable to correctly revert the configuration
+
+ """
+ super(ApacheConfigurator, self).rollback_checkpoints(rollback)
+ self.parser.aug.load()
+
+ def _verify_exe_availability(self, exe):
+ """Checks availability of Apache executable"""
+ if not util.exe_exists(exe):
+ if not path_surgery(exe):
+ raise errors.NoInstallationError(
+ 'Cannot find Apache executable {0}'.format(exe))
+
+ def get_parser(self):
+ """Initializes the ApacheParser"""
+ # If user provided vhost_root value in command line, use it
+ return parser.ApacheParser(
+ self.option("server_root"), self.conf("vhost-root"),
+ self.version, configurator=self)
+
+ def _wildcard_domain(self, domain):
+ """
+ Checks if domain is a wildcard domain
+
+ :param str domain: Domain to check
+
+ :returns: If the domain is wildcard domain
+ :rtype: bool
+ """
+ if isinstance(domain, six.text_type):
+ wildcard_marker = u"*."
+ else:
+ wildcard_marker = b"*."
+ return domain.startswith(wildcard_marker)
+
+ def deploy_cert(self, domain, cert_path, key_path,
+ chain_path=None, fullchain_path=None):
+ """Deploys certificate to specified virtual host.
+
+ Currently tries to find the last directives to deploy the certificate
+ in the VHost associated with the given domain. If it can't find the
+ directives, it searches the "included" confs. The function verifies
+ that it has located the three directives and finally modifies them
+ to point to the correct destination. After the certificate is
+ installed, the VirtualHost is enabled if it isn't already.
+
+ .. todo:: Might be nice to remove chain directive if none exists
+ This shouldn't happen within certbot though
+
+ :raises errors.PluginError: When unable to deploy certificate due to
+ a lack of directives
+
+ """
+ vhosts = self.choose_vhosts(domain)
+ for vhost in vhosts:
+ self._deploy_cert(vhost, cert_path, key_path, chain_path, fullchain_path)
+
+ def choose_vhosts(self, domain, create_if_no_ssl=True):
+ """
+ Finds VirtualHosts that can be used with the provided domain
+
+ :param str domain: Domain name to match VirtualHosts to
+ :param bool create_if_no_ssl: If found VirtualHost doesn't have a HTTPS
+ counterpart, should one get created
+
+ :returns: List of VirtualHosts or None
+ :rtype: `list` of :class:`~certbot_apache._internal.obj.VirtualHost`
+ """
+
+ if self._wildcard_domain(domain):
+ if domain in self._wildcard_vhosts:
+ # Vhosts for a wildcard domain were already selected
+ return self._wildcard_vhosts[domain]
+ # Ask user which VHosts to support.
+ # Returned objects are guaranteed to be ssl vhosts
+ return self._choose_vhosts_wildcard(domain, create_if_no_ssl)
+ else:
+ return [self.choose_vhost(domain, create_if_no_ssl)]
+
+ def _vhosts_for_wildcard(self, domain):
+ """
+ Get VHost objects for every VirtualHost that the user wants to handle
+ with the wildcard certificate.
+ """
+
+ # Collect all vhosts that match the name
+ matched = set()
+ for vhost in self.vhosts:
+ for name in vhost.get_names():
+ if self._in_wildcard_scope(name, domain):
+ matched.add(vhost)
+
+ return list(matched)
+
+ def _in_wildcard_scope(self, name, domain):
+ """
+ Helper method for _vhosts_for_wildcard() that makes sure that the domain
+ is in the scope of wildcard domain.
+
+ eg. in scope: domain = *.wild.card, name = 1.wild.card
+ not in scope: domain = *.wild.card, name = 1.2.wild.card
+ """
+ if len(name.split(".")) == len(domain.split(".")):
+ return fnmatch.fnmatch(name, domain)
+ return None
+
+ def _choose_vhosts_wildcard(self, domain, create_ssl=True):
+ """Prompts user to choose vhosts to install a wildcard certificate for"""
+
+ # Get all vhosts that are covered by the wildcard domain
+ vhosts = self._vhosts_for_wildcard(domain)
+
+ # Go through the vhosts, making sure that we cover all the names
+ # present, but preferring the SSL vhosts
+ filtered_vhosts = dict()
+ for vhost in vhosts:
+ for name in vhost.get_names():
+ if vhost.ssl:
+ # Always prefer SSL vhosts
+ filtered_vhosts[name] = vhost
+ elif name not in filtered_vhosts and create_ssl:
+ # Add if not in list previously
+ filtered_vhosts[name] = vhost
+
+ # Only unique VHost objects
+ dialog_input = set(filtered_vhosts.values())
+
+ # Ask the user which of names to enable, expect list of names back
+ dialog_output = display_ops.select_vhost_multiple(list(dialog_input))
+
+ if not dialog_output:
+ logger.error(
+ "No vhost exists with servername or alias for domain %s. "
+ "No vhost was selected. Please specify ServerName or ServerAlias "
+ "in the Apache config.",
+ domain)
+ raise errors.PluginError("No vhost selected")
+
+ # Make sure we create SSL vhosts for the ones that are HTTP only
+ # if requested.
+ return_vhosts = list()
+ for vhost in dialog_output:
+ if not vhost.ssl:
+ return_vhosts.append(self.make_vhost_ssl(vhost))
+ else:
+ return_vhosts.append(vhost)
+
+ self._wildcard_vhosts[domain] = return_vhosts
+ return return_vhosts
+
+ def _deploy_cert(self, vhost, cert_path, key_path, chain_path, fullchain_path):
+ """
+ Helper function for deploy_cert() that handles the actual deployment
+ this exists because we might want to do multiple deployments per
+ domain originally passed for deploy_cert(). This is especially true
+ with wildcard certificates
+ """
+ # This is done first so that ssl module is enabled and cert_path,
+ # cert_key... can all be parsed appropriately
+ self.prepare_server_https("443")
+
+ # Add directives and remove duplicates
+ self._add_dummy_ssl_directives(vhost.path)
+ self._clean_vhost(vhost)
+
+ path = {"cert_path": self.parser.find_dir("SSLCertificateFile",
+ None, vhost.path),
+ "cert_key": self.parser.find_dir("SSLCertificateKeyFile",
+ None, vhost.path)}
+
+ # Only include if a certificate chain is specified
+ if chain_path is not None:
+ path["chain_path"] = self.parser.find_dir(
+ "SSLCertificateChainFile", None, vhost.path)
+
+ # Handle errors when certificate/key directives cannot be found
+ if not path["cert_path"]:
+ logger.warning(
+ "Cannot find an SSLCertificateFile directive in %s. "
+ "VirtualHost was not modified", vhost.path)
+ raise errors.PluginError(
+ "Unable to find an SSLCertificateFile directive")
+ elif not path["cert_key"]:
+ logger.warning(
+ "Cannot find an SSLCertificateKeyFile directive for "
+ "certificate in %s. VirtualHost was not modified", vhost.path)
+ raise errors.PluginError(
+ "Unable to find an SSLCertificateKeyFile directive for "
+ "certificate")
+
+ logger.info("Deploying Certificate to VirtualHost %s", vhost.filep)
+
+ if self.version < (2, 4, 8) or (chain_path and not fullchain_path):
+ # install SSLCertificateFile, SSLCertificateKeyFile,
+ # and SSLCertificateChainFile directives
+ set_cert_path = cert_path
+ self.parser.aug.set(path["cert_path"][-1], cert_path)
+ self.parser.aug.set(path["cert_key"][-1], key_path)
+ if chain_path is not None:
+ self.parser.add_dir(vhost.path,
+ "SSLCertificateChainFile", chain_path)
+ else:
+ raise errors.PluginError("--chain-path is required for your "
+ "version of Apache")
+ else:
+ if not fullchain_path:
+ raise errors.PluginError("Please provide the --fullchain-path "
+ "option pointing to your full chain file")
+ set_cert_path = fullchain_path
+ self.parser.aug.set(path["cert_path"][-1], fullchain_path)
+ self.parser.aug.set(path["cert_key"][-1], key_path)
+
+ # Enable the new vhost if needed
+ if not vhost.enabled:
+ self.enable_site(vhost)
+
+ # Save notes about the transaction that took place
+ self.save_notes += ("Changed vhost at %s with addresses of %s\n"
+ "\tSSLCertificateFile %s\n"
+ "\tSSLCertificateKeyFile %s\n" %
+ (vhost.filep,
+ ", ".join(str(addr) for addr in vhost.addrs),
+ set_cert_path, key_path))
+ if chain_path is not None:
+ self.save_notes += "\tSSLCertificateChainFile %s\n" % chain_path
+
+ def choose_vhost(self, target_name, create_if_no_ssl=True):
+ """Chooses a virtual host based on the given domain name.
+
+ If there is no clear virtual host to be selected, the user is prompted
+ with all available choices.
+
+ The returned vhost is guaranteed to have TLS enabled unless
+ create_if_no_ssl is set to False, in which case there is no such guarantee
+ and the result is not cached.
+
+ :param str target_name: domain name
+ :param bool create_if_no_ssl: If found VirtualHost doesn't have a HTTPS
+ counterpart, should one get created
+
+ :returns: vhost associated with name
+ :rtype: :class:`~certbot_apache._internal.obj.VirtualHost`
+
+ :raises .errors.PluginError: If no vhost is available or chosen
+
+ """
+ # Allows for domain names to be associated with a virtual host
+ if target_name in self.assoc:
+ return self.assoc[target_name]
+
+ # Try to find a reasonable vhost
+ vhost = self._find_best_vhost(target_name)
+ if vhost is not None:
+ if not create_if_no_ssl:
+ return vhost
+ if not vhost.ssl:
+ vhost = self.make_vhost_ssl(vhost)
+
+ self._add_servername_alias(target_name, vhost)
+ self.assoc[target_name] = vhost
+ return vhost
+
+ # Negate create_if_no_ssl value to indicate if we want a SSL vhost
+ # to get created if a non-ssl vhost is selected.
+ return self._choose_vhost_from_list(target_name, temp=not create_if_no_ssl)
+
+ def _choose_vhost_from_list(self, target_name, temp=False):
+ # Select a vhost from a list
+ vhost = display_ops.select_vhost(target_name, self.vhosts)
+ if vhost is None:
+ logger.error(
+ "No vhost exists with servername or alias of %s. "
+ "No vhost was selected. Please specify ServerName or ServerAlias "
+ "in the Apache config.",
+ target_name)
+ raise errors.PluginError("No vhost selected")
+ if temp:
+ return vhost
+ if not vhost.ssl:
+ addrs = self._get_proposed_addrs(vhost, "443")
+ # TODO: Conflicts is too conservative
+ if not any(vhost.enabled and vhost.conflicts(addrs) for
+ vhost in self.vhosts):
+ vhost = self.make_vhost_ssl(vhost)
+ else:
+ logger.error(
+ "The selected vhost would conflict with other HTTPS "
+ "VirtualHosts within Apache. Please select another "
+ "vhost or add ServerNames to your configuration.")
+ raise errors.PluginError(
+ "VirtualHost not able to be selected.")
+
+ self._add_servername_alias(target_name, vhost)
+ self.assoc[target_name] = vhost
+ return vhost
+
+ def domain_in_names(self, names, target_name):
+ """Checks if target domain is covered by one or more of the provided
+ names. The target name is matched by wildcard as well as exact match.
+
+ :param names: server aliases
+ :type names: `collections.Iterable` of `str`
+ :param str target_name: name to compare with wildcards
+
+ :returns: True if target_name is covered by a wildcard,
+ otherwise, False
+ :rtype: bool
+
+ """
+ # use lowercase strings because fnmatch can be case sensitive
+ target_name = target_name.lower()
+ for name in names:
+ name = name.lower()
+ # fnmatch treats "[seq]" specially and [ or ] characters aren't
+ # valid in Apache but Apache doesn't error out if they are present
+ if "[" not in name and fnmatch.fnmatch(target_name, name):
+ return True
+ return False
+
+ def find_best_http_vhost(self, target, filter_defaults, port="80"):
+ """Returns non-HTTPS vhost objects found from the Apache config
+
+ :param str target: Domain name of the desired VirtualHost
+ :param bool filter_defaults: whether _default_ vhosts should be
+ included if it is the best match
+ :param str port: port number the vhost should be listening on
+
+ :returns: VirtualHost object that's the best match for target name
+ :rtype: `obj.VirtualHost` or None
+ """
+ filtered_vhosts = []
+ for vhost in self.vhosts:
+ if any(a.is_wildcard() or a.get_port() == port for a in vhost.addrs) and not vhost.ssl:
+ filtered_vhosts.append(vhost)
+ return self._find_best_vhost(target, filtered_vhosts, filter_defaults)
+
+ def _find_best_vhost(self, target_name, vhosts=None, filter_defaults=True):
+ """Finds the best vhost for a target_name.
+
+ This does not upgrade a vhost to HTTPS... it only finds the most
+ appropriate vhost for the given target_name.
+
+ :param str target_name: domain handled by the desired vhost
+ :param vhosts: vhosts to consider
+ :type vhosts: `collections.Iterable` of :class:`~certbot_apache._internal.obj.VirtualHost`
+ :param bool filter_defaults: whether a vhost with a _default_
+ addr is acceptable
+
+ :returns: VHost or None
+
+ """
+ # Points 6 - Servername SSL
+ # Points 5 - Wildcard SSL
+ # Points 4 - Address name with SSL
+ # Points 3 - Servername no SSL
+ # Points 2 - Wildcard no SSL
+ # Points 1 - Address name with no SSL
+ best_candidate = None
+ best_points = 0
+
+ if vhosts is None:
+ vhosts = self.vhosts
+
+ for vhost in vhosts:
+ if vhost.modmacro is True:
+ continue
+ names = vhost.get_names()
+ if target_name in names:
+ points = 3
+ elif self.domain_in_names(names, target_name):
+ points = 2
+ elif any(addr.get_addr() == target_name for addr in vhost.addrs):
+ points = 1
+ else:
+ # No points given if names can't be found.
+ # This gets hit but doesn't register
+ continue # pragma: no cover
+
+ if vhost.ssl:
+ points += 3
+
+ if points > best_points:
+ best_points = points
+ best_candidate = vhost
+
+ # No winners here... is there only one reasonable vhost?
+ if best_candidate is None:
+ if filter_defaults:
+ vhosts = self._non_default_vhosts(vhosts)
+ # remove mod_macro hosts from reasonable vhosts
+ reasonable_vhosts = [vh for vh
+ in vhosts if vh.modmacro is False]
+ if len(reasonable_vhosts) == 1:
+ best_candidate = reasonable_vhosts[0]
+
+ return best_candidate
+
+ def _non_default_vhosts(self, vhosts):
+ """Return all non _default_ only vhosts."""
+ return [vh for vh in vhosts if not all(
+ addr.get_addr() == "_default_" for addr in vh.addrs
+ )]
+
+ def get_all_names(self):
+ """Returns all names found in the Apache Configuration.
+
+ :returns: All ServerNames, ServerAliases, and reverse DNS entries for
+ virtual host addresses
+ :rtype: set
+
+ """
+ all_names = set() # type: Set[str]
+
+ vhost_macro = []
+
+ for vhost in self.vhosts:
+ all_names.update(vhost.get_names())
+ if vhost.modmacro:
+ vhost_macro.append(vhost.filep)
+
+ for addr in vhost.addrs:
+ if common.hostname_regex.match(addr.get_addr()):
+ all_names.add(addr.get_addr())
+ else:
+ name = self.get_name_from_ip(addr)
+ if name:
+ all_names.add(name)
+
+ if vhost_macro:
+ zope.component.getUtility(interfaces.IDisplay).notification(
+ "Apache mod_macro seems to be in use in file(s):\n{0}"
+ "\n\nUnfortunately mod_macro is not yet supported".format(
+ "\n ".join(vhost_macro)), force_interactive=True)
+
+ return util.get_filtered_names(all_names)
+
+ def get_name_from_ip(self, addr): # pylint: disable=no-self-use
+ """Returns a reverse dns name if available.
+
+ :param addr: IP Address
+ :type addr: ~.common.Addr
+
+ :returns: name or empty string if name cannot be determined
+ :rtype: str
+
+ """
+ # If it isn't a private IP, do a reverse DNS lookup
+ if not common.private_ips_regex.match(addr.get_addr()):
+ try:
+ socket.inet_aton(addr.get_addr())
+ return socket.gethostbyaddr(addr.get_addr())[0]
+ except (socket.error, socket.herror, socket.timeout):
+ pass
+
+ return ""
+
+ def _get_vhost_names(self, path):
+ """Helper method for getting the ServerName and
+ ServerAlias values from vhost in path
+
+ :param path: Path to read ServerName and ServerAliases from
+
+ :returns: Tuple including ServerName and `list` of ServerAlias strings
+ """
+
+ servername_match = self.parser.find_dir(
+ "ServerName", None, start=path, exclude=False)
+ serveralias_match = self.parser.find_dir(
+ "ServerAlias", None, start=path, exclude=False)
+
+ serveraliases = []
+ for alias in serveralias_match:
+ serveralias = self.parser.get_arg(alias)
+ serveraliases.append(serveralias)
+
+ servername = None
+ if servername_match:
+ # Get last ServerName as each overwrites the previous
+ servername = self.parser.get_arg(servername_match[-1])
+
+ return (servername, serveraliases)
+
+ def _add_servernames(self, host):
+ """Helper function for get_virtual_hosts().
+
+ :param host: In progress vhost whose names will be added
+ :type host: :class:`~certbot_apache._internal.obj.VirtualHost`
+
+ """
+
+ servername, serveraliases = self._get_vhost_names(host.path)
+
+ for alias in serveraliases:
+ if not host.modmacro:
+ host.aliases.add(alias)
+
+ if not host.modmacro:
+ host.name = servername
+
+ def _create_vhost(self, path):
+ """Used by get_virtual_hosts to create vhost objects
+
+ :param str path: Augeas path to virtual host
+
+ :returns: newly created vhost
+ :rtype: :class:`~certbot_apache._internal.obj.VirtualHost`
+
+ """
+ addrs = set()
+ try:
+ args = self.parser.aug.match(path + "/arg")
+ except RuntimeError:
+ logger.warning("Encountered a problem while parsing file: %s, skipping", path)
+ return None
+ for arg in args:
+ addrs.add(obj.Addr.fromstring(self.parser.get_arg(arg)))
+ is_ssl = False
+
+ if self.parser.find_dir("SSLEngine", "on", start=path, exclude=False):
+ is_ssl = True
+
+ # "SSLEngine on" might be set outside of <VirtualHost>
+ # Treat vhosts with port 443 as ssl vhosts
+ for addr in addrs:
+ if addr.get_port() == "443":
+ is_ssl = True
+
+ filename = apache_util.get_file_path(
+ self.parser.aug.get("/augeas/files%s/path" % apache_util.get_file_path(path)))
+ if filename is None:
+ return None
+
+ macro = False
+ if "/macro/" in path.lower():
+ macro = True
+
+ vhost_enabled = self.parser.parsed_in_original(filename)
+
+ vhost = obj.VirtualHost(filename, path, addrs, is_ssl,
+ vhost_enabled, modmacro=macro)
+ self._add_servernames(vhost)
+ return vhost
+
+ def get_virtual_hosts(self):
+ """Returns list of virtual hosts found in the Apache configuration.
+
+ :returns: List of :class:`~certbot_apache._internal.obj.VirtualHost`
+ objects found in configuration
+ :rtype: list
+
+ """
+ # Search base config, and all included paths for VirtualHosts
+ file_paths = {} # type: Dict[str, str]
+ internal_paths = defaultdict(set) # type: DefaultDict[str, Set[str]]
+ vhs = []
+ # Make a list of parser paths because the parser_paths
+ # dictionary may be modified during the loop.
+ for vhost_path in list(self.parser.parser_paths):
+ paths = self.parser.aug.match(
+ ("/files%s//*[label()=~regexp('%s')]" %
+ (vhost_path, parser.case_i("VirtualHost"))))
+ paths = [path for path in paths if
+ "virtualhost" in os.path.basename(path).lower()]
+ for path in paths:
+ new_vhost = self._create_vhost(path)
+ if not new_vhost:
+ continue
+ internal_path = apache_util.get_internal_aug_path(new_vhost.path)
+ realpath = filesystem.realpath(new_vhost.filep)
+ if realpath not in file_paths:
+ file_paths[realpath] = new_vhost.filep
+ internal_paths[realpath].add(internal_path)
+ vhs.append(new_vhost)
+ elif (realpath == new_vhost.filep and
+ realpath != file_paths[realpath]):
+ # Prefer "real" vhost paths instead of symlinked ones
+ # ex: sites-enabled/vh.conf -> sites-available/vh.conf
+
+ # remove old (most likely) symlinked one
+ new_vhs = []
+ for v in vhs:
+ if v.filep == file_paths[realpath]:
+ internal_paths[realpath].remove(
+ apache_util.get_internal_aug_path(v.path))
+ else:
+ new_vhs.append(v)
+ vhs = new_vhs
+
+ file_paths[realpath] = realpath
+ internal_paths[realpath].add(internal_path)
+ vhs.append(new_vhost)
+ elif internal_path not in internal_paths[realpath]:
+ internal_paths[realpath].add(internal_path)
+ vhs.append(new_vhost)
+ return vhs
+
+ def is_name_vhost(self, target_addr):
+ """Returns if vhost is a name based vhost
+
+ NameVirtualHost was deprecated in Apache 2.4 as all VirtualHosts are
+ now NameVirtualHosts. If version is earlier than 2.4, check if addr
+ has a NameVirtualHost directive in the Apache config
+
+ :param certbot_apache._internal.obj.Addr target_addr: vhost address
+
+ :returns: Success
+ :rtype: bool
+
+ """
+ # Mixed and matched wildcard NameVirtualHost with VirtualHost
+ # behavior is undefined. Make sure that an exact match exists
+
+ # search for NameVirtualHost directive for ip_addr
+ # note ip_addr can be FQDN although Apache does not recommend it
+ return (self.version >= (2, 4) or
+ self.parser.find_dir("NameVirtualHost", str(target_addr)))
+
+ def add_name_vhost(self, addr):
+ """Adds NameVirtualHost directive for given address.
+
+ :param addr: Address that will be added as NameVirtualHost directive
+ :type addr: :class:`~certbot_apache._internal.obj.Addr`
+
+ """
+
+ loc = parser.get_aug_path(self.parser.loc["name"])
+ if addr.get_port() == "443":
+ self.parser.add_dir_to_ifmodssl(
+ loc, "NameVirtualHost", [str(addr)])
+ else:
+ self.parser.add_dir(loc, "NameVirtualHost", [str(addr)])
+
+ msg = "Setting {0} to be NameBasedVirtualHost\n".format(addr)
+ logger.debug(msg)
+ self.save_notes += msg
+
+ def prepare_server_https(self, port, temp=False):
+ """Prepare the server for HTTPS.
+
+ Make sure that the ssl_module is loaded and that the server
+ is appropriately listening on port.
+
+ :param str port: Port to listen on
+
+ """
+
+ self.prepare_https_modules(temp)
+ self.ensure_listen(port, https=True)
+
+ def ensure_listen(self, port, https=False):
+ """Make sure that Apache is listening on the port. Checks if the
+ Listen statement for the port already exists, and adds it to the
+ configuration if necessary.
+
+ :param str port: Port number to check and add Listen for if not in
+ place already
+ :param bool https: If the port will be used for HTTPS
+
+ """
+
+ # If HTTPS requested for nonstandard port, add service definition
+ if https and port != "443":
+ port_service = "%s %s" % (port, "https")
+ else:
+ port_service = port
+
+ # Check for Listen <port>
+ # Note: This could be made to also look for ip:443 combo
+ listens = [self.parser.get_arg(x).split()[0] for
+ x in self.parser.find_dir("Listen")]
+
+ # Listen already in place
+ if self._has_port_already(listens, port):
+ return
+
+ listen_dirs = set(listens)
+
+ if not listens:
+ listen_dirs.add(port_service)
+
+ for listen in listens:
+ # For any listen statement, check if the machine also listens on
+ # the given port. If not, add such a listen statement.
+ if len(listen.split(":")) == 1:
+ # Its listening to all interfaces
+ if port not in listen_dirs and port_service not in listen_dirs:
+ listen_dirs.add(port_service)
+ else:
+ # The Listen statement specifies an ip
+ _, ip = listen[::-1].split(":", 1)
+ ip = ip[::-1]
+ if "%s:%s" % (ip, port_service) not in listen_dirs and (
+ "%s:%s" % (ip, port_service) not in listen_dirs):
+ listen_dirs.add("%s:%s" % (ip, port_service))
+ if https:
+ self._add_listens_https(listen_dirs, listens, port)
+ else:
+ self._add_listens_http(listen_dirs, listens, port)
+
+ def _add_listens_http(self, listens, listens_orig, port):
+ """Helper method for ensure_listen to figure out which new
+ listen statements need adding for listening HTTP on port
+
+ :param set listens: Set of all needed Listen statements
+ :param list listens_orig: List of existing listen statements
+ :param string port: Port number we're adding
+ """
+
+ new_listens = listens.difference(listens_orig)
+
+ if port in new_listens:
+ # We have wildcard, skip the rest
+ self.parser.add_dir(parser.get_aug_path(self.parser.loc["listen"]),
+ "Listen", port)
+ self.save_notes += "Added Listen %s directive to %s\n" % (
+ port, self.parser.loc["listen"])
+ else:
+ for listen in new_listens:
+ self.parser.add_dir(parser.get_aug_path(
+ self.parser.loc["listen"]), "Listen", listen.split(" "))
+ self.save_notes += ("Added Listen %s directive to "
+ "%s\n") % (listen,
+ self.parser.loc["listen"])
+
+ def _add_listens_https(self, listens, listens_orig, port):
+ """Helper method for ensure_listen to figure out which new
+ listen statements need adding for listening HTTPS on port
+
+ :param set listens: Set of all needed Listen statements
+ :param list listens_orig: List of existing listen statements
+ :param string port: Port number we're adding
+ """
+
+ # Add service definition for non-standard ports
+ if port != "443":
+ port_service = "%s %s" % (port, "https")
+ else:
+ port_service = port
+
+ new_listens = listens.difference(listens_orig)
+
+ if port in new_listens or port_service in new_listens:
+ # We have wildcard, skip the rest
+ self.parser.add_dir_to_ifmodssl(
+ parser.get_aug_path(self.parser.loc["listen"]),
+ "Listen", port_service.split(" "))
+ self.save_notes += "Added Listen %s directive to %s\n" % (
+ port_service, self.parser.loc["listen"])
+ else:
+ for listen in new_listens:
+ self.parser.add_dir_to_ifmodssl(
+ parser.get_aug_path(self.parser.loc["listen"]),
+ "Listen", listen.split(" "))
+ self.save_notes += ("Added Listen %s directive to "
+ "%s\n") % (listen,
+ self.parser.loc["listen"])
+
+ def _has_port_already(self, listens, port):
+ """Helper method for prepare_server_https to find out if user
+ already has an active Listen statement for the port we need
+
+ :param list listens: List of listen variables
+ :param string port: Port in question
+ """
+
+ if port in listens:
+ return True
+ # Check if Apache is already listening on a specific IP
+ for listen in listens:
+ if len(listen.split(":")) > 1:
+ # Ugly but takes care of protocol def, eg: 1.1.1.1:443 https
+ if listen.split(":")[-1].split(" ")[0] == port:
+ return True
+ return None
+
+ def prepare_https_modules(self, temp):
+ """Helper method for prepare_server_https, taking care of enabling
+ needed modules
+
+ :param boolean temp: If the change is temporary
+ """
+
+ if self.option("handle_modules"):
+ if self.version >= (2, 4) and ("socache_shmcb_module" not in
+ self.parser.modules):
+ self.enable_mod("socache_shmcb", temp=temp)
+ if "ssl_module" not in self.parser.modules:
+ self.enable_mod("ssl", temp=temp)
+
+ def make_vhost_ssl(self, nonssl_vhost):
+ """Makes an ssl_vhost version of a nonssl_vhost.
+
+ Duplicates vhost and adds default ssl options
+ New vhost will reside as (nonssl_vhost.path) +
+ ``self.option("le_vhost_ext")``
+
+ .. note:: This function saves the configuration
+
+ :param nonssl_vhost: Valid VH that doesn't have SSLEngine on
+ :type nonssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
+
+ :returns: SSL vhost
+ :rtype: :class:`~certbot_apache._internal.obj.VirtualHost`
+
+ :raises .errors.PluginError: If more than one virtual host is in
+ the file or if plugin is unable to write/read vhost files.
+
+ """
+ avail_fp = nonssl_vhost.filep
+ ssl_fp = self._get_ssl_vhost_path(avail_fp)
+
+ orig_matches = self.parser.aug.match("/files%s//* [label()=~regexp('%s')]" %
+ (self._escape(ssl_fp),
+ parser.case_i("VirtualHost")))
+
+ self._copy_create_ssl_vhost_skeleton(nonssl_vhost, ssl_fp)
+
+ # Reload augeas to take into account the new vhost
+ self.parser.aug.load()
+ # Get Vhost augeas path for new vhost
+ new_matches = self.parser.aug.match("/files%s//* [label()=~regexp('%s')]" %
+ (self._escape(ssl_fp),
+ parser.case_i("VirtualHost")))
+
+ vh_p = self._get_new_vh_path(orig_matches, new_matches)
+
+ if not vh_p:
+ # The vhost was not found on the currently parsed paths
+ # Make Augeas aware of the new vhost
+ self.parser.parse_file(ssl_fp)
+ # Try to search again
+ new_matches = self.parser.aug.match(
+ "/files%s//* [label()=~regexp('%s')]" %
+ (self._escape(ssl_fp),
+ parser.case_i("VirtualHost")))
+ vh_p = self._get_new_vh_path(orig_matches, new_matches)
+ if not vh_p:
+ raise errors.PluginError(
+ "Could not reverse map the HTTPS VirtualHost to the original")
+
+
+ # Update Addresses
+ self._update_ssl_vhosts_addrs(vh_p)
+
+ # Log actions and create save notes
+ logger.info("Created an SSL vhost at %s", ssl_fp)
+ self.save_notes += "Created ssl vhost at %s\n" % ssl_fp
+ self.save()
+
+ # We know the length is one because of the assertion above
+ # Create the Vhost object
+ ssl_vhost = self._create_vhost(vh_p)
+ ssl_vhost.ancestor = nonssl_vhost
+
+ self.vhosts.append(ssl_vhost)
+
+ # NOTE: Searches through Augeas seem to ruin changes to directives
+ # The configuration must also be saved before being searched
+ # for the new directives; For these reasons... this is tacked
+ # on after fully creating the new vhost
+
+ # Now check if addresses need to be added as NameBasedVhost addrs
+ # This is for compliance with versions of Apache < 2.4
+ self._add_name_vhost_if_necessary(ssl_vhost)
+
+ return ssl_vhost
+
+ def _get_new_vh_path(self, orig_matches, new_matches):
+ """ Helper method for make_vhost_ssl for matching augeas paths. Returns
+ VirtualHost path from new_matches that's not present in orig_matches.
+
+ Paths are normalized, because augeas leaves indices out for paths
+ with only single directive with a similar key """
+
+ orig_matches = [i.replace("[1]", "") for i in orig_matches]
+ for match in new_matches:
+ if match.replace("[1]", "") not in orig_matches:
+ # Return the unmodified path
+ return match
+ return None
+
+ def _get_ssl_vhost_path(self, non_ssl_vh_fp):
+ """ Get a file path for SSL vhost, uses user defined path as priority,
+ but if the value is invalid or not defined, will fall back to non-ssl
+ vhost filepath.
+
+ :param str non_ssl_vh_fp: Filepath of non-SSL vhost
+
+ :returns: Filepath for SSL vhost
+ :rtype: str
+ """
+
+ if self.conf("vhost-root") and os.path.exists(self.conf("vhost-root")):
+ fp = os.path.join(filesystem.realpath(self.option("vhost_root")),
+ os.path.basename(non_ssl_vh_fp))
+ else:
+ # Use non-ssl filepath
+ fp = filesystem.realpath(non_ssl_vh_fp)
+
+ if fp.endswith(".conf"):
+ return fp[:-(len(".conf"))] + self.option("le_vhost_ext")
+ return fp + self.option("le_vhost_ext")
+
+ def _sift_rewrite_rule(self, line):
+ """Decides whether a line should be copied to a SSL vhost.
+
+ A canonical example of when sifting a line is required:
+ When the http vhost contains a RewriteRule that unconditionally
+ redirects any request to the https version of the same site.
+ e.g:
+ RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [L,QSA,R=permanent]
+ Copying the above line to the ssl vhost would cause a
+ redirection loop.
+
+ :param str line: a line extracted from the http vhost.
+
+ :returns: True - don't copy line from http vhost to SSL vhost.
+ :rtype: bool
+
+ """
+ if not line.lower().lstrip().startswith("rewriterule"):
+ return False
+
+ # According to: http://httpd.apache.org/docs/2.4/rewrite/flags.html
+ # The syntax of a RewriteRule is:
+ # RewriteRule pattern target [Flag1,Flag2,Flag3]
+ # i.e. target is required, so it must exist.
+ target = line.split()[2].strip()
+
+ # target may be surrounded with quotes
+ if target[0] in ("'", '"') and target[0] == target[-1]:
+ target = target[1:-1]
+
+ # Sift line if it redirects the request to a HTTPS site
+ return target.startswith("https://")
+
+ def _copy_create_ssl_vhost_skeleton(self, vhost, ssl_fp):
+ """Copies over existing Vhost with IfModule mod_ssl.c> skeleton.
+
+ :param obj.VirtualHost vhost: Original VirtualHost object
+ :param str ssl_fp: Full path where the new ssl_vhost will reside.
+
+ A new file is created on the filesystem.
+
+ """
+ # First register the creation so that it is properly removed if
+ # configuration is rolled back
+ if os.path.exists(ssl_fp):
+ notes = "Appended new VirtualHost directive to file %s" % ssl_fp
+ files = set()
+ files.add(ssl_fp)
+ self.reverter.add_to_checkpoint(files, notes)
+ else:
+ self.reverter.register_file_creation(False, ssl_fp)
+ sift = False
+
+ try:
+ orig_contents = self._get_vhost_block(vhost)
+ ssl_vh_contents, sift = self._sift_rewrite_rules(orig_contents)
+
+ with open(ssl_fp, "a") as new_file:
+ new_file.write("<IfModule mod_ssl.c>\n")
+ new_file.write("\n".join(ssl_vh_contents))
+ # The content does not include the closing tag, so add it
+ new_file.write("</VirtualHost>\n")
+ new_file.write("</IfModule>\n")
+ # Add new file to augeas paths if we're supposed to handle
+ # activation (it's not included as default)
+ if not self.parser.parsed_in_current(ssl_fp):
+ self.parser.parse_file(ssl_fp)
+ except IOError:
+ logger.critical("Error writing/reading to file in make_vhost_ssl", exc_info=True)
+ raise errors.PluginError("Unable to write/read in make_vhost_ssl")
+
+ if sift:
+ reporter = zope.component.getUtility(interfaces.IReporter)
+ reporter.add_message(
+ "Some rewrite rules copied from {0} were disabled in the "
+ "vhost for your HTTPS site located at {1} because they have "
+ "the potential to create redirection loops.".format(
+ vhost.filep, ssl_fp), reporter.MEDIUM_PRIORITY)
+ self.parser.aug.set("/augeas/files%s/mtime" % (self._escape(ssl_fp)), "0")
+ self.parser.aug.set("/augeas/files%s/mtime" % (self._escape(vhost.filep)), "0")
+
+ def _sift_rewrite_rules(self, contents):
+ """ Helper function for _copy_create_ssl_vhost_skeleton to prepare the
+ new HTTPS VirtualHost contents. Currently disabling the rewrites """
+
+ result = []
+ sift = False
+ contents = iter(contents)
+
+ comment = ("# Some rewrite rules in this file were "
+ "disabled on your HTTPS site,\n"
+ "# because they have the potential to create "
+ "redirection loops.\n")
+
+ for line in contents:
+ A = line.lower().lstrip().startswith("rewritecond")
+ B = line.lower().lstrip().startswith("rewriterule")
+
+ if not (A or B):
+ result.append(line)
+ continue
+
+ # A RewriteRule that doesn't need filtering
+ if B and not self._sift_rewrite_rule(line):
+ result.append(line)
+ continue
+
+ # A RewriteRule that does need filtering
+ if B and self._sift_rewrite_rule(line):
+ if not sift:
+ result.append(comment)
+ sift = True
+ result.append("# " + line)
+ continue
+
+ # We save RewriteCond(s) and their corresponding
+ # RewriteRule in 'chunk'.
+ # We then decide whether we comment out the entire
+ # chunk based on its RewriteRule.
+ chunk = []
+ if A:
+ chunk.append(line)
+ line = next(contents)
+
+ # RewriteCond(s) must be followed by one RewriteRule
+ while not line.lower().lstrip().startswith("rewriterule"):
+ chunk.append(line)
+ line = next(contents)
+
+ # Now, current line must start with a RewriteRule
+ chunk.append(line)
+
+ if self._sift_rewrite_rule(line):
+ if not sift:
+ result.append(comment)
+ sift = True
+
+ result.append('\n'.join(['# ' + l for l in chunk]))
+ else:
+ result.append('\n'.join(chunk))
+ return result, sift
+
+ def _get_vhost_block(self, vhost):
+ """ Helper method to get VirtualHost contents from the original file.
+ This is done with help of augeas span, which returns the span start and
+ end positions
+
+ :returns: `list` of VirtualHost block content lines without closing tag
+ """
+
+ try:
+ span_val = self.parser.aug.span(vhost.path)
+ except ValueError:
+ logger.critical("Error while reading the VirtualHost %s from "
+ "file %s", vhost.name, vhost.filep, exc_info=True)
+ raise errors.PluginError("Unable to read VirtualHost from file")
+ span_filep = span_val[0]
+ span_start = span_val[5]
+ span_end = span_val[6]
+ with open(span_filep, 'r') as fh:
+ fh.seek(span_start)
+ vh_contents = fh.read(span_end-span_start).split("\n")
+ self._remove_closing_vhost_tag(vh_contents)
+ return vh_contents
+
+ def _remove_closing_vhost_tag(self, vh_contents):
+ """Removes the closing VirtualHost tag if it exists.
+
+ This method modifies vh_contents directly to remove the closing
+ tag. If the closing vhost tag is found, everything on the line
+ after it is also removed. Whether or not this tag is included
+ in the result of span depends on the Augeas version.
+
+ :param list vh_contents: VirtualHost block contents to check
+
+ """
+ for offset, line in enumerate(reversed(vh_contents)):
+ if line:
+ line_index = line.lower().find("</virtualhost>")
+ if line_index != -1:
+ content_index = len(vh_contents) - offset - 1
+ vh_contents[content_index] = line[:line_index]
+ break
+
+ def _update_ssl_vhosts_addrs(self, vh_path):
+ ssl_addrs = set()
+ ssl_addr_p = self.parser.aug.match(vh_path + "/arg")
+
+ for addr in ssl_addr_p:
+ old_addr = obj.Addr.fromstring(
+ str(self.parser.get_arg(addr)))
+ ssl_addr = old_addr.get_addr_obj("443")
+ self.parser.aug.set(addr, str(ssl_addr))
+ ssl_addrs.add(ssl_addr)
+
+ return ssl_addrs
+
+ def _clean_vhost(self, vhost):
+ # remove duplicated or conflicting ssl directives
+ self._deduplicate_directives(vhost.path,
+ ["SSLCertificateFile",
+ "SSLCertificateKeyFile"])
+ # remove all problematic directives
+ self._remove_directives(vhost.path, ["SSLCertificateChainFile"])
+
+ def _deduplicate_directives(self, vh_path, directives):
+ for directive in directives:
+ while len(self.parser.find_dir(directive, None,
+ vh_path, False)) > 1:
+ directive_path = self.parser.find_dir(directive, None,
+ vh_path, False)
+ self.parser.aug.remove(re.sub(r"/\w*$", "", directive_path[0]))
+
+ def _remove_directives(self, vh_path, directives):
+ for directive in directives:
+ while self.parser.find_dir(directive, None, vh_path, False):
+ directive_path = self.parser.find_dir(directive, None,
+ vh_path, False)
+ self.parser.aug.remove(re.sub(r"/\w*$", "", directive_path[0]))
+
+ def _add_dummy_ssl_directives(self, vh_path):
+ self.parser.add_dir(vh_path, "SSLCertificateFile",
+ "insert_cert_file_path")
+ self.parser.add_dir(vh_path, "SSLCertificateKeyFile",
+ "insert_key_file_path")
+ # Only include the TLS configuration if not already included
+ existing_inc = self.parser.find_dir("Include", self.mod_ssl_conf, vh_path)
+ if not existing_inc:
+ self.parser.add_dir(vh_path, "Include", self.mod_ssl_conf)
+
+ def _add_servername_alias(self, target_name, vhost):
+ vh_path = vhost.path
+ sname, saliases = self._get_vhost_names(vh_path)
+ if target_name == sname or target_name in saliases:
+ return
+ if self._has_matching_wildcard(vh_path, target_name):
+ return
+ if not self.parser.find_dir("ServerName", None,
+ start=vh_path, exclude=False):
+ self.parser.add_dir(vh_path, "ServerName", target_name)
+ else:
+ self.parser.add_dir(vh_path, "ServerAlias", target_name)
+ self._add_servernames(vhost)
+
+ def _has_matching_wildcard(self, vh_path, target_name):
+ """Is target_name already included in a wildcard in the vhost?
+
+ :param str vh_path: Augeas path to the vhost
+ :param str target_name: name to compare with wildcards
+
+ :returns: True if there is a wildcard covering target_name in
+ the vhost in vhost_path, otherwise, False
+ :rtype: bool
+
+ """
+ matches = self.parser.find_dir(
+ "ServerAlias", start=vh_path, exclude=False)
+ aliases = (self.parser.aug.get(match) for match in matches)
+ return self.domain_in_names(aliases, target_name)
+
+ def _add_name_vhost_if_necessary(self, vhost):
+ """Add NameVirtualHost Directives if necessary for new vhost.
+
+ NameVirtualHosts was a directive in Apache < 2.4
+ https://httpd.apache.org/docs/2.2/mod/core.html#namevirtualhost
+
+ :param vhost: New virtual host that was recently created.
+ :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
+
+ """
+ need_to_save = False
+
+ # See if the exact address appears in any other vhost
+ # Remember 1.1.1.1:* == 1.1.1.1 -> hence any()
+ for addr in vhost.addrs:
+ # In Apache 2.2, when a NameVirtualHost directive is not
+ # set, "*" and "_default_" will conflict when sharing a port
+ addrs = set((addr,))
+ if addr.get_addr() in ("*", "_default_"):
+ addrs.update(obj.Addr((a, addr.get_port(),))
+ for a in ("*", "_default_"))
+
+ for test_vh in self.vhosts:
+ if (vhost.filep != test_vh.filep and
+ any(test_addr in addrs for
+ test_addr in test_vh.addrs) and
+ not self.is_name_vhost(addr)):
+ self.add_name_vhost(addr)
+ logger.info("Enabling NameVirtualHosts on %s", addr)
+ need_to_save = True
+ break
+
+ if need_to_save:
+ self.save()
+
+ def find_vhost_by_id(self, id_str):
+ """
+ Searches through VirtualHosts and tries to match the id in a comment
+
+ :param str id_str: Id string for matching
+
+ :returns: The matched VirtualHost or None
+ :rtype: :class:`~certbot_apache._internal.obj.VirtualHost` or None
+
+ :raises .errors.PluginError: If no VirtualHost is found
+ """
+
+ for vh in self.vhosts:
+ if self._find_vhost_id(vh) == id_str:
+ return vh
+ msg = "No VirtualHost with ID {} was found.".format(id_str)
+ logger.warning(msg)
+ raise errors.PluginError(msg)
+
+ def _find_vhost_id(self, vhost):
+ """Tries to find the unique ID from the VirtualHost comments. This is
+ used for keeping track of VirtualHost directive over time.
+
+ :param vhost: Virtual host to add the id
+ :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
+
+ :returns: The unique ID or None
+ :rtype: str or None
+ """
+
+ # Strip the {} off from the format string
+ search_comment = constants.MANAGED_COMMENT_ID.format("")
+
+ id_comment = self.parser.find_comments(search_comment, vhost.path)
+ if id_comment:
+ # Use the first value, multiple ones shouldn't exist
+ comment = self.parser.get_arg(id_comment[0])
+ return comment.split(" ")[-1]
+ return None
+
+ def add_vhost_id(self, vhost):
+ """Adds an unique ID to the VirtualHost as a comment for mapping back
+ to it on later invocations, as the config file order might have changed.
+ If ID already exists, returns that instead.
+
+ :param vhost: Virtual host to add or find the id
+ :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
+
+ :returns: The unique ID for vhost
+ :rtype: str or None
+ """
+
+ vh_id = self._find_vhost_id(vhost)
+ if vh_id:
+ return vh_id
+
+ id_string = apache_util.unique_id()
+ comment = constants.MANAGED_COMMENT_ID.format(id_string)
+ self.parser.add_comment(vhost.path, comment)
+ return id_string
+
+ def _escape(self, fp):
+ fp = fp.replace(",", "\\,")
+ fp = fp.replace("[", "\\[")
+ fp = fp.replace("]", "\\]")
+ fp = fp.replace("|", "\\|")
+ fp = fp.replace("=", "\\=")
+ fp = fp.replace("(", "\\(")
+ fp = fp.replace(")", "\\)")
+ fp = fp.replace("!", "\\!")
+ return fp
+
+ ######################################################################
+ # Enhancements
+ ######################################################################
+ def supported_enhancements(self): # pylint: disable=no-self-use
+ """Returns currently supported enhancements."""
+ return ["redirect", "ensure-http-header", "staple-ocsp"]
+
+ def enhance(self, domain, enhancement, options=None):
+ """Enhance configuration.
+
+ :param str domain: domain to enhance
+ :param str enhancement: enhancement type defined in
+ :const:`~certbot.plugins.enhancements.ENHANCEMENTS`
+ :param options: options for the enhancement
+ See :const:`~certbot.plugins.enhancements.ENHANCEMENTS`
+ documentation for appropriate parameter.
+
+ :raises .errors.PluginError: If Enhancement is not supported, or if
+ there is any other problem with the enhancement.
+
+ """
+ try:
+ func = self._enhance_func[enhancement]
+ except KeyError:
+ raise errors.PluginError(
+ "Unsupported enhancement: {0}".format(enhancement))
+
+ matched_vhosts = self.choose_vhosts(domain, create_if_no_ssl=False)
+ # We should be handling only SSL vhosts for enhancements
+ vhosts = [vhost for vhost in matched_vhosts if vhost.ssl]
+
+ if not vhosts:
+ msg_tmpl = ("Certbot was not able to find SSL VirtualHost for a "
+ "domain {0} for enabling enhancement \"{1}\". The requested "
+ "enhancement was not configured.")
+ msg_enhancement = enhancement
+ if options:
+ msg_enhancement += ": " + options
+ msg = msg_tmpl.format(domain, msg_enhancement)
+ logger.warning(msg)
+ raise errors.PluginError(msg)
+ try:
+ for vhost in vhosts:
+ func(vhost, options)
+ except errors.PluginError:
+ logger.warning("Failed %s for %s", enhancement, domain)
+ raise
+
+ def _autohsts_increase(self, vhost, id_str, nextstep):
+ """Increase the AutoHSTS max-age value
+
+ :param vhost: Virtual host object to modify
+ :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
+
+ :param str id_str: The unique ID string of VirtualHost
+
+ :param int nextstep: Next AutoHSTS max-age value index
+
+ """
+ nextstep_value = constants.AUTOHSTS_STEPS[nextstep]
+ self._autohsts_write(vhost, nextstep_value)
+ self._autohsts[id_str] = {"laststep": nextstep, "timestamp": time.time()}
+
+ def _autohsts_write(self, vhost, nextstep_value):
+ """
+ Write the new HSTS max-age value to the VirtualHost file
+ """
+
+ hsts_dirpath = None
+ header_path = self.parser.find_dir("Header", None, vhost.path)
+ if header_path:
+ pat = '(?:[ "]|^)(strict-transport-security)(?:[ "]|$)'
+ for match in header_path:
+ if re.search(pat, self.parser.aug.get(match).lower()):
+ hsts_dirpath = match
+ if not hsts_dirpath:
+ err_msg = ("Certbot was unable to find the existing HSTS header "
+ "from the VirtualHost at path {0}.").format(vhost.filep)
+ raise errors.PluginError(err_msg)
+
+ # Prepare the HSTS header value
+ hsts_maxage = "\"max-age={0}\"".format(nextstep_value)
+
+ # Update the header
+ # Our match statement was for string strict-transport-security, but
+ # we need to update the value instead. The next index is for the value
+ hsts_dirpath = hsts_dirpath.replace("arg[3]", "arg[4]")
+ self.parser.aug.set(hsts_dirpath, hsts_maxage)
+ note_msg = ("Increasing HSTS max-age value to {0} for VirtualHost "
+ "in {1}\n".format(nextstep_value, vhost.filep))
+ logger.debug(note_msg)
+ self.save_notes += note_msg
+ self.save(note_msg)
+
+ def _autohsts_fetch_state(self):
+ """
+ Populates the AutoHSTS state from the pluginstorage
+ """
+ try:
+ self._autohsts = self.storage.fetch("autohsts")
+ except KeyError:
+ self._autohsts = dict()
+
+ def _autohsts_save_state(self):
+ """
+ Saves the state of AutoHSTS object to pluginstorage
+ """
+ self.storage.put("autohsts", self._autohsts)
+ self.storage.save()
+
+ def _autohsts_vhost_in_lineage(self, vhost, lineage):
+ """
+ Searches AutoHSTS managed VirtualHosts that belong to the lineage.
+ Matches the private key path.
+ """
+
+ return bool(
+ self.parser.find_dir("SSLCertificateKeyFile",
+ lineage.key_path, vhost.path))
+
+ def _enable_ocsp_stapling(self, ssl_vhost, unused_options):
+ """Enables OCSP Stapling
+
+ In OCSP, each client (e.g. browser) would have to query the
+ OCSP Responder to validate that the site certificate was not revoked.
+
+ Enabling OCSP Stapling, would allow the web-server to query the OCSP
+ Responder, and staple its response to the offered certificate during
+ TLS. i.e. clients would not have to query the OCSP responder.
+
+ OCSP Stapling enablement on Apache implicitly depends on
+ SSLCertificateChainFile being set by other code.
+
+ .. note:: This function saves the configuration
+
+ :param ssl_vhost: Destination of traffic, an ssl enabled vhost
+ :type ssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
+
+ :param unused_options: Not currently used
+ :type unused_options: Not Available
+
+ :returns: Success, general_vhost (HTTP vhost)
+ :rtype: (bool, :class:`~certbot_apache._internal.obj.VirtualHost`)
+
+ """
+ min_apache_ver = (2, 3, 3)
+ if self.get_version() < min_apache_ver:
+ raise errors.PluginError(
+ "Unable to set OCSP directives.\n"
+ "Apache version is below 2.3.3.")
+
+ if "socache_shmcb_module" not in self.parser.modules:
+ self.enable_mod("socache_shmcb")
+
+ # Check if there's an existing SSLUseStapling directive on.
+ use_stapling_aug_path = self.parser.find_dir("SSLUseStapling",
+ "on", start=ssl_vhost.path)
+ if not use_stapling_aug_path:
+ self.parser.add_dir(ssl_vhost.path, "SSLUseStapling", "on")
+
+ ssl_vhost_aug_path = self._escape(parser.get_aug_path(ssl_vhost.filep))
+
+ # Check if there's an existing SSLStaplingCache directive.
+ stapling_cache_aug_path = self.parser.find_dir('SSLStaplingCache',
+ None, ssl_vhost_aug_path)
+
+ # We'll simply delete the directive, so that we'll have a
+ # consistent OCSP cache path.
+ if stapling_cache_aug_path:
+ self.parser.aug.remove(
+ re.sub(r"/\w*$", "", stapling_cache_aug_path[0]))
+
+ self.parser.add_dir_to_ifmodssl(ssl_vhost_aug_path,
+ "SSLStaplingCache",
+ ["shmcb:/var/run/apache2/stapling_cache(128000)"])
+
+ msg = "OCSP Stapling was enabled on SSL Vhost: %s.\n"%(
+ ssl_vhost.filep)
+ self.save_notes += msg
+ self.save()
+ logger.info(msg)
+
+ def _set_http_header(self, ssl_vhost, header_substring):
+ """Enables header that is identified by header_substring on ssl_vhost.
+
+ If the header identified by header_substring is not already set,
+ a new Header directive is placed in ssl_vhost's configuration with
+ arguments from: constants.HTTP_HEADER[header_substring]
+
+ .. note:: This function saves the configuration
+
+ :param ssl_vhost: Destination of traffic, an ssl enabled vhost
+ :type ssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
+
+ :param header_substring: string that uniquely identifies a header.
+ e.g: Strict-Transport-Security, Upgrade-Insecure-Requests.
+ :type str
+
+ :returns: Success, general_vhost (HTTP vhost)
+ :rtype: (bool, :class:`~certbot_apache._internal.obj.VirtualHost`)
+
+ :raises .errors.PluginError: If no viable HTTP host can be created or
+ set with header header_substring.
+
+ """
+ if "headers_module" not in self.parser.modules:
+ self.enable_mod("headers")
+
+ # Check if selected header is already set
+ self._verify_no_matching_http_header(ssl_vhost, header_substring)
+
+ # Add directives to server
+ self.parser.add_dir(ssl_vhost.path, "Header",
+ constants.HEADER_ARGS[header_substring])
+
+ self.save_notes += ("Adding %s header to ssl vhost in %s\n" %
+ (header_substring, ssl_vhost.filep))
+
+ self.save()
+ logger.info("Adding %s header to ssl vhost in %s", header_substring,
+ ssl_vhost.filep)
+
+ def _verify_no_matching_http_header(self, ssl_vhost, header_substring):
+ """Checks to see if there is an existing Header directive that
+ contains the string header_substring.
+
+ :param ssl_vhost: vhost to check
+ :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
+
+ :param header_substring: string that uniquely identifies a header.
+ e.g: Strict-Transport-Security, Upgrade-Insecure-Requests.
+ :type str
+
+ :returns: boolean
+ :rtype: (bool)
+
+ :raises errors.PluginEnhancementAlreadyPresent When header
+ header_substring exists
+
+ """
+ header_path = self.parser.find_dir("Header", None,
+ start=ssl_vhost.path)
+ if header_path:
+ # "Existing Header directive for virtualhost"
+ pat = '(?:[ "]|^)(%s)(?:[ "]|$)' % (header_substring.lower())
+ for match in header_path:
+ if re.search(pat, self.parser.aug.get(match).lower()):
+ raise errors.PluginEnhancementAlreadyPresent(
+ "Existing %s header" % (header_substring))
+
+ def _enable_redirect(self, ssl_vhost, unused_options):
+ """Redirect all equivalent HTTP traffic to ssl_vhost.
+
+ .. todo:: This enhancement should be rewritten and will
+ unfortunately require lots of debugging by hand.
+
+ Adds Redirect directive to the port 80 equivalent of ssl_vhost
+ First the function attempts to find the vhost with equivalent
+ ip addresses that serves on non-ssl ports
+ The function then adds the directive
+
+ .. note:: This function saves the configuration
+
+ :param ssl_vhost: Destination of traffic, an ssl enabled vhost
+ :type ssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
+
+ :param unused_options: Not currently used
+ :type unused_options: Not Available
+
+ :raises .errors.PluginError: If no viable HTTP host can be created or
+ used for the redirect.
+
+ """
+ if "rewrite_module" not in self.parser.modules:
+ self.enable_mod("rewrite")
+ general_vh = self._get_http_vhost(ssl_vhost)
+
+ if general_vh is None:
+ # Add virtual_server with redirect
+ logger.debug("Did not find http version of ssl virtual host "
+ "attempting to create")
+ redirect_addrs = self._get_proposed_addrs(ssl_vhost)
+ for vhost in self.vhosts:
+ if vhost.enabled and vhost.conflicts(redirect_addrs):
+ raise errors.PluginError(
+ "Unable to find corresponding HTTP vhost; "
+ "Unable to create one as intended addresses conflict; "
+ "Current configuration does not support automated "
+ "redirection")
+ self._create_redirect_vhost(ssl_vhost)
+ else:
+ if general_vh in self._enhanced_vhosts["redirect"]:
+ logger.debug("Already enabled redirect for this vhost")
+ return
+
+ # Check if Certbot redirection already exists
+ self._verify_no_certbot_redirect(general_vh)
+
+ # Note: if code flow gets here it means we didn't find the exact
+ # certbot RewriteRule config for redirection. Finding
+ # another RewriteRule is likely to be fine in most or all cases,
+ # but redirect loops are possible in very obscure cases; see #1620
+ # for reasoning.
+ if self._is_rewrite_exists(general_vh):
+ logger.warning("Added an HTTP->HTTPS rewrite in addition to "
+ "other RewriteRules; you may wish to check for "
+ "overall consistency.")
+
+ # Add directives to server
+ # Note: These are not immediately searchable in sites-enabled
+ # even with save() and load()
+ if not self._is_rewrite_engine_on(general_vh):
+ self.parser.add_dir(general_vh.path, "RewriteEngine", "on")
+
+ names = ssl_vhost.get_names()
+ for idx, name in enumerate(names):
+ args = ["%{SERVER_NAME}", "={0}".format(name), "[OR]"]
+ if idx == len(names) - 1:
+ args.pop()
+ self.parser.add_dir(general_vh.path, "RewriteCond", args)
+
+ self._set_https_redirection_rewrite_rule(general_vh)
+
+ self.save_notes += ("Redirecting host in %s to ssl vhost in %s\n" %
+ (general_vh.filep, ssl_vhost.filep))
+ self.save()
+
+ self._enhanced_vhosts["redirect"].add(general_vh)
+ logger.info("Redirecting vhost in %s to ssl vhost in %s",
+ general_vh.filep, ssl_vhost.filep)
+
+ def _set_https_redirection_rewrite_rule(self, vhost):
+ if self.get_version() >= (2, 3, 9):
+ self.parser.add_dir(vhost.path, "RewriteRule",
+ constants.REWRITE_HTTPS_ARGS_WITH_END)
+ else:
+ self.parser.add_dir(vhost.path, "RewriteRule",
+ constants.REWRITE_HTTPS_ARGS)
+
+ def _verify_no_certbot_redirect(self, vhost):
+ """Checks to see if a redirect was already installed by certbot.
+
+ Checks to see if virtualhost already contains a rewrite rule that is
+ identical to Certbot's redirection rewrite rule.
+
+ For graceful transition to new rewrite rules for HTTPS redireciton we
+ delete certbot's old rewrite rules and set the new one instead.
+
+ :param vhost: vhost to check
+ :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
+
+ :raises errors.PluginEnhancementAlreadyPresent: When the exact
+ certbot redirection WriteRule exists in virtual host.
+ """
+ rewrite_path = self.parser.find_dir(
+ "RewriteRule", None, start=vhost.path)
+
+ # There can be other RewriteRule directive lines in vhost config.
+ # rewrite_args_dict keys are directive ids and the corresponding value
+ # for each is a list of arguments to that directive.
+ rewrite_args_dict = defaultdict(list) # type: DefaultDict[str, List[str]]
+ pat = r'(.*directive\[\d+\]).*'
+ for match in rewrite_path:
+ m = re.match(pat, match)
+ if m:
+ dir_path = m.group(1)
+ rewrite_args_dict[dir_path].append(match)
+
+ if rewrite_args_dict:
+ redirect_args = [constants.REWRITE_HTTPS_ARGS,
+ constants.REWRITE_HTTPS_ARGS_WITH_END]
+
+ for dir_path, args_paths in rewrite_args_dict.items():
+ arg_vals = [self.parser.aug.get(x) for x in args_paths]
+
+ # Search for past redirection rule, delete it, set the new one
+ if arg_vals in constants.OLD_REWRITE_HTTPS_ARGS:
+ self.parser.aug.remove(dir_path)
+ self._set_https_redirection_rewrite_rule(vhost)
+ self.save()
+ raise errors.PluginEnhancementAlreadyPresent(
+ "Certbot has already enabled redirection")
+
+ if arg_vals in redirect_args:
+ raise errors.PluginEnhancementAlreadyPresent(
+ "Certbot has already enabled redirection")
+
+ def _is_rewrite_exists(self, vhost):
+ """Checks if there exists a RewriteRule directive in vhost
+
+ :param vhost: vhost to check
+ :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
+
+ :returns: True if a RewriteRule directive exists.
+ :rtype: bool
+
+ """
+ rewrite_path = self.parser.find_dir(
+ "RewriteRule", None, start=vhost.path)
+ return bool(rewrite_path)
+
+ def _is_rewrite_engine_on(self, vhost):
+ """Checks if a RewriteEngine directive is on
+
+ :param vhost: vhost to check
+ :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
+
+ """
+ rewrite_engine_path_list = self.parser.find_dir("RewriteEngine", "on",
+ start=vhost.path)
+ if rewrite_engine_path_list:
+ for re_path in rewrite_engine_path_list:
+ # A RewriteEngine directive may also be included in per
+ # directory .htaccess files. We only care about the VirtualHost.
+ if 'virtualhost' in re_path.lower():
+ return self.parser.get_arg(re_path)
+ return False
+
+ def _create_redirect_vhost(self, ssl_vhost):
+ """Creates an http_vhost specifically to redirect for the ssl_vhost.
+
+ :param ssl_vhost: ssl vhost
+ :type ssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
+
+ :returns: tuple of the form
+ (`success`, :class:`~certbot_apache._internal.obj.VirtualHost`)
+ :rtype: tuple
+
+ """
+ text = self._get_redirect_config_str(ssl_vhost)
+
+ redirect_filepath = self._write_out_redirect(ssl_vhost, text)
+
+ self.parser.aug.load()
+ # Make a new vhost data structure and add it to the lists
+ new_vhost = self._create_vhost(parser.get_aug_path(self._escape(redirect_filepath)))
+ self.vhosts.append(new_vhost)
+ self._enhanced_vhosts["redirect"].add(new_vhost)
+
+ # Finally create documentation for the change
+ self.save_notes += ("Created a port 80 vhost, %s, for redirection to "
+ "ssl vhost %s\n" %
+ (new_vhost.filep, ssl_vhost.filep))
+
+ def _get_redirect_config_str(self, ssl_vhost):
+ # get servernames and serveraliases
+ serveralias = ""
+ servername = ""
+
+ if ssl_vhost.name is not None:
+ servername = "ServerName " + ssl_vhost.name
+ if ssl_vhost.aliases:
+ serveralias = "ServerAlias " + " ".join(ssl_vhost.aliases)
+
+ rewrite_rule_args = [] # type: List[str]
+ if self.get_version() >= (2, 3, 9):
+ rewrite_rule_args = constants.REWRITE_HTTPS_ARGS_WITH_END
+ else:
+ rewrite_rule_args = constants.REWRITE_HTTPS_ARGS
+
+ return ("<VirtualHost %s>\n"
+ "%s \n"
+ "%s \n"
+ "ServerSignature Off\n"
+ "\n"
+ "RewriteEngine On\n"
+ "RewriteRule %s\n"
+ "\n"
+ "ErrorLog %s/redirect.error.log\n"
+ "LogLevel warn\n"
+ "</VirtualHost>\n"
+ % (" ".join(str(addr) for
+ addr in self._get_proposed_addrs(ssl_vhost)),
+ servername, serveralias,
+ " ".join(rewrite_rule_args),
+ self.option("logs_root")))
+
+ def _write_out_redirect(self, ssl_vhost, text):
+ # This is the default name
+ redirect_filename = "le-redirect.conf"
+
+ # See if a more appropriate name can be applied
+ if ssl_vhost.name is not None:
+ # make sure servername doesn't exceed filename length restriction
+ if len(ssl_vhost.name) < (255 - (len(redirect_filename) + 1)):
+ redirect_filename = "le-redirect-%s.conf" % ssl_vhost.name
+
+ redirect_filepath = os.path.join(self.option("vhost_root"),
+ redirect_filename)
+
+ # Register the new file that will be created
+ # Note: always register the creation before writing to ensure file will
+ # be removed in case of unexpected program exit
+ self.reverter.register_file_creation(False, redirect_filepath)
+
+ # Write out file
+ with open(redirect_filepath, "w") as redirect_file:
+ redirect_file.write(text)
+
+ # Add new include to configuration if it doesn't exist yet
+ if not self.parser.parsed_in_current(redirect_filepath):
+ self.parser.parse_file(redirect_filepath)
+
+ logger.info("Created redirect file: %s", redirect_filename)
+
+ return redirect_filepath
+
+ def _get_http_vhost(self, ssl_vhost):
+ """Find appropriate HTTP vhost for ssl_vhost."""
+ # First candidate vhosts filter
+ if ssl_vhost.ancestor:
+ return ssl_vhost.ancestor
+ candidate_http_vhs = [
+ vhost for vhost in self.vhosts if not vhost.ssl
+ ]
+
+ # Second filter - check addresses
+ for http_vh in candidate_http_vhs:
+ if http_vh.same_server(ssl_vhost):
+ return http_vh
+ # Third filter - if none with same names, return generic
+ for http_vh in candidate_http_vhs:
+ if http_vh.same_server(ssl_vhost, generic=True):
+ return http_vh
+
+ return None
+
+ def _get_proposed_addrs(self, vhost, port="80"):
+ """Return all addrs of vhost with the port replaced with the specified.
+
+ :param obj.VirtualHost ssl_vhost: Original Vhost
+ :param str port: Desired port for new addresses
+
+ :returns: `set` of :class:`~obj.Addr`
+
+ """
+ redirects = set()
+ for addr in vhost.addrs:
+ redirects.add(addr.get_addr_obj(port))
+
+ return redirects
+
+ def enable_site(self, vhost):
+ """Enables an available site, Apache reload required.
+
+ .. note:: Does not make sure that the site correctly works or that all
+ modules are enabled appropriately.
+ .. note:: The distribution specific override replaces functionality
+ of this method where available.
+
+ :param vhost: vhost to enable
+ :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
+
+ :raises .errors.NotSupportedError: If filesystem layout is not
+ supported.
+
+ """
+ if vhost.enabled:
+ return
+
+ if not self.parser.parsed_in_original(vhost.filep):
+ # Add direct include to root conf
+ logger.info("Enabling site %s by adding Include to root configuration",
+ vhost.filep)
+ self.save_notes += "Enabled site %s\n" % vhost.filep
+ self.parser.add_include(self.parser.loc["default"], vhost.filep)
+ vhost.enabled = True
+ return
+
+ def enable_mod(self, mod_name, temp=False): # pylint: disable=unused-argument
+ """Enables module in Apache.
+
+ Both enables and reloads Apache so module is active.
+
+ :param str mod_name: Name of the module to enable. (e.g. 'ssl')
+ :param bool temp: Whether or not this is a temporary action.
+
+ .. note:: The distribution specific override replaces functionality
+ of this method where available.
+
+ :raises .errors.MisconfigurationError: We cannot enable modules in
+ generic fashion.
+
+ """
+ mod_message = ("Apache needs to have module \"{0}\" active for the " +
+ "requested installation options. Unfortunately Certbot is unable " +
+ "to install or enable it for you. Please install the module, and " +
+ "run Certbot again.")
+ raise errors.MisconfigurationError(mod_message.format(mod_name))
+
+ def restart(self):
+ """Runs a config test and reloads the Apache server.
+
+ :raises .errors.MisconfigurationError: If either the config test
+ or reload fails.
+
+ """
+ self.config_test()
+ self._reload()
+
+ def _reload(self):
+ """Reloads the Apache server.
+
+ :raises .errors.MisconfigurationError: If reload fails
+
+ """
+ try:
+ util.run_script(self.option("restart_cmd"))
+ except errors.SubprocessError as err:
+ logger.info("Unable to restart apache using %s",
+ self.option("restart_cmd"))
+ alt_restart = self.option("restart_cmd_alt")
+ if alt_restart:
+ logger.debug("Trying alternative restart command: %s",
+ alt_restart)
+ # There is an alternative restart command available
+ # This usually is "restart" verb while original is "graceful"
+ try:
+ util.run_script(self.option(
+ "restart_cmd_alt"))
+ return
+ except errors.SubprocessError as secerr:
+ error = str(secerr)
+ else:
+ error = str(err)
+ raise errors.MisconfigurationError(error)
+
+ def config_test(self): # pylint: disable=no-self-use
+ """Check the configuration of Apache for errors.
+
+ :raises .errors.MisconfigurationError: If config_test fails
+
+ """
+ try:
+ util.run_script(self.option("conftest_cmd"))
+ except errors.SubprocessError as err:
+ raise errors.MisconfigurationError(str(err))
+
+ def get_version(self):
+ """Return version of Apache Server.
+
+ Version is returned as tuple. (ie. 2.4.7 = (2, 4, 7))
+
+ :returns: version
+ :rtype: tuple
+
+ :raises .PluginError: if unable to find Apache version
+
+ """
+ try:
+ stdout, _ = util.run_script(self.option("version_cmd"))
+ except errors.SubprocessError:
+ raise errors.PluginError(
+ "Unable to run %s -v" %
+ self.option("version_cmd"))
+
+ regex = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE)
+ matches = regex.findall(stdout)
+
+ if len(matches) != 1:
+ raise errors.PluginError("Unable to find Apache version")
+
+ return tuple([int(i) for i in matches[0].split(".")])
+
+ def more_info(self):
+ """Human-readable string to help understand the module"""
+ return (
+ "Configures Apache to authenticate and install HTTPS.{0}"
+ "Server root: {root}{0}"
+ "Version: {version}".format(
+ os.linesep, root=self.parser.loc["root"],
+ version=".".join(str(i) for i in self.version))
+ )
+
+ ###########################################################################
+ # Challenges Section
+ ###########################################################################
+ def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use
+ """Return list of challenge preferences."""
+ return [challenges.HTTP01]
+
+ def perform(self, achalls):
+ """Perform the configuration related challenge.
+
+ This function currently assumes all challenges will be fulfilled.
+ If this turns out not to be the case in the future. Cleanup and
+ outstanding challenges will have to be designed better.
+
+ """
+ self._chall_out.update(achalls)
+ responses = [None] * len(achalls)
+ http_doer = http_01.ApacheHttp01(self)
+
+ for i, achall in enumerate(achalls):
+ # Currently also have chall_doer hold associated index of the
+ # challenge. This helps to put all of the responses back together
+ # when they are all complete.
+ http_doer.add_chall(achall, i)
+
+ http_response = http_doer.perform()
+ if http_response:
+ # Must reload in order to activate the challenges.
+ # Handled here because we may be able to load up other challenge
+ # types
+ self.restart()
+
+ # TODO: Remove this dirty hack. We need to determine a reliable way
+ # of identifying when the new configuration is being used.
+ time.sleep(3)
+
+ self._update_responses(responses, http_response, http_doer)
+
+ return responses
+
+ def _update_responses(self, responses, chall_response, chall_doer):
+ # Go through all of the challenges and assign them to the proper
+ # place in the responses return value. All responses must be in the
+ # same order as the original challenges.
+ for i, resp in enumerate(chall_response):
+ responses[chall_doer.indices[i]] = resp
+
+ def cleanup(self, achalls):
+ """Revert all challenges."""
+ self._chall_out.difference_update(achalls)
+
+ # If all of the challenges have been finished, clean up everything
+ if not self._chall_out:
+ self.revert_challenge_config()
+ self.restart()
+ self.parser.reset_modules()
+
+ def install_ssl_options_conf(self, options_ssl, options_ssl_digest):
+ """Copy Certbot's SSL options file into the system's config dir if required."""
+
+ # XXX if we ever try to enforce a local privilege boundary (eg, running
+ # certbot for unprivileged users via setuid), this function will need
+ # to be modified.
+ return common.install_version_controlled_file(options_ssl, options_ssl_digest,
+ self.option("MOD_SSL_CONF_SRC"), constants.ALL_SSL_OPTIONS_HASHES)
+
+ def enable_autohsts(self, _unused_lineage, domains):
+ """
+ Enable the AutoHSTS enhancement for defined domains
+
+ :param _unused_lineage: Certificate lineage object, unused
+ :type _unused_lineage: certbot._internal.storage.RenewableCert
+
+ :param domains: List of domains in certificate to enhance
+ :type domains: str
+ """
+
+ self._autohsts_fetch_state()
+ _enhanced_vhosts = []
+ for d in domains:
+ matched_vhosts = self.choose_vhosts(d, create_if_no_ssl=False)
+ # We should be handling only SSL vhosts for AutoHSTS
+ vhosts = [vhost for vhost in matched_vhosts if vhost.ssl]
+
+ if not vhosts:
+ msg_tmpl = ("Certbot was not able to find SSL VirtualHost for a "
+ "domain {0} for enabling AutoHSTS enhancement.")
+ msg = msg_tmpl.format(d)
+ logger.warning(msg)
+ raise errors.PluginError(msg)
+ for vh in vhosts:
+ try:
+ self._enable_autohsts_domain(vh)
+ _enhanced_vhosts.append(vh)
+ except errors.PluginEnhancementAlreadyPresent:
+ if vh in _enhanced_vhosts:
+ continue
+ msg = ("VirtualHost for domain {0} in file {1} has a " +
+ "String-Transport-Security header present, exiting.")
+ raise errors.PluginEnhancementAlreadyPresent(
+ msg.format(d, vh.filep))
+ if _enhanced_vhosts:
+ note_msg = "Enabling AutoHSTS"
+ self.save(note_msg)
+ logger.info(note_msg)
+ self.restart()
+
+ # Save the current state to pluginstorage
+ self._autohsts_save_state()
+
+ def _enable_autohsts_domain(self, ssl_vhost):
+ """Do the initial AutoHSTS deployment to a vhost
+
+ :param ssl_vhost: The VirtualHost object to deploy the AutoHSTS
+ :type ssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost` or None
+
+ :raises errors.PluginEnhancementAlreadyPresent: When already enhanced
+
+ """
+ # This raises the exception
+ self._verify_no_matching_http_header(ssl_vhost,
+ "Strict-Transport-Security")
+
+ if "headers_module" not in self.parser.modules:
+ self.enable_mod("headers")
+ # Prepare the HSTS header value
+ hsts_header = constants.HEADER_ARGS["Strict-Transport-Security"][:-1]
+ initial_maxage = constants.AUTOHSTS_STEPS[0]
+ hsts_header.append("\"max-age={0}\"".format(initial_maxage))
+
+ # Add ID to the VirtualHost for mapping back to it later
+ uniq_id = self.add_vhost_id(ssl_vhost)
+ self.save_notes += "Adding unique ID {0} to VirtualHost in {1}\n".format(
+ uniq_id, ssl_vhost.filep)
+ # Add the actual HSTS header
+ self.parser.add_dir(ssl_vhost.path, "Header", hsts_header)
+ note_msg = ("Adding gradually increasing HSTS header with initial value "
+ "of {0} to VirtualHost in {1}\n".format(
+ initial_maxage, ssl_vhost.filep))
+ self.save_notes += note_msg
+
+ # Save the current state to pluginstorage
+ self._autohsts[uniq_id] = {"laststep": 0, "timestamp": time.time()}
+
+ def update_autohsts(self, _unused_domain):
+ """
+ Increase the AutoHSTS values of VirtualHosts that the user has enabled
+ this enhancement for.
+
+ :param _unused_domain: Not currently used
+ :type _unused_domain: Not Available
+
+ """
+ self._autohsts_fetch_state()
+ if not self._autohsts:
+ # No AutoHSTS enabled for any domain
+ return
+ curtime = time.time()
+ save_and_restart = False
+ for id_str, config in list(self._autohsts.items()):
+ if config["timestamp"] + constants.AUTOHSTS_FREQ > curtime:
+ # Skip if last increase was < AUTOHSTS_FREQ ago
+ continue
+ nextstep = config["laststep"] + 1
+ if nextstep < len(constants.AUTOHSTS_STEPS):
+ # If installer hasn't been prepared yet, do it now
+ if not self._prepared:
+ self.prepare()
+ # Have not reached the max value yet
+ try:
+ vhost = self.find_vhost_by_id(id_str)
+ except errors.PluginError:
+ msg = ("Could not find VirtualHost with ID {0}, disabling "
+ "AutoHSTS for this VirtualHost").format(id_str)
+ logger.warning(msg)
+ # Remove the orphaned AutoHSTS entry from pluginstorage
+ self._autohsts.pop(id_str)
+ continue
+ self._autohsts_increase(vhost, id_str, nextstep)
+ msg = ("Increasing HSTS max-age value for VirtualHost with id "
+ "{0}").format(id_str)
+ self.save_notes += msg
+ save_and_restart = True
+
+ if save_and_restart:
+ self.save("Increased HSTS max-age values")
+ self.restart()
+
+ self._autohsts_save_state()
+
+ def deploy_autohsts(self, lineage):
+ """
+ Checks if autohsts vhost has reached maximum auto-increased value
+ and changes the HSTS max-age to a high value.
+
+ :param lineage: Certificate lineage object
+ :type lineage: certbot._internal.storage.RenewableCert
+ """
+ self._autohsts_fetch_state()
+ if not self._autohsts:
+ # No autohsts enabled for any vhost
+ return
+
+ vhosts = []
+ affected_ids = []
+ # Copy, as we are removing from the dict inside the loop
+ for id_str, config in list(self._autohsts.items()):
+ if config["laststep"]+1 >= len(constants.AUTOHSTS_STEPS):
+ # max value reached, try to make permanent
+ try:
+ vhost = self.find_vhost_by_id(id_str)
+ except errors.PluginError:
+ msg = ("VirtualHost with id {} was not found, unable to "
+ "make HSTS max-age permanent.").format(id_str)
+ logger.warning(msg)
+ self._autohsts.pop(id_str)
+ continue
+ if self._autohsts_vhost_in_lineage(vhost, lineage):
+ vhosts.append(vhost)
+ affected_ids.append(id_str)
+
+ save_and_restart = False
+ for vhost in vhosts:
+ self._autohsts_write(vhost, constants.AUTOHSTS_PERMANENT)
+ msg = ("Strict-Transport-Security max-age value for "
+ "VirtualHost in {0} was made permanent.").format(vhost.filep)
+ logger.debug(msg)
+ self.save_notes += msg+"\n"
+ save_and_restart = True
+
+ if save_and_restart:
+ self.save("Made HSTS max-age permanent")
+ self.restart()
+
+ for id_str in affected_ids:
+ self._autohsts.pop(id_str)
+
+ # Update AutoHSTS storage (We potentially removed vhosts from managed)
+ self._autohsts_save_state()
+
+
+AutoHSTSEnhancement.register(ApacheConfigurator)
diff --git a/certbot-apache/certbot_apache/_internal/constants.py b/certbot-apache/certbot_apache/_internal/constants.py
new file mode 100644
index 000000000..a37bebac5
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/constants.py
@@ -0,0 +1,71 @@
+"""Apache plugin constants."""
+import pkg_resources
+
+from certbot.compat import os
+
+MOD_SSL_CONF_DEST = "options-ssl-apache.conf"
+"""Name of the mod_ssl config file as saved in `IConfig.config_dir`."""
+
+
+UPDATED_MOD_SSL_CONF_DIGEST = ".updated-options-ssl-apache-conf-digest.txt"
+"""Name of the hash of the updated or informed mod_ssl_conf as saved in `IConfig.config_dir`."""
+
+# NEVER REMOVE A SINGLE HASH FROM THIS LIST UNLESS YOU KNOW EXACTLY WHAT YOU ARE DOING!
+ALL_SSL_OPTIONS_HASHES = [
+ '2086bca02db48daf93468332543c60ac6acdb6f0b58c7bfdf578a5d47092f82a',
+ '4844d36c9a0f587172d9fa10f4f1c9518e3bcfa1947379f155e16a70a728c21a',
+ '5a922826719981c0a234b1fbcd495f3213e49d2519e845ea0748ba513044b65b',
+ '4066b90268c03c9ba0201068eaa39abbc02acf9558bb45a788b630eb85dadf27',
+ 'f175e2e7c673bd88d0aff8220735f385f916142c44aa83b09f1df88dd4767a88',
+ 'cfdd7c18d2025836ea3307399f509cfb1ebf2612c87dd600a65da2a8e2f2797b',
+ '80720bd171ccdc2e6b917ded340defae66919e4624962396b992b7218a561791',
+ 'c0c022ea6b8a51ecc8f1003d0a04af6c3f2bc1c3ce506b3c2dfc1f11ef931082',
+ '717b0a89f5e4c39b09a42813ac6e747cfbdeb93439499e73f4f70a1fe1473f20',
+ '0fcdc81280cd179a07ec4d29d3595068b9326b455c488de4b09f585d5dafc137',
+ '86cc09ad5415cd6d5f09a947fe2501a9344328b1e8a8b458107ea903e80baa6c',
+ '06675349e457eae856120cdebb564efe546f0b87399f2264baeb41e442c724c7',
+ '5cc003edd93fb9cd03d40c7686495f8f058f485f75b5e764b789245a386e6daf',
+ '007cd497a56a3bb8b6a2c1aeb4997789e7e38992f74e44cc5d13a625a738ac73',
+]
+"""SHA256 hashes of the contents of previous versions of all versions of MOD_SSL_CONF_SRC"""
+
+AUGEAS_LENS_DIR = pkg_resources.resource_filename(
+ "certbot_apache", os.path.join("_internal", "augeas_lens"))
+"""Path to the Augeas lens directory"""
+
+REWRITE_HTTPS_ARGS = [
+ "^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,NE,R=permanent]"]
+"""Apache version<2.3.9 rewrite rule arguments used for redirections to
+https vhost"""
+
+REWRITE_HTTPS_ARGS_WITH_END = [
+ "^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[END,NE,R=permanent]"]
+"""Apache version >= 2.3.9 rewrite rule arguments used for redirections to
+ https vhost"""
+
+OLD_REWRITE_HTTPS_ARGS = [
+ ["^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,QSA,R=permanent]"],
+ ["^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[END,QSA,R=permanent]"]]
+
+HSTS_ARGS = ["always", "set", "Strict-Transport-Security",
+ "\"max-age=31536000\""]
+"""Apache header arguments for HSTS"""
+
+UIR_ARGS = ["always", "set", "Content-Security-Policy",
+ "upgrade-insecure-requests"]
+
+HEADER_ARGS = {"Strict-Transport-Security": HSTS_ARGS,
+ "Upgrade-Insecure-Requests": UIR_ARGS}
+
+AUTOHSTS_STEPS = [60, 300, 900, 3600, 21600, 43200, 86400]
+"""AutoHSTS increase steps: 1min, 5min, 15min, 1h, 6h, 12h, 24h"""
+
+AUTOHSTS_PERMANENT = 31536000
+"""Value for the last max-age of HSTS"""
+
+AUTOHSTS_FREQ = 172800
+"""Minimum time since last increase to perform a new one: 48h"""
+
+MANAGED_COMMENT = "DO NOT REMOVE - Managed by Certbot"
+MANAGED_COMMENT_ID = MANAGED_COMMENT+", VirtualHost id: {0}"
+"""Managed by Certbot comments and the VirtualHost identification template"""
diff --git a/certbot-apache/certbot_apache/_internal/display_ops.py b/certbot-apache/certbot_apache/_internal/display_ops.py
new file mode 100644
index 000000000..1ae32bb47
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/display_ops.py
@@ -0,0 +1,125 @@
+"""Contains UI methods for Apache operations."""
+import logging
+
+import zope.component
+
+from certbot import errors
+from certbot import interfaces
+from certbot.compat import os
+import certbot.display.util as display_util
+
+logger = logging.getLogger(__name__)
+
+
+def select_vhost_multiple(vhosts):
+ """Select multiple Vhosts to install the certificate for
+
+ :param vhosts: Available Apache VirtualHosts
+ :type vhosts: :class:`list` of type `~obj.Vhost`
+
+ :returns: List of VirtualHosts
+ :rtype: :class:`list`of type `~obj.Vhost`
+ """
+ if not vhosts:
+ return list()
+ tags_list = [vhost.display_repr()+"\n" for vhost in vhosts]
+ # Remove the extra newline from the last entry
+ if tags_list:
+ tags_list[-1] = tags_list[-1][:-1]
+ code, names = zope.component.getUtility(interfaces.IDisplay).checklist(
+ "Which VirtualHosts would you like to install the wildcard certificate for?",
+ tags=tags_list, force_interactive=True)
+ if code == display_util.OK:
+ return_vhosts = _reversemap_vhosts(names, vhosts)
+ return return_vhosts
+ return []
+
+def _reversemap_vhosts(names, vhosts):
+ """Helper function for select_vhost_multiple for mapping string
+ representations back to actual vhost objects"""
+ return_vhosts = list()
+
+ for selection in names:
+ for vhost in vhosts:
+ if vhost.display_repr().strip() == selection.strip():
+ return_vhosts.append(vhost)
+ return return_vhosts
+
+def select_vhost(domain, vhosts):
+ """Select an appropriate Apache Vhost.
+
+ :param vhosts: Available Apache VirtualHosts
+ :type vhosts: :class:`list` of type `~obj.Vhost`
+
+ :returns: VirtualHost or `None`
+ :rtype: `~obj.Vhost` or `None`
+
+ """
+ if not vhosts:
+ return None
+ code, tag = _vhost_menu(domain, vhosts)
+ if code == display_util.OK:
+ return vhosts[tag]
+ return None
+
+def _vhost_menu(domain, vhosts):
+ """Select an appropriate Apache Vhost.
+
+ :param vhosts: Available Apache Virtual Hosts
+ :type vhosts: :class:`list` of type `~obj.Vhost`
+
+ :returns: Display tuple - ('code', tag')
+ :rtype: `tuple`
+
+ """
+ # Free characters in the line of display text (9 is for ' | ' formatting)
+ free_chars = display_util.WIDTH - len("HTTPS") - len("Enabled") - 9
+
+ if free_chars < 2:
+ logger.debug("Display size is too small for "
+ "certbot_apache._internal.display_ops._vhost_menu()")
+ # This runs the edge off the screen, but it doesn't cause an "error"
+ filename_size = 1
+ disp_name_size = 1
+ else:
+ # Filename is a bit more important and probably longer with 000-*
+ filename_size = int(free_chars * .6)
+ disp_name_size = free_chars - filename_size
+
+ choices = []
+ for vhost in vhosts:
+ if len(vhost.get_names()) == 1:
+ disp_name = next(iter(vhost.get_names()))
+ elif not vhost.get_names():
+ disp_name = ""
+ else:
+ disp_name = "Multiple Names"
+
+ choices.append(
+ "{fn:{fn_size}s} | {name:{name_size}s} | {https:5s} | "
+ "{active:7s}".format(
+ fn=os.path.basename(vhost.filep)[:filename_size],
+ name=disp_name[:disp_name_size],
+ https="HTTPS" if vhost.ssl else "",
+ active="Enabled" if vhost.enabled else "",
+ fn_size=filename_size,
+ name_size=disp_name_size)
+ )
+
+ try:
+ code, tag = zope.component.getUtility(interfaces.IDisplay).menu(
+ "We were unable to find a vhost with a ServerName "
+ "or Address of {0}.{1}Which virtual host would you "
+ "like to choose?".format(domain, os.linesep),
+ choices, force_interactive=True)
+ except errors.MissingCommandlineFlag:
+ msg = (
+ "Encountered vhost ambiguity when trying to find a vhost for "
+ "{0} but was unable to ask for user "
+ "guidance in non-interactive mode. Certbot may need "
+ "vhosts to be explicitly labelled with ServerName or "
+ "ServerAlias directives.".format(domain))
+ logger.warning(msg)
+ raise errors.MissingCommandlineFlag(msg)
+
+ return code, tag
diff --git a/certbot-apache/certbot_apache/_internal/entrypoint.py b/certbot-apache/certbot_apache/_internal/entrypoint.py
new file mode 100644
index 000000000..d43094976
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/entrypoint.py
@@ -0,0 +1,68 @@
+""" Entry point for Apache Plugin """
+# Pylint does not like disutils.version when running inside a venv.
+# See: https://github.com/PyCQA/pylint/issues/73
+from distutils.version import LooseVersion # pylint: disable=no-name-in-module,import-error
+
+from certbot import util
+from certbot_apache._internal import configurator
+from certbot_apache._internal import override_arch
+from certbot_apache._internal import override_centos
+from certbot_apache._internal import override_darwin
+from certbot_apache._internal import override_debian
+from certbot_apache._internal import override_fedora
+from certbot_apache._internal import override_gentoo
+from certbot_apache._internal import override_suse
+
+OVERRIDE_CLASSES = {
+ "arch": override_arch.ArchConfigurator,
+ "cloudlinux": override_centos.CentOSConfigurator,
+ "darwin": override_darwin.DarwinConfigurator,
+ "debian": override_debian.DebianConfigurator,
+ "ubuntu": override_debian.DebianConfigurator,
+ "centos": override_centos.CentOSConfigurator,
+ "centos linux": override_centos.CentOSConfigurator,
+ "fedora_old": override_centos.CentOSConfigurator,
+ "fedora": override_fedora.FedoraConfigurator,
+ "linuxmint": override_debian.DebianConfigurator,
+ "ol": override_centos.CentOSConfigurator,
+ "oracle": override_centos.CentOSConfigurator,
+ "redhatenterpriseserver": override_centos.CentOSConfigurator,
+ "red hat enterprise linux server": override_centos.CentOSConfigurator,
+ "rhel": override_centos.CentOSConfigurator,
+ "amazon": override_centos.CentOSConfigurator,
+ "gentoo": override_gentoo.GentooConfigurator,
+ "gentoo base system": override_gentoo.GentooConfigurator,
+ "opensuse": override_suse.OpenSUSEConfigurator,
+ "suse": override_suse.OpenSUSEConfigurator,
+ "sles": override_suse.OpenSUSEConfigurator,
+ "scientific": override_centos.CentOSConfigurator,
+ "scientific linux": override_centos.CentOSConfigurator,
+}
+
+
+def get_configurator():
+ """ Get correct configurator class based on the OS fingerprint """
+ os_name, os_version = util.get_os_info()
+ os_name = os_name.lower()
+ override_class = None
+
+ # Special case for older Fedora versions
+ if os_name == 'fedora' and LooseVersion(os_version) < LooseVersion('29'):
+ os_name = 'fedora_old'
+
+ try:
+ override_class = OVERRIDE_CLASSES[os_name]
+ except KeyError:
+ # OS not found in the list
+ os_like = util.get_systemd_os_like()
+ if os_like:
+ for os_name in os_like:
+ if os_name in OVERRIDE_CLASSES.keys():
+ override_class = OVERRIDE_CLASSES[os_name]
+ if not override_class:
+ # No override class found, return the generic configurator
+ override_class = configurator.ApacheConfigurator
+ return override_class
+
+
+ENTRYPOINT = get_configurator()
diff --git a/certbot-apache/certbot_apache/_internal/http_01.py b/certbot-apache/certbot_apache/_internal/http_01.py
new file mode 100644
index 000000000..c34abc2b4
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/http_01.py
@@ -0,0 +1,209 @@
+"""A class that performs HTTP-01 challenges for Apache"""
+import logging
+
+from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
+from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
+from certbot import errors
+from certbot.compat import filesystem
+from certbot.compat import os
+from certbot.plugins import common
+from certbot_apache._internal.obj import VirtualHost # pylint: disable=unused-import
+from certbot_apache._internal.parser import get_aug_path
+
+logger = logging.getLogger(__name__)
+
+
+class ApacheHttp01(common.ChallengePerformer):
+ """Class that performs HTTP-01 challenges within the Apache configurator."""
+
+ CONFIG_TEMPLATE22_PRE = """\
+ RewriteEngine on
+ RewriteRule ^/\\.well-known/acme-challenge/([A-Za-z0-9-_=]+)$ {0}/$1 [L]
+
+ """
+ CONFIG_TEMPLATE22_POST = """\
+ <Directory {0}>
+ Order Allow,Deny
+ Allow from all
+ </Directory>
+ <Location /.well-known/acme-challenge>
+ Order Allow,Deny
+ Allow from all
+ </Location>
+ """
+
+ CONFIG_TEMPLATE24_PRE = """\
+ RewriteEngine on
+ RewriteRule ^/\\.well-known/acme-challenge/([A-Za-z0-9-_=]+)$ {0}/$1 [END]
+ """
+ CONFIG_TEMPLATE24_POST = """\
+ <Directory {0}>
+ Require all granted
+ </Directory>
+ <Location /.well-known/acme-challenge>
+ Require all granted
+ </Location>
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(ApacheHttp01, self).__init__(*args, **kwargs)
+ self.challenge_conf_pre = os.path.join(
+ self.configurator.conf("challenge-location"),
+ "le_http_01_challenge_pre.conf")
+ self.challenge_conf_post = os.path.join(
+ self.configurator.conf("challenge-location"),
+ "le_http_01_challenge_post.conf")
+ self.challenge_dir = os.path.join(
+ self.configurator.config.work_dir,
+ "http_challenges")
+ self.moded_vhosts = set() # type: Set[VirtualHost]
+
+ def perform(self):
+ """Perform all HTTP-01 challenges."""
+ if not self.achalls:
+ return []
+ # Save any changes to the configuration as a precaution
+ # About to make temporary changes to the config
+ self.configurator.save("Changes before challenge setup", True)
+
+ self.configurator.ensure_listen(str(
+ self.configurator.config.http01_port))
+ self.prepare_http01_modules()
+
+ responses = self._set_up_challenges()
+
+ self._mod_config()
+ # Save reversible changes
+ self.configurator.save("HTTP Challenge", True)
+
+ return responses
+
+ def prepare_http01_modules(self):
+ """Make sure that we have the needed modules available for http01"""
+
+ if self.configurator.conf("handle-modules"):
+ needed_modules = ["rewrite"]
+ if self.configurator.version < (2, 4):
+ needed_modules.append("authz_host")
+ else:
+ needed_modules.append("authz_core")
+ for mod in needed_modules:
+ if mod + "_module" not in self.configurator.parser.modules:
+ self.configurator.enable_mod(mod, temp=True)
+
+ def _mod_config(self):
+ selected_vhosts = [] # type: List[VirtualHost]
+ http_port = str(self.configurator.config.http01_port)
+ for chall in self.achalls:
+ # Search for matching VirtualHosts
+ for vh in self._matching_vhosts(chall.domain):
+ selected_vhosts.append(vh)
+
+ # Ensure that we have one or more VirtualHosts that we can continue
+ # with. (one that listens to port configured with --http-01-port)
+ found = False
+ for vhost in selected_vhosts:
+ if any(a.is_wildcard() or a.get_port() == http_port for a in vhost.addrs):
+ found = True
+
+ if not found:
+ for vh in self._relevant_vhosts():
+ selected_vhosts.append(vh)
+
+ # Add the challenge configuration
+ for vh in selected_vhosts:
+ self._set_up_include_directives(vh)
+
+ self.configurator.reverter.register_file_creation(
+ True, self.challenge_conf_pre)
+ self.configurator.reverter.register_file_creation(
+ True, self.challenge_conf_post)
+
+ if self.configurator.version < (2, 4):
+ config_template_pre = self.CONFIG_TEMPLATE22_PRE
+ config_template_post = self.CONFIG_TEMPLATE22_POST
+ else:
+ config_template_pre = self.CONFIG_TEMPLATE24_PRE
+ config_template_post = self.CONFIG_TEMPLATE24_POST
+
+ config_text_pre = config_template_pre.format(self.challenge_dir)
+ config_text_post = config_template_post.format(self.challenge_dir)
+
+ logger.debug("writing a pre config file with text:\n %s", config_text_pre)
+ with open(self.challenge_conf_pre, "w") as new_conf:
+ new_conf.write(config_text_pre)
+ logger.debug("writing a post config file with text:\n %s", config_text_post)
+ with open(self.challenge_conf_post, "w") as new_conf:
+ new_conf.write(config_text_post)
+
+ def _matching_vhosts(self, domain):
+ """Return all VirtualHost objects that have the requested domain name or
+ a wildcard name that would match the domain in ServerName or ServerAlias
+ directive.
+ """
+ matching_vhosts = []
+ for vhost in self.configurator.vhosts:
+ if self.configurator.domain_in_names(vhost.get_names(), domain):
+ # domain_in_names also matches the exact names, so no need
+ # to check "domain in vhost.get_names()" explicitly here
+ matching_vhosts.append(vhost)
+
+ return matching_vhosts
+
+ def _relevant_vhosts(self):
+ http01_port = str(self.configurator.config.http01_port)
+ relevant_vhosts = []
+ for vhost in self.configurator.vhosts:
+ if any(a.is_wildcard() or a.get_port() == http01_port for a in vhost.addrs):
+ if not vhost.ssl:
+ relevant_vhosts.append(vhost)
+ if not relevant_vhosts:
+ raise errors.PluginError(
+ "Unable to find a virtual host listening on port {0} which is"
+ " currently needed for Certbot to prove to the CA that you"
+ " control your domain. Please add a virtual host for port"
+ " {0}.".format(http01_port))
+
+ return relevant_vhosts
+
+ def _set_up_challenges(self):
+ if not os.path.isdir(self.challenge_dir):
+ filesystem.makedirs(self.challenge_dir, 0o755)
+
+ responses = []
+ for achall in self.achalls:
+ responses.append(self._set_up_challenge(achall))
+
+ return responses
+
+ def _set_up_challenge(self, achall):
+ response, validation = achall.response_and_validation()
+
+ name = os.path.join(self.challenge_dir, achall.chall.encode("token"))
+
+ self.configurator.reverter.register_file_creation(True, name)
+ with open(name, 'wb') as f:
+ f.write(validation.encode())
+ filesystem.chmod(name, 0o644)
+
+ return response
+
+ def _set_up_include_directives(self, vhost):
+ """Includes override configuration to the beginning and to the end of
+ VirtualHost. Note that this include isn't added to Augeas search tree"""
+
+ if vhost not in self.moded_vhosts:
+ logger.debug(
+ "Adding a temporary challenge validation Include for name: %s in: %s",
+ vhost.name, vhost.filep)
+ self.configurator.parser.add_dir_beginning(
+ vhost.path, "Include", self.challenge_conf_pre)
+ self.configurator.parser.add_dir(
+ vhost.path, "Include", self.challenge_conf_post)
+
+ if not vhost.enabled:
+ self.configurator.parser.add_dir(
+ get_aug_path(self.configurator.parser.loc["default"]),
+ "Include", vhost.filep)
+
+ self.moded_vhosts.add(vhost)
diff --git a/certbot-apache/certbot_apache/_internal/obj.py b/certbot-apache/certbot_apache/_internal/obj.py
new file mode 100644
index 000000000..8b3aeb376
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/obj.py
@@ -0,0 +1,269 @@
+"""Module contains classes used by the Apache Configurator."""
+import re
+
+from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
+from certbot.plugins import common
+
+
+class Addr(common.Addr):
+ """Represents an Apache address."""
+
+ def __eq__(self, other):
+ """This is defined as equivalent within Apache.
+
+ ip_addr:* == ip_addr
+
+ """
+ if isinstance(other, self.__class__):
+ return ((self.tup == other.tup) or
+ (self.tup[0] == other.tup[0] and
+ self.is_wildcard() and other.is_wildcard()))
+ return False
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __repr__(self):
+ return "certbot_apache._internal.obj.Addr(" + repr(self.tup) + ")"
+
+ def __hash__(self): # pylint: disable=useless-super-delegation
+ # Python 3 requires explicit overridden for __hash__ if __eq__ or
+ # __cmp__ is overridden. See https://bugs.python.org/issue2235
+ return super(Addr, self).__hash__()
+
+ def _addr_less_specific(self, addr):
+ """Returns if addr.get_addr() is more specific than self.get_addr()."""
+ # pylint: disable=protected-access
+ return addr._rank_specific_addr() > self._rank_specific_addr()
+
+ def _rank_specific_addr(self):
+ """Returns numerical rank for get_addr()
+
+ :returns: 2 - FQ, 1 - wildcard, 0 - _default_
+ :rtype: int
+
+ """
+ if self.get_addr() == "_default_":
+ return 0
+ elif self.get_addr() == "*":
+ return 1
+ return 2
+
+ def conflicts(self, addr):
+ r"""Returns if address could conflict with correct function of self.
+
+ Could addr take away service provided by self within Apache?
+
+ .. note::IP Address is more important than wildcard.
+ Connection from 127.0.0.1:80 with choices of *:80 and 127.0.0.1:*
+ chooses 127.0.0.1:\*
+
+ .. todo:: Handle domain name addrs...
+
+ Examples:
+
+ ========================================= =====
+ ``127.0.0.1:\*.conflicts(127.0.0.1:443)`` True
+ ``127.0.0.1:443.conflicts(127.0.0.1:\*)`` False
+ ``\*:443.conflicts(\*:80)`` False
+ ``_default_:443.conflicts(\*:443)`` True
+ ========================================= =====
+
+ """
+ if self._addr_less_specific(addr):
+ return True
+ elif self.get_addr() == addr.get_addr():
+ if self.is_wildcard() or self.get_port() == addr.get_port():
+ return True
+ return False
+
+ def is_wildcard(self):
+ """Returns if address has a wildcard port."""
+ return self.tup[1] == "*" or not self.tup[1]
+
+ def get_sni_addr(self, port):
+ """Returns the least specific address that resolves on the port.
+
+ Examples:
+
+ - ``1.2.3.4:443`` -> ``1.2.3.4:<port>``
+ - ``1.2.3.4:*`` -> ``1.2.3.4:*``
+
+ :param str port: Desired port
+
+ """
+ if self.is_wildcard():
+ return self
+
+ return self.get_addr_obj(port)
+
+
+class VirtualHost(object):
+ """Represents an Apache Virtualhost.
+
+ :ivar str filep: file path of VH
+ :ivar str path: Augeas path to virtual host
+ :ivar set addrs: Virtual Host addresses (:class:`set` of
+ :class:`common.Addr`)
+ :ivar str name: ServerName of VHost
+ :ivar list aliases: Server aliases of vhost
+ (:class:`list` of :class:`str`)
+
+ :ivar bool ssl: SSLEngine on in vhost
+ :ivar bool enabled: Virtual host is enabled
+ :ivar bool modmacro: VirtualHost is using mod_macro
+ :ivar VirtualHost ancestor: A non-SSL VirtualHost this is based on
+
+ https://httpd.apache.org/docs/2.4/vhosts/details.html
+
+ .. todo:: Any vhost that includes the magic _default_ wildcard is given the
+ same ServerName as the main server.
+
+ """
+ # ?: is used for not returning enclosed characters
+ strip_name = re.compile(r"^(?:.+://)?([^ :$]*)")
+
+ def __init__(self, filep, path, addrs, ssl, enabled, name=None,
+ aliases=None, modmacro=False, ancestor=None):
+
+ """Initialize a VH."""
+ self.filep = filep
+ self.path = path
+ self.addrs = addrs
+ self.name = name
+ self.aliases = aliases if aliases is not None else set()
+ self.ssl = ssl
+ self.enabled = enabled
+ self.modmacro = modmacro
+ self.ancestor = ancestor
+
+ def get_names(self):
+ """Return a set of all names."""
+ all_names = set() # type: Set[str]
+ all_names.update(self.aliases)
+ # Strip out any scheme:// and <port> field from servername
+ if self.name is not None:
+ all_names.add(VirtualHost.strip_name.findall(self.name)[0])
+
+ return all_names
+
+ def __str__(self):
+ return (
+ "File: {filename}\n"
+ "Vhost path: {vhpath}\n"
+ "Addresses: {addrs}\n"
+ "Name: {name}\n"
+ "Aliases: {aliases}\n"
+ "TLS Enabled: {tls}\n"
+ "Site Enabled: {active}\n"
+ "mod_macro Vhost: {modmacro}".format(
+ filename=self.filep,
+ vhpath=self.path,
+ addrs=", ".join(str(addr) for addr in self.addrs),
+ name=self.name if self.name is not None else "",
+ aliases=", ".join(name for name in self.aliases),
+ tls="Yes" if self.ssl else "No",
+ active="Yes" if self.enabled else "No",
+ modmacro="Yes" if self.modmacro else "No"))
+
+ def display_repr(self):
+ """Return a representation of VHost to be used in dialog"""
+ return (
+ "File: {filename}\n"
+ "Addresses: {addrs}\n"
+ "Names: {names}\n"
+ "HTTPS: {https}\n".format(
+ filename=self.filep,
+ addrs=", ".join(str(addr) for addr in self.addrs),
+ names=", ".join(self.get_names()),
+ https="Yes" if self.ssl else "No"))
+
+
+ def __eq__(self, other):
+ if isinstance(other, self.__class__):
+ return (self.filep == other.filep and self.path == other.path and
+ self.addrs == other.addrs and
+ self.get_names() == other.get_names() and
+ self.ssl == other.ssl and
+ self.enabled == other.enabled and
+ self.modmacro == other.modmacro)
+
+ return False
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __hash__(self):
+ return hash((self.filep, self.path,
+ tuple(self.addrs), tuple(self.get_names()),
+ self.ssl, self.enabled, self.modmacro))
+
+ def conflicts(self, addrs):
+ """See if vhost conflicts with any of the addrs.
+
+ This determines whether or not these addresses would/could overwrite
+ the vhost addresses.
+
+ :param addrs: Iterable Addresses
+ :type addrs: Iterable :class:~obj.Addr
+
+ :returns: If addresses conflicts with vhost
+ :rtype: bool
+
+ """
+ for pot_addr in addrs:
+ for addr in self.addrs:
+ if addr.conflicts(pot_addr):
+ return True
+ return False
+
+ def same_server(self, vhost, generic=False):
+ """Determines if the vhost is the same 'server'.
+
+ Used in redirection - indicates whether or not the two virtual hosts
+ serve on the exact same IP combinations, but different ports.
+ The generic flag indicates that that we're trying to match to a
+ default or generic vhost
+
+ .. todo:: Handle _default_
+
+ """
+
+ if not generic:
+ if vhost.get_names() != self.get_names():
+ return False
+
+ # If equal and set is not empty... assume same server
+ if self.name is not None or self.aliases:
+ return True
+ # If we're looking for a generic vhost,
+ # don't return one with a ServerName
+ elif self.name:
+ return False
+
+ # Both sets of names are empty.
+
+ # Make conservative educated guess... this is very restrictive
+ # Consider adding more safety checks.
+ if len(vhost.addrs) != len(self.addrs):
+ return False
+
+ # already_found acts to keep everything very conservative.
+ # Don't allow multiple ip:ports in same set.
+ already_found = set() # type: Set[str]
+
+ for addr in vhost.addrs:
+ for local_addr in self.addrs:
+ if (local_addr.get_addr() == addr.get_addr() and
+ local_addr != addr and
+ local_addr.get_addr() not in already_found):
+
+ # This intends to make sure we aren't double counting...
+ # e.g. 127.0.0.1:* - We require same number of addrs
+ # currently
+ already_found.add(local_addr.get_addr())
+ break
+ else:
+ return False
+
+ return True
diff --git a/certbot-apache/certbot_apache/_internal/options-ssl-apache.conf b/certbot-apache/certbot_apache/_internal/options-ssl-apache.conf
new file mode 100644
index 000000000..1a3799628
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/options-ssl-apache.conf
@@ -0,0 +1,18 @@
+# This file contains important security parameters. If you modify this file
+# manually, Certbot will be unable to automatically provide future security
+# updates. Instead, Certbot will print and log an error message with a path to
+# the up-to-date file that you will need to refer to when manually updating
+# this file.
+
+SSLEngine on
+
+# Intermediate configuration, tweak to your needs
+SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
+SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
+SSLHonorCipherOrder off
+
+SSLOptions +StrictRequire
+
+# Add vhost name to log entries:
+LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" vhost_combined
+LogFormat "%v %h %l %u %t \"%r\" %>s %b" vhost_common
diff --git a/certbot-apache/certbot_apache/_internal/override_arch.py b/certbot-apache/certbot_apache/_internal/override_arch.py
new file mode 100644
index 000000000..2765bd238
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/override_arch.py
@@ -0,0 +1,31 @@
+""" Distribution specific override class for Arch Linux """
+import pkg_resources
+import zope.interface
+
+from certbot import interfaces
+from certbot.compat import os
+from certbot_apache._internal import configurator
+
+
+@zope.interface.provider(interfaces.IPluginFactory)
+class ArchConfigurator(configurator.ApacheConfigurator):
+ """Arch Linux specific ApacheConfigurator override class"""
+
+ OS_DEFAULTS = dict(
+ server_root="/etc/httpd",
+ vhost_root="/etc/httpd/conf",
+ vhost_files="*.conf",
+ logs_root="/var/log/httpd",
+ ctl="apachectl",
+ version_cmd=['apachectl', '-v'],
+ restart_cmd=['apachectl', 'graceful'],
+ conftest_cmd=['apachectl', 'configtest'],
+ enmod=None,
+ dismod=None,
+ le_vhost_ext="-le-ssl.conf",
+ handle_modules=False,
+ handle_sites=False,
+ challenge_location="/etc/httpd/conf",
+ MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
+ "certbot_apache", os.path.join("_internal", "options-ssl-apache.conf"))
+ )
diff --git a/certbot-apache/certbot_apache/_internal/override_centos.py b/certbot-apache/certbot_apache/_internal/override_centos.py
new file mode 100644
index 000000000..a3ef2d760
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/override_centos.py
@@ -0,0 +1,215 @@
+""" Distribution specific override class for CentOS family (RHEL, Fedora) """
+import logging
+
+import pkg_resources
+import zope.interface
+
+from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
+from certbot import errors
+from certbot import interfaces
+from certbot import util
+from certbot.compat import os
+from certbot.errors import MisconfigurationError
+from certbot_apache._internal import apache_util
+from certbot_apache._internal import configurator
+from certbot_apache._internal import parser
+
+logger = logging.getLogger(__name__)
+
+
+@zope.interface.provider(interfaces.IPluginFactory)
+class CentOSConfigurator(configurator.ApacheConfigurator):
+ """CentOS specific ApacheConfigurator override class"""
+
+ OS_DEFAULTS = dict(
+ server_root="/etc/httpd",
+ vhost_root="/etc/httpd/conf.d",
+ vhost_files="*.conf",
+ logs_root="/var/log/httpd",
+ ctl="apachectl",
+ version_cmd=['apachectl', '-v'],
+ restart_cmd=['apachectl', 'graceful'],
+ restart_cmd_alt=['apachectl', 'restart'],
+ conftest_cmd=['apachectl', 'configtest'],
+ enmod=None,
+ dismod=None,
+ le_vhost_ext="-le-ssl.conf",
+ handle_modules=False,
+ handle_sites=False,
+ challenge_location="/etc/httpd/conf.d",
+ MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
+ "certbot_apache", os.path.join("_internal", "options-ssl-apache.conf"))
+ )
+
+ def config_test(self):
+ """
+ Override config_test to mitigate configtest error in vanilla installation
+ of mod_ssl in Fedora. The error is caused by non-existent self-signed
+ certificates referenced by the configuration, that would be autogenerated
+ during the first (re)start of httpd.
+ """
+
+ os_info = util.get_os_info()
+ fedora = os_info[0].lower() == "fedora"
+
+ try:
+ super(CentOSConfigurator, self).config_test()
+ except errors.MisconfigurationError:
+ if fedora:
+ self._try_restart_fedora()
+ else:
+ raise
+
+ def _try_restart_fedora(self):
+ """
+ Tries to restart httpd using systemctl to generate the self signed keypair.
+ """
+
+ try:
+ util.run_script(['systemctl', 'restart', 'httpd'])
+ except errors.SubprocessError as err:
+ raise errors.MisconfigurationError(str(err))
+
+ # Finish with actual config check to see if systemctl restart helped
+ super(CentOSConfigurator, self).config_test()
+
+ def _prepare_options(self):
+ """
+ Override the options dictionary initialization in order to support
+ alternative restart cmd used in CentOS.
+ """
+ super(CentOSConfigurator, self)._prepare_options()
+ self.options["restart_cmd_alt"][0] = self.option("ctl")
+
+ def get_parser(self):
+ """Initializes the ApacheParser"""
+ return CentOSParser(
+ self.option("server_root"), self.option("vhost_root"),
+ self.version, configurator=self)
+
+ def _deploy_cert(self, *args, **kwargs): # pylint: disable=arguments-differ
+ """
+ Override _deploy_cert in order to ensure that the Apache configuration
+ has "LoadModule ssl_module..." before parsing the VirtualHost configuration
+ that was created by Certbot
+ """
+ super(CentOSConfigurator, self)._deploy_cert(*args, **kwargs)
+ if self.version < (2, 4, 0):
+ self._deploy_loadmodule_ssl_if_needed()
+
+ def _deploy_loadmodule_ssl_if_needed(self):
+ """
+ Add "LoadModule ssl_module <pre-existing path>" to main httpd.conf if
+ it doesn't exist there already.
+ """
+
+ loadmods = self.parser.find_dir("LoadModule", "ssl_module", exclude=False)
+
+ correct_ifmods = [] # type: List[str]
+ loadmod_args = [] # type: List[str]
+ loadmod_paths = [] # type: List[str]
+ for m in loadmods:
+ noarg_path = m.rpartition("/")[0]
+ path_args = self.parser.get_all_args(noarg_path)
+ if loadmod_args:
+ if loadmod_args != path_args:
+ msg = ("Certbot encountered multiple LoadModule directives "
+ "for LoadModule ssl_module with differing library paths. "
+ "Please remove or comment out the one(s) that are not in "
+ "use, and run Certbot again.")
+ raise MisconfigurationError(msg)
+ else:
+ loadmod_args = path_args
+
+ if self.parser.not_modssl_ifmodule(noarg_path): # pylint: disable=no-member
+ if self.parser.loc["default"] in noarg_path:
+ # LoadModule already in the main configuration file
+ if ("ifmodule/" in noarg_path.lower() or
+ "ifmodule[1]" in noarg_path.lower()):
+ # It's the first or only IfModule in the file
+ return
+ # Populate the list of known !mod_ssl.c IfModules
+ nodir_path = noarg_path.rpartition("/directive")[0]
+ correct_ifmods.append(nodir_path)
+ else:
+ loadmod_paths.append(noarg_path)
+
+ if not loadmod_args:
+ # Do not try to enable mod_ssl
+ return
+
+ # Force creation as the directive wasn't found from the beginning of
+ # httpd.conf
+ rootconf_ifmod = self.parser.create_ifmod(
+ parser.get_aug_path(self.parser.loc["default"]),
+ "!mod_ssl.c", beginning=True)
+ # parser.get_ifmod returns a path postfixed with "/", remove that
+ self.parser.add_dir(rootconf_ifmod[:-1], "LoadModule", loadmod_args)
+ correct_ifmods.append(rootconf_ifmod[:-1])
+ self.save_notes += "Added LoadModule ssl_module to main configuration.\n"
+
+ # Wrap LoadModule mod_ssl inside of <IfModule !mod_ssl.c> if it's not
+ # configured like this already.
+ for loadmod_path in loadmod_paths:
+ nodir_path = loadmod_path.split("/directive")[0]
+ # Remove the old LoadModule directive
+ self.parser.aug.remove(loadmod_path)
+
+ # Create a new IfModule !mod_ssl.c if not already found on path
+ ssl_ifmod = self.parser.get_ifmod(nodir_path, "!mod_ssl.c",
+ beginning=True)[:-1]
+ if ssl_ifmod not in correct_ifmods:
+ self.parser.add_dir(ssl_ifmod, "LoadModule", loadmod_args)
+ correct_ifmods.append(ssl_ifmod)
+ self.save_notes += ("Wrapped pre-existing LoadModule ssl_module "
+ "inside of <IfModule !mod_ssl> block.\n")
+
+
+class CentOSParser(parser.ApacheParser):
+ """CentOS specific ApacheParser override class"""
+ def __init__(self, *args, **kwargs):
+ # CentOS specific configuration file for Apache
+ self.sysconfig_filep = "/etc/sysconfig/httpd"
+ super(CentOSParser, self).__init__(*args, **kwargs)
+
+ def update_runtime_variables(self):
+ """ Override for update_runtime_variables for custom parsing """
+ # Opportunistic, works if SELinux not enforced
+ super(CentOSParser, self).update_runtime_variables()
+ self.parse_sysconfig_var()
+
+ def parse_sysconfig_var(self):
+ """ Parses Apache CLI options from CentOS configuration file """
+ defines = apache_util.parse_define_file(self.sysconfig_filep, "OPTIONS")
+ for k in defines:
+ self.variables[k] = defines[k]
+
+ def not_modssl_ifmodule(self, path):
+ """Checks if the provided Augeas path has argument !mod_ssl"""
+
+ if "ifmodule" not in path.lower():
+ return False
+
+ # Trim the path to the last ifmodule
+ workpath = path.lower()
+ while workpath:
+ # Get path to the last IfModule (ignore the tail)
+ parts = workpath.rpartition("ifmodule")
+
+ if not parts[0]:
+ # IfModule not found
+ break
+ ifmod_path = parts[0] + parts[1]
+ # Check if ifmodule had an index
+ if parts[2].startswith("["):
+ # Append the index from tail
+ ifmod_path += parts[2].partition("/")[0]
+ # Get the original path trimmed to correct length
+ # This is required to preserve cases
+ ifmod_real_path = path[0:len(ifmod_path)]
+ if "!mod_ssl.c" in self.get_all_args(ifmod_real_path):
+ return True
+ # Set the workpath to the heading part
+ workpath = parts[0]
+
+ return False
diff --git a/certbot-apache/certbot_apache/_internal/override_darwin.py b/certbot-apache/certbot_apache/_internal/override_darwin.py
new file mode 100644
index 000000000..00faff623
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/override_darwin.py
@@ -0,0 +1,31 @@
+""" Distribution specific override class for macOS """
+import pkg_resources
+import zope.interface
+
+from certbot import interfaces
+from certbot.compat import os
+from certbot_apache._internal import configurator
+
+
+@zope.interface.provider(interfaces.IPluginFactory)
+class DarwinConfigurator(configurator.ApacheConfigurator):
+ """macOS specific ApacheConfigurator override class"""
+
+ OS_DEFAULTS = dict(
+ server_root="/etc/apache2",
+ vhost_root="/etc/apache2/other",
+ vhost_files="*.conf",
+ logs_root="/var/log/apache2",
+ ctl="apachectl",
+ version_cmd=['apachectl', '-v'],
+ restart_cmd=['apachectl', 'graceful'],
+ conftest_cmd=['apachectl', 'configtest'],
+ enmod=None,
+ dismod=None,
+ le_vhost_ext="-le-ssl.conf",
+ handle_modules=False,
+ handle_sites=False,
+ challenge_location="/etc/apache2/other",
+ MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
+ "certbot_apache", os.path.join("_internal", "options-ssl-apache.conf"))
+ )
diff --git a/certbot-apache/certbot_apache/_internal/override_debian.py b/certbot-apache/certbot_apache/_internal/override_debian.py
new file mode 100644
index 000000000..77ced6a3f
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/override_debian.py
@@ -0,0 +1,144 @@
+""" Distribution specific override class for Debian family (Ubuntu/Debian) """
+import logging
+
+import pkg_resources
+import zope.interface
+
+from certbot import errors
+from certbot import interfaces
+from certbot import util
+from certbot.compat import filesystem
+from certbot.compat import os
+from certbot_apache._internal import apache_util
+from certbot_apache._internal import configurator
+
+logger = logging.getLogger(__name__)
+
+
+@zope.interface.provider(interfaces.IPluginFactory)
+class DebianConfigurator(configurator.ApacheConfigurator):
+ """Debian specific ApacheConfigurator override class"""
+
+ OS_DEFAULTS = dict(
+ server_root="/etc/apache2",
+ vhost_root="/etc/apache2/sites-available",
+ vhost_files="*",
+ logs_root="/var/log/apache2",
+ ctl="apache2ctl",
+ version_cmd=['apache2ctl', '-v'],
+ restart_cmd=['apache2ctl', 'graceful'],
+ conftest_cmd=['apache2ctl', 'configtest'],
+ enmod="a2enmod",
+ dismod="a2dismod",
+ le_vhost_ext="-le-ssl.conf",
+ handle_modules=True,
+ handle_sites=True,
+ challenge_location="/etc/apache2",
+ MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
+ "certbot_apache", os.path.join("_internal", "options-ssl-apache.conf"))
+ )
+
+ def enable_site(self, vhost):
+ """Enables an available site, Apache reload required.
+
+ .. note:: Does not make sure that the site correctly works or that all
+ modules are enabled appropriately.
+
+ :param vhost: vhost to enable
+ :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`
+
+ :raises .errors.NotSupportedError: If filesystem layout is not
+ supported.
+
+ """
+ if vhost.enabled:
+ return None
+
+ enabled_path = ("%s/sites-enabled/%s" %
+ (self.parser.root,
+ os.path.basename(vhost.filep)))
+ if not os.path.isdir(os.path.dirname(enabled_path)):
+ # For some reason, sites-enabled / sites-available do not exist
+ # Call the parent method
+ return super(DebianConfigurator, self).enable_site(vhost)
+ self.reverter.register_file_creation(False, enabled_path)
+ try:
+ os.symlink(vhost.filep, enabled_path)
+ except OSError as err:
+ if os.path.islink(enabled_path) and filesystem.realpath(
+ enabled_path) == vhost.filep:
+ # Already in shape
+ vhost.enabled = True
+ return None
+ logger.warning(
+ "Could not symlink %s to %s, got error: %s", enabled_path,
+ vhost.filep, err.strerror)
+ errstring = ("Encountered error while trying to enable a " +
+ "newly created VirtualHost located at {0} by " +
+ "linking to it from {1}")
+ raise errors.NotSupportedError(errstring.format(vhost.filep,
+ enabled_path))
+ vhost.enabled = True
+ logger.info("Enabling available site: %s", vhost.filep)
+ self.save_notes += "Enabled site %s\n" % vhost.filep
+ return None
+
+ def enable_mod(self, mod_name, temp=False):
+ """Enables module in Apache.
+
+ Both enables and reloads Apache so module is active.
+
+ :param str mod_name: Name of the module to enable. (e.g. 'ssl')
+ :param bool temp: Whether or not this is a temporary action.
+
+ :raises .errors.NotSupportedError: If the filesystem layout is not
+ supported.
+ :raises .errors.MisconfigurationError: If a2enmod or a2dismod cannot be
+ run.
+
+ """
+ avail_path = os.path.join(self.parser.root, "mods-available")
+ enabled_path = os.path.join(self.parser.root, "mods-enabled")
+ if not os.path.isdir(avail_path) or not os.path.isdir(enabled_path):
+ raise errors.NotSupportedError(
+ "Unsupported directory layout. You may try to enable mod %s "
+ "and try again." % mod_name)
+
+ deps = apache_util.get_mod_deps(mod_name)
+
+ # Enable all dependencies
+ for dep in deps:
+ if (dep + "_module") not in self.parser.modules:
+ self._enable_mod_debian(dep, temp)
+ self.parser.add_mod(dep)
+ note = "Enabled dependency of %s module - %s" % (mod_name, dep)
+ if not temp:
+ self.save_notes += note + os.linesep
+ logger.debug(note)
+
+ # Enable actual module
+ self._enable_mod_debian(mod_name, temp)
+ self.parser.add_mod(mod_name)
+
+ if not temp:
+ self.save_notes += "Enabled %s module in Apache\n" % mod_name
+ logger.info("Enabled Apache %s module", mod_name)
+
+ # Modules can enable additional config files. Variables may be defined
+ # within these new configuration sections.
+ # Reload is not necessary as DUMP_RUN_CFG uses latest config.
+ self.parser.update_runtime_variables()
+
+ def _enable_mod_debian(self, mod_name, temp):
+ """Assumes mods-available, mods-enabled layout."""
+ # Generate reversal command.
+ # Try to be safe here... check that we can probably reverse before
+ # applying enmod command
+ if not util.exe_exists(self.option("dismod")):
+ raise errors.MisconfigurationError(
+ "Unable to find a2dismod, please make sure a2enmod and "
+ "a2dismod are configured correctly for certbot.")
+
+ self.reverter.register_undo_command(
+ temp, [self.option("dismod"), "-f", mod_name])
+ util.run_script([self.option("enmod"), mod_name])
diff --git a/certbot-apache/certbot_apache/_internal/override_fedora.py b/certbot-apache/certbot_apache/_internal/override_fedora.py
new file mode 100644
index 000000000..8197b0dcd
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/override_fedora.py
@@ -0,0 +1,98 @@
+""" Distribution specific override class for Fedora 29+ """
+import pkg_resources
+import zope.interface
+
+from certbot import errors
+from certbot import interfaces
+from certbot import util
+from certbot.compat import os
+from certbot_apache._internal import apache_util
+from certbot_apache._internal import configurator
+from certbot_apache._internal import parser
+
+
+@zope.interface.provider(interfaces.IPluginFactory)
+class FedoraConfigurator(configurator.ApacheConfigurator):
+ """Fedora 29+ specific ApacheConfigurator override class"""
+
+ OS_DEFAULTS = dict(
+ server_root="/etc/httpd",
+ vhost_root="/etc/httpd/conf.d",
+ vhost_files="*.conf",
+ logs_root="/var/log/httpd",
+ ctl="httpd",
+ version_cmd=['httpd', '-v'],
+ restart_cmd=['apachectl', 'graceful'],
+ restart_cmd_alt=['apachectl', 'restart'],
+ conftest_cmd=['apachectl', 'configtest'],
+ enmod=None,
+ dismod=None,
+ le_vhost_ext="-le-ssl.conf",
+ handle_modules=False,
+ handle_sites=False,
+ challenge_location="/etc/httpd/conf.d",
+ MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
+ # TODO: eventually newest version of Fedora will need their own config
+ "certbot_apache", os.path.join("_internal", "options-ssl-apache.conf"))
+ )
+
+ def config_test(self):
+ """
+ Override config_test to mitigate configtest error in vanilla installation
+ of mod_ssl in Fedora. The error is caused by non-existent self-signed
+ certificates referenced by the configuration, that would be autogenerated
+ during the first (re)start of httpd.
+ """
+ try:
+ super(FedoraConfigurator, self).config_test()
+ except errors.MisconfigurationError:
+ self._try_restart_fedora()
+
+ def get_parser(self):
+ """Initializes the ApacheParser"""
+ return FedoraParser(
+ self.option("server_root"), self.option("vhost_root"),
+ self.version, configurator=self)
+
+ def _try_restart_fedora(self):
+ """
+ Tries to restart httpd using systemctl to generate the self signed keypair.
+ """
+ try:
+ util.run_script(['systemctl', 'restart', 'httpd'])
+ except errors.SubprocessError as err:
+ raise errors.MisconfigurationError(str(err))
+
+ # Finish with actual config check to see if systemctl restart helped
+ super(FedoraConfigurator, self).config_test()
+
+ def _prepare_options(self):
+ """
+ Override the options dictionary initialization to keep using apachectl
+ instead of httpd and so take advantages of this new bash script in newer versions
+ of Fedora to restart httpd.
+ """
+ super(FedoraConfigurator, self)._prepare_options()
+ self.options["restart_cmd"][0] = 'apachectl'
+ self.options["restart_cmd_alt"][0] = 'apachectl'
+ self.options["conftest_cmd"][0] = 'apachectl'
+
+
+class FedoraParser(parser.ApacheParser):
+ """Fedora 29+ specific ApacheParser override class"""
+ def __init__(self, *args, **kwargs):
+ # Fedora 29+ specific configuration file for Apache
+ self.sysconfig_filep = "/etc/sysconfig/httpd"
+ super(FedoraParser, self).__init__(*args, **kwargs)
+
+ def update_runtime_variables(self):
+ """ Override for update_runtime_variables for custom parsing """
+ # Opportunistic, works if SELinux not enforced
+ super(FedoraParser, self).update_runtime_variables()
+ self._parse_sysconfig_var()
+
+ def _parse_sysconfig_var(self):
+ """ Parses Apache CLI options from Fedora configuration file """
+ defines = apache_util.parse_define_file(self.sysconfig_filep, "OPTIONS")
+ for k in defines:
+ self.variables[k] = defines[k]
diff --git a/certbot-apache/certbot_apache/_internal/override_gentoo.py b/certbot-apache/certbot_apache/_internal/override_gentoo.py
new file mode 100644
index 000000000..38f8aebe9
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/override_gentoo.py
@@ -0,0 +1,75 @@
+""" Distribution specific override class for Gentoo Linux """
+import pkg_resources
+import zope.interface
+
+from certbot import interfaces
+from certbot.compat import os
+from certbot_apache._internal import apache_util
+from certbot_apache._internal import configurator
+from certbot_apache._internal import parser
+
+
+@zope.interface.provider(interfaces.IPluginFactory)
+class GentooConfigurator(configurator.ApacheConfigurator):
+ """Gentoo specific ApacheConfigurator override class"""
+
+ OS_DEFAULTS = dict(
+ server_root="/etc/apache2",
+ vhost_root="/etc/apache2/vhosts.d",
+ vhost_files="*.conf",
+ logs_root="/var/log/apache2",
+ ctl="apache2ctl",
+ version_cmd=['apache2ctl', '-v'],
+ restart_cmd=['apache2ctl', 'graceful'],
+ restart_cmd_alt=['apache2ctl', 'restart'],
+ conftest_cmd=['apache2ctl', 'configtest'],
+ enmod=None,
+ dismod=None,
+ le_vhost_ext="-le-ssl.conf",
+ handle_modules=False,
+ handle_sites=False,
+ challenge_location="/etc/apache2/vhosts.d",
+ MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
+ "certbot_apache", os.path.join("_internal", "options-ssl-apache.conf"))
+ )
+
+ def _prepare_options(self):
+ """
+ Override the options dictionary initialization in order to support
+ alternative restart cmd used in Gentoo.
+ """
+ super(GentooConfigurator, self)._prepare_options()
+ self.options["restart_cmd_alt"][0] = self.option("ctl")
+
+ def get_parser(self):
+ """Initializes the ApacheParser"""
+ return GentooParser(
+ self.option("server_root"), self.option("vhost_root"),
+ self.version, configurator=self)
+
+
+class GentooParser(parser.ApacheParser):
+ """Gentoo specific ApacheParser override class"""
+ def __init__(self, *args, **kwargs):
+ # Gentoo specific configuration file for Apache2
+ self.apacheconfig_filep = "/etc/conf.d/apache2"
+ super(GentooParser, self).__init__(*args, **kwargs)
+
+ def update_runtime_variables(self):
+ """ Override for update_runtime_variables for custom parsing """
+ self.parse_sysconfig_var()
+ self.update_modules()
+
+ def parse_sysconfig_var(self):
+ """ Parses Apache CLI options from Gentoo configuration file """
+ defines = apache_util.parse_define_file(self.apacheconfig_filep,
+ "APACHE2_OPTS")
+ for k in defines:
+ self.variables[k] = defines[k]
+
+ def update_modules(self):
+ """Get loaded modules from httpd process, and add them to DOM"""
+ mod_cmd = [self.configurator.option("ctl"), "modules"]
+ matches = self.parse_from_subprocess(mod_cmd, r"(.*)_module")
+ for mod in matches:
+ self.add_mod(mod.strip())
diff --git a/certbot-apache/certbot_apache/_internal/override_suse.py b/certbot-apache/certbot_apache/_internal/override_suse.py
new file mode 100644
index 000000000..0c9219e6d
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/override_suse.py
@@ -0,0 +1,31 @@
+""" Distribution specific override class for OpenSUSE """
+import pkg_resources
+import zope.interface
+
+from certbot import interfaces
+from certbot.compat import os
+from certbot_apache._internal import configurator
+
+
+@zope.interface.provider(interfaces.IPluginFactory)
+class OpenSUSEConfigurator(configurator.ApacheConfigurator):
+ """OpenSUSE specific ApacheConfigurator override class"""
+
+ OS_DEFAULTS = dict(
+ server_root="/etc/apache2",
+ vhost_root="/etc/apache2/vhosts.d",
+ vhost_files="*.conf",
+ logs_root="/var/log/apache2",
+ ctl="apache2ctl",
+ version_cmd=['apache2ctl', '-v'],
+ restart_cmd=['apache2ctl', 'graceful'],
+ conftest_cmd=['apache2ctl', 'configtest'],
+ enmod="a2enmod",
+ dismod="a2dismod",
+ le_vhost_ext="-le-ssl.conf",
+ handle_modules=False,
+ handle_sites=False,
+ challenge_location="/etc/apache2/vhosts.d",
+ MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
+ "certbot_apache", os.path.join("_internal", "options-ssl-apache.conf"))
+ )
diff --git a/certbot-apache/certbot_apache/_internal/parser.py b/certbot-apache/certbot_apache/_internal/parser.py
new file mode 100644
index 000000000..0703b8fb5
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/parser.py
@@ -0,0 +1,1008 @@
+"""ApacheParser is a member object of the ApacheConfigurator class."""
+import copy
+import fnmatch
+import logging
+import re
+import subprocess
+import sys
+
+import six
+
+from acme.magic_typing import Dict # pylint: disable=unused-import, no-name-in-module
+from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
+from acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
+from certbot import errors
+from certbot.compat import os
+from certbot_apache._internal import constants
+
+logger = logging.getLogger(__name__)
+
+
+class ApacheParser(object):
+ """Class handles the fine details of parsing the Apache Configuration.
+
+ .. todo:: Make parsing general... remove sites-available etc...
+
+ :ivar str root: Normalized absolute path to the server root
+ directory. Without trailing slash.
+ :ivar set modules: All module names that are currently enabled.
+ :ivar dict loc: Location to place directives, root - configuration origin,
+ default - user config file, name - NameVirtualHost,
+
+ """
+ arg_var_interpreter = re.compile(r"\$\{[^ \}]*}")
+ fnmatch_chars = set(["*", "?", "\\", "[", "]"])
+
+ def __init__(self, root, vhostroot=None, version=(2, 4),
+ configurator=None):
+ # Note: Order is important here.
+
+ # Needed for calling save() with reverter functionality that resides in
+ # AugeasConfigurator superclass of ApacheConfigurator. This resolves
+ # issues with aug.load() after adding new files / defines to parse tree
+ self.configurator = configurator
+
+ # Initialize augeas
+ self.aug = None
+ self.init_augeas()
+
+ if not self.check_aug_version():
+ raise errors.NotSupportedError(
+ "Apache plugin support requires libaugeas0 and augeas-lenses "
+ "version 1.2.0 or higher, please make sure you have you have "
+ "those installed.")
+
+ self.modules = set() # type: Set[str]
+ self.parser_paths = {} # type: Dict[str, List[str]]
+ self.variables = {} # type: Dict[str, str]
+
+ # Find configuration root and make sure augeas can parse it.
+ self.root = os.path.abspath(root)
+ self.loc = {"root": self._find_config_root()}
+ self.parse_file(self.loc["root"])
+
+ if version >= (2, 4):
+ # Look up variables from httpd and add to DOM if not already parsed
+ self.update_runtime_variables()
+
+ # This problem has been fixed in Augeas 1.0
+ self.standardize_excl()
+
+ # Parse LoadModule directives from configuration files
+ self.parse_modules()
+
+ # Set up rest of locations
+ self.loc.update(self._set_locations())
+
+ # list of the active include paths, before modifications
+ self.existing_paths = copy.deepcopy(self.parser_paths)
+
+ # Must also attempt to parse additional virtual host root
+ if vhostroot:
+ self.parse_file(os.path.abspath(vhostroot) + "/" +
+ self.configurator.option("vhost_files"))
+
+ # check to see if there were unparsed define statements
+ if version < (2, 4):
+ if self.find_dir("Define", exclude=False):
+ raise errors.PluginError("Error parsing runtime variables")
+
+ def init_augeas(self):
+ """ Initialize the actual Augeas instance """
+
+ try:
+ import augeas
+ except ImportError: # pragma: no cover
+ raise errors.NoInstallationError("Problem in Augeas installation")
+
+ self.aug = augeas.Augeas(
+ # specify a directory to load our preferred lens from
+ loadpath=constants.AUGEAS_LENS_DIR,
+ # Do not save backup (we do it ourselves), do not load
+ # anything by default
+ flags=(augeas.Augeas.NONE |
+ augeas.Augeas.NO_MODL_AUTOLOAD |
+ augeas.Augeas.ENABLE_SPAN))
+
+ def check_parsing_errors(self, lens):
+ """Verify Augeas can parse all of the lens files.
+
+ :param str lens: lens to check for errors
+
+ :raises .errors.PluginError: If there has been an error in parsing with
+ the specified lens.
+
+ """
+ error_files = self.aug.match("/augeas//error")
+
+ for path in error_files:
+ # Check to see if it was an error resulting from the use of
+ # the httpd lens
+ lens_path = self.aug.get(path + "/lens")
+ # As aug.get may return null
+ if lens_path and lens in lens_path:
+ msg = (
+ "There has been an error in parsing the file {0} on line {1}: "
+ "{2}".format(
+ # Strip off /augeas/files and /error
+ path[13:len(path) - 6],
+ self.aug.get(path + "/line"),
+ self.aug.get(path + "/message")))
+ raise errors.PluginError(msg)
+
+ def check_aug_version(self):
+ """ Checks that we have recent enough version of libaugeas.
+ If augeas version is recent enough, it will support case insensitive
+ regexp matching"""
+
+ self.aug.set("/test/path/testing/arg", "aRgUMeNT")
+ try:
+ matches = self.aug.match(
+ "/test//*[self::arg=~regexp('argument', 'i')]")
+ except RuntimeError:
+ self.aug.remove("/test/path")
+ return False
+ self.aug.remove("/test/path")
+ return matches
+
+ def unsaved_files(self):
+ """Lists files that have modified Augeas DOM but the changes have not
+ been written to the filesystem yet, used by `self.save()` and
+ ApacheConfigurator to check the file state.
+
+ :raises .errors.PluginError: If there was an error in Augeas, in
+ an attempt to save the configuration, or an error creating a
+ checkpoint
+
+ :returns: `set` of unsaved files
+ """
+ save_state = self.aug.get("/augeas/save")
+ self.aug.set("/augeas/save", "noop")
+ # Existing Errors
+ ex_errs = self.aug.match("/augeas//error")
+ try:
+ # This is a noop save
+ self.aug.save()
+ except (RuntimeError, IOError):
+ self._log_save_errors(ex_errs)
+ # Erase Save Notes
+ self.configurator.save_notes = ""
+ raise errors.PluginError(
+ "Error saving files, check logs for more info.")
+
+ # Return the original save method
+ self.aug.set("/augeas/save", save_state)
+
+ # Retrieve list of modified files
+ # Note: Noop saves can cause the file to be listed twice, I used a
+ # set to remove this possibility. This is a known augeas 0.10 error.
+ save_paths = self.aug.match("/augeas/events/saved")
+
+ save_files = set()
+ if save_paths:
+ for path in save_paths:
+ save_files.add(self.aug.get(path)[6:])
+ return save_files
+
+ def ensure_augeas_state(self):
+ """Makes sure that all Augeas dom changes are written to files to avoid
+ loss of configuration directives when doing additional augeas parsing,
+ causing a possible augeas.load() resulting dom reset
+ """
+
+ if self.unsaved_files():
+ self.configurator.save_notes += "(autosave)"
+ self.configurator.save()
+
+ def save(self, save_files):
+ """Saves all changes to the configuration files.
+
+ save() is called from ApacheConfigurator to handle the parser specific
+ tasks of saving.
+
+ :param list save_files: list of strings of file paths that we need to save.
+
+ """
+ self.configurator.save_notes = ""
+ self.aug.save()
+
+ # Force reload if files were modified
+ # This is needed to recalculate augeas directive span
+ if save_files:
+ for sf in save_files:
+ self.aug.remove("/files/"+sf)
+ self.aug.load()
+
+ def _log_save_errors(self, ex_errs):
+ """Log errors due to bad Augeas save.
+
+ :param list ex_errs: Existing errors before save
+
+ """
+ # Check for the root of save problems
+ new_errs = self.aug.match("/augeas//error")
+ # logger.error("During Save - %s", mod_conf)
+ logger.error("Unable to save files: %s. Attempted Save Notes: %s",
+ ", ".join(err[13:len(err) - 6] for err in new_errs
+ # Only new errors caused by recent save
+ if err not in ex_errs), self.configurator.save_notes)
+
+ def add_include(self, main_config, inc_path):
+ """Add Include for a new configuration file if one does not exist
+
+ :param str main_config: file path to main Apache config file
+ :param str inc_path: path of file to include
+
+ """
+ if not self.find_dir(case_i("Include"), inc_path):
+ logger.debug("Adding Include %s to %s",
+ inc_path, get_aug_path(main_config))
+ self.add_dir(
+ get_aug_path(main_config),
+ "Include", inc_path)
+
+ # Add new path to parser paths
+ new_dir = os.path.dirname(inc_path)
+ new_file = os.path.basename(inc_path)
+ self.existing_paths.setdefault(new_dir, []).append(new_file)
+
+ def add_mod(self, mod_name):
+ """Shortcut for updating parser modules."""
+ if mod_name + "_module" not in self.modules:
+ self.modules.add(mod_name + "_module")
+ if "mod_" + mod_name + ".c" not in self.modules:
+ self.modules.add("mod_" + mod_name + ".c")
+
+ def reset_modules(self):
+ """Reset the loaded modules list. This is called from cleanup to clear
+ temporarily loaded modules."""
+ self.modules = set()
+ self.update_modules()
+ self.parse_modules()
+
+ def parse_modules(self):
+ """Iterates on the configuration until no new modules are loaded.
+
+ ..todo:: This should be attempted to be done with a binary to avoid
+ the iteration issue. Else... parse and enable mods at same time.
+
+ """
+ mods = set() # type: Set[str]
+ matches = self.find_dir("LoadModule")
+ iterator = iter(matches)
+ # Make sure prev_size != cur_size for do: while: iteration
+ prev_size = -1
+
+ while len(mods) != prev_size:
+ prev_size = len(mods)
+
+ for match_name, match_filename in six.moves.zip(
+ iterator, iterator):
+ mod_name = self.get_arg(match_name)
+ mod_filename = self.get_arg(match_filename)
+ if mod_name and mod_filename:
+ mods.add(mod_name)
+ mods.add(os.path.basename(mod_filename)[:-2] + "c")
+ else:
+ logger.debug("Could not read LoadModule directive from Augeas path: %s",
+ match_name[6:])
+ self.modules.update(mods)
+
+ def update_runtime_variables(self):
+ """Update Includes, Defines and Includes from httpd config dump data"""
+ self.update_defines()
+ self.update_includes()
+ self.update_modules()
+
+ def update_defines(self):
+ """Get Defines from httpd process"""
+
+ variables = dict()
+ define_cmd = [self.configurator.option("ctl"), "-t", "-D",
+ "DUMP_RUN_CFG"]
+ matches = self.parse_from_subprocess(define_cmd, r"Define: ([^ \n]*)")
+ try:
+ matches.remove("DUMP_RUN_CFG")
+ except ValueError:
+ return
+
+ for match in matches:
+ if match.count("=") > 1:
+ logger.error("Unexpected number of equal signs in "
+ "runtime config dump.")
+ raise errors.PluginError(
+ "Error parsing Apache runtime variables")
+ parts = match.partition("=")
+ variables[parts[0]] = parts[2]
+
+ self.variables = variables
+
+ def update_includes(self):
+ """Get includes from httpd process, and add them to DOM if needed"""
+
+ # Find_dir iterates over configuration for Include and IncludeOptional
+ # directives to make sure we see the full include tree present in the
+ # configuration files
+ _ = self.find_dir("Include")
+
+ inc_cmd = [self.configurator.option("ctl"), "-t", "-D",
+ "DUMP_INCLUDES"]
+ matches = self.parse_from_subprocess(inc_cmd, r"\(.*\) (.*)")
+ if matches:
+ for i in matches:
+ if not self.parsed_in_current(i):
+ self.parse_file(i)
+
+ def update_modules(self):
+ """Get loaded modules from httpd process, and add them to DOM"""
+
+ mod_cmd = [self.configurator.option("ctl"), "-t", "-D",
+ "DUMP_MODULES"]
+ matches = self.parse_from_subprocess(mod_cmd, r"(.*)_module")
+ for mod in matches:
+ self.add_mod(mod.strip())
+
+ def parse_from_subprocess(self, command, regexp):
+ """Get values from stdout of subprocess command
+
+ :param list command: Command to run
+ :param str regexp: Regexp for parsing
+
+ :returns: list parsed from command output
+ :rtype: list
+
+ """
+ stdout = self._get_runtime_cfg(command)
+ return re.compile(regexp).findall(stdout)
+
+ def _get_runtime_cfg(self, command): # pylint: disable=no-self-use
+ """Get runtime configuration info.
+ :param command: Command to run
+
+ :returns: stdout from command
+
+ """
+ try:
+ proc = subprocess.Popen(
+ command,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ universal_newlines=True)
+ stdout, stderr = proc.communicate()
+
+ except (OSError, ValueError):
+ logger.error(
+ "Error running command %s for runtime parameters!%s",
+ command, os.linesep)
+ raise errors.MisconfigurationError(
+ "Error accessing loaded Apache parameters: {0}".format(
+ command))
+ # Small errors that do not impede
+ if proc.returncode != 0:
+ logger.warning("Error in checking parameter list: %s", stderr)
+ raise errors.MisconfigurationError(
+ "Apache is unable to check whether or not the module is "
+ "loaded because Apache is misconfigured.")
+
+ return stdout
+
+ def filter_args_num(self, matches, args): # pylint: disable=no-self-use
+ """Filter out directives with specific number of arguments.
+
+ This function makes the assumption that all related arguments are given
+ in order. Thus /files/apache/directive[5]/arg[2] must come immediately
+ after /files/apache/directive[5]/arg[1]. Runs in 1 linear pass.
+
+ :param string matches: Matches of all directives with arg nodes
+ :param int args: Number of args you would like to filter
+
+ :returns: List of directives that contain # of arguments.
+ (arg is stripped off)
+
+ """
+ filtered = []
+ if args == 1:
+ for i, match in enumerate(matches):
+ if match.endswith("/arg"):
+ filtered.append(matches[i][:-4])
+ else:
+ for i, match in enumerate(matches):
+ if match.endswith("/arg[%d]" % args):
+ # Make sure we don't cause an IndexError (end of list)
+ # Check to make sure arg + 1 doesn't exist
+ if (i == (len(matches) - 1) or
+ not matches[i + 1].endswith("/arg[%d]" %
+ (args + 1))):
+ filtered.append(matches[i][:-len("/arg[%d]" % args)])
+
+ return filtered
+
+ def add_dir_to_ifmodssl(self, aug_conf_path, directive, args):
+ """Adds directive and value to IfMod ssl block.
+
+ Adds given directive and value along configuration path within
+ an IfMod mod_ssl.c block. If the IfMod block does not exist in
+ the file, it is created.
+
+ :param str aug_conf_path: Desired Augeas config path to add directive
+ :param str directive: Directive you would like to add, e.g. Listen
+ :param args: Values of the directive; str "443" or list of str
+ :type args: list
+
+ """
+ # TODO: Add error checking code... does the path given even exist?
+ # Does it throw exceptions?
+ if_mod_path = self.get_ifmod(aug_conf_path, "mod_ssl.c")
+ # IfModule can have only one valid argument, so append after
+ self.aug.insert(if_mod_path + "arg", "directive", False)
+ nvh_path = if_mod_path + "directive[1]"
+ self.aug.set(nvh_path, directive)
+ if len(args) == 1:
+ self.aug.set(nvh_path + "/arg", args[0])
+ else:
+ for i, arg in enumerate(args):
+ self.aug.set("%s/arg[%d]" % (nvh_path, i + 1), arg)
+
+ def get_ifmod(self, aug_conf_path, mod, beginning=False):
+ """Returns the path to <IfMod mod> and creates one if it doesn't exist.
+
+ :param str aug_conf_path: Augeas configuration path
+ :param str mod: module ie. mod_ssl.c
+ :param bool beginning: If the IfModule should be created to the beginning
+ of augeas path DOM tree.
+
+ :returns: Augeas path of the requested IfModule directive that pre-existed
+ or was created during the process. The path may be dynamic,
+ i.e. .../IfModule[last()]
+ :rtype: str
+
+ """
+ if_mods = self.aug.match(("%s/IfModule/*[self::arg='%s']" %
+ (aug_conf_path, mod)))
+ if not if_mods:
+ return self.create_ifmod(aug_conf_path, mod, beginning)
+
+ # Strip off "arg" at end of first ifmod path
+ return if_mods[0].rpartition("arg")[0]
+
+ def create_ifmod(self, aug_conf_path, mod, beginning=False):
+ """Creates a new <IfMod mod> and returns its path.
+
+ :param str aug_conf_path: Augeas configuration path
+ :param str mod: module ie. mod_ssl.c
+ :param bool beginning: If the IfModule should be created to the beginning
+ of augeas path DOM tree.
+
+ :returns: Augeas path of the newly created IfModule directive.
+ The path may be dynamic, i.e. .../IfModule[last()]
+ :rtype: str
+
+ """
+ if beginning:
+ c_path_arg = "{}/IfModule[1]/arg".format(aug_conf_path)
+ # Insert IfModule before the first directive
+ self.aug.insert("{}/directive[1]".format(aug_conf_path),
+ "IfModule", True)
+ retpath = "{}/IfModule[1]/".format(aug_conf_path)
+ else:
+ c_path = "{}/IfModule[last() + 1]".format(aug_conf_path)
+ c_path_arg = "{}/IfModule[last()]/arg".format(aug_conf_path)
+ self.aug.set(c_path, "")
+ retpath = "{}/IfModule[last()]/".format(aug_conf_path)
+ self.aug.set(c_path_arg, mod)
+ return retpath
+
+ def add_dir(self, aug_conf_path, directive, args):
+ """Appends directive to the end fo the file given by aug_conf_path.
+
+ .. note:: Not added to AugeasConfigurator because it may depend
+ on the lens
+
+ :param str aug_conf_path: Augeas configuration path to add directive
+ :param str directive: Directive to add
+ :param args: Value of the directive. ie. Listen 443, 443 is arg
+ :type args: list or str
+
+ """
+ self.aug.set(aug_conf_path + "/directive[last() + 1]", directive)
+ if isinstance(args, list):
+ for i, value in enumerate(args, 1):
+ self.aug.set(
+ "%s/directive[last()]/arg[%d]" % (aug_conf_path, i), value)
+ else:
+ self.aug.set(aug_conf_path + "/directive[last()]/arg", args)
+
+ def add_dir_beginning(self, aug_conf_path, dirname, args):
+ """Adds the directive to the beginning of defined aug_conf_path.
+
+ :param str aug_conf_path: Augeas configuration path to add directive
+ :param str dirname: Directive to add
+ :param args: Value of the directive. ie. Listen 443, 443 is arg
+ :type args: list or str
+ """
+ first_dir = aug_conf_path + "/directive[1]"
+ self.aug.insert(first_dir, "directive", True)
+ self.aug.set(first_dir, dirname)
+ if isinstance(args, list):
+ for i, value in enumerate(args, 1):
+ self.aug.set(first_dir + "/arg[%d]" % (i), value)
+ else:
+ self.aug.set(first_dir + "/arg", args)
+
+ def add_comment(self, aug_conf_path, comment):
+ """Adds the comment to the augeas path
+
+ :param str aug_conf_path: Augeas configuration path to add directive
+ :param str comment: Comment content
+
+ """
+ self.aug.set(aug_conf_path + "/#comment[last() + 1]", comment)
+
+ def find_comments(self, arg, start=None):
+ """Finds a comment with specified content from the provided DOM path
+
+ :param str arg: Comment content to search
+ :param str start: Beginning Augeas path to begin looking
+
+ :returns: List of augeas paths containing the comment content
+ :rtype: list
+
+ """
+ if not start:
+ start = get_aug_path(self.root)
+
+ comments = self.aug.match("%s//*[label() = '#comment']" % start)
+
+ results = []
+ for comment in comments:
+ c_content = self.aug.get(comment)
+ if c_content and arg in c_content:
+ results.append(comment)
+ return results
+
+ def find_dir(self, directive, arg=None, start=None, exclude=True):
+ """Finds directive in the configuration.
+
+ Recursively searches through config files to find directives
+ Directives should be in the form of a case insensitive regex currently
+
+ .. todo:: arg should probably be a list
+ .. todo:: arg search currently only supports direct matching. It does
+ not handle the case of variables or quoted arguments. This should
+ be adapted to use a generic search for the directive and then do a
+ case-insensitive self.get_arg filter
+
+ Note: Augeas is inherently case sensitive while Apache is case
+ insensitive. Augeas 1.0 allows case insensitive regexes like
+ regexp(/Listen/, "i"), however the version currently supported
+ by Ubuntu 0.10 does not. Thus I have included my own case insensitive
+ transformation by calling case_i() on everything to maintain
+ compatibility.
+
+ :param str directive: Directive to look for
+ :param arg: Specific value directive must have, None if all should
+ be considered
+ :type arg: str or None
+
+ :param str start: Beginning Augeas path to begin looking
+ :param bool exclude: Whether or not to exclude directives based on
+ variables and enabled modules
+
+ """
+ # Cannot place member variable in the definition of the function so...
+ if not start:
+ start = get_aug_path(self.loc["root"])
+
+ # No regexp code
+ # if arg is None:
+ # matches = self.aug.match(start +
+ # "//*[self::directive='" + directive + "']/arg")
+ # else:
+ # matches = self.aug.match(start +
+ # "//*[self::directive='" + directive +
+ # "']/* [self::arg='" + arg + "']")
+
+ # includes = self.aug.match(start +
+ # "//* [self::directive='Include']/* [label()='arg']")
+
+ regex = "(%s)|(%s)|(%s)" % (case_i(directive),
+ case_i("Include"),
+ case_i("IncludeOptional"))
+ matches = self.aug.match(
+ "%s//*[self::directive=~regexp('%s')]" % (start, regex))
+
+ if exclude:
+ matches = self._exclude_dirs(matches)
+
+ if arg is None:
+ arg_suffix = "/arg"
+ else:
+ arg_suffix = "/*[self::arg=~regexp('%s')]" % case_i(arg)
+
+ ordered_matches = [] # type: List[str]
+
+ # TODO: Wildcards should be included in alphabetical order
+ # https://httpd.apache.org/docs/2.4/mod/core.html#include
+ for match in matches:
+ dir_ = self.aug.get(match).lower()
+ if dir_ in ("include", "includeoptional"):
+ ordered_matches.extend(self.find_dir(
+ directive, arg,
+ self._get_include_path(self.get_arg(match + "/arg")),
+ exclude))
+ # This additionally allows Include
+ if dir_ == directive.lower():
+ ordered_matches.extend(self.aug.match(match + arg_suffix))
+
+ return ordered_matches
+
+ def get_all_args(self, match):
+ """
+ Tries to fetch all arguments for a directive. See get_arg.
+
+ Note that if match is an ancestor node, it returns all names of
+ child directives as well as the list of arguments.
+
+ """
+
+ if match[-1] != "/":
+ match = match+"/"
+ allargs = self.aug.match(match + '*')
+ return [self.get_arg(arg) for arg in allargs]
+
+ def get_arg(self, match):
+ """Uses augeas.get to get argument value and interprets result.
+
+ This also converts all variables and parameters appropriately.
+
+ """
+ value = self.aug.get(match)
+
+ # No need to strip quotes for variables, as apache2ctl already does
+ # this, but we do need to strip quotes for all normal arguments.
+
+ # Note: normal argument may be a quoted variable
+ # e.g. strip now, not later
+ if not value:
+ return None
+ value = value.strip("'\"")
+
+ variables = ApacheParser.arg_var_interpreter.findall(value)
+
+ for var in variables:
+ # Strip off ${ and }
+ try:
+ value = value.replace(var, self.variables[var[2:-1]])
+ except KeyError:
+ raise errors.PluginError("Error Parsing variable: %s" % var)
+
+ return value
+
+ def _exclude_dirs(self, matches):
+ """Exclude directives that are not loaded into the configuration."""
+ filters = [("ifmodule", self.modules), ("ifdefine", self.variables)]
+
+ valid_matches = []
+
+ for match in matches:
+ for filter_ in filters:
+ if not self._pass_filter(match, filter_):
+ break
+ else:
+ valid_matches.append(match)
+ return valid_matches
+
+ def _pass_filter(self, match, filter_):
+ """Determine if directive passes a filter.
+
+ :param str match: Augeas path
+ :param list filter: list of tuples of form
+ [("lowercase if directive", set of relevant parameters)]
+
+ """
+ match_l = match.lower()
+ last_match_idx = match_l.find(filter_[0])
+
+ while last_match_idx != -1:
+ # Check args
+ end_of_if = match_l.find("/", last_match_idx)
+ # This should be aug.get (vars are not used e.g. parser.aug_get)
+ expression = self.aug.get(match[:end_of_if] + "/arg")
+
+ if expression.startswith("!"):
+ # Strip off "!"
+ if expression[1:] in filter_[1]:
+ return False
+ else:
+ if expression not in filter_[1]:
+ return False
+
+ last_match_idx = match_l.find(filter_[0], end_of_if)
+
+ return True
+
+ def _get_include_path(self, arg):
+ """Converts an Apache Include directive into Augeas path.
+
+ Converts an Apache Include directive argument into an Augeas
+ searchable path
+
+ .. todo:: convert to use os.path.join()
+
+ :param str arg: Argument of Include directive
+
+ :returns: Augeas path string
+ :rtype: str
+
+ """
+ # Check to make sure only expected characters are used <- maybe remove
+ # validChars = re.compile("[a-zA-Z0-9.*?_-/]*")
+ # matchObj = validChars.match(arg)
+ # if matchObj.group() != arg:
+ # logger.error("Error: Invalid regexp characters in %s", arg)
+ # return []
+
+ # Remove beginning and ending quotes
+ arg = arg.strip("'\"")
+
+ # Standardize the include argument based on server root
+ if not arg.startswith("/"):
+ # Normpath will condense ../
+ arg = os.path.normpath(os.path.join(self.root, arg))
+ else:
+ arg = os.path.normpath(arg)
+
+ # Attempts to add a transform to the file if one does not already exist
+ if os.path.isdir(arg):
+ self.parse_file(os.path.join(arg, "*"))
+ else:
+ self.parse_file(arg)
+
+ # Argument represents an fnmatch regular expression, convert it
+ # Split up the path and convert each into an Augeas accepted regex
+ # then reassemble
+ split_arg = arg.split("/")
+ for idx, split in enumerate(split_arg):
+ if any(char in ApacheParser.fnmatch_chars for char in split):
+ # Turn it into an augeas regex
+ # TODO: Can this instead be an augeas glob instead of regex
+ split_arg[idx] = ("* [label()=~regexp('%s')]" %
+ self.fnmatch_to_re(split))
+ # Reassemble the argument
+ # Note: This also normalizes the argument /serverroot/ -> /serverroot
+ arg = "/".join(split_arg)
+
+ return get_aug_path(arg)
+
+ def fnmatch_to_re(self, clean_fn_match): # pylint: disable=no-self-use
+ """Method converts Apache's basic fnmatch to regular expression.
+
+ Assumption - Configs are assumed to be well-formed and only writable by
+ privileged users.
+
+ https://apr.apache.org/docs/apr/2.0/apr__fnmatch_8h_source.html
+ http://apache2.sourcearchive.com/documentation/2.2.16-6/apr__fnmatch_8h_source.html
+
+ :param str clean_fn_match: Apache style filename match, like globs
+
+ :returns: regex suitable for augeas
+ :rtype: str
+
+ """
+ if sys.version_info < (3, 6):
+ # This strips off final /Z(?ms)
+ return fnmatch.translate(clean_fn_match)[:-7]
+ # Since Python 3.6, it returns a different pattern like (?s:.*\.load)\Z
+ return fnmatch.translate(clean_fn_match)[4:-3] # pragma: no cover
+
+ def parse_file(self, filepath):
+ """Parse file with Augeas
+
+ Checks to see if file_path is parsed by Augeas
+ If filepath isn't parsed, the file is added and Augeas is reloaded
+
+ :param str filepath: Apache config file path
+
+ """
+ use_new, remove_old = self._check_path_actions(filepath)
+ # Ensure that we have the latest Augeas DOM state on disk before
+ # calling aug.load() which reloads the state from disk
+ self.ensure_augeas_state()
+ # Test if augeas included file for Httpd.lens
+ # Note: This works for augeas globs, ie. *.conf
+ if use_new:
+ inc_test = self.aug.match(
+ "/augeas/load/Httpd['%s' =~ glob(incl)]" % filepath)
+ if not inc_test:
+ # Load up files
+ # This doesn't seem to work on TravisCI
+ # self.aug.add_transform("Httpd.lns", [filepath])
+ if remove_old:
+ self._remove_httpd_transform(filepath)
+ self._add_httpd_transform(filepath)
+ self.aug.load()
+
+ def parsed_in_current(self, filep):
+ """Checks if the file path is parsed by current Augeas parser config
+ ie. returns True if the file is found on a path that's found in live
+ Augeas configuration.
+
+ :param str filep: Path to match
+
+ :returns: True if file is parsed in existing configuration tree
+ :rtype: bool
+ """
+ return self._parsed_by_parser_paths(filep, self.parser_paths)
+
+ def parsed_in_original(self, filep):
+ """Checks if the file path is parsed by existing Apache config.
+ ie. returns True if the file is found on a path that matches Include or
+ IncludeOptional statement in the Apache configuration.
+
+ :param str filep: Path to match
+
+ :returns: True if file is parsed in existing configuration tree
+ :rtype: bool
+ """
+ return self._parsed_by_parser_paths(filep, self.existing_paths)
+
+ def _parsed_by_parser_paths(self, filep, paths):
+ """Helper function that searches through provided paths and returns
+ True if file path is found in the set"""
+ for directory in paths.keys():
+ for filename in paths[directory]:
+ if fnmatch.fnmatch(filep, os.path.join(directory, filename)):
+ return True
+ return False
+
+ def _check_path_actions(self, filepath):
+ """Determine actions to take with a new augeas path
+
+ This helper function will return a tuple that defines
+ if we should try to append the new filepath to augeas
+ parser paths, and / or remove the old one with more
+ narrow matching.
+
+ :param str filepath: filepath to check the actions for
+
+ """
+
+ try:
+ new_file_match = os.path.basename(filepath)
+ existing_matches = self.parser_paths[os.path.dirname(filepath)]
+ if "*" in existing_matches:
+ use_new = False
+ else:
+ use_new = True
+ remove_old = new_file_match == "*"
+ except KeyError:
+ use_new = True
+ remove_old = False
+ return use_new, remove_old
+
+ def _remove_httpd_transform(self, filepath):
+ """Remove path from Augeas transform
+
+ :param str filepath: filepath to remove
+ """
+
+ remove_basenames = self.parser_paths[os.path.dirname(filepath)]
+ remove_dirname = os.path.dirname(filepath)
+ for name in remove_basenames:
+ remove_path = remove_dirname + "/" + name
+ remove_inc = self.aug.match(
+ "/augeas/load/Httpd/incl [. ='%s']" % remove_path)
+ self.aug.remove(remove_inc[0])
+ self.parser_paths.pop(remove_dirname)
+
+ def _add_httpd_transform(self, incl):
+ """Add a transform to Augeas.
+
+ This function will correctly add a transform to augeas
+ The existing augeas.add_transform in python doesn't seem to work for
+ Travis CI as it loads in libaugeas.so.0.10.0
+
+ :param str incl: filepath to include for transform
+
+ """
+ last_include = self.aug.match("/augeas/load/Httpd/incl [last()]")
+ if last_include:
+ # Insert a new node immediately after the last incl
+ self.aug.insert(last_include[0], "incl", False)
+ self.aug.set("/augeas/load/Httpd/incl[last()]", incl)
+ # On first use... must load lens and add file to incl
+ else:
+ # Augeas uses base 1 indexing... insert at beginning...
+ self.aug.set("/augeas/load/Httpd/lens", "Httpd.lns")
+ self.aug.set("/augeas/load/Httpd/incl", incl)
+ # Add included path to paths dictionary
+ try:
+ self.parser_paths[os.path.dirname(incl)].append(
+ os.path.basename(incl))
+ except KeyError:
+ self.parser_paths[os.path.dirname(incl)] = [
+ os.path.basename(incl)]
+
+ def standardize_excl(self):
+ """Standardize the excl arguments for the Httpd lens in Augeas.
+
+ Note: Hack!
+ Standardize the excl arguments for the Httpd lens in Augeas
+ Servers sometimes give incorrect defaults
+ Note: This problem should be fixed in Augeas 1.0. Unfortunately,
+ Augeas 0.10 appears to be the most popular version currently.
+
+ """
+ # attempt to protect against augeas error in 0.10.0 - ubuntu
+ # *.augsave -> /*.augsave upon augeas.load()
+ # Try to avoid bad httpd files
+ # There has to be a better way... but after a day and a half of testing
+ # I had no luck
+ # This is a hack... work around... submit to augeas if still not fixed
+
+ excl = ["*.augnew", "*.augsave", "*.dpkg-dist", "*.dpkg-bak",
+ "*.dpkg-new", "*.dpkg-old", "*.rpmsave", "*.rpmnew",
+ "*~",
+ self.root + "/*.augsave",
+ self.root + "/*~",
+ self.root + "/*/*augsave",
+ self.root + "/*/*~",
+ self.root + "/*/*/*.augsave",
+ self.root + "/*/*/*~"]
+
+ for i, excluded in enumerate(excl, 1):
+ self.aug.set("/augeas/load/Httpd/excl[%d]" % i, excluded)
+
+ self.aug.load()
+
+ def _set_locations(self):
+ """Set default location for directives.
+
+ Locations are given as file_paths
+ .. todo:: Make sure that files are included
+
+ """
+ default = self.loc["root"]
+
+ temp = os.path.join(self.root, "ports.conf")
+ if os.path.isfile(temp):
+ listen = temp
+ name = temp
+ else:
+ listen = default
+ name = default
+
+ return {"default": default, "listen": listen, "name": name}
+
+ def _find_config_root(self):
+ """Find the Apache Configuration Root file."""
+ location = ["apache2.conf", "httpd.conf", "conf/httpd.conf"]
+ for name in location:
+ if os.path.isfile(os.path.join(self.root, name)):
+ return os.path.join(self.root, name)
+ raise errors.NoInstallationError("Could not find configuration root")
+
+
+def case_i(string):
+ """Returns case insensitive regex.
+
+ Returns a sloppy, but necessary version of a case insensitive regex.
+ Any string should be able to be submitted and the string is
+ escaped and then made case insensitive.
+ May be replaced by a more proper /i once augeas 1.0 is widely
+ supported.
+
+ :param str string: string to make case i regex
+
+ """
+ return "".join(["[" + c.upper() + c.lower() + "]"
+ if c.isalpha() else c for c in re.escape(string)])
+
+
+def get_aug_path(file_path):
+ """Return augeas path for full filepath.
+
+ :param str file_path: Full filepath
+
+ """
+ return "/files%s" % file_path