diff options
author | Filip Lajszczak <filip.lajszczak@gmail.com> | 2020-02-06 18:14:17 +0300 |
---|---|---|
committer | Filip Lajszczak <filip.lajszczak@gmail.com> | 2020-02-06 18:14:17 +0300 |
commit | 2b051dd197804b5c93b6e38aae91c991278afdba (patch) | |
tree | e2872c7a8e89f03e0e4675e2d217c595d50a768c /certbot-nginx/certbot_nginx/_internal/nginxparser.py | |
parent | b27e5804b9671e28f37a6da7e2f1f7fa9455d24a (diff) | |
parent | 7da5196206b33d5593bd15cd1dcce4d790db7e6d (diff) |
Merge branch 'master' of https://github.com/certbot/certbot
Diffstat (limited to 'certbot-nginx/certbot_nginx/_internal/nginxparser.py')
-rw-r--r-- | certbot-nginx/certbot_nginx/_internal/nginxparser.py | 275 |
1 files changed, 275 insertions, 0 deletions
diff --git a/certbot-nginx/certbot_nginx/_internal/nginxparser.py b/certbot-nginx/certbot_nginx/_internal/nginxparser.py new file mode 100644 index 000000000..4fa1362a0 --- /dev/null +++ b/certbot-nginx/certbot_nginx/_internal/nginxparser.py @@ -0,0 +1,275 @@ +"""Very low-level nginx config parser based on pyparsing.""" +# Forked from https://github.com/fatiherikli/nginxparser (MIT Licensed) +import copy +import logging + +from pyparsing import Combine +from pyparsing import Forward +from pyparsing import Group +from pyparsing import Literal +from pyparsing import OneOrMore +from pyparsing import Optional +from pyparsing import QuotedString +from pyparsing import Regex +from pyparsing import restOfLine +from pyparsing import stringEnd +from pyparsing import White +from pyparsing import ZeroOrMore +import six + +logger = logging.getLogger(__name__) + +class RawNginxParser(object): + # pylint: disable=expression-not-assigned + # pylint: disable=pointless-statement + """A class that parses nginx configuration with pyparsing.""" + + # constants + space = Optional(White()).leaveWhitespace() + required_space = White().leaveWhitespace() + + left_bracket = Literal("{").suppress() + right_bracket = space + Literal("}").suppress() + semicolon = Literal(";").suppress() + dquoted = QuotedString('"', multiline=True, unquoteResults=False, escChar='\\') + squoted = QuotedString("'", multiline=True, unquoteResults=False, escChar='\\') + quoted = dquoted | squoted + head_tokenchars = Regex(r"(\$\{)|[^{};\s'\"]") # if (last_space) + tail_tokenchars = Regex(r"(\$\{)|[^{;\s]") # else + tokenchars = Combine(head_tokenchars + ZeroOrMore(tail_tokenchars)) + paren_quote_extend = Combine(quoted + Literal(')') + ZeroOrMore(tail_tokenchars)) + # note: ')' allows extension, but then we fall into else, not last_space. + + token = paren_quote_extend | tokenchars | quoted + + whitespace_token_group = space + token + ZeroOrMore(required_space + token) + space + assignment = whitespace_token_group + semicolon + + comment = space + Literal('#') + restOfLine + + block = Forward() + + # order matters! see issue 518, and also http { # server { \n} + contents = Group(comment) | Group(block) | Group(assignment) + + block_begin = Group(whitespace_token_group) + block_innards = Group(ZeroOrMore(contents) + space).leaveWhitespace() + block << block_begin + left_bracket + block_innards + right_bracket + + script = OneOrMore(contents) + space + stringEnd + script.parseWithTabs().leaveWhitespace() + + def __init__(self, source): + self.source = source + + def parse(self): + """Returns the parsed tree.""" + return self.script.parseString(self.source) + + def as_list(self): + """Returns the parsed tree as a list.""" + return self.parse().asList() + +class RawNginxDumper(object): + """A class that dumps nginx configuration from the provided tree.""" + def __init__(self, blocks): + self.blocks = blocks + + def __iter__(self, blocks=None): + """Iterates the dumped nginx content.""" + blocks = blocks or self.blocks + for b0 in blocks: + if isinstance(b0, six.string_types): + yield b0 + continue + item = copy.deepcopy(b0) + if spacey(item[0]): + yield item.pop(0) # indentation + if not item: + continue + + if isinstance(item[0], list): # block + yield "".join(item.pop(0)) + '{' + for parameter in item.pop(0): + for line in self.__iter__([parameter]): # negate "for b0 in blocks" + yield line + yield '}' + else: # not a block - list of strings + semicolon = ";" + if isinstance(item[0], six.string_types) and item[0].strip() == '#': # comment + semicolon = "" + yield "".join(item) + semicolon + + def __str__(self): + """Return the parsed block as a string.""" + return ''.join(self) + + +# Shortcut functions to respect Python's serialization interface +# (like pyyaml, picker or json) + +def loads(source): + """Parses from a string. + + :param str source: The string to parse + :returns: The parsed tree + :rtype: list + + """ + return UnspacedList(RawNginxParser(source).as_list()) + + +def load(_file): + """Parses from a file. + + :param file _file: The file to parse + :returns: The parsed tree + :rtype: list + + """ + return loads(_file.read()) + + +def dumps(blocks): + """Dump to a string. + + :param UnspacedList block: The parsed tree + :param int indentation: The number of spaces to indent + :rtype: str + + """ + return str(RawNginxDumper(blocks.spaced)) + + +def dump(blocks, _file): + """Dump to a file. + + :param UnspacedList block: The parsed tree + :param file _file: The file to dump to + :param int indentation: The number of spaces to indent + :rtype: NoneType + + """ + return _file.write(dumps(blocks)) + + +spacey = lambda x: (isinstance(x, six.string_types) and x.isspace()) or x == '' + +class UnspacedList(list): + """Wrap a list [of lists], making any whitespace entries magically invisible""" + + def __init__(self, list_source): + # ensure our argument is not a generator, and duplicate any sublists + self.spaced = copy.deepcopy(list(list_source)) + self.dirty = False + + # Turn self into a version of the source list that has spaces removed + # and all sub-lists also UnspacedList()ed + list.__init__(self, list_source) + for i, entry in reversed(list(enumerate(self))): + if isinstance(entry, list): + sublist = UnspacedList(entry) + list.__setitem__(self, i, sublist) + self.spaced[i] = sublist.spaced + elif spacey(entry): + # don't delete comments + if "#" not in self[:i]: + list.__delitem__(self, i) + + def _coerce(self, inbound): + """ + Coerce some inbound object to be appropriately usable in this object + + :param inbound: string or None or list or UnspacedList + :returns: (coerced UnspacedList or string or None, spaced equivalent) + :rtype: tuple + + """ + if not isinstance(inbound, list): # str or None + return inbound, inbound + else: + if not hasattr(inbound, "spaced"): + inbound = UnspacedList(inbound) + return inbound, inbound.spaced + + def insert(self, i, x): + item, spaced_item = self._coerce(x) + slicepos = self._spaced_position(i) if i < len(self) else len(self.spaced) + self.spaced.insert(slicepos, spaced_item) + if not spacey(item): + list.insert(self, i, item) + self.dirty = True + + def append(self, x): + item, spaced_item = self._coerce(x) + self.spaced.append(spaced_item) + if not spacey(item): + list.append(self, item) + self.dirty = True + + def extend(self, x): + item, spaced_item = self._coerce(x) + self.spaced.extend(spaced_item) + list.extend(self, item) + self.dirty = True + + def __add__(self, other): + l = copy.deepcopy(self) + l.extend(other) + l.dirty = True + return l + + def pop(self, _i=None): + raise NotImplementedError("UnspacedList.pop() not yet implemented") + def remove(self, _): + raise NotImplementedError("UnspacedList.remove() not yet implemented") + def reverse(self): + raise NotImplementedError("UnspacedList.reverse() not yet implemented") + def sort(self, _cmp=None, _key=None, _Rev=None): + raise NotImplementedError("UnspacedList.sort() not yet implemented") + def __setslice__(self, _i, _j, _newslice): + raise NotImplementedError("Slice operations on UnspacedLists not yet implemented") + + def __setitem__(self, i, value): + if isinstance(i, slice): + raise NotImplementedError("Slice operations on UnspacedLists not yet implemented") + item, spaced_item = self._coerce(value) + self.spaced.__setitem__(self._spaced_position(i), spaced_item) + if not spacey(item): + list.__setitem__(self, i, item) + self.dirty = True + + def __delitem__(self, i): + self.spaced.__delitem__(self._spaced_position(i)) + list.__delitem__(self, i) + self.dirty = True + + def __deepcopy__(self, memo): + new_spaced = copy.deepcopy(self.spaced, memo=memo) + l = UnspacedList(new_spaced) + l.dirty = self.dirty + return l + + def is_dirty(self): + """Recurse through the parse tree to figure out if any sublists are dirty""" + if self.dirty: + return True + return any((isinstance(x, UnspacedList) and x.is_dirty() for x in self)) + + def _spaced_position(self, idx): + "Convert from indexes in the unspaced list to positions in the spaced one" + pos = spaces = 0 + # Normalize indexes like list[-1] etc, and save the result + if idx < 0: + idx = len(self) + idx + if not 0 <= idx < len(self): + raise IndexError("list index out of range") + idx0 = idx + # Count the number of spaces in the spaced list before idx in the unspaced one + while idx != -1: + if spacey(self.spaced[pos]): + spaces += 1 + else: + idx -= 1 + pos += 1 + return idx0 + spaces |