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:
authorohemorange <ebportnoy@gmail.com>2020-02-07 02:29:28 +0300
committerGitHub <noreply@github.com>2020-02-07 02:29:28 +0300
commitc5a2ba03da7c027bad7288631a8425dc89587db5 (patch)
tree66a877c63ec05fdaebe1106a6aa43f365f50a09e
parent995e70542adf74d1854351945e3319e11cc73f88 (diff)
parent715899d5a8e195e66a79eefe7d824f6d381b1216 (diff)
Merge pull request #7735 from certbot/apache-parser-v2
[Apache v2] Merge apache-parser-v2 feature branch back to master
-rw-r--r--.travis.yml2
-rw-r--r--certbot-apache/certbot_apache/_internal/apache_util.py136
-rw-r--r--certbot-apache/certbot_apache/_internal/apacheparser.py169
-rw-r--r--certbot-apache/certbot_apache/_internal/assertions.py142
-rw-r--r--certbot-apache/certbot_apache/_internal/augeasparser.py538
-rw-r--r--certbot-apache/certbot_apache/_internal/configurator.py131
-rw-r--r--certbot-apache/certbot_apache/_internal/dualparser.py306
-rw-r--r--certbot-apache/certbot_apache/_internal/interfaces.py516
-rw-r--r--certbot-apache/certbot_apache/_internal/obj.py3
-rw-r--r--certbot-apache/certbot_apache/_internal/override_gentoo.py2
-rw-r--r--certbot-apache/certbot_apache/_internal/parser.py87
-rw-r--r--certbot-apache/certbot_apache/_internal/parsernode_util.py129
-rw-r--r--certbot-apache/setup.py6
-rw-r--r--certbot-apache/tests/augeasnode_test.py319
-rw-r--r--certbot-apache/tests/centos_test.py4
-rw-r--r--certbot-apache/tests/configurator_test.py7
-rw-r--r--certbot-apache/tests/debian_test.py2
-rw-r--r--certbot-apache/tests/dualnode_test.py442
-rw-r--r--certbot-apache/tests/fedora_test.py4
-rw-r--r--certbot-apache/tests/gentoo_test.py4
-rw-r--r--certbot-apache/tests/parser_test.py12
-rw-r--r--certbot-apache/tests/parsernode_configurator_test.py37
-rw-r--r--certbot-apache/tests/parsernode_test.py128
-rw-r--r--certbot-apache/tests/parsernode_util_test.py115
-rw-r--r--certbot-apache/tests/util.py31
-rw-r--r--tools/dev_constraints.txt2
-rw-r--r--tools/oldest_constraints.txt1
-rw-r--r--tox.ini6
28 files changed, 3174 insertions, 107 deletions
diff --git a/.travis.yml b/.travis.yml
index 6c5147603..23d957a21 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -59,7 +59,7 @@ matrix:
# cryptography we support cannot be compiled against the version of
# OpenSSL in Xenial or newer.
dist: trusty
- env: TOXENV='py27-{acme,apache,certbot,dns,nginx}-oldest'
+ env: TOXENV='py27-{acme,apache,apache-v2,certbot,dns,nginx}-oldest'
<<: *not-on-master
- python: "3.5"
env: TOXENV=py35
diff --git a/certbot-apache/certbot_apache/_internal/apache_util.py b/certbot-apache/certbot_apache/_internal/apache_util.py
index 7a2ecf49b..085ccddc8 100644
--- a/certbot-apache/certbot_apache/_internal/apache_util.py
+++ b/certbot-apache/certbot_apache/_internal/apache_util.py
@@ -1,9 +1,17 @@
""" Utility functions for certbot-apache plugin """
import binascii
+import fnmatch
+import logging
+import re
+import subprocess
+from certbot import errors
from certbot import util
+
from certbot.compat import os
+logger = logging.getLogger(__name__)
+
def get_mod_deps(mod_name):
"""Get known module dependencies.
@@ -105,3 +113,131 @@ def parse_define_file(filepath, varname):
def unique_id():
""" Returns an unique id to be used as a VirtualHost identifier"""
return binascii.hexlify(os.urandom(16)).decode("utf-8")
+
+
+def included_in_paths(filepath, paths):
+ """
+ Returns true if the filepath is included in the list of paths
+ that may contain full paths or wildcard paths that need to be
+ expanded.
+
+ :param str filepath: Filepath to check
+ :params list paths: List of paths to check against
+
+ :returns: True if included
+ :rtype: bool
+ """
+
+ return any([fnmatch.fnmatch(filepath, path) for path in paths])
+
+
+def parse_defines(apachectl):
+ """
+ Gets Defines from httpd process and returns a dictionary of
+ the defined variables.
+
+ :param str apachectl: Path to apachectl executable
+
+ :returns: dictionary of defined variables
+ :rtype: dict
+ """
+
+ variables = dict()
+ define_cmd = [apachectl, "-t", "-D",
+ "DUMP_RUN_CFG"]
+ matches = 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]
+
+ return variables
+
+
+def parse_includes(apachectl):
+ """
+ Gets Include directives from httpd process and returns a list of
+ their values.
+
+ :param str apachectl: Path to apachectl executable
+
+ :returns: list of found Include directive values
+ :rtype: list of str
+ """
+
+ inc_cmd = [apachectl, "-t", "-D",
+ "DUMP_INCLUDES"]
+ return parse_from_subprocess(inc_cmd, r"\(.*\) (.*)")
+
+
+def parse_modules(apachectl):
+ """
+ Get loaded modules from httpd process, and return the list
+ of loaded module names.
+
+ :param str apachectl: Path to apachectl executable
+
+ :returns: list of found LoadModule module names
+ :rtype: list of str
+ """
+
+ mod_cmd = [apachectl, "-t", "-D",
+ "DUMP_MODULES"]
+ return parse_from_subprocess(mod_cmd, r"(.*)_module")
+
+
+def parse_from_subprocess(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 = _get_runtime_cfg(command)
+ return re.compile(regexp).findall(stdout)
+
+
+def _get_runtime_cfg(command):
+ """
+ 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
diff --git a/certbot-apache/certbot_apache/_internal/apacheparser.py b/certbot-apache/certbot_apache/_internal/apacheparser.py
new file mode 100644
index 000000000..77f4517fe
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/apacheparser.py
@@ -0,0 +1,169 @@
+""" apacheconfig implementation of the ParserNode interfaces """
+
+from certbot_apache._internal import assertions
+from certbot_apache._internal import interfaces
+from certbot_apache._internal import parsernode_util as util
+
+
+class ApacheParserNode(interfaces.ParserNode):
+ """ apacheconfig implementation of ParserNode interface.
+
+ Expects metadata `ac_ast` to be passed in, where `ac_ast` is the AST provided
+ by parsing the equivalent configuration text using the apacheconfig library.
+ """
+
+ def __init__(self, **kwargs):
+ ancestor, dirty, filepath, metadata = util.parsernode_kwargs(kwargs) # pylint: disable=unused-variable
+ super(ApacheParserNode, self).__init__(**kwargs)
+ self.ancestor = ancestor
+ self.filepath = filepath
+ self.dirty = dirty
+ self.metadata = metadata
+ self._raw = self.metadata["ac_ast"]
+
+ def save(self, msg): # pragma: no cover
+ pass
+
+ def find_ancestors(self, name): # pylint: disable=unused-variable
+ """Find ancestor BlockNodes with a given name"""
+ return [ApacheBlockNode(name=assertions.PASS,
+ parameters=assertions.PASS,
+ ancestor=self,
+ filepath=assertions.PASS,
+ metadata=self.metadata)]
+
+
+class ApacheCommentNode(ApacheParserNode):
+ """ apacheconfig implementation of CommentNode interface """
+
+ def __init__(self, **kwargs):
+ comment, kwargs = util.commentnode_kwargs(kwargs) # pylint: disable=unused-variable
+ super(ApacheCommentNode, self).__init__(**kwargs)
+ self.comment = comment
+
+ def __eq__(self, other): # pragma: no cover
+ if isinstance(other, self.__class__):
+ return (self.comment == other.comment and
+ self.dirty == other.dirty and
+ self.ancestor == other.ancestor and
+ self.metadata == other.metadata and
+ self.filepath == other.filepath)
+ return False
+
+
+class ApacheDirectiveNode(ApacheParserNode):
+ """ apacheconfig implementation of DirectiveNode interface """
+
+ def __init__(self, **kwargs):
+ name, parameters, enabled, kwargs = util.directivenode_kwargs(kwargs)
+ super(ApacheDirectiveNode, self).__init__(**kwargs)
+ self.name = name
+ self.parameters = parameters
+ self.enabled = enabled
+ self.include = None
+
+ def __eq__(self, other): # pragma: no cover
+ if isinstance(other, self.__class__):
+ return (self.name == other.name and
+ self.filepath == other.filepath and
+ self.parameters == other.parameters and
+ self.enabled == other.enabled and
+ self.dirty == other.dirty and
+ self.ancestor == other.ancestor and
+ self.metadata == other.metadata)
+ return False
+
+ def set_parameters(self, _parameters):
+ """Sets the parameters for DirectiveNode"""
+ return
+
+
+class ApacheBlockNode(ApacheDirectiveNode):
+ """ apacheconfig implementation of BlockNode interface """
+
+ def __init__(self, **kwargs):
+ super(ApacheBlockNode, self).__init__(**kwargs)
+ self.children = ()
+
+ def __eq__(self, other): # pragma: no cover
+ if isinstance(other, self.__class__):
+ return (self.name == other.name and
+ self.filepath == other.filepath and
+ self.parameters == other.parameters and
+ self.children == other.children and
+ self.enabled == other.enabled and
+ self.dirty == other.dirty and
+ self.ancestor == other.ancestor and
+ self.metadata == other.metadata)
+ return False
+
+ def add_child_block(self, name, parameters=None, position=None): # pylint: disable=unused-argument
+ """Adds a new BlockNode to the sequence of children"""
+ new_block = ApacheBlockNode(name=assertions.PASS,
+ parameters=assertions.PASS,
+ ancestor=self,
+ filepath=assertions.PASS,
+ metadata=self.metadata)
+ self.children += (new_block,)
+ return new_block
+
+ def add_child_directive(self, name, parameters=None, position=None): # pylint: disable=unused-argument
+ """Adds a new DirectiveNode to the sequence of children"""
+ new_dir = ApacheDirectiveNode(name=assertions.PASS,
+ parameters=assertions.PASS,
+ ancestor=self,
+ filepath=assertions.PASS,
+ metadata=self.metadata)
+ self.children += (new_dir,)
+ return new_dir
+
+ # pylint: disable=unused-argument
+ def add_child_comment(self, comment="", position=None): # pragma: no cover
+
+ """Adds a new CommentNode to the sequence of children"""
+ new_comment = ApacheCommentNode(comment=assertions.PASS,
+ ancestor=self,
+ filepath=assertions.PASS,
+ metadata=self.metadata)
+ self.children += (new_comment,)
+ return new_comment
+
+ def find_blocks(self, name, exclude=True): # pylint: disable=unused-argument
+ """Recursive search of BlockNodes from the sequence of children"""
+ return [ApacheBlockNode(name=assertions.PASS,
+ parameters=assertions.PASS,
+ ancestor=self,
+ filepath=assertions.PASS,
+ metadata=self.metadata)]
+
+ def find_directives(self, name, exclude=True): # pylint: disable=unused-argument
+ """Recursive search of DirectiveNodes from the sequence of children"""
+ return [ApacheDirectiveNode(name=assertions.PASS,
+ parameters=assertions.PASS,
+ ancestor=self,
+ filepath=assertions.PASS,
+ metadata=self.metadata)]
+
+ def find_comments(self, comment, exact=False): # pylint: disable=unused-argument
+ """Recursive search of DirectiveNodes from the sequence of children"""
+ return [ApacheCommentNode(comment=assertions.PASS,
+ ancestor=self,
+ filepath=assertions.PASS,
+ metadata=self.metadata)]
+
+ def delete_child(self, child): # pragma: no cover
+ """Deletes a ParserNode from the sequence of children"""
+ return
+
+ def unsaved_files(self): # pragma: no cover
+ """Returns a list of unsaved filepaths"""
+ return [assertions.PASS]
+
+ def parsed_paths(self): # pragma: no cover
+ """Returns a list of parsed configuration file paths"""
+ return [assertions.PASS]
+
+
+interfaces.CommentNode.register(ApacheCommentNode)
+interfaces.DirectiveNode.register(ApacheDirectiveNode)
+interfaces.BlockNode.register(ApacheBlockNode)
diff --git a/certbot-apache/certbot_apache/_internal/assertions.py b/certbot-apache/certbot_apache/_internal/assertions.py
new file mode 100644
index 000000000..e1b4cdcc8
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/assertions.py
@@ -0,0 +1,142 @@
+"""Dual parser node assertions"""
+import fnmatch
+
+from certbot_apache._internal import interfaces
+
+
+PASS = "CERTBOT_PASS_ASSERT"
+
+
+def assertEqual(first, second):
+ """ Equality assertion """
+
+ if isinstance(first, interfaces.CommentNode):
+ assertEqualComment(first, second)
+ elif isinstance(first, interfaces.DirectiveNode):
+ assertEqualDirective(first, second)
+
+ # Do an extra interface implementation assertion, as the contents were
+ # already checked for BlockNode in the assertEqualDirective
+ if isinstance(first, interfaces.BlockNode):
+ assert isinstance(second, interfaces.BlockNode)
+
+ # Skip tests if filepath includes the pass value. This is done
+ # because filepath is variable of the base ParserNode interface, and
+ # unless the implementation is actually done, we cannot assume getting
+ # correct results from boolean assertion for dirty
+ if not isPass(first.filepath) and not isPass(second.filepath):
+ assert first.dirty == second.dirty
+ # We might want to disable this later if testing with two separate
+ # (but identical) directory structures.
+ assert first.filepath == second.filepath
+
+def assertEqualComment(first, second): # pragma: no cover
+ """ Equality assertion for CommentNode """
+
+ assert isinstance(first, interfaces.CommentNode)
+ assert isinstance(second, interfaces.CommentNode)
+
+ if not isPass(first.comment) and not isPass(second.comment): # type: ignore
+ assert first.comment == second.comment # type: ignore
+
+def _assertEqualDirectiveComponents(first, second): # pragma: no cover
+ """ Handles assertion for instance variables for DirectiveNode and BlockNode"""
+
+ # Enabled value cannot be asserted, because Augeas implementation
+ # is unable to figure that out.
+ # assert first.enabled == second.enabled
+ if not isPass(first.name) and not isPass(second.name):
+ assert first.name == second.name
+
+ if not isPass(first.parameters) and not isPass(second.parameters):
+ assert first.parameters == second.parameters
+
+def assertEqualDirective(first, second):
+ """ Equality assertion for DirectiveNode """
+
+ assert isinstance(first, interfaces.DirectiveNode)
+ assert isinstance(second, interfaces.DirectiveNode)
+ _assertEqualDirectiveComponents(first, second)
+
+def isPass(value): # pragma: no cover
+ """Checks if the value is set to PASS"""
+ if isinstance(value, bool):
+ return True
+ return PASS in value
+
+def isPassDirective(block):
+ """ Checks if BlockNode or DirectiveNode should pass the assertion """
+
+ if isPass(block.name):
+ return True
+ if isPass(block.parameters): # pragma: no cover
+ return True
+ if isPass(block.filepath): # pragma: no cover
+ return True
+ return False
+
+def isPassComment(comment):
+ """ Checks if CommentNode should pass the assertion """
+
+ if isPass(comment.comment):
+ return True
+ if isPass(comment.filepath): # pragma: no cover
+ return True
+ return False
+
+def isPassNodeList(nodelist): # pragma: no cover
+ """ Checks if a ParserNode in the nodelist should pass the assertion,
+ this function is used for results of find_* methods. Unimplemented find_*
+ methods should return a sequence containing a single ParserNode instance
+ with assertion pass string."""
+
+ try:
+ node = nodelist[0]
+ except IndexError:
+ node = None
+
+ if not node: # pragma: no cover
+ return False
+
+ if isinstance(node, interfaces.DirectiveNode):
+ return isPassDirective(node)
+ return isPassComment(node)
+
+def assertEqualSimple(first, second):
+ """ Simple assertion """
+ if not isPass(first) and not isPass(second):
+ assert first == second
+
+def isEqualVirtualHost(first, second):
+ """
+ Checks that two VirtualHost objects are similar. There are some built
+ in differences with the implementations: VirtualHost created by ParserNode
+ implementation doesn't have "path" defined, as it was used for Augeas path
+ and that cannot obviously be used in the future. Similarly the legacy
+ version lacks "node" variable, that has a reference to the BlockNode for the
+ VirtualHost.
+ """
+ return (
+ first.name == second.name and
+ first.aliases == second.aliases and
+ first.filep == second.filep and
+ first.addrs == second.addrs and
+ first.ssl == second.ssl and
+ first.enabled == second.enabled and
+ first.modmacro == second.modmacro and
+ first.ancestor == second.ancestor
+ )
+
+def assertEqualPathsList(first, second): # pragma: no cover
+ """
+ Checks that the two lists of file paths match. This assertion allows for wildcard
+ paths.
+ """
+ if any([isPass(path) for path in first]):
+ return
+ if any([isPass(path) for path in second]):
+ return
+ for fpath in first:
+ assert any([fnmatch.fnmatch(fpath, spath) for spath in second])
+ for spath in second:
+ assert any([fnmatch.fnmatch(fpath, spath) for fpath in first])
diff --git a/certbot-apache/certbot_apache/_internal/augeasparser.py b/certbot-apache/certbot_apache/_internal/augeasparser.py
new file mode 100644
index 000000000..e1d7c941d
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/augeasparser.py
@@ -0,0 +1,538 @@
+"""
+Augeas implementation of the ParserNode interfaces.
+
+Augeas works internally by using XPATH notation. The following is a short example
+of how this all works internally, to better understand what's going on under the
+hood.
+
+A configuration file /etc/apache2/apache2.conf with the following content:
+
+ # First comment line
+ # Second comment line
+ WhateverDirective whatevervalue
+ <ABlock>
+ DirectiveInABlock dirvalue
+ </ABlock>
+ SomeDirective somedirectivevalue
+ <ABlock>
+ AnotherDirectiveInABlock dirvalue
+ </ABlock>
+ # Yet another comment
+
+
+Translates over to Augeas path notation (of immediate children), when calling
+for example: aug.match("/files/etc/apache2/apache2.conf/*")
+
+[
+ "/files/etc/apache2/apache2.conf/#comment[1]",
+ "/files/etc/apache2/apache2.conf/#comment[2]",
+ "/files/etc/apache2/apache2.conf/directive[1]",
+ "/files/etc/apache2/apache2.conf/ABlock[1]",
+ "/files/etc/apache2/apache2.conf/directive[2]",
+ "/files/etc/apache2/apache2.conf/ABlock[2]",
+ "/files/etc/apache2/apache2.conf/#comment[3]"
+]
+
+Regardless of directives name, its key in the Augeas tree is always "directive",
+with index where needed of course. Comments work similarly, while blocks
+have their own key in the Augeas XPATH notation.
+
+It's important to note that all of the unique keys have their own indices.
+
+Augeas paths are case sensitive, while Apache configuration is case insensitive.
+It looks like this:
+
+ <block>
+ directive value
+ </block>
+ <Block>
+ Directive Value
+ </Block>
+ <block>
+ directive value
+ </block>
+ <bLoCk>
+ DiReCtiVe VaLuE
+ </bLoCk>
+
+Translates over to:
+
+[
+ "/files/etc/apache2/apache2.conf/block[1]",
+ "/files/etc/apache2/apache2.conf/Block[1]",
+ "/files/etc/apache2/apache2.conf/block[2]",
+ "/files/etc/apache2/apache2.conf/bLoCk[1]",
+]
+"""
+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 apache_util
+from certbot_apache._internal import assertions
+from certbot_apache._internal import interfaces
+from certbot_apache._internal import parser
+from certbot_apache._internal import parsernode_util as util
+
+
+class AugeasParserNode(interfaces.ParserNode):
+ """ Augeas implementation of ParserNode interface """
+
+ def __init__(self, **kwargs):
+ ancestor, dirty, filepath, metadata = util.parsernode_kwargs(kwargs) # pylint: disable=unused-variable
+ super(AugeasParserNode, self).__init__(**kwargs)
+ self.ancestor = ancestor
+ self.filepath = filepath
+ self.dirty = dirty
+ self.metadata = metadata
+ self.parser = self.metadata.get("augeasparser")
+ try:
+ if self.metadata["augeaspath"].endswith("/"):
+ raise errors.PluginError(
+ "Augeas path: {} has a trailing slash".format(
+ self.metadata["augeaspath"]
+ )
+ )
+ except KeyError:
+ raise errors.PluginError("Augeas path is required")
+
+ def save(self, msg):
+ self.parser.save(msg)
+
+ def find_ancestors(self, name):
+ """
+ Searches for ancestor BlockNodes with a given name.
+
+ :param str name: Name of the BlockNode parent to search for
+
+ :returns: List of matching ancestor nodes.
+ :rtype: list of AugeasBlockNode
+ """
+
+ ancestors = []
+
+ parent = self.metadata["augeaspath"]
+ while True:
+ # Get the path of ancestor node
+ parent = parent.rpartition("/")[0]
+ # Root of the tree
+ if not parent or parent == "/files":
+ break
+ anc = self._create_blocknode(parent)
+ if anc.name.lower() == name.lower():
+ ancestors.append(anc)
+
+ return ancestors
+
+ def _create_blocknode(self, path):
+ """
+ Helper function to create a BlockNode from Augeas path. This is used by
+ AugeasParserNode.find_ancestors and AugeasBlockNode.
+ and AugeasBlockNode.find_blocks
+
+ """
+
+ name = self._aug_get_name(path)
+ metadata = {"augeasparser": self.parser, "augeaspath": path}
+
+ # Check if the file was included from the root config or initial state
+ enabled = self.parser.parsed_in_original(
+ apache_util.get_file_path(path)
+ )
+
+ return AugeasBlockNode(name=name,
+ enabled=enabled,
+ ancestor=assertions.PASS,
+ filepath=apache_util.get_file_path(path),
+ metadata=metadata)
+
+ def _aug_get_name(self, path):
+ """
+ Helper function to get name of a configuration block or variable from path.
+ """
+
+ # Remove the ending slash if any
+ if path[-1] == "/": # pragma: no cover
+ path = path[:-1]
+
+ # Get the block name
+ name = path.split("/")[-1]
+
+ # remove [...], it's not allowed in Apache configuration and is used
+ # for indexing within Augeas
+ name = name.split("[")[0]
+ return name
+
+
+class AugeasCommentNode(AugeasParserNode):
+ """ Augeas implementation of CommentNode interface """
+
+ def __init__(self, **kwargs):
+ comment, kwargs = util.commentnode_kwargs(kwargs) # pylint: disable=unused-variable
+ super(AugeasCommentNode, self).__init__(**kwargs)
+ # self.comment = comment
+ self.comment = comment
+
+ def __eq__(self, other):
+ if isinstance(other, self.__class__):
+ return (self.comment == other.comment and
+ self.filepath == other.filepath and
+ self.dirty == other.dirty and
+ self.ancestor == other.ancestor and
+ self.metadata == other.metadata)
+ return False
+
+
+class AugeasDirectiveNode(AugeasParserNode):
+ """ Augeas implementation of DirectiveNode interface """
+
+ def __init__(self, **kwargs):
+ name, parameters, enabled, kwargs = util.directivenode_kwargs(kwargs)
+ super(AugeasDirectiveNode, self).__init__(**kwargs)
+ self.name = name
+ self.enabled = enabled
+ if parameters:
+ self.set_parameters(parameters)
+
+ def __eq__(self, other):
+ if isinstance(other, self.__class__):
+ return (self.name == other.name and
+ self.filepath == other.filepath and
+ self.parameters == other.parameters and
+ self.enabled == other.enabled and
+ self.dirty == other.dirty and
+ self.ancestor == other.ancestor and
+ self.metadata == other.metadata)
+ return False
+
+ def set_parameters(self, parameters):
+ """
+ Sets parameters of a DirectiveNode or BlockNode object.
+
+ :param list parameters: List of all parameters for the node to set.
+ """
+ orig_params = self._aug_get_params(self.metadata["augeaspath"])
+
+ # Clear out old parameters
+ for _ in orig_params:
+ # When the first parameter is removed, the indices get updated
+ param_path = "{}/arg[1]".format(self.metadata["augeaspath"])
+ self.parser.aug.remove(param_path)
+ # Insert new ones
+ for pi, param in enumerate(parameters):
+ param_path = "{}/arg[{}]".format(self.metadata["augeaspath"], pi+1)
+ self.parser.aug.set(param_path, param)
+
+ @property
+ def parameters(self):
+ """
+ Fetches the parameters from Augeas tree, ensuring that the sequence always
+ represents the current state
+
+ :returns: Tuple of parameters for this DirectiveNode
+ :rtype: tuple:
+ """
+ return tuple(self._aug_get_params(self.metadata["augeaspath"]))
+
+ def _aug_get_params(self, path):
+ """Helper function to get parameters for DirectiveNodes and BlockNodes"""
+
+ arg_paths = self.parser.aug.match(path + "/arg")
+ return [self.parser.get_arg(apath) for apath in arg_paths]
+
+
+class AugeasBlockNode(AugeasDirectiveNode):
+ """ Augeas implementation of BlockNode interface """
+
+ def __init__(self, **kwargs):
+ super(AugeasBlockNode, self).__init__(**kwargs)
+ self.children = ()
+
+ def __eq__(self, other):
+ if isinstance(other, self.__class__):
+ return (self.name == other.name and
+ self.filepath == other.filepath and
+ self.parameters == other.parameters and
+ self.children == other.children and
+ self.enabled == other.enabled and
+ self.dirty == other.dirty and
+ self.ancestor == other.ancestor and
+ self.metadata == other.metadata)
+ return False
+
+ # pylint: disable=unused-argument
+ def add_child_block(self, name, parameters=None, position=None): # pragma: no cover
+ """Adds a new BlockNode to the sequence of children"""
+
+ insertpath, realpath, before = self._aug_resolve_child_position(
+ name,
+ position
+ )
+ new_metadata = {"augeasparser": self.parser, "augeaspath": realpath}
+
+ # Create the new block
+ self.parser.aug.insert(insertpath, name, before)
+ # Check if the file was included from the root config or initial state
+ enabled = self.parser.parsed_in_original(
+ apache_util.get_file_path(realpath)
+ )
+
+ # Parameters will be set at the initialization of the new object
+ new_block = AugeasBlockNode(name=name,
+ parameters=parameters,
+ enabled=enabled,
+ ancestor=assertions.PASS,
+ filepath=apache_util.get_file_path(realpath),
+ metadata=new_metadata)
+ return new_block
+
+ # pylint: disable=unused-argument
+ def add_child_directive(self, name, parameters=None, position=None): # pragma: no cover
+ """Adds a new DirectiveNode to the sequence of children"""
+
+ if not parameters:
+ raise errors.PluginError("Directive requires parameters and none were set.")
+
+ insertpath, realpath, before = self._aug_resolve_child_position(
+ "directive",
+ position
+ )
+ new_metadata = {"augeasparser": self.parser, "augeaspath": realpath}
+
+ # Create the new directive
+ self.parser.aug.insert(insertpath, "directive", before)
+ # Set the directive key
+ self.parser.aug.set(realpath, name)
+ # Check if the file was included from the root config or initial state
+ enabled = self.parser.parsed_in_original(
+ apache_util.get_file_path(realpath)
+ )
+
+ new_dir = AugeasDirectiveNode(name=name,
+ parameters=parameters,
+ enabled=enabled,
+ ancestor=assertions.PASS,
+ filepath=apache_util.get_file_path(realpath),
+ metadata=new_metadata)
+ return new_dir
+
+ def add_child_comment(self, comment="", position=None):
+ """Adds a new CommentNode to the sequence of children"""
+
+ insertpath, realpath, before = self._aug_resolve_child_position(
+ "#comment",
+ position
+ )
+ new_metadata = {"augeasparser": self.parser, "augeaspath": realpath}
+
+ # Create the new comment
+ self.parser.aug.insert(insertpath, "#comment", before)
+ # Set the comment content
+ self.parser.aug.set(realpath, comment)
+
+ new_comment = AugeasCommentNode(comment=comment,
+ ancestor=assertions.PASS,
+ filepath=apache_util.get_file_path(realpath),
+ metadata=new_metadata)
+ return new_comment
+
+ def find_blocks(self, name, exclude=True):
+ """Recursive search of BlockNodes from the sequence of children"""
+
+ nodes = list()
+ paths = self._aug_find_blocks(name)
+ if exclude:
+ paths = self.parser.exclude_dirs(paths)
+ for path in paths:
+ nodes.append(self._create_blocknode(path))
+
+ return nodes
+
+ def find_directives(self, name, exclude=True):
+ """Recursive search of DirectiveNodes from the sequence of children"""
+
+ nodes = list()
+ ownpath = self.metadata.get("augeaspath")
+
+ directives = self.parser.find_dir(name, start=ownpath, exclude=exclude)
+ already_parsed = set() # type: Set[str]
+ for directive in directives:
+ # Remove the /arg part from the Augeas path
+ directive = directive.partition("/arg")[0]
+ # find_dir returns an object for each _parameter_ of a directive
+ # so we need to filter out duplicates.
+ if directive not in already_parsed:
+ nodes.append(self._create_directivenode(directive))
+ already_parsed.add(directive)
+
+ return nodes
+
+ def find_comments(self, comment):
+ """
+ Recursive search of DirectiveNodes from the sequence of children.
+
+ :param str comment: Comment content to search for.
+ """
+
+ nodes = list()
+ ownpath = self.metadata.get("augeaspath")
+
+ comments = self.parser.find_comments(comment, start=ownpath)
+ for com in comments:
+ nodes.append(self._create_commentnode(com))
+
+ return nodes
+
+ def delete_child(self, child):
+ """
+ Deletes a ParserNode from the sequence of children, and raises an
+ exception if it's unable to do so.
+ :param AugeasParserNode: child: A node to delete.
+ """
+ if not self.parser.aug.remove(child.metadata["augeaspath"]):
+
+ raise errors.PluginError(
+ ("Could not delete child node, the Augeas path: {} doesn't " +
+ "seem to exist.").format(child.metadata["augeaspath"])
+ )
+
+ def unsaved_files(self):
+ """Returns a list of unsaved filepaths"""
+ return self.parser.unsaved_files()
+
+ def parsed_paths(self):
+ """
+ Returns a list of file paths that have currently been parsed into the parser
+ tree. The returned list may include paths with wildcard characters, for
+ example: ['/etc/apache2/conf.d/*.load']
+
+ This is typically called on the root node of the ParserNode tree.
+
+ :returns: list of file paths of files that have been parsed
+ """
+
+ res_paths = []
+
+ paths = self.parser.existing_paths
+ for directory in paths:
+ for filename in paths[directory]:
+ res_paths.append(os.path.join(directory, filename))
+
+ return res_paths
+
+ def _create_commentnode(self, path):
+ """Helper function to create a CommentNode from Augeas path"""
+
+ comment = self.parser.aug.get(path)
+ metadata = {"augeasparser": self.parser, "augeaspath": path}
+
+ # Because of the dynamic nature of AugeasParser and the fact that we're
+ # not populating the complete node tree, the ancestor has a dummy value
+ return AugeasCommentNode(comment=comment,
+ ancestor=assertions.PASS,
+ filepath=apache_util.get_file_path(path),
+ metadata=metadata)
+
+ def _create_directivenode(self, path):
+ """Helper function to create a DirectiveNode from Augeas path"""
+
+ name = self.parser.get_arg(path)
+ metadata = {"augeasparser": self.parser, "augeaspath": path}
+
+ # Check if the file was included from the root config or initial state
+ enabled = self.parser.parsed_in_original(
+ apache_util.get_file_path(path)
+ )
+ return AugeasDirectiveNode(name=name,
+ ancestor=assertions.PASS,
+ enabled=enabled,
+ filepath=apache_util.get_file_path(path),
+ metadata=metadata)
+
+ def _aug_find_blocks(self, name):
+ """Helper function to perform a search to Augeas DOM tree to search
+ configuration blocks with a given name"""
+
+ # The code here is modified from configurator.get_virtual_hosts()
+ blk_paths = set()
+ for vhost_path in list(self.parser.parser_paths):
+ paths = self.parser.aug.match(
+ ("/files%s//*[label()=~regexp('%s')]" %
+ (vhost_path, parser.case_i(name))))
+ blk_paths.update([path for path in paths if
+ name.lower() in os.path.basename(path).lower()])
+ return blk_paths
+
+ def _aug_resolve_child_position(self, name, position):
+ """
+ Helper function that iterates through the immediate children and figures
+ out the insertion path for a new AugeasParserNode.
+
+ Augeas also generalizes indices for directives and comments, simply by
+ using "directive" or "comment" respectively as their names.
+
+ This function iterates over the existing children of the AugeasBlockNode,
+ returning their insertion path, resulting Augeas path and if the new node
+ should be inserted before or after the returned insertion path.
+
+ Note: while Apache is case insensitive, Augeas is not, and blocks like
+ Nameofablock and NameOfABlock have different indices.
+
+ :param str name: Name of the AugeasBlockNode to insert, "directive" for
+ AugeasDirectiveNode or "comment" for AugeasCommentNode
+ :param int position: The position to insert the child AugeasParserNode to
+
+ :returns: Tuple of insert path, resulting path and a boolean if the new
+ node should be inserted before it.
+ :rtype: tuple of str, str, bool
+ """
+
+ # Default to appending
+ before = False
+
+ all_children = self.parser.aug.match("{}/*".format(
+ self.metadata["augeaspath"])
+ )
+
+ # Calculate resulting_path
+ # Augeas indices start at 1. We use counter to calculate the index to
+ # be used in resulting_path.
+ counter = 1
+ for i, child in enumerate(all_children):
+ if position is not None and i >= position:
+ # We're not going to insert the new node to an index after this
+ break
+ childname = self._aug_get_name(child)
+ if name == childname:
+ counter += 1
+
+ resulting_path = "{}/{}[{}]".format(
+ self.metadata["augeaspath"],
+ name,
+ counter
+ )
+
+ # Form the correct insert_path
+ # Inserting the only child and appending as the last child work
+ # similarly in Augeas.
+ append = not all_children or position is None or position >= len(all_children)
+ if append:
+ insert_path = "{}/*[last()]".format(
+ self.metadata["augeaspath"]
+ )
+ elif position == 0:
+ # Insert as the first child, before the current first one.
+ insert_path = all_children[0]
+ before = True
+ else:
+ insert_path = "{}/*[{}]".format(
+ self.metadata["augeaspath"],
+ position
+ )
+
+ return (insert_path, resulting_path, before)
+
+
+interfaces.CommentNode.register(AugeasCommentNode)
+interfaces.DirectiveNode.register(AugeasDirectiveNode)
+interfaces.BlockNode.register(AugeasBlockNode)
diff --git a/certbot-apache/certbot_apache/_internal/configurator.py b/certbot-apache/certbot_apache/_internal/configurator.py
index 84b59d2c7..e9ed1f8ab 100644
--- a/certbot-apache/certbot_apache/_internal/configurator.py
+++ b/certbot-apache/certbot_apache/_internal/configurator.py
@@ -29,8 +29,10 @@ 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 assertions
from certbot_apache._internal import constants
from certbot_apache._internal import display_ops
+from certbot_apache._internal import dualparser
from certbot_apache._internal import http_01
from certbot_apache._internal import obj
from certbot_apache._internal import parser
@@ -181,6 +183,7 @@ class ApacheConfigurator(common.Installer):
"""
version = kwargs.pop("version", None)
+ use_parsernode = kwargs.pop("use_parsernode", False)
super(ApacheConfigurator, self).__init__(*args, **kwargs)
# Add name_server association dict
@@ -196,10 +199,15 @@ class ApacheConfigurator(common.Installer):
self._autohsts = {} # type: Dict[str, Dict[str, Union[int, float]]]
# Reverter save notes
self.save_notes = ""
-
+ # Should we use ParserNode implementation instead of the old behavior
+ self.USE_PARSERNODE = use_parsernode
+ # Saves the list of file paths that were parsed initially, and
+ # not added to parser tree by self.conf("vhost-root") for example.
+ self.parsed_paths = [] # type: List[str]
# These will be set in the prepare function
self._prepared = False
self.parser = None
+ self.parser_root = None
self.version = version
self.vhosts = None
self.options = copy.deepcopy(self.OS_DEFAULTS)
@@ -249,6 +257,14 @@ class ApacheConfigurator(common.Installer):
# Perform the actual Augeas initialization to be able to react
self.parser = self.get_parser()
+ # Set up ParserNode root
+ pn_meta = {"augeasparser": self.parser,
+ "augeaspath": self.parser.get_root_augpath(),
+ "ac_ast": None}
+ if self.USE_PARSERNODE:
+ self.parser_root = self.get_parsernode_root(pn_meta)
+ self.parsed_paths = self.parser_root.parsed_paths()
+
# Check for errors in parsing files with Augeas
self.parser.check_parsing_errors("httpd.aug")
@@ -344,6 +360,22 @@ class ApacheConfigurator(common.Installer):
self.option("server_root"), self.conf("vhost-root"),
self.version, configurator=self)
+ def get_parsernode_root(self, metadata):
+ """Initializes the ParserNode parser root instance."""
+
+ apache_vars = dict()
+ apache_vars["defines"] = apache_util.parse_defines(self.option("ctl"))
+ apache_vars["includes"] = apache_util.parse_includes(self.option("ctl"))
+ apache_vars["modules"] = apache_util.parse_modules(self.option("ctl"))
+ metadata["apache_vars"] = apache_vars
+
+ return dualparser.DualBlockNode(
+ name=assertions.PASS,
+ ancestor=None,
+ filepath=self.parser.loc["root"],
+ metadata=metadata
+ )
+
def _wildcard_domain(self, domain):
"""
Checks if domain is a wildcard domain
@@ -868,6 +900,29 @@ class ApacheConfigurator(common.Installer):
return vhost
def get_virtual_hosts(self):
+ """
+ Temporary wrapper for legacy and ParserNode version for
+ get_virtual_hosts. This should be replaced with the ParserNode
+ implementation when ready.
+ """
+
+ v1_vhosts = self.get_virtual_hosts_v1()
+ if self.USE_PARSERNODE:
+ v2_vhosts = self.get_virtual_hosts_v2()
+
+ for v1_vh in v1_vhosts:
+ found = False
+ for v2_vh in v2_vhosts:
+ if assertions.isEqualVirtualHost(v1_vh, v2_vh):
+ found = True
+ break
+ if not found:
+ raise AssertionError("Equivalent for {} was not found".format(v1_vh.path))
+
+ return v2_vhosts
+ return v1_vhosts
+
+ def get_virtual_hosts_v1(self):
"""Returns list of virtual hosts found in the Apache configuration.
:returns: List of :class:`~certbot_apache._internal.obj.VirtualHost`
@@ -920,6 +975,80 @@ class ApacheConfigurator(common.Installer):
vhs.append(new_vhost)
return vhs
+ def get_virtual_hosts_v2(self):
+ """Returns list of virtual hosts found in the Apache configuration using
+ ParserNode interface.
+ :returns: List of :class:`~certbot_apache.obj.VirtualHost`
+ objects found in configuration
+ :rtype: list
+ """
+
+ vhs = []
+ vhosts = self.parser_root.find_blocks("VirtualHost", exclude=False)
+ for vhblock in vhosts:
+ vhs.append(self._create_vhost_v2(vhblock))
+ return vhs
+
+ def _create_vhost_v2(self, node):
+ """Used by get_virtual_hosts_v2 to create vhost objects using ParserNode
+ interfaces.
+ :param interfaces.BlockNode node: The BlockNode object of VirtualHost block
+ :returns: newly created vhost
+ :rtype: :class:`~certbot_apache.obj.VirtualHost`
+ """
+ addrs = set()
+ for param in node.parameters:
+ addrs.add(obj.Addr.fromstring(param))
+
+ is_ssl = False
+ # Exclusion to match the behavior in get_virtual_hosts_v2
+ sslengine = node.find_directives("SSLEngine", exclude=False)
+ if sslengine:
+ for directive in sslengine:
+ if directive.parameters[0].lower() == "on":
+ is_ssl = True
+ break
+
+ # "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
+
+ enabled = apache_util.included_in_paths(node.filepath, self.parsed_paths)
+
+ macro = False
+ # Check if the VirtualHost is contained in a mod_macro block
+ if node.find_ancestors("Macro"):
+ macro = True
+ vhost = obj.VirtualHost(
+ node.filepath, None, addrs, is_ssl, enabled, modmacro=macro, node=node
+ )
+ self._populate_vhost_names_v2(vhost)
+ return vhost
+
+ def _populate_vhost_names_v2(self, vhost):
+ """Helper function that populates the VirtualHost names.
+ :param host: In progress vhost whose names will be added
+ :type host: :class:`~certbot_apache.obj.VirtualHost`
+ """
+
+ servername_match = vhost.node.find_directives("ServerName",
+ exclude=False)
+ serveralias_match = vhost.node.find_directives("ServerAlias",
+ exclude=False)
+
+ servername = None
+ if servername_match:
+ servername = servername_match[-1].parameters[-1]
+
+ if not vhost.modmacro:
+ for alias in serveralias_match:
+ for serveralias in alias.parameters:
+ vhost.aliases.add(serveralias)
+ vhost.name = servername
+
+
def is_name_vhost(self, target_addr):
"""Returns if vhost is a name based vhost
diff --git a/certbot-apache/certbot_apache/_internal/dualparser.py b/certbot-apache/certbot_apache/_internal/dualparser.py
new file mode 100644
index 000000000..aa66cf84c
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/dualparser.py
@@ -0,0 +1,306 @@
+""" Dual ParserNode implementation """
+from certbot_apache._internal import assertions
+from certbot_apache._internal import augeasparser
+from certbot_apache._internal import apacheparser
+
+
+class DualNodeBase(object):
+ """ Dual parser interface for in development testing. This is used as the
+ base class for dual parser interface classes. This class handles runtime
+ attribute value assertions."""
+
+ def save(self, msg): # pragma: no cover
+ """ Call save for both parsers """
+ self.primary.save(msg)
+ self.secondary.save(msg)
+
+ def __getattr__(self, aname):
+ """ Attribute value assertion """
+ firstval = getattr(self.primary, aname)
+ secondval = getattr(self.secondary, aname)
+ exclusions = [
+ # Metadata will inherently be different, as ApacheParserNode does
+ # not have Augeas paths and so on.
+ aname == "metadata",
+ callable(firstval)
+ ]
+ if not any(exclusions):
+ assertions.assertEqualSimple(firstval, secondval)
+ return firstval
+
+ def find_ancestors(self, name):
+ """ Traverses the ancestor tree and returns ancestors matching name """
+ return self._find_helper(DualBlockNode, "find_ancestors", name)
+
+ def _find_helper(self, nodeclass, findfunc, search, **kwargs):
+ """A helper for find_* functions. The function specific attributes should
+ be passed as keyword arguments.
+
+ :param interfaces.ParserNode nodeclass: The node class for results.
+ :param str findfunc: Name of the find function to call
+ :param str search: The search term
+ """
+
+ primary_res = getattr(self.primary, findfunc)(search, **kwargs)
+ secondary_res = getattr(self.secondary, findfunc)(search, **kwargs)
+
+ # The order of search results for Augeas implementation cannot be
+ # assured.
+
+ pass_primary = assertions.isPassNodeList(primary_res)
+ pass_secondary = assertions.isPassNodeList(secondary_res)
+ new_nodes = list()
+
+ if pass_primary and pass_secondary:
+ # Both unimplemented
+ new_nodes.append(nodeclass(primary=primary_res[0],
+ secondary=secondary_res[0])) # pragma: no cover
+ elif pass_primary:
+ for c in secondary_res:
+ new_nodes.append(nodeclass(primary=primary_res[0],
+ secondary=c))
+ elif pass_secondary:
+ for c in primary_res:
+ new_nodes.append(nodeclass(primary=c,
+ secondary=secondary_res[0]))
+ else:
+ assert len(primary_res) == len(secondary_res)
+ matches = self._create_matching_list(primary_res, secondary_res)
+ for p, s in matches:
+ new_nodes.append(nodeclass(primary=p, secondary=s))
+
+ return new_nodes
+
+
+class DualCommentNode(DualNodeBase):
+ """ Dual parser implementation of CommentNode interface """
+
+ def __init__(self, **kwargs):
+ """ This initialization implementation allows ordinary initialization
+ of CommentNode objects as well as creating a DualCommentNode object
+ using precreated or fetched CommentNode objects if provided as optional
+ arguments primary and secondary.
+
+ Parameters other than the following are from interfaces.CommentNode:
+
+ :param CommentNode primary: Primary pre-created CommentNode, mainly
+ used when creating new DualParser nodes using add_* methods.
+ :param CommentNode secondary: Secondary pre-created CommentNode
+ """
+
+ kwargs.setdefault("primary", None)
+ kwargs.setdefault("secondary", None)
+ primary = kwargs.pop("primary")
+ secondary = kwargs.pop("secondary")
+
+ if primary or secondary:
+ assert primary and secondary
+ self.primary = primary
+ self.secondary = secondary
+ else:
+ self.primary = augeasparser.AugeasCommentNode(**kwargs)
+ self.secondary = apacheparser.ApacheCommentNode(**kwargs)
+
+ assertions.assertEqual(self.primary, self.secondary)
+
+
+class DualDirectiveNode(DualNodeBase):
+ """ Dual parser implementation of DirectiveNode interface """
+
+ def __init__(self, **kwargs):
+ """ This initialization implementation allows ordinary initialization
+ of DirectiveNode objects as well as creating a DualDirectiveNode object
+ using precreated or fetched DirectiveNode objects if provided as optional
+ arguments primary and secondary.
+
+ Parameters other than the following are from interfaces.DirectiveNode:
+
+ :param DirectiveNode primary: Primary pre-created DirectiveNode, mainly
+ used when creating new DualParser nodes using add_* methods.
+ :param DirectiveNode secondary: Secondary pre-created DirectiveNode
+
+
+ """
+
+ kwargs.setdefault("primary", None)
+ kwargs.setdefault("secondary", None)
+ primary = kwargs.pop("primary")
+ secondary = kwargs.pop("secondary")
+
+ if primary or secondary:
+ assert primary and secondary
+ self.primary = primary
+ self.secondary = secondary
+ else:
+ self.primary = augeasparser.AugeasDirectiveNode(**kwargs)
+ self.secondary = apacheparser.ApacheDirectiveNode(**kwargs)
+
+ assertions.assertEqual(self.primary, self.secondary)
+
+ def set_parameters(self, parameters):
+ """ Sets parameters and asserts that both implementation successfully
+ set the parameter sequence """
+
+ self.primary.set_parameters(parameters)
+ self.secondary.set_parameters(parameters)
+ assertions.assertEqual(self.primary, self.secondary)
+
+
+class DualBlockNode(DualNodeBase):
+ """ Dual parser implementation of BlockNode interface """
+
+ def __init__(self, **kwargs):
+ """ This initialization implementation allows ordinary initialization
+ of BlockNode objects as well as creating a DualBlockNode object
+ using precreated or fetched BlockNode objects if provided as optional
+ arguments primary and secondary.
+
+ Parameters other than the following are from interfaces.BlockNode:
+
+ :param BlockNode primary: Primary pre-created BlockNode, mainly
+ used when creating new DualParser nodes using add_* methods.
+ :param BlockNode secondary: Secondary pre-created BlockNode
+ """
+
+ kwargs.setdefault("primary", None)
+ kwargs.setdefault("secondary", None)
+ primary = kwargs.pop("primary")
+ secondary = kwargs.pop("secondary")
+
+ if primary or secondary:
+ assert primary and secondary
+ self.primary = primary
+ self.secondary = secondary
+ else:
+ self.primary = augeasparser.AugeasBlockNode(**kwargs)
+ self.secondary = apacheparser.ApacheBlockNode(**kwargs)
+
+ assertions.assertEqual(self.primary, self.secondary)
+
+ def add_child_block(self, name, parameters=None, position=None):
+ """ Creates a new child BlockNode, asserts that both implementations
+ did it in a similar way, and returns a newly created DualBlockNode object
+ encapsulating both of the newly created objects """
+
+ primary_new = self.primary.add_child_block(name, parameters, position)
+ secondary_new = self.secondary.add_child_block(name, parameters, position)
+ assertions.assertEqual(primary_new, secondary_new)
+ new_block = DualBlockNode(primary=primary_new, secondary=secondary_new)
+ return new_block
+
+ def add_child_directive(self, name, parameters=None, position=None):
+ """ Creates a new child DirectiveNode, asserts that both implementations
+ did it in a similar way, and returns a newly created DualDirectiveNode
+ object encapsulating both of the newly created objects """
+
+ primary_new = self.primary.add_child_directive(name, parameters, position)
+ secondary_new = self.secondary.add_child_directive(name, parameters, position)
+ assertions.assertEqual(primary_new, secondary_new)
+ new_dir = DualDirectiveNode(primary=primary_new, secondary=secondary_new)
+ return new_dir
+
+ def add_child_comment(self, comment="", position=None):
+ """ Creates a new child CommentNode, asserts that both implementations
+ did it in a similar way, and returns a newly created DualCommentNode
+ object encapsulating both of the newly created objects """
+
+ primary_new = self.primary.add_child_comment(comment, position)
+ secondary_new = self.secondary.add_child_comment(comment, position)
+ assertions.assertEqual(primary_new, secondary_new)
+ new_comment = DualCommentNode(primary=primary_new, secondary=secondary_new)
+ return new_comment
+
+ def _create_matching_list(self, primary_list, secondary_list):
+ """ Matches the list of primary_list to a list of secondary_list and
+ returns a list of tuples. This is used to create results for find_
+ methods.
+
+ This helper function exists, because we cannot ensure that the list of
+ search results returned by primary.find_* and secondary.find_* are ordered
+ in a same way. The function pairs the same search results from both
+ implementations to a list of tuples.
+ """
+
+ matched = list()
+ for p in primary_list:
+ match = None
+ for s in secondary_list:
+ try:
+ assertions.assertEqual(p, s)
+ match = s
+ break
+ except AssertionError:
+ continue
+ if match:
+ matched.append((p, match))
+ else:
+ raise AssertionError("Could not find a matching node.")
+ return matched
+
+ def find_blocks(self, name, exclude=True):
+ """
+ Performs a search for BlockNodes using both implementations and does simple
+ checks for results. This is built upon the assumption that unimplemented
+ find_* methods return a list with a single assertion passing object.
+ After the assertion, it creates a list of newly created DualBlockNode
+ instances that encapsulate the pairs of returned BlockNode objects.
+ """
+
+ return self._find_helper(DualBlockNode, "find_blocks", name,
+ exclude=exclude)
+
+ def find_directives(self, name, exclude=True):
+ """
+ Performs a search for DirectiveNodes using both implementations and
+ checks the results. This is built upon the assumption that unimplemented
+ find_* methods return a list with a single assertion passing object.
+ After the assertion, it creates a list of newly created DualDirectiveNode
+ instances that encapsulate the pairs of returned DirectiveNode objects.
+ """
+
+ return self._find_helper(DualDirectiveNode, "find_directives", name,
+ exclude=exclude)
+
+ def find_comments(self, comment):
+ """
+ Performs a search for CommentNodes using both implementations and
+ checks the results. This is built upon the assumption that unimplemented
+ find_* methods return a list with a single assertion passing object.
+ After the assertion, it creates a list of newly created DualCommentNode
+ instances that encapsulate the pairs of returned CommentNode objects.
+ """
+
+ return self._find_helper(DualCommentNode, "find_comments", comment)
+
+ def delete_child(self, child):
+ """Deletes a child from the ParserNode implementations. The actual
+ ParserNode implementations are used here directly in order to be able
+ to match a child to the list of children."""
+
+ self.primary.delete_child(child.primary)
+ self.secondary.delete_child(child.secondary)
+
+ def unsaved_files(self):
+ """ Fetches the list of unsaved file paths and asserts that the lists
+ match """
+ primary_files = self.primary.unsaved_files()
+ secondary_files = self.secondary.unsaved_files()
+ assertions.assertEqualSimple(primary_files, secondary_files)
+
+ return primary_files
+
+ def parsed_paths(self):
+ """
+ Returns a list of file paths that have currently been parsed into the parser
+ tree. The returned list may include paths with wildcard characters, for
+ example: ['/etc/apache2/conf.d/*.load']
+
+ This is typically called on the root node of the ParserNode tree.
+
+ :returns: list of file paths of files that have been parsed
+ """
+
+ primary_paths = self.primary.parsed_paths()
+ secondary_paths = self.secondary.parsed_paths()
+ assertions.assertEqualPathsList(primary_paths, secondary_paths)
+ return primary_paths
diff --git a/certbot-apache/certbot_apache/_internal/interfaces.py b/certbot-apache/certbot_apache/_internal/interfaces.py
new file mode 100644
index 000000000..1b67be5c8
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/interfaces.py
@@ -0,0 +1,516 @@
+"""ParserNode interface for interacting with configuration tree.
+
+General description
+-------------------
+
+The ParserNode interfaces are designed to be able to contain all the parsing logic,
+while allowing their users to interact with the configuration tree in a Pythonic
+and well structured manner.
+
+The structure allows easy traversal of the tree of ParserNodes. Each ParserNode
+stores a reference to its ancestor and immediate children, allowing the user to
+traverse the tree using built in interface methods as well as accessing the interface
+properties directly.
+
+ParserNode interface implementation should stand between the actual underlying
+parser functionality and the business logic within Configurator code, interfacing
+with both. The ParserNode tree is a result of configuration parsing action.
+
+ParserNode tree will be in charge of maintaining the parser state and hence the
+abstract syntax tree (AST). Interactions between ParserNode tree and underlying
+parser should involve only parsing the configuration files to this structure, and
+writing it back to the filesystem - while preserving the format including whitespaces.
+
+For some implementations (Apache for example) it's important to keep track of and
+to use state information while parsing conditional blocks and directives. This
+allows the implementation to set a flag to parts of the parsed configuration
+structure as not being in effect in a case of unmatched conditional block. It's
+important to store these blocks in the tree as well in order to not to conduct
+destructive actions (failing to write back parts of the configuration) while writing
+the AST back to the filesystem.
+
+The ParserNode tree is in charge of maintaining the its own structure while every
+child node fetched with find - methods or by iterating its list of children can be
+changed in place. When making changes the affected nodes should be flagged as "dirty"
+in order for the parser implementation to figure out the parts of the configuration
+that need to be written back to disk during the save() operation.
+
+
+Metadata
+--------
+
+The metadata holds all the implementation specific attributes of the ParserNodes -
+things like the positional information related to the AST, file paths, whitespacing,
+and any other information relevant to the underlying parser engine.
+
+Access to the metadata should be handled by implementation specific methods, allowing
+the Configurator functionality to access the underlying information where needed.
+
+For some implementations the node can be initialized using the information carried
+in metadata alone. This is useful especially when populating the ParserNode tree
+while parsing the configuration.
+
+
+Apache implementation
+---------------------
+
+The Apache implementation of ParserNode interface requires some implementation
+specific functionalities that are not described by the interface itself.
+
+Initialization
+
+When the user of a ParserNode class is creating these objects, they must specify
+the parameters as described in the documentation for the __init__ methods below.
+When these objects are created internally, however, some parameters may not be
+needed because (possibly more detailed) information is included in the metadata
+parameter. In this case, implementations can deviate from the required parameters
+from __init__, however, they should still behave the same when metadata is not
+provided.
+
+For consistency internally, if an argument is provided directly in the ParserNode
+initialization parameters as well as within metadata it's recommended to establish
+clear behavior around this scenario within the implementation.
+
+Conditional blocks
+
+Apache configuration can have conditional blocks, for example: <IfModule ...>,
+resulting the directives and subblocks within it being either enabled or disabled.
+While find_* interface methods allow including the disabled parts of the configuration
+tree in searches a special care needs to be taken while parsing the structure in
+order to reflect the active state of configuration.
+
+Whitespaces
+
+Each ParserNode object is responsible of storing its prepending whitespace characters
+in order to be able to write the AST back to filesystem like it was, preserving the
+format, this applies for parameters of BlockNode and DirectiveNode as well.
+When parameters of ParserNode are changed, the pre-existing whitespaces in the
+parameter sequence are discarded, as the general reason for storing them is to
+maintain the ability to write the configuration back to filesystem exactly like
+it was. This loses its meaning when we have to change the directives or blocks
+parameters for other reasons.
+
+Searches and matching
+
+Apache configuration is largely case insensitive, so the Apache implementation of
+ParserNode interface needs to provide the user means to match block and directive
+names and parameters in case insensitive manner. This does not apply to everything
+however, for example the parameters of a conditional statement may be case sensitive.
+For this reason the internal representation of data should not ignore the case.
+"""
+
+import abc
+import six
+
+from acme.magic_typing import Any, Dict, Optional, Tuple # pylint: disable=unused-import, no-name-in-module
+
+
+@six.add_metaclass(abc.ABCMeta)
+class ParserNode(object):
+ """
+ ParserNode is the basic building block of the tree of such nodes,
+ representing the structure of the configuration. It is largely meant to keep
+ the structure information intact and idiomatically accessible.
+
+ The root node as well as the child nodes of it should be instances of ParserNode.
+ Nodes keep track of their differences to on-disk representation of configuration
+ by marking modified ParserNodes as dirty to enable partial write-to-disk for
+ different files in the configuration structure.
+
+ While for the most parts the usage and the child types are obvious, "include"-
+ and similar directives are an exception to this rule. This is because of the
+ nature of include directives - which unroll the contents of another file or
+ configuration block to their place. While we could unroll the included nodes
+ to the parent tree, it remains important to keep the context of include nodes
+ separate in order to write back the original configuration as it was.
+
+ For parsers that require the implementation to keep track of the whitespacing,
+ it's responsibility of each ParserNode object itself to store its prepending
+ whitespaces in order to be able to reconstruct the complete configuration file
+ as it was when originally read from the disk.
+
+ ParserNode objects should have the following attributes:
+
+ # Reference to ancestor node, or None if the node is the root node of the
+ # configuration tree.
+ ancestor: Optional[ParserNode]
+
+ # True if this node has been modified since last save.
+ dirty: bool
+
+ # Filepath of the file where the configuration element for this ParserNode
+ # object resides. For root node, the value for filepath is the httpd root
+ # configuration file. Filepath can be None if a configuration directive is
+ # defined in for example the httpd command line.
+ filepath: Optional[str]
+
+ # Metadata dictionary holds all the implementation specific key-value pairs
+ # for the ParserNode instance.
+ metadata: Dict[str, Any]
+ """
+
+ @abc.abstractmethod
+ def __init__(self, **kwargs):
+ """
+ Initializes the ParserNode instance, and sets the ParserNode specific
+ instance variables. This is not meant to be used directly, but through
+ specific classes implementing ParserNode interface.
+
+ :param ancestor: BlockNode ancestor for this CommentNode. Required.
+ :type ancestor: BlockNode or None
+
+ :param filepath: Filesystem path for the file where this CommentNode
+ does or should exist in the filesystem. Required.
+ :type filepath: str or None
+
+ :param dirty: Boolean flag for denoting if this CommentNode has been
+ created or changed after the last save. Default: False.
+ :type dirty: bool
+
+ :param metadata: Dictionary of metadata values for this ParserNode object.
+ Metadata information should be used only internally in the implementation.
+ Default: {}
+ :type metadata: dict
+ """
+
+ @abc.abstractmethod
+ def save(self, msg):
+ """
+ Save traverses the children, and attempts to write the AST to disk for
+ all the objects that are marked dirty. The actual operation of course
+ depends on the underlying implementation. save() shouldn't be called
+ from the Configurator outside of its designated save() method in order
+ to ensure that the Reverter checkpoints are created properly.
+
+ Note: this approach of keeping internal structure of the configuration
+ within the ParserNode tree does not represent the file inclusion structure
+ of actual configuration files that reside in the filesystem. To handle
+ file writes properly, the file specific temporary trees should be extracted
+ from the full ParserNode tree where necessary when writing to disk.
+
+ :param str msg: Message describing the reason for the save.
+
+ """
+
+ @abc.abstractmethod
+ def find_ancestors(self, name):
+ """
+ Traverses the ancestor tree up, searching for BlockNodes with a specific
+ name.
+
+ :param str name: Name of the ancestor BlockNode to search for
+
+ :returns: A list of ancestor BlockNodes that match the name
+ :rtype: list of BlockNode
+ """
+
+
+# Linter rule exclusion done because of https://github.com/PyCQA/pylint/issues/179
+@six.add_metaclass(abc.ABCMeta) # pylint: disable=abstract-method
+class CommentNode(ParserNode):
+ """
+ CommentNode class is used for representation of comments within the parsed
+ configuration structure. Because of the nature of comments, it is not able
+ to have child nodes and hence it is always treated as a leaf node.
+
+ CommentNode stores its contents in class variable 'comment' and does not
+ have a specific name.
+
+ CommentNode objects should have the following attributes in addition to
+ the ones described in ParserNode:
+
+ # Contains the contents of the comment without the directive notation
+ # (typically # or /* ... */).
+ comment: str
+
+ """
+
+ @abc.abstractmethod
+ def __init__(self, **kwargs):
+ """
+ Initializes the CommentNode instance and sets its instance variables.
+
+ :param comment: Contents of the comment. Required.
+ :type comment: str
+
+ :param ancestor: BlockNode ancestor for this CommentNode. Required.
+ :type ancestor: BlockNode or None
+
+ :param filepath: Filesystem path for the file where this CommentNode
+ does or should exist in the filesystem. Required.
+ :type filepath: str or None
+
+ :param dirty: Boolean flag for denoting if this CommentNode has been
+ created or changed after the last save. Default: False.
+ :type dirty: bool
+ """
+ super(CommentNode, self).__init__(ancestor=kwargs['ancestor'],
+ dirty=kwargs.get('dirty', False),
+ filepath=kwargs['filepath'],
+ metadata=kwargs.get('metadata', {})) # pragma: no cover
+
+
+@six.add_metaclass(abc.ABCMeta)
+class DirectiveNode(ParserNode):
+ """
+ DirectiveNode class represents a configuration directive within the configuration.
+ It can have zero or more parameters attached to it. Because of the nature of
+ single directives, it is not able to have child nodes and hence it is always
+ treated as a leaf node.
+
+ If a this directive was defined on the httpd command line, the ancestor instance
+ variable for this DirectiveNode should be None, and it should be inserted to the
+ beginning of root BlockNode children sequence.
+
+ DirectiveNode objects should have the following attributes in addition to
+ the ones described in ParserNode:
+
+ # True if this DirectiveNode is enabled and False if it is inside of an
+ # inactive conditional block.
+ enabled: bool
+
+ # Name, or key of the configuration directive. If BlockNode subclass of
+ # DirectiveNode is the root configuration node, the name should be None.
+ name: Optional[str]
+
+ # Tuple of parameters of this ParserNode object, excluding whitespaces.
+ parameters: Tuple[str, ...]
+
+ """
+
+ @abc.abstractmethod
+ def __init__(self, **kwargs):
+ """
+ Initializes the DirectiveNode instance and sets its instance variables.
+
+ :param name: Name or key of the DirectiveNode object. Required.
+ :type name: str or None
+
+ :param tuple parameters: Tuple of str parameters for this DirectiveNode.
+ Default: ().
+ :type parameters: tuple
+
+ :param ancestor: BlockNode ancestor for this DirectiveNode, or None for
+ root configuration node. Required.
+ :type ancestor: BlockNode or None
+
+ :param filepath: Filesystem path for the file where this DirectiveNode
+ does or should exist in the filesystem, or None for directives introduced
+ in the httpd command line. Required.
+ :type filepath: str or None
+
+ :param dirty: Boolean flag for denoting if this DirectiveNode has been
+ created or changed after the last save. Default: False.
+ :type dirty: bool
+
+ :param enabled: True if this DirectiveNode object is parsed in the active
+ configuration of the httpd. False if the DirectiveNode exists within a
+ unmatched conditional configuration block. Default: True.
+ :type enabled: bool
+
+ """
+ super(DirectiveNode, self).__init__(ancestor=kwargs['ancestor'],
+ dirty=kwargs.get('dirty', False),
+ filepath=kwargs['filepath'],
+ metadata=kwargs.get('metadata', {})) # pragma: no cover
+
+ @abc.abstractmethod
+ def set_parameters(self, parameters):
+ """
+ Sets the sequence of parameters for this ParserNode object without
+ whitespaces. While the whitespaces for parameters are discarded when using
+ this method, the whitespacing preceeding the ParserNode itself should be
+ kept intact.
+
+ :param list parameters: sequence of parameters
+ """
+
+
+@six.add_metaclass(abc.ABCMeta)
+class BlockNode(DirectiveNode):
+ """
+ BlockNode class represents a block of nested configuration directives, comments
+ and other blocks as its children. A BlockNode can have zero or more parameters
+ attached to it.
+
+ Configuration blocks typically consist of one or more child nodes of all possible
+ types. Because of this, the BlockNode class has various discovery and structure
+ management methods.
+
+ Lists of parameters used as an optional argument for some of the methods should
+ be lists of strings that are applicable parameters for each specific BlockNode
+ or DirectiveNode type. As an example, for a following configuration example:
+
+ <VirtualHost *:80>
+ ...
+ </VirtualHost>
+
+ The node type would be BlockNode, name would be 'VirtualHost' and its parameters
+ would be: ['*:80'].
+
+ While for the following example:
+
+ LoadModule alias_module /usr/lib/apache2/modules/mod_alias.so
+
+ The node type would be DirectiveNode, name would be 'LoadModule' and its
+ parameters would be: ['alias_module', '/usr/lib/apache2/modules/mod_alias.so']
+
+ The applicable parameters are dependent on the underlying configuration language
+ and its grammar.
+
+ BlockNode objects should have the following attributes in addition to
+ the ones described in DirectiveNode:
+
+ # Tuple of direct children of this BlockNode object. The order of children
+ # in this tuple retain the order of elements in the parsed configuration
+ # block.
+ children: Tuple[ParserNode, ...]
+
+ """
+
+ @abc.abstractmethod
+ def add_child_block(self, name, parameters=None, position=None):
+ """
+ Adds a new BlockNode child node with provided values and marks the callee
+ BlockNode dirty. This is used to add new children to the AST. The preceeding
+ whitespaces should not be added based on the ancestor or siblings for the
+ newly created object. This is to match the current behavior of the legacy
+ parser implementation.
+
+ :param str name: The name of the child node to add
+ :param list parameters: list of parameters for the node
+ :param int position: Position in the list of children to add the new child
+ node to. Defaults to None, which appends the newly created node to the list.
+ If an integer is given, the child is inserted before that index in the
+ list similar to list().insert.
+
+ :returns: BlockNode instance of the created child block
+
+ """
+
+ @abc.abstractmethod
+ def add_child_directive(self, name, parameters=None, position=None):
+ """
+ Adds a new DirectiveNode child node with provided values and marks the
+ callee BlockNode dirty. This is used to add new children to the AST. The
+ preceeding whitespaces should not be added based on the ancestor or siblings
+ for the newly created object. This is to match the current behavior of the
+ legacy parser implementation.
+
+
+ :param str name: The name of the child node to add
+ :param list parameters: list of parameters for the node
+ :param int position: Position in the list of children to add the new child
+ node to. Defaults to None, which appends the newly created node to the list.
+ If an integer is given, the child is inserted before that index in the
+ list similar to list().insert.
+
+ :returns: DirectiveNode instance of the created child directive
+
+ """
+
+ @abc.abstractmethod
+ def add_child_comment(self, comment="", position=None):
+ """
+ Adds a new CommentNode child node with provided value and marks the
+ callee BlockNode dirty. This is used to add new children to the AST. The
+ preceeding whitespaces should not be added based on the ancestor or siblings
+ for the newly created object. This is to match the current behavior of the
+ legacy parser implementation.
+
+
+ :param str comment: Comment contents
+ :param int position: Position in the list of children to add the new child
+ node to. Defaults to None, which appends the newly created node to the list.
+ If an integer is given, the child is inserted before that index in the
+ list similar to list().insert.
+
+ :returns: CommentNode instance of the created child comment
+
+ """
+
+ @abc.abstractmethod
+ def find_blocks(self, name, exclude=True):
+ """
+ Find a configuration block by name. This method walks the child tree of
+ ParserNodes under the instance it was called from. This way it is possible
+ to search for the whole configuration tree, when starting from root node or
+ to do a partial search when starting from a specified branch. The lookup
+ should be case insensitive.
+
+ :param str name: The name of the directive to search for
+ :param bool exclude: If the search results should exclude the contents of
+ ParserNode objects that reside within conditional blocks and because
+ of current state are not enabled.
+
+ :returns: A list of found BlockNode objects.
+ """
+
+ @abc.abstractmethod
+ def find_directives(self, name, exclude=True):
+ """
+ Find a directive by name. This method walks the child tree of ParserNodes
+ under the instance it was called from. This way it is possible to search
+ for the whole configuration tree, when starting from root node, or to do
+ a partial search when starting from a specified branch. The lookup should
+ be case insensitive.
+
+ :param str name: The name of the directive to search for
+ :param bool exclude: If the search results should exclude the contents of
+ ParserNode objects that reside within conditional blocks and because
+ of current state are not enabled.
+
+ :returns: A list of found DirectiveNode objects.
+
+ """
+
+ @abc.abstractmethod
+ def find_comments(self, comment):
+ """
+ Find comments with value containing the search term.
+
+ This method walks the child tree of ParserNodes under the instance it was
+ called from. This way it is possible to search for the whole configuration
+ tree, when starting from root node, or to do a partial search when starting
+ from a specified branch. The lookup should be case sensitive.
+
+ :param str comment: The content of comment to search for
+
+ :returns: A list of found CommentNode objects.
+
+ """
+
+ @abc.abstractmethod
+ def delete_child(self, child):
+ """
+ Remove a specified child node from the list of children of the called
+ BlockNode object.
+
+ :param ParserNode child: Child ParserNode object to remove from the list
+ of children of the callee.
+ """
+
+ @abc.abstractmethod
+ def unsaved_files(self):
+ """
+ Returns a list of file paths that have been changed since the last save
+ (or the initial configuration parse). The intended use for this method
+ is to tell the Reverter which files need to be included in a checkpoint.
+
+ This is typically called for the root of the ParserNode tree.
+
+ :returns: list of file paths of files that have been changed but not yet
+ saved to disk.
+ """
+
+ @abc.abstractmethod
+ def parsed_paths(self):
+ """
+ Returns a list of file paths that have currently been parsed into the parser
+ tree. The returned list may include paths with wildcard characters, for
+ example: ['/etc/apache2/conf.d/*.load']
+
+ This is typically called on the root node of the ParserNode tree.
+
+ :returns: list of file paths of files that have been parsed
+ """
diff --git a/certbot-apache/certbot_apache/_internal/obj.py b/certbot-apache/certbot_apache/_internal/obj.py
index 8b3aeb376..940bb6144 100644
--- a/certbot-apache/certbot_apache/_internal/obj.py
+++ b/certbot-apache/certbot_apache/_internal/obj.py
@@ -124,7 +124,7 @@ class VirtualHost(object):
strip_name = re.compile(r"^(?:.+://)?([^ :$]*)")
def __init__(self, filep, path, addrs, ssl, enabled, name=None,
- aliases=None, modmacro=False, ancestor=None):
+ aliases=None, modmacro=False, ancestor=None, node=None):
"""Initialize a VH."""
self.filep = filep
@@ -136,6 +136,7 @@ class VirtualHost(object):
self.enabled = enabled
self.modmacro = modmacro
self.ancestor = ancestor
+ self.node = node
def get_names(self):
"""Return a set of all names."""
diff --git a/certbot-apache/certbot_apache/_internal/override_gentoo.py b/certbot-apache/certbot_apache/_internal/override_gentoo.py
index 38f8aebe9..c215771e6 100644
--- a/certbot-apache/certbot_apache/_internal/override_gentoo.py
+++ b/certbot-apache/certbot_apache/_internal/override_gentoo.py
@@ -70,6 +70,6 @@ class GentooParser(parser.ApacheParser):
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")
+ matches = apache_util.parse_from_subprocess(mod_cmd, r"(.*)_module")
for mod in matches:
self.add_mod(mod.strip())
diff --git a/certbot-apache/certbot_apache/_internal/parser.py b/certbot-apache/certbot_apache/_internal/parser.py
index 0703b8fb5..aae3dc6e4 100644
--- a/certbot-apache/certbot_apache/_internal/parser.py
+++ b/certbot-apache/certbot_apache/_internal/parser.py
@@ -3,7 +3,6 @@ import copy
import fnmatch
import logging
import re
-import subprocess
import sys
import six
@@ -13,6 +12,7 @@ from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-
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 apache_util
from certbot_apache._internal import constants
logger = logging.getLogger(__name__)
@@ -290,32 +290,15 @@ class ApacheParser(object):
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]
+ """Updates the dictionary of known variables in the configuration"""
- self.variables = variables
+ self.variables = apache_util.parse_defines(self.configurator.option("ctl"))
def update_includes(self):
"""Get includes from httpd process, and add them to DOM if needed"""
@@ -325,9 +308,7 @@ class ApacheParser(object):
# configuration files
_ = self.find_dir("Include")
- inc_cmd = [self.configurator.option("ctl"), "-t", "-D",
- "DUMP_INCLUDES"]
- matches = self.parse_from_subprocess(inc_cmd, r"\(.*\) (.*)")
+ matches = apache_util.parse_includes(self.configurator.option("ctl"))
if matches:
for i in matches:
if not self.parsed_in_current(i):
@@ -336,56 +317,10 @@ class ApacheParser(object):
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")
+ matches = apache_util.parse_modules(self.configurator.option("ctl"))
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.
@@ -612,7 +547,7 @@ class ApacheParser(object):
"%s//*[self::directive=~regexp('%s')]" % (start, regex))
if exclude:
- matches = self._exclude_dirs(matches)
+ matches = self.exclude_dirs(matches)
if arg is None:
arg_suffix = "/arg"
@@ -678,7 +613,13 @@ class ApacheParser(object):
return value
- def _exclude_dirs(self, matches):
+ def get_root_augpath(self):
+ """
+ Returns the Augeas path of root configuration.
+ """
+ return get_aug_path(self.loc["root"])
+
+ def exclude_dirs(self, matches):
"""Exclude directives that are not loaded into the configuration."""
filters = [("ifmodule", self.modules), ("ifdefine", self.variables)]
diff --git a/certbot-apache/certbot_apache/_internal/parsernode_util.py b/certbot-apache/certbot_apache/_internal/parsernode_util.py
new file mode 100644
index 000000000..d9646862a
--- /dev/null
+++ b/certbot-apache/certbot_apache/_internal/parsernode_util.py
@@ -0,0 +1,129 @@
+"""ParserNode utils"""
+
+
+def validate_kwargs(kwargs, required_names):
+ """
+ Ensures that the kwargs dict has all the expected values. This function modifies
+ the kwargs dictionary, and hence the returned dictionary should be used instead
+ in the caller function instead of the original kwargs.
+
+ :param dict kwargs: Dictionary of keyword arguments to validate.
+ :param list required_names: List of required parameter names.
+ """
+
+ validated_kwargs = dict()
+ for name in required_names:
+ try:
+ validated_kwargs[name] = kwargs.pop(name)
+ except KeyError:
+ raise TypeError("Required keyword argument: {} undefined.".format(name))
+
+ # Raise exception if unknown key word arguments are found.
+ if kwargs:
+ unknown = ", ".join(kwargs.keys())
+ raise TypeError("Unknown keyword argument(s): {}".format(unknown))
+ return validated_kwargs
+
+
+def parsernode_kwargs(kwargs):
+ """
+ Validates keyword arguments for ParserNode. This function modifies the kwargs
+ dictionary, and hence the returned dictionary should be used instead in the
+ caller function instead of the original kwargs.
+
+ If metadata is provided, the otherwise required argument "filepath" may be
+ omitted if the implementation is able to extract its value from the metadata.
+ This usecase is handled within this function. Filepath defaults to None.
+
+ :param dict kwargs: Keyword argument dictionary to validate.
+
+ :returns: Tuple of validated and prepared arguments.
+ """
+
+ # As many values of ParserNode instances can be derived from the metadata,
+ # (ancestor being a common exception here) make sure we permit it here as well.
+ if "metadata" in kwargs:
+ # Filepath can be derived from the metadata in Augeas implementation.
+ # Default is None, as in this case the responsibility of populating this
+ # variable lies on the implementation.
+ kwargs.setdefault("filepath", None)
+
+ kwargs.setdefault("dirty", False)
+ kwargs.setdefault("metadata", {})
+
+ kwargs = validate_kwargs(kwargs, ["ancestor", "dirty", "filepath", "metadata"])
+ return kwargs["ancestor"], kwargs["dirty"], kwargs["filepath"], kwargs["metadata"]
+
+
+def commentnode_kwargs(kwargs):
+ """
+ Validates keyword arguments for CommentNode and sets the default values for
+ optional kwargs. This function modifies the kwargs dictionary, and hence the
+ returned dictionary should be used instead in the caller function instead of
+ the original kwargs.
+
+ If metadata is provided, the otherwise required argument "comment" may be
+ omitted if the implementation is able to extract its value from the metadata.
+ This usecase is handled within this function.
+
+ :param dict kwargs: Keyword argument dictionary to validate.
+
+ :returns: Tuple of validated and prepared arguments and ParserNode kwargs.
+ """
+
+ # As many values of ParserNode instances can be derived from the metadata,
+ # (ancestor being a common exception here) make sure we permit it here as well.
+ if "metadata" in kwargs:
+ kwargs.setdefault("comment", None)
+ # Filepath can be derived from the metadata in Augeas implementation.
+ # Default is None, as in this case the responsibility of populating this
+ # variable lies on the implementation.
+ kwargs.setdefault("filepath", None)
+
+ kwargs.setdefault("dirty", False)
+ kwargs.setdefault("metadata", {})
+
+ kwargs = validate_kwargs(kwargs, ["ancestor", "dirty", "filepath", "comment",
+ "metadata"])
+
+ comment = kwargs.pop("comment")
+ return comment, kwargs
+
+
+def directivenode_kwargs(kwargs):
+ """
+ Validates keyword arguments for DirectiveNode and BlockNode and sets the
+ default values for optional kwargs. This function modifies the kwargs
+ dictionary, and hence the returned dictionary should be used instead in the
+ caller function instead of the original kwargs.
+
+ If metadata is provided, the otherwise required argument "name" may be
+ omitted if the implementation is able to extract its value from the metadata.
+ This usecase is handled within this function.
+
+ :param dict kwargs: Keyword argument dictionary to validate.
+
+ :returns: Tuple of validated and prepared arguments and ParserNode kwargs.
+ """
+
+ # As many values of ParserNode instances can be derived from the metadata,
+ # (ancestor being a common exception here) make sure we permit it here as well.
+ if "metadata" in kwargs:
+ kwargs.setdefault("name", None)
+ # Filepath can be derived from the metadata in Augeas implementation.
+ # Default is None, as in this case the responsibility of populating this
+ # variable lies on the implementation.
+ kwargs.setdefault("filepath", None)
+
+ kwargs.setdefault("dirty", False)
+ kwargs.setdefault("enabled", True)
+ kwargs.setdefault("parameters", ())
+ kwargs.setdefault("metadata", {})
+
+ kwargs = validate_kwargs(kwargs, ["ancestor", "dirty", "filepath", "name",
+ "parameters", "enabled", "metadata"])
+
+ name = kwargs.pop("name")
+ parameters = kwargs.pop("parameters")
+ enabled = kwargs.pop("enabled")
+ return name, parameters, enabled, kwargs
diff --git a/certbot-apache/setup.py b/certbot-apache/setup.py
index f9b85008b..b37ee3972 100644
--- a/certbot-apache/setup.py
+++ b/certbot-apache/setup.py
@@ -18,6 +18,9 @@ install_requires = [
'zope.interface',
]
+dev_extras = [
+ 'apacheconfig>=0.3.1',
+]
class PyTest(TestCommand):
user_options = []
@@ -68,6 +71,9 @@ setup(
packages=find_packages(),
include_package_data=True,
install_requires=install_requires,
+ extras_require={
+ 'dev': dev_extras,
+ },
entry_points={
'certbot.plugins': [
'apache = certbot_apache._internal.entrypoint:ENTRYPOINT',
diff --git a/certbot-apache/tests/augeasnode_test.py b/certbot-apache/tests/augeasnode_test.py
new file mode 100644
index 000000000..9d663a05f
--- /dev/null
+++ b/certbot-apache/tests/augeasnode_test.py
@@ -0,0 +1,319 @@
+"""Tests for AugeasParserNode classes"""
+import mock
+
+import util
+
+from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
+from certbot import errors
+
+from certbot_apache._internal import assertions
+
+
+
+class AugeasParserNodeTest(util.ApacheTest): # pylint: disable=too-many-public-methods
+ """Test AugeasParserNode using available test configurations"""
+
+ def setUp(self): # pylint: disable=arguments-differ
+ super(AugeasParserNodeTest, self).setUp()
+
+ self.config = util.get_apache_configurator(
+ self.config_path, self.vhost_path, self.config_dir, self.work_dir, use_parsernode=True)
+ self.vh_truth = util.get_vh_truth(
+ self.temp_dir, "debian_apache_2_4/multiple_vhosts")
+
+ def test_save(self):
+ with mock.patch('certbot_apache._internal.parser.ApacheParser.save') as mock_save:
+ self.config.parser_root.save("A save message")
+ self.assertTrue(mock_save.called)
+ self.assertEqual(mock_save.call_args[0][0], "A save message")
+
+ def test_unsaved_files(self):
+ with mock.patch('certbot_apache._internal.parser.ApacheParser.unsaved_files') as mock_uf:
+ mock_uf.return_value = ["first", "second"]
+ files = self.config.parser_root.unsaved_files()
+ self.assertEqual(files, ["first", "second"])
+
+ def test_get_block_node_name(self):
+ from certbot_apache._internal.augeasparser import AugeasBlockNode
+ block = AugeasBlockNode(
+ name=assertions.PASS,
+ ancestor=None,
+ filepath=assertions.PASS,
+ metadata={"augeasparser": mock.Mock(), "augeaspath": "/files/anything"}
+ )
+ testcases = {
+ "/some/path/FirstNode/SecondNode": "SecondNode",
+ "/some/path/FirstNode/SecondNode/": "SecondNode",
+ "OnlyPathItem": "OnlyPathItem",
+ "/files/etc/apache2/apache2.conf/VirtualHost": "VirtualHost",
+ "/Anything": "Anything",
+ }
+ for test in testcases:
+ self.assertEqual(block._aug_get_name(test), testcases[test]) # pylint: disable=protected-access
+
+ def test_find_blocks(self):
+ blocks = self.config.parser_root.find_blocks("VirtualHost", exclude=False)
+ self.assertEqual(len(blocks), 12)
+
+ def test_find_blocks_case_insensitive(self):
+ vhs = self.config.parser_root.find_blocks("VirtualHost")
+ vhs2 = self.config.parser_root.find_blocks("viRtuAlHoST")
+ self.assertEqual(len(vhs), len(vhs2))
+
+ def test_find_directive_found(self):
+ directives = self.config.parser_root.find_directives("Listen")
+ self.assertEqual(len(directives), 1)
+ self.assertTrue(directives[0].filepath.endswith("/apache2/ports.conf"))
+ self.assertEqual(directives[0].parameters, (u'80',))
+
+ def test_find_directive_notfound(self):
+ directives = self.config.parser_root.find_directives("Nonexistent")
+ self.assertEqual(len(directives), 0)
+
+ def test_find_directive_from_block(self):
+ blocks = self.config.parser_root.find_blocks("virtualhost")
+ found = False
+ for vh in blocks:
+ if vh.filepath.endswith("sites-enabled/certbot.conf"):
+ servername = vh.find_directives("servername")
+ self.assertEqual(servername[0].parameters[0], "certbot.demo")
+ found = True
+ self.assertTrue(found)
+
+ def test_find_comments(self):
+ rootcomment = self.config.parser_root.find_comments(
+ "This is the main Apache server configuration file. "
+ )
+ self.assertEqual(len(rootcomment), 1)
+ self.assertTrue(rootcomment[0].filepath.endswith(
+ "debian_apache_2_4/multiple_vhosts/apache2/apache2.conf"
+ ))
+
+ def test_set_parameters(self):
+ servernames = self.config.parser_root.find_directives("servername")
+ names = [] # type: List[str]
+ for servername in servernames:
+ names += servername.parameters
+ self.assertFalse("going_to_set_this" in names)
+ servernames[0].set_parameters(["something", "going_to_set_this"])
+ servernames = self.config.parser_root.find_directives("servername")
+ names = []
+ for servername in servernames:
+ names += servername.parameters
+ self.assertTrue("going_to_set_this" in names)
+
+ def test_set_parameters_atinit(self):
+ from certbot_apache._internal.augeasparser import AugeasDirectiveNode
+ servernames = self.config.parser_root.find_directives("servername")
+ setparam = "certbot_apache._internal.augeasparser.AugeasDirectiveNode.set_parameters"
+ with mock.patch(setparam) as mock_set:
+ AugeasDirectiveNode(
+ name=servernames[0].name,
+ parameters=["test", "setting", "these"],
+ ancestor=assertions.PASS,
+ metadata=servernames[0].primary.metadata
+ )
+ self.assertTrue(mock_set.called)
+ self.assertEqual(
+ mock_set.call_args_list[0][0][0],
+ ["test", "setting", "these"]
+ )
+
+ def test_set_parameters_delete(self):
+ # Set params
+ servername = self.config.parser_root.find_directives("servername")[0]
+ servername.set_parameters(["thisshouldnotexistpreviously", "another",
+ "third"])
+
+ # Delete params
+ servernames = self.config.parser_root.find_directives("servername")
+ found = False
+ for servername in servernames:
+ if "thisshouldnotexistpreviously" in servername.parameters:
+ self.assertEqual(len(servername.parameters), 3)
+ servername.set_parameters(["thisshouldnotexistpreviously"])
+ found = True
+ self.assertTrue(found)
+
+ # Verify params
+ servernames = self.config.parser_root.find_directives("servername")
+ found = False
+ for servername in servernames:
+ if "thisshouldnotexistpreviously" in servername.parameters:
+ self.assertEqual(len(servername.parameters), 1)
+ servername.set_parameters(["thisshouldnotexistpreviously"])
+ found = True
+ self.assertTrue(found)
+
+ def test_add_child_comment(self):
+ newc = self.config.parser_root.primary.add_child_comment("The content")
+ comments = self.config.parser_root.find_comments("The content")
+ self.assertEqual(len(comments), 1)
+ self.assertEqual(
+ newc.metadata["augeaspath"],
+ comments[0].primary.metadata["augeaspath"]
+ )
+ self.assertEqual(newc.comment, comments[0].comment)
+
+ def test_delete_child(self):
+ listens = self.config.parser_root.primary.find_directives("Listen")
+ self.assertEqual(len(listens), 1)
+ self.config.parser_root.primary.delete_child(listens[0])
+
+ listens = self.config.parser_root.primary.find_directives("Listen")
+ self.assertEqual(len(listens), 0)
+
+ def test_delete_child_not_found(self):
+ listen = self.config.parser_root.find_directives("Listen")[0]
+ listen.primary.metadata["augeaspath"] = "/files/something/nonexistent"
+
+ self.assertRaises(
+ errors.PluginError,
+ self.config.parser_root.delete_child,
+ listen
+ )
+
+ def test_add_child_block(self):
+ nb = self.config.parser_root.add_child_block(
+ "NewBlock",
+ ["first", "second"]
+ )
+ rpath, _, directive = nb.primary.metadata["augeaspath"].rpartition("/")
+ self.assertEqual(
+ rpath,
+ self.config.parser_root.primary.metadata["augeaspath"]
+ )
+ self.assertTrue(directive.startswith("NewBlock"))
+
+ def test_add_child_block_beginning(self):
+ self.config.parser_root.add_child_block(
+ "Beginning",
+ position=0
+ )
+ parser = self.config.parser_root.primary.parser
+ root_path = self.config.parser_root.primary.metadata["augeaspath"]
+ # Get first child
+ first = parser.aug.match("{}/*[1]".format(root_path))
+ self.assertTrue(first[0].endswith("Beginning"))
+
+ def test_add_child_block_append(self):
+ self.config.parser_root.add_child_block(
+ "VeryLast",
+ )
+ parser = self.config.parser_root.primary.parser
+ root_path = self.config.parser_root.primary.metadata["augeaspath"]
+ # Get last child
+ last = parser.aug.match("{}/*[last()]".format(root_path))
+ self.assertTrue(last[0].endswith("VeryLast"))
+
+ def test_add_child_block_append_alt(self):
+ self.config.parser_root.add_child_block(
+ "VeryLastAlt",
+ position=99999
+ )
+ parser = self.config.parser_root.primary.parser
+ root_path = self.config.parser_root.primary.metadata["augeaspath"]
+ # Get last child
+ last = parser.aug.match("{}/*[last()]".format(root_path))
+ self.assertTrue(last[0].endswith("VeryLastAlt"))
+
+ def test_add_child_block_middle(self):
+ self.config.parser_root.add_child_block(
+ "Middle",
+ position=5
+ )
+ parser = self.config.parser_root.primary.parser
+ root_path = self.config.parser_root.primary.metadata["augeaspath"]
+ # Augeas indices start at 1 :(
+ middle = parser.aug.match("{}/*[6]".format(root_path))
+ self.assertTrue(middle[0].endswith("Middle"))
+
+ def test_add_child_block_existing_name(self):
+ parser = self.config.parser_root.primary.parser
+ root_path = self.config.parser_root.primary.metadata["augeaspath"]
+ # There already exists a single VirtualHost in the base config
+ new_block = parser.aug.match("{}/VirtualHost[2]".format(root_path))
+ self.assertEqual(len(new_block), 0)
+ vh = self.config.parser_root.add_child_block(
+ "VirtualHost",
+ )
+ new_block = parser.aug.match("{}/VirtualHost[2]".format(root_path))
+ self.assertEqual(len(new_block), 1)
+ self.assertTrue(vh.primary.metadata["augeaspath"].endswith("VirtualHost[2]"))
+
+ def test_node_init_error_bad_augeaspath(self):
+ from certbot_apache._internal.augeasparser import AugeasBlockNode
+ parameters = {
+ "name": assertions.PASS,
+ "ancestor": None,
+ "filepath": assertions.PASS,
+ "metadata": {
+ "augeasparser": mock.Mock(),
+ "augeaspath": "/files/path/endswith/slash/"
+ }
+ }
+ self.assertRaises(
+ errors.PluginError,
+ AugeasBlockNode,
+ **parameters
+ )
+
+ def test_node_init_error_missing_augeaspath(self):
+ from certbot_apache._internal.augeasparser import AugeasBlockNode
+ parameters = {
+ "name": assertions.PASS,
+ "ancestor": None,
+ "filepath": assertions.PASS,
+ "metadata": {
+ "augeasparser": mock.Mock(),
+ }
+ }
+ self.assertRaises(
+ errors.PluginError,
+ AugeasBlockNode,
+ **parameters
+ )
+
+ def test_add_child_directive(self):
+ self.config.parser_root.add_child_directive(
+ "ThisWasAdded",
+ ["with", "parameters"],
+ position=0
+ )
+ dirs = self.config.parser_root.find_directives("ThisWasAdded")
+ self.assertEqual(len(dirs), 1)
+ self.assertEqual(dirs[0].parameters, ("with", "parameters"))
+ # The new directive was added to the very first line of the config
+ self.assertTrue(dirs[0].primary.metadata["augeaspath"].endswith("[1]"))
+
+ def test_add_child_directive_exception(self):
+ self.assertRaises(
+ errors.PluginError,
+ self.config.parser_root.add_child_directive,
+ "ThisRaisesErrorBecauseMissingParameters"
+ )
+
+ def test_parsed_paths(self):
+ paths = self.config.parser_root.parsed_paths()
+ self.assertEqual(len(paths), 6)
+
+ def test_find_ancestors(self):
+ vhsblocks = self.config.parser_root.find_blocks("VirtualHost")
+ macro_test = False
+ nonmacro_test = False
+ for vh in vhsblocks:
+ if "/macro/" in vh.metadata["augeaspath"].lower():
+ ancs = vh.find_ancestors("Macro")
+ self.assertEqual(len(ancs), 1)
+ macro_test = True
+ else:
+ ancs = vh.find_ancestors("Macro")
+ self.assertEqual(len(ancs), 0)
+ nonmacro_test = True
+ self.assertTrue(macro_test)
+ self.assertTrue(nonmacro_test)
+
+ def test_find_ancestors_bad_path(self):
+ self.config.parser_root.primary.metadata["augeaspath"] = ""
+ ancs = self.config.parser_root.primary.find_ancestors("Anything")
+ self.assertEqual(len(ancs), 0)
diff --git a/certbot-apache/tests/centos_test.py b/certbot-apache/tests/centos_test.py
index 8959d73b8..55fee3faa 100644
--- a/certbot-apache/tests/centos_test.py
+++ b/certbot-apache/tests/centos_test.py
@@ -106,7 +106,7 @@ class MultipleVhostsTestCentOS(util.ApacheTest):
def test_get_parser(self):
self.assertIsInstance(self.config.parser, override_centos.CentOSParser)
- @mock.patch("certbot_apache._internal.parser.ApacheParser._get_runtime_cfg")
+ @mock.patch("certbot_apache._internal.apache_util._get_runtime_cfg")
def test_opportunistic_httpd_runtime_parsing(self, mock_get):
define_val = (
'Define: TEST1\n'
@@ -155,7 +155,7 @@ class MultipleVhostsTestCentOS(util.ApacheTest):
raise Exception("Missed: %s" % vhost) # pragma: no cover
self.assertEqual(found, 2)
- @mock.patch("certbot_apache._internal.parser.ApacheParser._get_runtime_cfg")
+ @mock.patch("certbot_apache._internal.apache_util._get_runtime_cfg")
def test_get_sysconfig_vars(self, mock_cfg):
"""Make sure we read the sysconfig OPTIONS variable correctly"""
# Return nothing for the process calls
diff --git a/certbot-apache/tests/configurator_test.py b/certbot-apache/tests/configurator_test.py
index 9fab5ea5d..cbb052155 100644
--- a/certbot-apache/tests/configurator_test.py
+++ b/certbot-apache/tests/configurator_test.py
@@ -75,7 +75,8 @@ class MultipleVhostsTest(util.ApacheTest):
@mock.patch("certbot_apache._internal.parser.ApacheParser")
@mock.patch("certbot_apache._internal.configurator.util.exe_exists")
- def _test_prepare_locked(self, unused_parser, unused_exe_exists):
+ @mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.get_parsernode_root")
+ def _test_prepare_locked(self, _node, _exists, _parser):
try:
self.config.prepare()
except errors.PluginError as err:
@@ -799,7 +800,7 @@ class MultipleVhostsTest(util.ApacheTest):
self.assertEqual(mock_restart.call_count, 1)
@mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.restart")
- @mock.patch("certbot_apache._internal.parser.ApacheParser._get_runtime_cfg")
+ @mock.patch("certbot_apache._internal.apache_util._get_runtime_cfg")
def test_cleanup(self, mock_cfg, mock_restart):
mock_cfg.return_value = ""
_, achalls = self.get_key_and_achalls()
@@ -815,7 +816,7 @@ class MultipleVhostsTest(util.ApacheTest):
self.assertFalse(mock_restart.called)
@mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.restart")
- @mock.patch("certbot_apache._internal.parser.ApacheParser._get_runtime_cfg")
+ @mock.patch("certbot_apache._internal.apache_util._get_runtime_cfg")
def test_cleanup_no_errors(self, mock_cfg, mock_restart):
mock_cfg.return_value = ""
_, achalls = self.get_key_and_achalls()
diff --git a/certbot-apache/tests/debian_test.py b/certbot-apache/tests/debian_test.py
index 6e63a9bd3..400e503fb 100644
--- a/certbot-apache/tests/debian_test.py
+++ b/certbot-apache/tests/debian_test.py
@@ -46,7 +46,7 @@ class MultipleVhostsTestDebian(util.ApacheTest):
@mock.patch("certbot.util.run_script")
@mock.patch("certbot.util.exe_exists")
- @mock.patch("certbot_apache._internal.parser.subprocess.Popen")
+ @mock.patch("certbot_apache._internal.apache_util.subprocess.Popen")
def test_enable_mod(self, mock_popen, mock_exe_exists, mock_run_script):
mock_popen().communicate.return_value = ("Define: DUMP_RUN_CFG", "")
mock_popen().returncode = 0
diff --git a/certbot-apache/tests/dualnode_test.py b/certbot-apache/tests/dualnode_test.py
new file mode 100644
index 000000000..0871bac78
--- /dev/null
+++ b/certbot-apache/tests/dualnode_test.py
@@ -0,0 +1,442 @@
+"""Tests for DualParserNode implementation"""
+import unittest
+
+import mock
+
+from certbot_apache._internal import assertions
+from certbot_apache._internal import augeasparser
+from certbot_apache._internal import dualparser
+
+
+class DualParserNodeTest(unittest.TestCase): # pylint: disable=too-many-public-methods
+ """DualParserNode tests"""
+
+ def setUp(self): # pylint: disable=arguments-differ
+ parser_mock = mock.MagicMock()
+ parser_mock.aug.match.return_value = []
+ parser_mock.get_arg.return_value = []
+ self.metadata = {"augeasparser": parser_mock, "augeaspath": "/invalid", "ac_ast": None}
+ self.block = dualparser.DualBlockNode(name="block",
+ ancestor=None,
+ filepath="/tmp/something",
+ metadata=self.metadata)
+ self.block_two = dualparser.DualBlockNode(name="block",
+ ancestor=self.block,
+ filepath="/tmp/something",
+ metadata=self.metadata)
+ self.directive = dualparser.DualDirectiveNode(name="directive",
+ ancestor=self.block,
+ filepath="/tmp/something",
+ metadata=self.metadata)
+ self.comment = dualparser.DualCommentNode(comment="comment",
+ ancestor=self.block,
+ filepath="/tmp/something",
+ metadata=self.metadata)
+
+ def test_create_with_precreated(self):
+ cnode = dualparser.DualCommentNode(comment="comment",
+ ancestor=self.block,
+ filepath="/tmp/something",
+ primary=self.comment.secondary,
+ secondary=self.comment.primary)
+ dnode = dualparser.DualDirectiveNode(name="directive",
+ ancestor=self.block,
+ filepath="/tmp/something",
+ primary=self.directive.secondary,
+ secondary=self.directive.primary)
+ bnode = dualparser.DualBlockNode(name="block",
+ ancestor=self.block,
+ filepath="/tmp/something",
+ primary=self.block.secondary,
+ secondary=self.block.primary)
+ # Switched around
+ self.assertTrue(cnode.primary is self.comment.secondary)
+ self.assertTrue(cnode.secondary is self.comment.primary)
+ self.assertTrue(dnode.primary is self.directive.secondary)
+ self.assertTrue(dnode.secondary is self.directive.primary)
+ self.assertTrue(bnode.primary is self.block.secondary)
+ self.assertTrue(bnode.secondary is self.block.primary)
+
+ def test_set_params(self):
+ params = ("first", "second")
+ self.directive.primary.set_parameters = mock.Mock()
+ self.directive.secondary.set_parameters = mock.Mock()
+ self.directive.set_parameters(params)
+ self.assertTrue(self.directive.primary.set_parameters.called)
+ self.assertTrue(self.directive.secondary.set_parameters.called)
+
+ def test_set_parameters(self):
+ pparams = mock.MagicMock()
+ sparams = mock.MagicMock()
+ pparams.parameters = ("a", "b")
+ sparams.parameters = ("a", "b")
+ self.directive.primary.set_parameters = pparams
+ self.directive.secondary.set_parameters = sparams
+ self.directive.set_parameters(("param", "seq"))
+ self.assertTrue(pparams.called)
+ self.assertTrue(sparams.called)
+
+ def test_delete_child(self):
+ pdel = mock.MagicMock()
+ sdel = mock.MagicMock()
+ self.block.primary.delete_child = pdel
+ self.block.secondary.delete_child = sdel
+ self.block.delete_child(self.comment)
+ self.assertTrue(pdel.called)
+ self.assertTrue(sdel.called)
+
+ def test_unsaved_files(self):
+ puns = mock.MagicMock()
+ suns = mock.MagicMock()
+ puns.return_value = assertions.PASS
+ suns.return_value = assertions.PASS
+ self.block.primary.unsaved_files = puns
+ self.block.secondary.unsaved_files = suns
+ self.block.unsaved_files()
+ self.assertTrue(puns.called)
+ self.assertTrue(suns.called)
+
+ def test_getattr_equality(self):
+ self.directive.primary.variableexception = "value"
+ self.directive.secondary.variableexception = "not_value"
+ with self.assertRaises(AssertionError):
+ _ = self.directive.variableexception
+
+ self.directive.primary.variable = "value"
+ self.directive.secondary.variable = "value"
+ try:
+ self.directive.variable
+ except AssertionError: # pragma: no cover
+ self.fail("getattr check raised an AssertionError where it shouldn't have")
+
+ def test_parsernode_dirty_assert(self):
+ # disable assertion pass
+ self.comment.primary.comment = "value"
+ self.comment.secondary.comment = "value"
+ self.comment.primary.filepath = "x"
+ self.comment.secondary.filepath = "x"
+
+ self.comment.primary.dirty = False
+ self.comment.secondary.dirty = True
+ with self.assertRaises(AssertionError):
+ assertions.assertEqual(self.comment.primary, self.comment.secondary)
+
+ def test_parsernode_filepath_assert(self):
+ # disable assertion pass
+ self.comment.primary.comment = "value"
+ self.comment.secondary.comment = "value"
+
+ self.comment.primary.filepath = "first"
+ self.comment.secondary.filepath = "second"
+ with self.assertRaises(AssertionError):
+ assertions.assertEqual(self.comment.primary, self.comment.secondary)
+
+ def test_add_child_block(self):
+ mock_first = mock.MagicMock(return_value=self.block.primary)
+ mock_second = mock.MagicMock(return_value=self.block.secondary)
+ self.block.primary.add_child_block = mock_first
+ self.block.secondary.add_child_block = mock_second
+ self.block.add_child_block("Block")
+ self.assertTrue(mock_first.called)
+ self.assertTrue(mock_second.called)
+
+ def test_add_child_directive(self):
+ mock_first = mock.MagicMock(return_value=self.directive.primary)
+ mock_second = mock.MagicMock(return_value=self.directive.secondary)
+ self.block.primary.add_child_directive = mock_first
+ self.block.secondary.add_child_directive = mock_second
+ self.block.add_child_directive("Directive")
+ self.assertTrue(mock_first.called)
+ self.assertTrue(mock_second.called)
+
+ def test_add_child_comment(self):
+ mock_first = mock.MagicMock(return_value=self.comment.primary)
+ mock_second = mock.MagicMock(return_value=self.comment.secondary)
+ self.block.primary.add_child_comment = mock_first
+ self.block.secondary.add_child_comment = mock_second
+ self.block.add_child_comment("Comment")
+ self.assertTrue(mock_first.called)
+ self.assertTrue(mock_second.called)
+
+ def test_find_comments(self):
+ pri_comments = [augeasparser.AugeasCommentNode(comment="some comment",
+ ancestor=self.block,
+ filepath="/path/to/whatever",
+ metadata=self.metadata)]
+ sec_comments = [augeasparser.AugeasCommentNode(comment=assertions.PASS,
+ ancestor=self.block,
+ filepath=assertions.PASS,
+ metadata=self.metadata)]
+ find_coms_primary = mock.MagicMock(return_value=pri_comments)
+ find_coms_secondary = mock.MagicMock(return_value=sec_comments)
+ self.block.primary.find_comments = find_coms_primary
+ self.block.secondary.find_comments = find_coms_secondary
+
+ dcoms = self.block.find_comments("comment")
+ p_dcoms = [d.primary for d in dcoms]
+ s_dcoms = [d.secondary for d in dcoms]
+ p_coms = self.block.primary.find_comments("comment")
+ s_coms = self.block.secondary.find_comments("comment")
+ # Check that every comment response is represented in the list of
+ # DualParserNode instances.
+ for p in p_dcoms:
+ self.assertTrue(p in p_coms)
+ for s in s_dcoms:
+ self.assertTrue(s in s_coms)
+
+ def test_find_blocks_first_passing(self):
+ youshallnotpass = [augeasparser.AugeasBlockNode(name="notpassing",
+ ancestor=self.block,
+ filepath="/path/to/whatever",
+ metadata=self.metadata)]
+ youshallpass = [augeasparser.AugeasBlockNode(name=assertions.PASS,
+ ancestor=self.block,
+ filepath=assertions.PASS,
+ metadata=self.metadata)]
+ find_blocks_primary = mock.MagicMock(return_value=youshallpass)
+ find_blocks_secondary = mock.MagicMock(return_value=youshallnotpass)
+ self.block.primary.find_blocks = find_blocks_primary
+ self.block.secondary.find_blocks = find_blocks_secondary
+
+ blocks = self.block.find_blocks("something")
+ for block in blocks:
+ try:
+ assertions.assertEqual(block.primary, block.secondary)
+ except AssertionError: # pragma: no cover
+ self.fail("Assertion should have passed")
+ self.assertTrue(assertions.isPassDirective(block.primary))
+ self.assertFalse(assertions.isPassDirective(block.secondary))
+
+ def test_find_blocks_second_passing(self):
+ youshallnotpass = [augeasparser.AugeasBlockNode(name="notpassing",
+ ancestor=self.block,
+ filepath="/path/to/whatever",
+ metadata=self.metadata)]
+ youshallpass = [augeasparser.AugeasBlockNode(name=assertions.PASS,
+ ancestor=self.block,
+ filepath=assertions.PASS,
+ metadata=self.metadata)]
+ find_blocks_primary = mock.MagicMock(return_value=youshallnotpass)
+ find_blocks_secondary = mock.MagicMock(return_value=youshallpass)
+ self.block.primary.find_blocks = find_blocks_primary
+ self.block.secondary.find_blocks = find_blocks_secondary
+
+ blocks = self.block.find_blocks("something")
+ for block in blocks:
+ try:
+ assertions.assertEqual(block.primary, block.secondary)
+ except AssertionError: # pragma: no cover
+ self.fail("Assertion should have passed")
+ self.assertFalse(assertions.isPassDirective(block.primary))
+ self.assertTrue(assertions.isPassDirective(block.secondary))
+
+ def test_find_dirs_first_passing(self):
+ notpassing = [augeasparser.AugeasDirectiveNode(name="notpassing",
+ ancestor=self.block,
+ filepath="/path/to/whatever",
+ metadata=self.metadata)]
+ passing = [augeasparser.AugeasDirectiveNode(name=assertions.PASS,
+ ancestor=self.block,
+ filepath=assertions.PASS,
+ metadata=self.metadata)]
+ find_dirs_primary = mock.MagicMock(return_value=passing)
+ find_dirs_secondary = mock.MagicMock(return_value=notpassing)
+ self.block.primary.find_directives = find_dirs_primary
+ self.block.secondary.find_directives = find_dirs_secondary
+
+ directives = self.block.find_directives("something")
+ for directive in directives:
+ try:
+ assertions.assertEqual(directive.primary, directive.secondary)
+ except AssertionError: # pragma: no cover
+ self.fail("Assertion should have passed")
+ self.assertTrue(assertions.isPassDirective(directive.primary))
+ self.assertFalse(assertions.isPassDirective(directive.secondary))
+
+ def test_find_dirs_second_passing(self):
+ notpassing = [augeasparser.AugeasDirectiveNode(name="notpassing",
+ ancestor=self.block,
+ filepath="/path/to/whatever",
+ metadata=self.metadata)]
+ passing = [augeasparser.AugeasDirectiveNode(name=assertions.PASS,
+ ancestor=self.block,
+ filepath=assertions.PASS,
+ metadata=self.metadata)]
+ find_dirs_primary = mock.MagicMock(return_value=notpassing)
+ find_dirs_secondary = mock.MagicMock(return_value=passing)
+ self.block.primary.find_directives = find_dirs_primary
+ self.block.secondary.find_directives = find_dirs_secondary
+
+ directives = self.block.find_directives("something")
+ for directive in directives:
+ try:
+ assertions.assertEqual(directive.primary, directive.secondary)
+ except AssertionError: # pragma: no cover
+ self.fail("Assertion should have passed")
+ self.assertFalse(assertions.isPassDirective(directive.primary))
+ self.assertTrue(assertions.isPassDirective(directive.secondary))
+
+ def test_find_coms_first_passing(self):
+ notpassing = [augeasparser.AugeasCommentNode(comment="notpassing",
+ ancestor=self.block,
+ filepath="/path/to/whatever",
+ metadata=self.metadata)]
+ passing = [augeasparser.AugeasCommentNode(comment=assertions.PASS,
+ ancestor=self.block,
+ filepath=assertions.PASS,
+ metadata=self.metadata)]
+ find_coms_primary = mock.MagicMock(return_value=passing)
+ find_coms_secondary = mock.MagicMock(return_value=notpassing)
+ self.block.primary.find_comments = find_coms_primary
+ self.block.secondary.find_comments = find_coms_secondary
+
+ comments = self.block.find_comments("something")
+ for comment in comments:
+ try:
+ assertions.assertEqual(comment.primary, comment.secondary)
+ except AssertionError: # pragma: no cover
+ self.fail("Assertion should have passed")
+ self.assertTrue(assertions.isPassComment(comment.primary))
+ self.assertFalse(assertions.isPassComment(comment.secondary))
+
+ def test_find_coms_second_passing(self):
+ notpassing = [augeasparser.AugeasCommentNode(comment="notpassing",
+ ancestor=self.block,
+ filepath="/path/to/whatever",
+ metadata=self.metadata)]
+ passing = [augeasparser.AugeasCommentNode(comment=assertions.PASS,
+ ancestor=self.block,
+ filepath=assertions.PASS,
+ metadata=self.metadata)]
+ find_coms_primary = mock.MagicMock(return_value=notpassing)
+ find_coms_secondary = mock.MagicMock(return_value=passing)
+ self.block.primary.find_comments = find_coms_primary
+ self.block.secondary.find_comments = find_coms_secondary
+
+ comments = self.block.find_comments("something")
+ for comment in comments:
+ try:
+ assertions.assertEqual(comment.primary, comment.secondary)
+ except AssertionError: # pragma: no cover
+ self.fail("Assertion should have passed")
+ self.assertFalse(assertions.isPassComment(comment.primary))
+ self.assertTrue(assertions.isPassComment(comment.secondary))
+
+ def test_find_blocks_no_pass_equal(self):
+ notpassing1 = [augeasparser.AugeasBlockNode(name="notpassing",
+ ancestor=self.block,
+ filepath="/path/to/whatever",
+ metadata=self.metadata)]
+ notpassing2 = [augeasparser.AugeasBlockNode(name="notpassing",
+ ancestor=self.block,
+ filepath="/path/to/whatever",
+ metadata=self.metadata)]
+ find_blocks_primary = mock.MagicMock(return_value=notpassing1)
+ find_blocks_secondary = mock.MagicMock(return_value=notpassing2)
+ self.block.primary.find_blocks = find_blocks_primary
+ self.block.secondary.find_blocks = find_blocks_secondary
+
+ blocks = self.block.find_blocks("anything")
+ for block in blocks:
+ self.assertEqual(block.primary, block.secondary)
+ self.assertTrue(block.primary is not block.secondary)
+
+ def test_find_dirs_no_pass_equal(self):
+ notpassing1 = [augeasparser.AugeasDirectiveNode(name="notpassing",
+ ancestor=self.block,
+ filepath="/path/to/whatever",
+ metadata=self.metadata)]
+ notpassing2 = [augeasparser.AugeasDirectiveNode(name="notpassing",
+ ancestor=self.block,
+ filepath="/path/to/whatever",
+ metadata=self.metadata)]
+ find_dirs_primary = mock.MagicMock(return_value=notpassing1)
+ find_dirs_secondary = mock.MagicMock(return_value=notpassing2)
+ self.block.primary.find_directives = find_dirs_primary
+ self.block.secondary.find_directives = find_dirs_secondary
+
+ directives = self.block.find_directives("anything")
+ for directive in directives:
+ self.assertEqual(directive.primary, directive.secondary)
+ self.assertTrue(directive.primary is not directive.secondary)
+
+ def test_find_comments_no_pass_equal(self):
+ notpassing1 = [augeasparser.AugeasCommentNode(comment="notpassing",
+ ancestor=self.block,
+ filepath="/path/to/whatever",
+ metadata=self.metadata)]
+ notpassing2 = [augeasparser.AugeasCommentNode(comment="notpassing",
+ ancestor=self.block,
+ filepath="/path/to/whatever",
+ metadata=self.metadata)]
+ find_coms_primary = mock.MagicMock(return_value=notpassing1)
+ find_coms_secondary = mock.MagicMock(return_value=notpassing2)
+ self.block.primary.find_comments = find_coms_primary
+ self.block.secondary.find_comments = find_coms_secondary
+
+ comments = self.block.find_comments("anything")
+ for comment in comments:
+ self.assertEqual(comment.primary, comment.secondary)
+ self.assertTrue(comment.primary is not comment.secondary)
+
+ def test_find_blocks_no_pass_notequal(self):
+ notpassing1 = [augeasparser.AugeasBlockNode(name="notpassing",
+ ancestor=self.block,
+ filepath="/path/to/whatever",
+ metadata=self.metadata)]
+ notpassing2 = [augeasparser.AugeasBlockNode(name="different",
+ ancestor=self.block,
+ filepath="/path/to/whatever",
+ metadata=self.metadata)]
+ find_blocks_primary = mock.MagicMock(return_value=notpassing1)
+ find_blocks_secondary = mock.MagicMock(return_value=notpassing2)
+ self.block.primary.find_blocks = find_blocks_primary
+ self.block.secondary.find_blocks = find_blocks_secondary
+
+ with self.assertRaises(AssertionError):
+ _ = self.block.find_blocks("anything")
+
+ def test_parsernode_notequal(self):
+ ne_block = augeasparser.AugeasBlockNode(name="different",
+ ancestor=self.block,
+ filepath="/path/to/whatever",
+ metadata=self.metadata)
+ ne_directive = augeasparser.AugeasDirectiveNode(name="different",
+ ancestor=self.block,
+ filepath="/path/to/whatever",
+ metadata=self.metadata)
+ ne_comment = augeasparser.AugeasCommentNode(comment="different",
+ ancestor=self.block,
+ filepath="/path/to/whatever",
+ metadata=self.metadata)
+ self.assertFalse(self.block == ne_block)
+ self.assertFalse(self.directive == ne_directive)
+ self.assertFalse(self.comment == ne_comment)
+
+ def test_parsed_paths(self):
+ mock_p = mock.MagicMock(return_value=['/path/file.conf',
+ '/another/path',
+ '/path/other.conf'])
+ mock_s = mock.MagicMock(return_value=['/path/*.conf', '/another/path'])
+ self.block.primary.parsed_paths = mock_p
+ self.block.secondary.parsed_paths = mock_s
+ self.block.parsed_paths()
+ self.assertTrue(mock_p.called)
+ self.assertTrue(mock_s.called)
+
+ def test_parsed_paths_error(self):
+ mock_p = mock.MagicMock(return_value=['/path/file.conf'])
+ mock_s = mock.MagicMock(return_value=['/path/*.conf', '/another/path'])
+ self.block.primary.parsed_paths = mock_p
+ self.block.secondary.parsed_paths = mock_s
+ with self.assertRaises(AssertionError):
+ self.block.parsed_paths()
+
+ def test_find_ancestors(self):
+ primarymock = mock.MagicMock(return_value=[])
+ secondarymock = mock.MagicMock(return_value=[])
+ self.block.primary.find_ancestors = primarymock
+ self.block.secondary.find_ancestors = secondarymock
+ self.block.find_ancestors("anything")
+ self.assertTrue(primarymock.called)
+ self.assertTrue(secondarymock.called)
diff --git a/certbot-apache/tests/fedora_test.py b/certbot-apache/tests/fedora_test.py
index 2bfd6babb..cb1614278 100644
--- a/certbot-apache/tests/fedora_test.py
+++ b/certbot-apache/tests/fedora_test.py
@@ -100,7 +100,7 @@ class MultipleVhostsTestFedora(util.ApacheTest):
def test_get_parser(self):
self.assertIsInstance(self.config.parser, override_fedora.FedoraParser)
- @mock.patch("certbot_apache._internal.parser.ApacheParser._get_runtime_cfg")
+ @mock.patch("certbot_apache._internal.apache_util._get_runtime_cfg")
def test_opportunistic_httpd_runtime_parsing(self, mock_get):
define_val = (
'Define: TEST1\n'
@@ -155,7 +155,7 @@ class MultipleVhostsTestFedora(util.ApacheTest):
raise Exception("Missed: %s" % vhost) # pragma: no cover
self.assertEqual(found, 2)
- @mock.patch("certbot_apache._internal.parser.ApacheParser._get_runtime_cfg")
+ @mock.patch("certbot_apache._internal.apache_util._get_runtime_cfg")
def test_get_sysconfig_vars(self, mock_cfg):
"""Make sure we read the sysconfig OPTIONS variable correctly"""
# Return nothing for the process calls
diff --git a/certbot-apache/tests/gentoo_test.py b/certbot-apache/tests/gentoo_test.py
index 90a163fd3..fb5d192d0 100644
--- a/certbot-apache/tests/gentoo_test.py
+++ b/certbot-apache/tests/gentoo_test.py
@@ -90,7 +90,7 @@ class MultipleVhostsTestGentoo(util.ApacheTest):
for define in defines:
self.assertTrue(define in self.config.parser.variables.keys())
- @mock.patch("certbot_apache._internal.parser.ApacheParser.parse_from_subprocess")
+ @mock.patch("certbot_apache._internal.apache_util.parse_from_subprocess")
def test_no_binary_configdump(self, mock_subprocess):
"""Make sure we don't call binary dumps other than modules from Apache
as this is not supported in Gentoo currently"""
@@ -104,7 +104,7 @@ class MultipleVhostsTestGentoo(util.ApacheTest):
self.config.parser.reset_modules()
self.assertTrue(mock_subprocess.called)
- @mock.patch("certbot_apache._internal.parser.ApacheParser._get_runtime_cfg")
+ @mock.patch("certbot_apache._internal.apache_util._get_runtime_cfg")
def test_opportunistic_httpd_runtime_parsing(self, mock_get):
mod_val = (
'Loaded Modules:\n'
diff --git a/certbot-apache/tests/parser_test.py b/certbot-apache/tests/parser_test.py
index b334ce52e..f5a0a3d11 100644
--- a/certbot-apache/tests/parser_test.py
+++ b/certbot-apache/tests/parser_test.py
@@ -165,7 +165,7 @@ class BasicParserTest(util.ParserTest):
self.assertTrue(mock_logger.debug.called)
@mock.patch("certbot_apache._internal.parser.ApacheParser.find_dir")
- @mock.patch("certbot_apache._internal.parser.ApacheParser._get_runtime_cfg")
+ @mock.patch("certbot_apache._internal.apache_util._get_runtime_cfg")
def test_update_runtime_variables(self, mock_cfg, _):
define_val = (
'ServerRoot: "/etc/apache2"\n'
@@ -271,7 +271,7 @@ class BasicParserTest(util.ParserTest):
self.assertEqual(mock_parse.call_count, 25)
@mock.patch("certbot_apache._internal.parser.ApacheParser.find_dir")
- @mock.patch("certbot_apache._internal.parser.ApacheParser._get_runtime_cfg")
+ @mock.patch("certbot_apache._internal.apache_util._get_runtime_cfg")
def test_update_runtime_variables_alt_values(self, mock_cfg, _):
inc_val = (
'Included configuration files:\n'
@@ -293,7 +293,7 @@ class BasicParserTest(util.ParserTest):
# path derived from root configuration Include statements
self.assertEqual(mock_parse.call_count, 1)
- @mock.patch("certbot_apache._internal.parser.ApacheParser._get_runtime_cfg")
+ @mock.patch("certbot_apache._internal.apache_util._get_runtime_cfg")
def test_update_runtime_vars_bad_output(self, mock_cfg):
mock_cfg.return_value = "Define: TLS=443=24"
self.parser.update_runtime_variables()
@@ -303,7 +303,7 @@ class BasicParserTest(util.ParserTest):
errors.PluginError, self.parser.update_runtime_variables)
@mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.option")
- @mock.patch("certbot_apache._internal.parser.subprocess.Popen")
+ @mock.patch("certbot_apache._internal.apache_util.subprocess.Popen")
def test_update_runtime_vars_bad_ctl(self, mock_popen, mock_opt):
mock_popen.side_effect = OSError
mock_opt.return_value = "nonexistent"
@@ -311,7 +311,7 @@ class BasicParserTest(util.ParserTest):
errors.MisconfigurationError,
self.parser.update_runtime_variables)
- @mock.patch("certbot_apache._internal.parser.subprocess.Popen")
+ @mock.patch("certbot_apache._internal.apache_util.subprocess.Popen")
def test_update_runtime_vars_bad_exit(self, mock_popen):
mock_popen().communicate.return_value = ("", "")
mock_popen.returncode = -1
@@ -355,7 +355,7 @@ class ParserInitTest(util.ApacheTest):
ApacheParser, os.path.relpath(self.config_path),
"/dummy/vhostpath", version=(2, 4, 22), configurator=self.config)
- @mock.patch("certbot_apache._internal.parser.ApacheParser._get_runtime_cfg")
+ @mock.patch("certbot_apache._internal.apache_util._get_runtime_cfg")
def test_unparseable(self, mock_cfg):
from certbot_apache._internal.parser import ApacheParser
mock_cfg.return_value = ('Define: TEST')
diff --git a/certbot-apache/tests/parsernode_configurator_test.py b/certbot-apache/tests/parsernode_configurator_test.py
new file mode 100644
index 000000000..67d65995a
--- /dev/null
+++ b/certbot-apache/tests/parsernode_configurator_test.py
@@ -0,0 +1,37 @@
+"""Tests for ApacheConfigurator for AugeasParserNode classes"""
+import unittest
+
+import mock
+
+import util
+
+
+class ConfiguratorParserNodeTest(util.ApacheTest): # pylint: disable=too-many-public-methods
+ """Test AugeasParserNode using available test configurations"""
+
+ def setUp(self): # pylint: disable=arguments-differ
+ super(ConfiguratorParserNodeTest, self).setUp()
+
+ self.config = util.get_apache_configurator(
+ self.config_path, self.vhost_path, self.config_dir,
+ self.work_dir, use_parsernode=True)
+ self.vh_truth = util.get_vh_truth(
+ self.temp_dir, "debian_apache_2_4/multiple_vhosts")
+
+ def test_parsernode_get_vhosts(self):
+ self.config.USE_PARSERNODE = True
+ vhosts = self.config.get_virtual_hosts()
+ # Legacy get_virtual_hosts() do not set the node
+ self.assertTrue(vhosts[0].node is not None)
+
+ def test_parsernode_get_vhosts_mismatch(self):
+ vhosts = self.config.get_virtual_hosts_v2()
+ # One of the returned VirtualHost objects differs
+ vhosts[0].name = "IdidntExpectThat"
+ self.config.get_virtual_hosts_v2 = mock.MagicMock(return_value=vhosts)
+ with self.assertRaises(AssertionError):
+ _ = self.config.get_virtual_hosts()
+
+
+if __name__ == "__main__":
+ unittest.main() # pragma: no cover
diff --git a/certbot-apache/tests/parsernode_test.py b/certbot-apache/tests/parsernode_test.py
new file mode 100644
index 000000000..a86952f53
--- /dev/null
+++ b/certbot-apache/tests/parsernode_test.py
@@ -0,0 +1,128 @@
+""" Tests for ParserNode interface """
+
+import unittest
+
+from certbot_apache._internal import interfaces
+from certbot_apache._internal import parsernode_util as util
+
+
+class DummyParserNode(interfaces.ParserNode):
+ """ A dummy class implementing ParserNode interface """
+
+ def __init__(self, **kwargs):
+ """
+ Initializes the ParserNode instance.
+ """
+ ancestor, dirty, filepath, metadata = util.parsernode_kwargs(kwargs)
+ self.ancestor = ancestor
+ self.dirty = dirty
+ self.filepath = filepath
+ self.metadata = metadata
+ super(DummyParserNode, self).__init__(**kwargs)
+
+ def save(self, msg): # pragma: no cover
+ """Save"""
+ pass
+
+ def find_ancestors(self, name): # pragma: no cover
+ """ Find ancestors """
+ return []
+
+
+class DummyCommentNode(DummyParserNode):
+ """ A dummy class implementing CommentNode interface """
+
+ def __init__(self, **kwargs):
+ """
+ Initializes the CommentNode instance and sets its instance variables.
+ """
+ comment, kwargs = util.commentnode_kwargs(kwargs)
+ self.comment = comment
+ super(DummyCommentNode, self).__init__(**kwargs)
+
+
+class DummyDirectiveNode(DummyParserNode):
+ """ A dummy class implementing DirectiveNode interface """
+
+ # pylint: disable=too-many-arguments
+ def __init__(self, **kwargs):
+ """
+ Initializes the DirectiveNode instance and sets its instance variables.
+ """
+ name, parameters, enabled, kwargs = util.directivenode_kwargs(kwargs)
+ self.name = name
+ self.parameters = parameters
+ self.enabled = enabled
+
+ super(DummyDirectiveNode, self).__init__(**kwargs)
+
+ def set_parameters(self, parameters): # pragma: no cover
+ """Set parameters"""
+ pass
+
+
+class DummyBlockNode(DummyDirectiveNode):
+ """ A dummy class implementing BlockNode interface """
+
+ def add_child_block(self, name, parameters=None, position=None): # pragma: no cover
+ """Add child block"""
+ pass
+
+ def add_child_directive(self, name, parameters=None, position=None): # pragma: no cover
+ """Add child directive"""
+ pass
+
+ def add_child_comment(self, comment="", position=None): # pragma: no cover
+ """Add child comment"""
+ pass
+
+ def find_blocks(self, name, exclude=True): # pragma: no cover
+ """Find blocks"""
+ pass
+
+ def find_directives(self, name, exclude=True): # pragma: no cover
+ """Find directives"""
+ pass
+
+ def find_comments(self, comment, exact=False): # pragma: no cover
+ """Find comments"""
+ pass
+
+ def delete_child(self, child): # pragma: no cover
+ """Delete child"""
+ pass
+
+ def unsaved_files(self): # pragma: no cover
+ """Unsaved files"""
+ pass
+
+
+interfaces.CommentNode.register(DummyCommentNode)
+interfaces.DirectiveNode.register(DummyDirectiveNode)
+interfaces.BlockNode.register(DummyBlockNode)
+
+class ParserNodeTest(unittest.TestCase):
+ """Dummy placeholder test case for ParserNode interfaces"""
+
+ def test_dummy(self):
+ dummyblock = DummyBlockNode(
+ name="None",
+ parameters=(),
+ ancestor=None,
+ dirty=False,
+ filepath="/some/random/path"
+ )
+ dummydirective = DummyDirectiveNode(
+ name="Name",
+ ancestor=None,
+ filepath="/another/path"
+ )
+ dummycomment = DummyCommentNode(
+ comment="Comment",
+ ancestor=dummyblock,
+ filepath="/some/file"
+ )
+
+
+if __name__ == "__main__":
+ unittest.main() # pragma: no cover
diff --git a/certbot-apache/tests/parsernode_util_test.py b/certbot-apache/tests/parsernode_util_test.py
new file mode 100644
index 000000000..715388da5
--- /dev/null
+++ b/certbot-apache/tests/parsernode_util_test.py
@@ -0,0 +1,115 @@
+""" Tests for ParserNode utils """
+import unittest
+
+from certbot_apache._internal import parsernode_util as util
+
+
+class ParserNodeUtilTest(unittest.TestCase):
+ """Tests for ParserNode utils"""
+
+ def _setup_parsernode(self):
+ """ Sets up kwargs dict for ParserNode """
+ return {
+ "ancestor": None,
+ "dirty": False,
+ "filepath": "/tmp",
+ }
+
+ def _setup_commentnode(self):
+ """ Sets up kwargs dict for CommentNode """
+
+ pn = self._setup_parsernode()
+ pn["comment"] = "x"
+ return pn
+
+ def _setup_directivenode(self):
+ """ Sets up kwargs dict for DirectiveNode """
+
+ pn = self._setup_parsernode()
+ pn["name"] = "Name"
+ pn["parameters"] = ("first",)
+ pn["enabled"] = True
+ return pn
+
+ def test_unknown_parameter(self):
+ params = self._setup_parsernode()
+ params["unknown"] = "unknown"
+ self.assertRaises(TypeError, util.parsernode_kwargs, params)
+
+ params = self._setup_commentnode()
+ params["unknown"] = "unknown"
+ self.assertRaises(TypeError, util.commentnode_kwargs, params)
+
+ params = self._setup_directivenode()
+ params["unknown"] = "unknown"
+ self.assertRaises(TypeError, util.directivenode_kwargs, params)
+
+ def test_parsernode(self):
+ params = self._setup_parsernode()
+ ctrl = self._setup_parsernode()
+
+ ancestor, dirty, filepath, metadata = util.parsernode_kwargs(params)
+ self.assertEqual(ancestor, ctrl["ancestor"])
+ self.assertEqual(dirty, ctrl["dirty"])
+ self.assertEqual(filepath, ctrl["filepath"])
+ self.assertEqual(metadata, {})
+
+ def test_parsernode_from_metadata(self):
+ params = self._setup_parsernode()
+ params.pop("filepath")
+ md = {"some": "value"}
+ params["metadata"] = md
+
+ # Just testing that error from missing required parameters is not raised
+ _, _, _, metadata = util.parsernode_kwargs(params)
+ self.assertEqual(metadata, md)
+
+ def test_commentnode(self):
+ params = self._setup_commentnode()
+ ctrl = self._setup_commentnode()
+
+ comment, _ = util.commentnode_kwargs(params)
+ self.assertEqual(comment, ctrl["comment"])
+
+ def test_commentnode_from_metadata(self):
+ params = self._setup_commentnode()
+ params.pop("comment")
+ params["metadata"] = {}
+
+ # Just testing that error from missing required parameters is not raised
+ util.commentnode_kwargs(params)
+
+ def test_directivenode(self):
+ params = self._setup_directivenode()
+ ctrl = self._setup_directivenode()
+
+ name, parameters, enabled, _ = util.directivenode_kwargs(params)
+ self.assertEqual(name, ctrl["name"])
+ self.assertEqual(parameters, ctrl["parameters"])
+ self.assertEqual(enabled, ctrl["enabled"])
+
+ def test_directivenode_from_metadata(self):
+ params = self._setup_directivenode()
+ params.pop("filepath")
+ params.pop("name")
+ params["metadata"] = {"irrelevant": "value"}
+
+ # Just testing that error from missing required parameters is not raised
+ util.directivenode_kwargs(params)
+
+ def test_missing_required(self):
+ c_params = self._setup_commentnode()
+ c_params.pop("comment")
+ self.assertRaises(TypeError, util.commentnode_kwargs, c_params)
+
+ d_params = self._setup_directivenode()
+ d_params.pop("ancestor")
+ self.assertRaises(TypeError, util.directivenode_kwargs, d_params)
+
+ p_params = self._setup_parsernode()
+ p_params.pop("filepath")
+ self.assertRaises(TypeError, util.parsernode_kwargs, p_params)
+
+
+if __name__ == "__main__":
+ unittest.main() # pragma: no cover
diff --git a/certbot-apache/tests/util.py b/certbot-apache/tests/util.py
index 57b20dc9d..ccd0b274d 100644
--- a/certbot-apache/tests/util.py
+++ b/certbot-apache/tests/util.py
@@ -84,7 +84,8 @@ def get_apache_configurator(
config_path, vhost_path,
config_dir, work_dir, version=(2, 4, 7),
os_info="generic",
- conf_vhost_path=None):
+ conf_vhost_path=None,
+ use_parsernode=False):
"""Create an Apache Configurator with the specified options.
:param conf: Function that returns binary paths. self.conf in Configurator
@@ -110,19 +111,21 @@ def get_apache_configurator(
mock_exe_exists.return_value = True
with mock.patch("certbot_apache._internal.parser.ApacheParser."
"update_runtime_variables"):
- try:
- config_class = entrypoint.OVERRIDE_CLASSES[os_info]
- except KeyError:
- config_class = configurator.ApacheConfigurator
- config = config_class(config=mock_le_config, name="apache",
- version=version)
- if not conf_vhost_path:
- config_class.OS_DEFAULTS["vhost_root"] = vhost_path
- else:
- # Custom virtualhost path was requested
- config.config.apache_vhost_root = conf_vhost_path
- config.config.apache_ctl = config_class.OS_DEFAULTS["ctl"]
- config.prepare()
+ with mock.patch("certbot_apache._internal.apache_util.parse_from_subprocess") as mock_sp:
+ mock_sp.return_value = []
+ try:
+ config_class = entrypoint.OVERRIDE_CLASSES[os_info]
+ except KeyError:
+ config_class = configurator.ApacheConfigurator
+ config = config_class(config=mock_le_config, name="apache",
+ version=version, use_parsernode=use_parsernode)
+ if not conf_vhost_path:
+ config_class.OS_DEFAULTS["vhost_root"] = vhost_path
+ else:
+ # Custom virtualhost path was requested
+ config.config.apache_vhost_root = conf_vhost_path
+ config.config.apache_ctl = config_class.OS_DEFAULTS["ctl"]
+ config.prepare()
return config
diff --git a/tools/dev_constraints.txt b/tools/dev_constraints.txt
index 265d967d8..7d2013c7a 100644
--- a/tools/dev_constraints.txt
+++ b/tools/dev_constraints.txt
@@ -3,6 +3,7 @@
# Some dev package versions specified here may be overridden by higher level constraints
# files during tests (eg. letsencrypt-auto-source/pieces/dependency-requirements.txt).
alabaster==0.7.10
+apacheconfig==0.3.1
apipkg==1.4
appnope==0.1.0
asn1crypto==0.22.0
@@ -64,6 +65,7 @@ pexpect==4.7.0
pickleshare==0.7.5
pkginfo==1.4.2
pluggy==0.13.0
+ply==3.4
prompt-toolkit==1.0.18
ptyprocess==0.6.0
py==1.8.0
diff --git a/tools/oldest_constraints.txt b/tools/oldest_constraints.txt
index c5a5c5aa0..6154b497a 100644
--- a/tools/oldest_constraints.txt
+++ b/tools/oldest_constraints.txt
@@ -40,6 +40,7 @@ pytz==2012rc0
google-api-python-client==1.5.5
# Our setup.py constraints
+apacheconfig==0.3.1
cloudflare==1.5.1
cryptography==1.2.3
parsedatetime==1.3
diff --git a/tox.ini b/tox.ini
index 6d9814192..b2710ce35 100644
--- a/tox.ini
+++ b/tox.ini
@@ -93,6 +93,12 @@ commands =
setenv =
{[testenv:py27-oldest]setenv}
+[testenv:py27-apache-v2-oldest]
+commands =
+ {[base]install_and_test} certbot-apache[dev]
+setenv =
+ {[testenv:py27-oldest]setenv}
+
[testenv:py27-certbot-oldest]
commands =
{[base]install_and_test} certbot[dev]