mirror of
https://github.com/certbot/certbot.git
synced 2025-08-08 04:02:10 +03:00
Merge pull request #7735 from certbot/apache-parser-v2
[Apache v2] Merge apache-parser-v2 feature branch back to master
This commit is contained in:
@@ -59,7 +59,7 @@ matrix:
|
|||||||
# cryptography we support cannot be compiled against the version of
|
# cryptography we support cannot be compiled against the version of
|
||||||
# OpenSSL in Xenial or newer.
|
# OpenSSL in Xenial or newer.
|
||||||
dist: trusty
|
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
|
<<: *not-on-master
|
||||||
- python: "3.5"
|
- python: "3.5"
|
||||||
env: TOXENV=py35
|
env: TOXENV=py35
|
||||||
|
@@ -1,9 +1,17 @@
|
|||||||
""" Utility functions for certbot-apache plugin """
|
""" Utility functions for certbot-apache plugin """
|
||||||
import binascii
|
import binascii
|
||||||
|
import fnmatch
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from certbot import errors
|
||||||
from certbot import util
|
from certbot import util
|
||||||
|
|
||||||
from certbot.compat import os
|
from certbot.compat import os
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_mod_deps(mod_name):
|
def get_mod_deps(mod_name):
|
||||||
"""Get known module dependencies.
|
"""Get known module dependencies.
|
||||||
@@ -105,3 +113,131 @@ def parse_define_file(filepath, varname):
|
|||||||
def unique_id():
|
def unique_id():
|
||||||
""" Returns an unique id to be used as a VirtualHost identifier"""
|
""" Returns an unique id to be used as a VirtualHost identifier"""
|
||||||
return binascii.hexlify(os.urandom(16)).decode("utf-8")
|
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
|
||||||
|
169
certbot-apache/certbot_apache/_internal/apacheparser.py
Normal file
169
certbot-apache/certbot_apache/_internal/apacheparser.py
Normal file
@@ -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)
|
142
certbot-apache/certbot_apache/_internal/assertions.py
Normal file
142
certbot-apache/certbot_apache/_internal/assertions.py
Normal file
@@ -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])
|
538
certbot-apache/certbot_apache/_internal/augeasparser.py
Normal file
538
certbot-apache/certbot_apache/_internal/augeasparser.py
Normal file
@@ -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)
|
@@ -29,8 +29,10 @@ from certbot.plugins import common
|
|||||||
from certbot.plugins.enhancements import AutoHSTSEnhancement
|
from certbot.plugins.enhancements import AutoHSTSEnhancement
|
||||||
from certbot.plugins.util import path_surgery
|
from certbot.plugins.util import path_surgery
|
||||||
from certbot_apache._internal import apache_util
|
from certbot_apache._internal import apache_util
|
||||||
|
from certbot_apache._internal import assertions
|
||||||
from certbot_apache._internal import constants
|
from certbot_apache._internal import constants
|
||||||
from certbot_apache._internal import display_ops
|
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 http_01
|
||||||
from certbot_apache._internal import obj
|
from certbot_apache._internal import obj
|
||||||
from certbot_apache._internal import parser
|
from certbot_apache._internal import parser
|
||||||
@@ -181,6 +183,7 @@ class ApacheConfigurator(common.Installer):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
version = kwargs.pop("version", None)
|
version = kwargs.pop("version", None)
|
||||||
|
use_parsernode = kwargs.pop("use_parsernode", False)
|
||||||
super(ApacheConfigurator, self).__init__(*args, **kwargs)
|
super(ApacheConfigurator, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Add name_server association dict
|
# Add name_server association dict
|
||||||
@@ -196,10 +199,15 @@ class ApacheConfigurator(common.Installer):
|
|||||||
self._autohsts = {} # type: Dict[str, Dict[str, Union[int, float]]]
|
self._autohsts = {} # type: Dict[str, Dict[str, Union[int, float]]]
|
||||||
# Reverter save notes
|
# Reverter save notes
|
||||||
self.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
|
# These will be set in the prepare function
|
||||||
self._prepared = False
|
self._prepared = False
|
||||||
self.parser = None
|
self.parser = None
|
||||||
|
self.parser_root = None
|
||||||
self.version = version
|
self.version = version
|
||||||
self.vhosts = None
|
self.vhosts = None
|
||||||
self.options = copy.deepcopy(self.OS_DEFAULTS)
|
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
|
# Perform the actual Augeas initialization to be able to react
|
||||||
self.parser = self.get_parser()
|
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
|
# Check for errors in parsing files with Augeas
|
||||||
self.parser.check_parsing_errors("httpd.aug")
|
self.parser.check_parsing_errors("httpd.aug")
|
||||||
|
|
||||||
@@ -344,6 +360,22 @@ class ApacheConfigurator(common.Installer):
|
|||||||
self.option("server_root"), self.conf("vhost-root"),
|
self.option("server_root"), self.conf("vhost-root"),
|
||||||
self.version, configurator=self)
|
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):
|
def _wildcard_domain(self, domain):
|
||||||
"""
|
"""
|
||||||
Checks if domain is a wildcard domain
|
Checks if domain is a wildcard domain
|
||||||
@@ -868,6 +900,29 @@ class ApacheConfigurator(common.Installer):
|
|||||||
return vhost
|
return vhost
|
||||||
|
|
||||||
def get_virtual_hosts(self):
|
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 virtual hosts found in the Apache configuration.
|
||||||
|
|
||||||
:returns: List of :class:`~certbot_apache._internal.obj.VirtualHost`
|
:returns: List of :class:`~certbot_apache._internal.obj.VirtualHost`
|
||||||
@@ -920,6 +975,80 @@ class ApacheConfigurator(common.Installer):
|
|||||||
vhs.append(new_vhost)
|
vhs.append(new_vhost)
|
||||||
return vhs
|
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):
|
def is_name_vhost(self, target_addr):
|
||||||
"""Returns if vhost is a name based vhost
|
"""Returns if vhost is a name based vhost
|
||||||
|
|
||||||
|
306
certbot-apache/certbot_apache/_internal/dualparser.py
Normal file
306
certbot-apache/certbot_apache/_internal/dualparser.py
Normal file
@@ -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
|
516
certbot-apache/certbot_apache/_internal/interfaces.py
Normal file
516
certbot-apache/certbot_apache/_internal/interfaces.py
Normal file
@@ -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
|
||||||
|
"""
|
@@ -124,7 +124,7 @@ class VirtualHost(object):
|
|||||||
strip_name = re.compile(r"^(?:.+://)?([^ :$]*)")
|
strip_name = re.compile(r"^(?:.+://)?([^ :$]*)")
|
||||||
|
|
||||||
def __init__(self, filep, path, addrs, ssl, enabled, name=None,
|
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."""
|
"""Initialize a VH."""
|
||||||
self.filep = filep
|
self.filep = filep
|
||||||
@@ -136,6 +136,7 @@ class VirtualHost(object):
|
|||||||
self.enabled = enabled
|
self.enabled = enabled
|
||||||
self.modmacro = modmacro
|
self.modmacro = modmacro
|
||||||
self.ancestor = ancestor
|
self.ancestor = ancestor
|
||||||
|
self.node = node
|
||||||
|
|
||||||
def get_names(self):
|
def get_names(self):
|
||||||
"""Return a set of all names."""
|
"""Return a set of all names."""
|
||||||
|
@@ -70,6 +70,6 @@ class GentooParser(parser.ApacheParser):
|
|||||||
def update_modules(self):
|
def update_modules(self):
|
||||||
"""Get loaded modules from httpd process, and add them to DOM"""
|
"""Get loaded modules from httpd process, and add them to DOM"""
|
||||||
mod_cmd = [self.configurator.option("ctl"), "modules"]
|
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:
|
for mod in matches:
|
||||||
self.add_mod(mod.strip())
|
self.add_mod(mod.strip())
|
||||||
|
@@ -3,7 +3,6 @@ import copy
|
|||||||
import fnmatch
|
import fnmatch
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import six
|
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 acme.magic_typing import Set # pylint: disable=unused-import, no-name-in-module
|
||||||
from certbot import errors
|
from certbot import errors
|
||||||
from certbot.compat import os
|
from certbot.compat import os
|
||||||
|
from certbot_apache._internal import apache_util
|
||||||
from certbot_apache._internal import constants
|
from certbot_apache._internal import constants
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -290,32 +290,15 @@ class ApacheParser(object):
|
|||||||
|
|
||||||
def update_runtime_variables(self):
|
def update_runtime_variables(self):
|
||||||
"""Update Includes, Defines and Includes from httpd config dump data"""
|
"""Update Includes, Defines and Includes from httpd config dump data"""
|
||||||
|
|
||||||
self.update_defines()
|
self.update_defines()
|
||||||
self.update_includes()
|
self.update_includes()
|
||||||
self.update_modules()
|
self.update_modules()
|
||||||
|
|
||||||
def update_defines(self):
|
def update_defines(self):
|
||||||
"""Get Defines from httpd process"""
|
"""Updates the dictionary of known variables in the configuration"""
|
||||||
|
|
||||||
variables = dict()
|
self.variables = apache_util.parse_defines(self.configurator.option("ctl"))
|
||||||
define_cmd = [self.configurator.option("ctl"), "-t", "-D",
|
|
||||||
"DUMP_RUN_CFG"]
|
|
||||||
matches = self.parse_from_subprocess(define_cmd, r"Define: ([^ \n]*)")
|
|
||||||
try:
|
|
||||||
matches.remove("DUMP_RUN_CFG")
|
|
||||||
except ValueError:
|
|
||||||
return
|
|
||||||
|
|
||||||
for match in matches:
|
|
||||||
if match.count("=") > 1:
|
|
||||||
logger.error("Unexpected number of equal signs in "
|
|
||||||
"runtime config dump.")
|
|
||||||
raise errors.PluginError(
|
|
||||||
"Error parsing Apache runtime variables")
|
|
||||||
parts = match.partition("=")
|
|
||||||
variables[parts[0]] = parts[2]
|
|
||||||
|
|
||||||
self.variables = variables
|
|
||||||
|
|
||||||
def update_includes(self):
|
def update_includes(self):
|
||||||
"""Get includes from httpd process, and add them to DOM if needed"""
|
"""Get includes from httpd process, and add them to DOM if needed"""
|
||||||
@@ -325,9 +308,7 @@ class ApacheParser(object):
|
|||||||
# configuration files
|
# configuration files
|
||||||
_ = self.find_dir("Include")
|
_ = self.find_dir("Include")
|
||||||
|
|
||||||
inc_cmd = [self.configurator.option("ctl"), "-t", "-D",
|
matches = apache_util.parse_includes(self.configurator.option("ctl"))
|
||||||
"DUMP_INCLUDES"]
|
|
||||||
matches = self.parse_from_subprocess(inc_cmd, r"\(.*\) (.*)")
|
|
||||||
if matches:
|
if matches:
|
||||||
for i in matches:
|
for i in matches:
|
||||||
if not self.parsed_in_current(i):
|
if not self.parsed_in_current(i):
|
||||||
@@ -336,56 +317,10 @@ class ApacheParser(object):
|
|||||||
def update_modules(self):
|
def update_modules(self):
|
||||||
"""Get loaded modules from httpd process, and add them to DOM"""
|
"""Get loaded modules from httpd process, and add them to DOM"""
|
||||||
|
|
||||||
mod_cmd = [self.configurator.option("ctl"), "-t", "-D",
|
matches = apache_util.parse_modules(self.configurator.option("ctl"))
|
||||||
"DUMP_MODULES"]
|
|
||||||
matches = self.parse_from_subprocess(mod_cmd, r"(.*)_module")
|
|
||||||
for mod in matches:
|
for mod in matches:
|
||||||
self.add_mod(mod.strip())
|
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
|
def filter_args_num(self, matches, args): # pylint: disable=no-self-use
|
||||||
"""Filter out directives with specific number of arguments.
|
"""Filter out directives with specific number of arguments.
|
||||||
|
|
||||||
@@ -612,7 +547,7 @@ class ApacheParser(object):
|
|||||||
"%s//*[self::directive=~regexp('%s')]" % (start, regex))
|
"%s//*[self::directive=~regexp('%s')]" % (start, regex))
|
||||||
|
|
||||||
if exclude:
|
if exclude:
|
||||||
matches = self._exclude_dirs(matches)
|
matches = self.exclude_dirs(matches)
|
||||||
|
|
||||||
if arg is None:
|
if arg is None:
|
||||||
arg_suffix = "/arg"
|
arg_suffix = "/arg"
|
||||||
@@ -678,7 +613,13 @@ class ApacheParser(object):
|
|||||||
|
|
||||||
return value
|
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."""
|
"""Exclude directives that are not loaded into the configuration."""
|
||||||
filters = [("ifmodule", self.modules), ("ifdefine", self.variables)]
|
filters = [("ifmodule", self.modules), ("ifdefine", self.variables)]
|
||||||
|
|
||||||
|
129
certbot-apache/certbot_apache/_internal/parsernode_util.py
Normal file
129
certbot-apache/certbot_apache/_internal/parsernode_util.py
Normal file
@@ -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
|
@@ -18,6 +18,9 @@ install_requires = [
|
|||||||
'zope.interface',
|
'zope.interface',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
dev_extras = [
|
||||||
|
'apacheconfig>=0.3.1',
|
||||||
|
]
|
||||||
|
|
||||||
class PyTest(TestCommand):
|
class PyTest(TestCommand):
|
||||||
user_options = []
|
user_options = []
|
||||||
@@ -68,6 +71,9 @@ setup(
|
|||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=install_requires,
|
install_requires=install_requires,
|
||||||
|
extras_require={
|
||||||
|
'dev': dev_extras,
|
||||||
|
},
|
||||||
entry_points={
|
entry_points={
|
||||||
'certbot.plugins': [
|
'certbot.plugins': [
|
||||||
'apache = certbot_apache._internal.entrypoint:ENTRYPOINT',
|
'apache = certbot_apache._internal.entrypoint:ENTRYPOINT',
|
||||||
|
319
certbot-apache/tests/augeasnode_test.py
Normal file
319
certbot-apache/tests/augeasnode_test.py
Normal file
@@ -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)
|
@@ -106,7 +106,7 @@ class MultipleVhostsTestCentOS(util.ApacheTest):
|
|||||||
def test_get_parser(self):
|
def test_get_parser(self):
|
||||||
self.assertIsInstance(self.config.parser, override_centos.CentOSParser)
|
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):
|
def test_opportunistic_httpd_runtime_parsing(self, mock_get):
|
||||||
define_val = (
|
define_val = (
|
||||||
'Define: TEST1\n'
|
'Define: TEST1\n'
|
||||||
@@ -155,7 +155,7 @@ class MultipleVhostsTestCentOS(util.ApacheTest):
|
|||||||
raise Exception("Missed: %s" % vhost) # pragma: no cover
|
raise Exception("Missed: %s" % vhost) # pragma: no cover
|
||||||
self.assertEqual(found, 2)
|
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):
|
def test_get_sysconfig_vars(self, mock_cfg):
|
||||||
"""Make sure we read the sysconfig OPTIONS variable correctly"""
|
"""Make sure we read the sysconfig OPTIONS variable correctly"""
|
||||||
# Return nothing for the process calls
|
# Return nothing for the process calls
|
||||||
|
@@ -75,7 +75,8 @@ class MultipleVhostsTest(util.ApacheTest):
|
|||||||
|
|
||||||
@mock.patch("certbot_apache._internal.parser.ApacheParser")
|
@mock.patch("certbot_apache._internal.parser.ApacheParser")
|
||||||
@mock.patch("certbot_apache._internal.configurator.util.exe_exists")
|
@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:
|
try:
|
||||||
self.config.prepare()
|
self.config.prepare()
|
||||||
except errors.PluginError as err:
|
except errors.PluginError as err:
|
||||||
@@ -799,7 +800,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
|||||||
self.assertEqual(mock_restart.call_count, 1)
|
self.assertEqual(mock_restart.call_count, 1)
|
||||||
|
|
||||||
@mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.restart")
|
@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):
|
def test_cleanup(self, mock_cfg, mock_restart):
|
||||||
mock_cfg.return_value = ""
|
mock_cfg.return_value = ""
|
||||||
_, achalls = self.get_key_and_achalls()
|
_, achalls = self.get_key_and_achalls()
|
||||||
@@ -815,7 +816,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
|||||||
self.assertFalse(mock_restart.called)
|
self.assertFalse(mock_restart.called)
|
||||||
|
|
||||||
@mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.restart")
|
@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):
|
def test_cleanup_no_errors(self, mock_cfg, mock_restart):
|
||||||
mock_cfg.return_value = ""
|
mock_cfg.return_value = ""
|
||||||
_, achalls = self.get_key_and_achalls()
|
_, achalls = self.get_key_and_achalls()
|
||||||
|
@@ -46,7 +46,7 @@ class MultipleVhostsTestDebian(util.ApacheTest):
|
|||||||
|
|
||||||
@mock.patch("certbot.util.run_script")
|
@mock.patch("certbot.util.run_script")
|
||||||
@mock.patch("certbot.util.exe_exists")
|
@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):
|
def test_enable_mod(self, mock_popen, mock_exe_exists, mock_run_script):
|
||||||
mock_popen().communicate.return_value = ("Define: DUMP_RUN_CFG", "")
|
mock_popen().communicate.return_value = ("Define: DUMP_RUN_CFG", "")
|
||||||
mock_popen().returncode = 0
|
mock_popen().returncode = 0
|
||||||
|
442
certbot-apache/tests/dualnode_test.py
Normal file
442
certbot-apache/tests/dualnode_test.py
Normal file
@@ -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)
|
@@ -100,7 +100,7 @@ class MultipleVhostsTestFedora(util.ApacheTest):
|
|||||||
def test_get_parser(self):
|
def test_get_parser(self):
|
||||||
self.assertIsInstance(self.config.parser, override_fedora.FedoraParser)
|
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):
|
def test_opportunistic_httpd_runtime_parsing(self, mock_get):
|
||||||
define_val = (
|
define_val = (
|
||||||
'Define: TEST1\n'
|
'Define: TEST1\n'
|
||||||
@@ -155,7 +155,7 @@ class MultipleVhostsTestFedora(util.ApacheTest):
|
|||||||
raise Exception("Missed: %s" % vhost) # pragma: no cover
|
raise Exception("Missed: %s" % vhost) # pragma: no cover
|
||||||
self.assertEqual(found, 2)
|
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):
|
def test_get_sysconfig_vars(self, mock_cfg):
|
||||||
"""Make sure we read the sysconfig OPTIONS variable correctly"""
|
"""Make sure we read the sysconfig OPTIONS variable correctly"""
|
||||||
# Return nothing for the process calls
|
# Return nothing for the process calls
|
||||||
|
@@ -90,7 +90,7 @@ class MultipleVhostsTestGentoo(util.ApacheTest):
|
|||||||
for define in defines:
|
for define in defines:
|
||||||
self.assertTrue(define in self.config.parser.variables.keys())
|
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):
|
def test_no_binary_configdump(self, mock_subprocess):
|
||||||
"""Make sure we don't call binary dumps other than modules from Apache
|
"""Make sure we don't call binary dumps other than modules from Apache
|
||||||
as this is not supported in Gentoo currently"""
|
as this is not supported in Gentoo currently"""
|
||||||
@@ -104,7 +104,7 @@ class MultipleVhostsTestGentoo(util.ApacheTest):
|
|||||||
self.config.parser.reset_modules()
|
self.config.parser.reset_modules()
|
||||||
self.assertTrue(mock_subprocess.called)
|
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):
|
def test_opportunistic_httpd_runtime_parsing(self, mock_get):
|
||||||
mod_val = (
|
mod_val = (
|
||||||
'Loaded Modules:\n'
|
'Loaded Modules:\n'
|
||||||
|
@@ -165,7 +165,7 @@ class BasicParserTest(util.ParserTest):
|
|||||||
self.assertTrue(mock_logger.debug.called)
|
self.assertTrue(mock_logger.debug.called)
|
||||||
|
|
||||||
@mock.patch("certbot_apache._internal.parser.ApacheParser.find_dir")
|
@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, _):
|
def test_update_runtime_variables(self, mock_cfg, _):
|
||||||
define_val = (
|
define_val = (
|
||||||
'ServerRoot: "/etc/apache2"\n'
|
'ServerRoot: "/etc/apache2"\n'
|
||||||
@@ -271,7 +271,7 @@ class BasicParserTest(util.ParserTest):
|
|||||||
self.assertEqual(mock_parse.call_count, 25)
|
self.assertEqual(mock_parse.call_count, 25)
|
||||||
|
|
||||||
@mock.patch("certbot_apache._internal.parser.ApacheParser.find_dir")
|
@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, _):
|
def test_update_runtime_variables_alt_values(self, mock_cfg, _):
|
||||||
inc_val = (
|
inc_val = (
|
||||||
'Included configuration files:\n'
|
'Included configuration files:\n'
|
||||||
@@ -293,7 +293,7 @@ class BasicParserTest(util.ParserTest):
|
|||||||
# path derived from root configuration Include statements
|
# path derived from root configuration Include statements
|
||||||
self.assertEqual(mock_parse.call_count, 1)
|
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):
|
def test_update_runtime_vars_bad_output(self, mock_cfg):
|
||||||
mock_cfg.return_value = "Define: TLS=443=24"
|
mock_cfg.return_value = "Define: TLS=443=24"
|
||||||
self.parser.update_runtime_variables()
|
self.parser.update_runtime_variables()
|
||||||
@@ -303,7 +303,7 @@ class BasicParserTest(util.ParserTest):
|
|||||||
errors.PluginError, self.parser.update_runtime_variables)
|
errors.PluginError, self.parser.update_runtime_variables)
|
||||||
|
|
||||||
@mock.patch("certbot_apache._internal.configurator.ApacheConfigurator.option")
|
@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):
|
def test_update_runtime_vars_bad_ctl(self, mock_popen, mock_opt):
|
||||||
mock_popen.side_effect = OSError
|
mock_popen.side_effect = OSError
|
||||||
mock_opt.return_value = "nonexistent"
|
mock_opt.return_value = "nonexistent"
|
||||||
@@ -311,7 +311,7 @@ class BasicParserTest(util.ParserTest):
|
|||||||
errors.MisconfigurationError,
|
errors.MisconfigurationError,
|
||||||
self.parser.update_runtime_variables)
|
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):
|
def test_update_runtime_vars_bad_exit(self, mock_popen):
|
||||||
mock_popen().communicate.return_value = ("", "")
|
mock_popen().communicate.return_value = ("", "")
|
||||||
mock_popen.returncode = -1
|
mock_popen.returncode = -1
|
||||||
@@ -355,7 +355,7 @@ class ParserInitTest(util.ApacheTest):
|
|||||||
ApacheParser, os.path.relpath(self.config_path),
|
ApacheParser, os.path.relpath(self.config_path),
|
||||||
"/dummy/vhostpath", version=(2, 4, 22), configurator=self.config)
|
"/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):
|
def test_unparseable(self, mock_cfg):
|
||||||
from certbot_apache._internal.parser import ApacheParser
|
from certbot_apache._internal.parser import ApacheParser
|
||||||
mock_cfg.return_value = ('Define: TEST')
|
mock_cfg.return_value = ('Define: TEST')
|
||||||
|
37
certbot-apache/tests/parsernode_configurator_test.py
Normal file
37
certbot-apache/tests/parsernode_configurator_test.py
Normal file
@@ -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
|
128
certbot-apache/tests/parsernode_test.py
Normal file
128
certbot-apache/tests/parsernode_test.py
Normal file
@@ -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
|
115
certbot-apache/tests/parsernode_util_test.py
Normal file
115
certbot-apache/tests/parsernode_util_test.py
Normal file
@@ -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
|
@@ -84,7 +84,8 @@ def get_apache_configurator(
|
|||||||
config_path, vhost_path,
|
config_path, vhost_path,
|
||||||
config_dir, work_dir, version=(2, 4, 7),
|
config_dir, work_dir, version=(2, 4, 7),
|
||||||
os_info="generic",
|
os_info="generic",
|
||||||
conf_vhost_path=None):
|
conf_vhost_path=None,
|
||||||
|
use_parsernode=False):
|
||||||
"""Create an Apache Configurator with the specified options.
|
"""Create an Apache Configurator with the specified options.
|
||||||
|
|
||||||
:param conf: Function that returns binary paths. self.conf in Configurator
|
: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
|
mock_exe_exists.return_value = True
|
||||||
with mock.patch("certbot_apache._internal.parser.ApacheParser."
|
with mock.patch("certbot_apache._internal.parser.ApacheParser."
|
||||||
"update_runtime_variables"):
|
"update_runtime_variables"):
|
||||||
try:
|
with mock.patch("certbot_apache._internal.apache_util.parse_from_subprocess") as mock_sp:
|
||||||
config_class = entrypoint.OVERRIDE_CLASSES[os_info]
|
mock_sp.return_value = []
|
||||||
except KeyError:
|
try:
|
||||||
config_class = configurator.ApacheConfigurator
|
config_class = entrypoint.OVERRIDE_CLASSES[os_info]
|
||||||
config = config_class(config=mock_le_config, name="apache",
|
except KeyError:
|
||||||
version=version)
|
config_class = configurator.ApacheConfigurator
|
||||||
if not conf_vhost_path:
|
config = config_class(config=mock_le_config, name="apache",
|
||||||
config_class.OS_DEFAULTS["vhost_root"] = vhost_path
|
version=version, use_parsernode=use_parsernode)
|
||||||
else:
|
if not conf_vhost_path:
|
||||||
# Custom virtualhost path was requested
|
config_class.OS_DEFAULTS["vhost_root"] = vhost_path
|
||||||
config.config.apache_vhost_root = conf_vhost_path
|
else:
|
||||||
config.config.apache_ctl = config_class.OS_DEFAULTS["ctl"]
|
# Custom virtualhost path was requested
|
||||||
config.prepare()
|
config.config.apache_vhost_root = conf_vhost_path
|
||||||
|
config.config.apache_ctl = config_class.OS_DEFAULTS["ctl"]
|
||||||
|
config.prepare()
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
# Some dev package versions specified here may be overridden by higher level constraints
|
# Some dev package versions specified here may be overridden by higher level constraints
|
||||||
# files during tests (eg. letsencrypt-auto-source/pieces/dependency-requirements.txt).
|
# files during tests (eg. letsencrypt-auto-source/pieces/dependency-requirements.txt).
|
||||||
alabaster==0.7.10
|
alabaster==0.7.10
|
||||||
|
apacheconfig==0.3.1
|
||||||
apipkg==1.4
|
apipkg==1.4
|
||||||
appnope==0.1.0
|
appnope==0.1.0
|
||||||
asn1crypto==0.22.0
|
asn1crypto==0.22.0
|
||||||
@@ -64,6 +65,7 @@ pexpect==4.7.0
|
|||||||
pickleshare==0.7.5
|
pickleshare==0.7.5
|
||||||
pkginfo==1.4.2
|
pkginfo==1.4.2
|
||||||
pluggy==0.13.0
|
pluggy==0.13.0
|
||||||
|
ply==3.4
|
||||||
prompt-toolkit==1.0.18
|
prompt-toolkit==1.0.18
|
||||||
ptyprocess==0.6.0
|
ptyprocess==0.6.0
|
||||||
py==1.8.0
|
py==1.8.0
|
||||||
|
@@ -40,6 +40,7 @@ pytz==2012rc0
|
|||||||
google-api-python-client==1.5.5
|
google-api-python-client==1.5.5
|
||||||
|
|
||||||
# Our setup.py constraints
|
# Our setup.py constraints
|
||||||
|
apacheconfig==0.3.1
|
||||||
cloudflare==1.5.1
|
cloudflare==1.5.1
|
||||||
cryptography==1.2.3
|
cryptography==1.2.3
|
||||||
parsedatetime==1.3
|
parsedatetime==1.3
|
||||||
|
6
tox.ini
6
tox.ini
@@ -93,6 +93,12 @@ commands =
|
|||||||
setenv =
|
setenv =
|
||||||
{[testenv:py27-oldest]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]
|
[testenv:py27-certbot-oldest]
|
||||||
commands =
|
commands =
|
||||||
{[base]install_and_test} certbot[dev]
|
{[base]install_and_test} certbot[dev]
|
||||||
|
Reference in New Issue
Block a user