certbot/certbot-nginx/certbot_nginx/nginxparser.py

266 lines
8.8 KiB
Python
Raw Permalink Normal View History

"""Very low-level nginx config parser based on pyparsing."""
2016-06-27 15:44:53 -04:00
# Forked from https://github.com/fatiherikli/nginxparser (MIT Licensed)
import copy
2016-06-16 21:18:33 -04:00
import logging
2015-03-23 13:53:44 -04:00
from pyparsing import (
Literal, White, Forward, Group, Optional, OneOrMore, QuotedString, Regex, ZeroOrMore, Combine)
from pyparsing import stringEnd
2015-07-11 03:15:59 -04:00
from pyparsing import restOfLine
2015-03-23 13:53:44 -04:00
2016-06-16 21:18:33 -04:00
logger = logging.getLogger(__name__)
2015-09-06 05:21:03 -04:00
2015-04-07 14:20:34 -04:00
class RawNginxParser(object):
2015-04-17 20:05:00 -04:00
# pylint: disable=expression-not-assigned
# pylint: disable=pointless-statement
2015-04-18 13:20:19 -04:00
"""A class that parses nginx configuration with pyparsing."""
2015-03-23 13:53:44 -04:00
# constants
space = Optional(White()).leaveWhitespace()
required_space = White().leaveWhitespace()
2015-03-23 13:53:44 -04:00
left_bracket = Literal("{").suppress()
right_bracket = space + Literal("}").suppress()
2015-03-23 13:53:44 -04:00
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.
2016-06-07 20:17:17 -04:00
token = paren_quote_extend | tokenchars | quoted
2016-06-30 18:07:28 -04:00
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
2016-06-30 18:07:28 -04:00
script = OneOrMore(contents) + space + stringEnd
script.parseWithTabs().leaveWhitespace()
2015-03-23 13:53:44 -04:00
2016-06-15 20:26:38 -04:00
def __init__(self, source):
self.source = source
2015-03-23 13:53:44 -04:00
def parse(self):
2015-04-18 13:20:19 -04:00
"""Returns the parsed tree."""
2015-03-23 13:53:44 -04:00
return self.script.parseString(self.source)
def as_list(self):
2015-04-18 13:20:19 -04:00
"""Returns the parsed tree as a list."""
2015-03-23 13:53:44 -04:00
return self.parse().asList()
2015-04-07 14:20:34 -04:00
class RawNginxDumper(object):
2016-06-07 17:46:49 -04:00
# pylint: disable=too-few-public-methods
"""A class that dumps nginx configuration from the provided tree."""
2016-06-20 21:17:47 -04:00
def __init__(self, blocks):
2016-06-07 17:46:49 -04:00
self.blocks = blocks
def __iter__(self, blocks=None):
2016-06-07 17:46:49 -04:00
"""Iterates the dumped nginx content."""
blocks = blocks or self.blocks
for b0 in blocks:
if isinstance(b0, str):
yield b0
2016-06-15 19:42:47 -04:00
continue
item = copy.deepcopy(b0)
if spacey(item[0]):
yield item.pop(0) # indentation
if not item:
continue
2016-06-07 21:19:58 -04:00
if isinstance(item[0], list): # block
yield "".join(item.pop(0)) + '{'
for parameter in item.pop(0):
2016-06-24 19:13:12 -04:00
for line in self.__iter__([parameter]): # negate "for b0 in blocks"
2016-06-07 17:46:49 -04:00
yield line
yield '}'
else: # not a block - list of strings
semicolon = ";"
if isinstance(item[0], str) and item[0].strip() == '#': # comment
semicolon = ""
yield "".join(item) + semicolon
2016-06-07 17:46:49 -04:00
def __str__(self):
"""Return the parsed block as a string."""
return ''.join(self)
2016-06-07 17:46:49 -04:00
2015-03-23 13:53:44 -04:00
# Shortcut functions to respect Python's serialization interface
# (like pyyaml, picker or json)
def loads(source):
2015-04-17 20:05:00 -04:00
"""Parses from a string.
:param str source: The string to parse
2015-04-17 20:05:00 -04:00
:returns: The parsed tree
:rtype: list
"""
2016-06-15 20:26:38 -04:00
return UnspacedList(RawNginxParser(source).as_list())
2015-03-23 13:53:44 -04:00
def load(_file):
2015-04-17 20:05:00 -04:00
"""Parses from a file.
:param file _file: The file to parse
:returns: The parsed tree
:rtype: list
"""
2016-06-07 20:17:17 -04:00
return loads(_file.read())
2015-03-23 13:53:44 -04:00
def dumps(blocks):
2015-04-17 20:05:00 -04:00
"""Dump to a string.
2016-06-07 17:46:49 -04:00
:param UnspacedList block: The parsed tree
2015-04-17 20:05:00 -04:00
:param int indentation: The number of spaces to indent
:rtype: str
"""
return str(RawNginxDumper(blocks.spaced))
2015-03-23 13:53:44 -04:00
def dump(blocks, _file):
2015-04-17 20:05:00 -04:00
"""Dump to a file.
2016-06-07 17:46:49 -04:00
:param UnspacedList block: The parsed tree
2015-04-17 20:05:00 -04:00
: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, str) and x.isspace()) or x == ''
class UnspacedList(list):
"""Wrap a list [of lists], making any whitespace entries magically invisible"""
2016-06-17 17:32:24 -04:00
def __init__(self, list_source):
2016-06-18 17:52:07 -04:00
# 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
2016-06-07 17:46:49 -04:00
# and all sub-lists also UnspacedList()ed
list.__init__(self, list_source)
for i, entry in reversed(list(enumerate(self))):
if isinstance(entry, list):
2016-06-17 17:32:24 -04:00
sublist = UnspacedList(entry)
list.__setitem__(self, i, sublist)
self.spaced[i] = sublist.spaced
elif spacey(entry):
2016-06-18 17:52:07 -04:00
# don't delete comments
if "#" not in self[:i]:
list.__delitem__(self, i)
2016-06-07 17:46:49 -04:00
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)
2016-06-18 17:52:07 -04:00
else:
2016-06-20 18:56:19 -04:00
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)
list.insert(self, i, item)
self.dirty = True
def append(self, x):
item, spaced_item = self._coerce(x)
self.spaced.append(spaced_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):
2016-06-17 17:32:24 -04:00
l = copy.deepcopy(self)
l.extend(other)
l.dirty = True
2016-06-17 17:32:24 -04:00
return l
2016-06-07 17:46:49 -04:00
2016-06-27 15:36:28 -04:00
def pop(self, _i=None):
raise NotImplementedError("UnspacedList.pop() not yet implemented")
def remove(self, _):
raise NotImplementedError("UnspacedList.remove() not yet implemented")
2016-06-27 15:00:41 -04:00
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")
2016-06-27 15:00:41 -04:00
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")
2016-06-20 18:56:19 -04:00
item, spaced_item = self._coerce(value)
self.spaced.__setitem__(self._spaced_position(i), spaced_item)
2016-06-20 18:56:19 -04:00
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):
2016-06-17 17:32:24 -04:00
l = UnspacedList(self[:])
l.spaced = copy.deepcopy(self.spaced, memo=memo)
l.dirty = self.dirty
2016-06-17 17:32:24 -04:00
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, list) and x.is_dirty() for x in self))
2016-06-17 17:32:24 -04:00
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