mirror of
https://github.com/certbot/certbot.git
synced 2026-07-28 00:35:50 +02:00
Merge pull request #3184 from certbot/nginx-space-preservation
Nginx space preservation
This commit is contained in:
@@ -152,17 +152,17 @@ class NginxConfigurator(common.Plugin):
|
||||
"install a cert.")
|
||||
|
||||
vhost = self.choose_vhost(domain)
|
||||
cert_directives = [['ssl_certificate', fullchain_path],
|
||||
['ssl_certificate_key', key_path]]
|
||||
cert_directives = [['\n', 'ssl_certificate', ' ', fullchain_path],
|
||||
['\n', 'ssl_certificate_key', ' ', key_path]]
|
||||
|
||||
# OCSP stapling was introduced in Nginx 1.3.7. If we have that version
|
||||
# or greater, add config settings for it.
|
||||
stapling_directives = []
|
||||
if self.version >= (1, 3, 7):
|
||||
stapling_directives = [
|
||||
['ssl_trusted_certificate', chain_path],
|
||||
['ssl_stapling', 'on'],
|
||||
['ssl_stapling_verify', 'on']]
|
||||
['\n', 'ssl_trusted_certificate', ' ', chain_path],
|
||||
['\n', 'ssl_stapling', ' ', 'on'],
|
||||
['\n', 'ssl_stapling_verify', ' ', 'on'], ['\n']]
|
||||
|
||||
if len(stapling_directives) != 0 and not chain_path:
|
||||
raise errors.PluginError(
|
||||
@@ -225,7 +225,7 @@ class NginxConfigurator(common.Plugin):
|
||||
if not matches:
|
||||
# No matches. Create a new vhost with this name in nginx.conf.
|
||||
filep = self.parser.loc["root"]
|
||||
new_block = [['server'], [['server_name', target_name]]]
|
||||
new_block = [['server'], [['\n', 'server_name', ' ', target_name]]]
|
||||
self.parser.add_http_directives(filep, new_block)
|
||||
vhost = obj.VirtualHost(filep, set([]), False, True,
|
||||
set([target_name]), list(new_block[1]))
|
||||
@@ -337,10 +337,10 @@ class NginxConfigurator(common.Plugin):
|
||||
|
||||
"""
|
||||
snakeoil_cert, snakeoil_key = self._get_snakeoil_paths()
|
||||
ssl_block = [['listen', '{0} ssl'.format(self.config.tls_sni_01_port)],
|
||||
['ssl_certificate', snakeoil_cert],
|
||||
['ssl_certificate_key', snakeoil_key],
|
||||
['include', self.parser.loc["ssl_options"]]]
|
||||
ssl_block = [['\n', 'listen', ' ', '{0} ssl'.format(self.config.tls_sni_01_port)],
|
||||
['\n', 'ssl_certificate', ' ', snakeoil_cert],
|
||||
['\n', 'ssl_certificate_key', ' ', snakeoil_key],
|
||||
['\n', 'include', ' ', self.parser.loc["ssl_options"]]]
|
||||
self.parser.add_server_directives(
|
||||
vhost.filep, vhost.names, ssl_block, replace=False)
|
||||
vhost.ssl = True
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
"""Very low-level nginx config parser based on pyparsing."""
|
||||
import copy
|
||||
import logging
|
||||
import string
|
||||
|
||||
from pyparsing import (
|
||||
Literal, White, Word, alphanums, CharsNotIn, Forward, Group,
|
||||
Literal, White, Word, alphanums, CharsNotIn, Combine, Forward, Group,
|
||||
Optional, OneOrMore, Regex, ZeroOrMore)
|
||||
from pyparsing import stringEnd
|
||||
from pyparsing import restOfLine
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class RawNginxParser(object):
|
||||
# pylint: disable=expression-not-assigned
|
||||
"""A class that parses nginx configuration with pyparsing."""
|
||||
|
||||
# constants
|
||||
space = Optional(White())
|
||||
nonspace = Regex(r"\S+")
|
||||
left_bracket = Literal("{").suppress()
|
||||
right_bracket = Literal("}").suppress()
|
||||
right_bracket = space.leaveWhitespace() + Literal("}").suppress()
|
||||
semicolon = Literal(";").suppress()
|
||||
space = White().suppress()
|
||||
key = Word(alphanums + "_/+-.")
|
||||
dollar_var = Combine(Literal('$') + nonspace)
|
||||
condition = Regex(r"\(.+\)")
|
||||
# Matches anything that is not a special character AND any chars in single
|
||||
# or double quotes
|
||||
value = Regex(r"((\".*\")?(\'.*\')?[^\{\};,]?)+")
|
||||
@@ -26,20 +32,25 @@ class RawNginxParser(object):
|
||||
modifier = Literal("=") | Literal("~*") | Literal("~") | Literal("^~")
|
||||
|
||||
# rules
|
||||
comment = Literal('#') + restOfLine()
|
||||
assignment = (key + Optional(space + value, default=None) + semicolon)
|
||||
location_statement = Optional(space + modifier) + Optional(space + location)
|
||||
if_statement = Literal("if") + space + Regex(r"\(.+\)") + space
|
||||
map_statement = Literal("map") + space + Regex(r"\S+") + space + Regex(r"\$\S+") + space
|
||||
comment = space + Literal('#') + restOfLine()
|
||||
|
||||
assignment = space + key + Optional(space + value, default=None) + semicolon
|
||||
location_statement = space + Optional(modifier) + Optional(space + location + space)
|
||||
if_statement = space + Literal("if") + space + condition + space
|
||||
map_statement = space + Literal("map") + space + nonspace + space + dollar_var + space
|
||||
block = Forward()
|
||||
|
||||
block << Group(
|
||||
(Group(key + location_statement) ^ Group(if_statement) ^ Group(map_statement)) +
|
||||
# key could for instance be "server" or "http", or "location" (in which case
|
||||
# location_statement needs to have a non-empty location)
|
||||
(Group(space + key + location_statement) ^ Group(if_statement) ^
|
||||
Group(map_statement)).leaveWhitespace() +
|
||||
left_bracket +
|
||||
Group(ZeroOrMore(Group(comment | assignment) | block)) +
|
||||
Group(ZeroOrMore(Group(comment | assignment) | block) + space).leaveWhitespace() +
|
||||
right_bracket)
|
||||
|
||||
script = OneOrMore(Group(comment | assignment) ^ block) + stringEnd
|
||||
script = OneOrMore(Group(comment | assignment) ^ block) + space + stringEnd
|
||||
script.parseWithTabs()
|
||||
|
||||
def __init__(self, source):
|
||||
self.source = source
|
||||
@@ -52,42 +63,48 @@ class RawNginxParser(object):
|
||||
"""Returns the parsed tree as a list."""
|
||||
return self.parse().asList()
|
||||
|
||||
|
||||
class RawNginxDumper(object):
|
||||
# pylint: disable=too-few-public-methods
|
||||
"""A class that dumps nginx configuration from the provided tree."""
|
||||
def __init__(self, blocks, indentation=4):
|
||||
def __init__(self, blocks):
|
||||
self.blocks = blocks
|
||||
self.indentation = indentation
|
||||
|
||||
def __iter__(self, blocks=None, current_indent=0, spacer=' '):
|
||||
def __iter__(self, blocks=None):
|
||||
"""Iterates the dumped nginx content."""
|
||||
blocks = blocks or self.blocks
|
||||
for key, values in blocks:
|
||||
indentation = spacer * current_indent
|
||||
for b0 in blocks:
|
||||
if isinstance(b0, str):
|
||||
yield b0
|
||||
continue
|
||||
b = copy.deepcopy(b0)
|
||||
if spacey(b[0]):
|
||||
yield b.pop(0) # indentation
|
||||
if not b:
|
||||
continue
|
||||
key = b.pop(0)
|
||||
values = b.pop(0)
|
||||
|
||||
if isinstance(key, list):
|
||||
if current_indent:
|
||||
yield ''
|
||||
yield indentation + spacer.join(key) + ' {'
|
||||
|
||||
yield "".join(key) + '{'
|
||||
for parameter in values:
|
||||
dumped = self.__iter__([parameter], current_indent + self.indentation)
|
||||
for line in dumped:
|
||||
for line in self.__iter__([parameter]): # negate "for b0 in blocks"
|
||||
yield line
|
||||
|
||||
yield indentation + '}'
|
||||
yield '}'
|
||||
else:
|
||||
if key == '#':
|
||||
yield spacer * current_indent + key + values
|
||||
if isinstance(key, str) and key.strip() == '#':
|
||||
yield key + values
|
||||
else:
|
||||
if values is None:
|
||||
yield spacer * current_indent + key + ';'
|
||||
else:
|
||||
yield spacer * current_indent + key + spacer + values + ';'
|
||||
gap = ""
|
||||
# Sometimes the parser has stuck some gap whitespace in here;
|
||||
# if so rotate it into gap
|
||||
if values and spacey(values):
|
||||
gap = values
|
||||
values = b.pop(0)
|
||||
yield key + gap + values + ';'
|
||||
|
||||
def __str__(self):
|
||||
"""Return the parsed block as a string."""
|
||||
return '\n'.join(self) + '\n'
|
||||
return ''.join(self)
|
||||
|
||||
|
||||
# Shortcut functions to respect Python's serialization interface
|
||||
@@ -101,7 +118,7 @@ def loads(source):
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
return RawNginxParser(source).as_list()
|
||||
return UnspacedList(RawNginxParser(source).as_list())
|
||||
|
||||
|
||||
def load(_file):
|
||||
@@ -115,24 +132,130 @@ def load(_file):
|
||||
return loads(_file.read())
|
||||
|
||||
|
||||
def dumps(blocks, indentation=4):
|
||||
def dumps(blocks):
|
||||
"""Dump to a string.
|
||||
|
||||
:param list block: The parsed tree
|
||||
:param UnspacedList block: The parsed tree
|
||||
:param int indentation: The number of spaces to indent
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
return str(RawNginxDumper(blocks, indentation))
|
||||
return str(RawNginxDumper(blocks.spaced))
|
||||
|
||||
|
||||
def dump(blocks, _file, indentation=4):
|
||||
def dump(blocks, _file):
|
||||
"""Dump to a file.
|
||||
|
||||
:param list block: The parsed tree
|
||||
: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, indentation))
|
||||
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"""
|
||||
|
||||
def __init__(self, list_source):
|
||||
# ensure our argument is not a generator, and duplicate any sublists
|
||||
self.spaced = copy.deepcopy(list(list_source))
|
||||
|
||||
# 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)
|
||||
self.spaced.insert(self._spaced_position(i), spaced_item)
|
||||
list.insert(self, i, item)
|
||||
|
||||
def append(self, x):
|
||||
item, spaced_item = self._coerce(x)
|
||||
self.spaced.append(spaced_item)
|
||||
list.append(self, item)
|
||||
|
||||
def extend(self, x):
|
||||
item, spaced_item = self._coerce(x)
|
||||
self.spaced.extend(spaced_item)
|
||||
list.extend(self, item)
|
||||
|
||||
def __add__(self, other):
|
||||
l = copy.deepcopy(self)
|
||||
l.extend(other)
|
||||
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)
|
||||
list.__setitem__(self, i, item)
|
||||
|
||||
def __delitem__(self, i):
|
||||
self.spaced.__delitem__(self._spaced_position(i))
|
||||
list.__delitem__(self, i)
|
||||
|
||||
def __deepcopy__(self, unused_memo):
|
||||
l = UnspacedList(self[:])
|
||||
l.spaced = copy.deepcopy(self.spaced)
|
||||
return l
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -85,6 +85,9 @@ class Addr(common.Addr):
|
||||
|
||||
return parts
|
||||
|
||||
def __repr__(self):
|
||||
return "Addr(" + self.__str__() + ")"
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return (self.tup == other.tup and
|
||||
@@ -126,6 +129,9 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
|
||||
"enabled: %s" % (self.filep, addr_str,
|
||||
self.names, self.ssl, self.enabled))
|
||||
|
||||
def __repr__(self):
|
||||
return "VirtualHost(" + self.__str__().replace("\n", ", ") + ")\n"
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return (self.filep == other.filep and
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""NginxParser is a member object of the NginxConfigurator class."""
|
||||
import copy
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
@@ -17,7 +18,7 @@ logger = logging.getLogger(__name__)
|
||||
class NginxParser(object):
|
||||
"""Class handles the fine details of parsing the Nginx Configuration.
|
||||
|
||||
:ivar str root: Normalized abosulte path to the server root
|
||||
:ivar str root: Normalized absolute path to the server root
|
||||
directory. Without trailing slash.
|
||||
:ivar dict parsed: Mapping of file paths to parsed trees
|
||||
|
||||
@@ -113,6 +114,7 @@ class NginxParser(object):
|
||||
for filename in servers:
|
||||
for server in servers[filename]:
|
||||
# Parse the server block into a VirtualHost object
|
||||
|
||||
parsed_server = parse_server(server)
|
||||
vhost = obj.VirtualHost(filename,
|
||||
parsed_server['addrs'],
|
||||
@@ -132,7 +134,7 @@ class NginxParser(object):
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
result = list(block) # Copy the list to keep self.parsed idempotent
|
||||
result = copy.deepcopy(block) # Copy the list to keep self.parsed idempotent
|
||||
for directive in block:
|
||||
if _is_include_directive(directive):
|
||||
included_files = glob.glob(
|
||||
@@ -153,7 +155,9 @@ class NginxParser(object):
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
files = glob.glob(filepath)
|
||||
files = glob.glob(filepath) # nginx on unix calls glob(3) for this
|
||||
# XXX Windows nginx uses FindFirstFile, and
|
||||
# should have a narrower call here
|
||||
trees = []
|
||||
for item in files:
|
||||
if item in self.parsed and not override:
|
||||
@@ -208,14 +212,17 @@ class NginxParser(object):
|
||||
empty, this overrides the existing conf files.
|
||||
|
||||
"""
|
||||
# Best-effort atomicity is enforced above us by reverter.py
|
||||
for filename in self.parsed:
|
||||
tree = self.parsed[filename]
|
||||
if ext:
|
||||
filename = filename + os.path.extsep + ext
|
||||
try:
|
||||
logger.debug('Dumping to %s:\n%s', filename, nginxparser.dumps(tree))
|
||||
out = nginxparser.dumps(tree)
|
||||
logger.debug('Writing nginx conf tree to %s:\n%s', filename, out)
|
||||
with open(filename, 'w') as _file:
|
||||
nginxparser.dump(tree, _file)
|
||||
_file.write(out)
|
||||
|
||||
except IOError:
|
||||
logger.error("Could not open file for writing: %s", filename)
|
||||
|
||||
@@ -463,6 +470,8 @@ def parse_server(server):
|
||||
'names': set()}
|
||||
|
||||
for directive in server:
|
||||
if not directive:
|
||||
continue
|
||||
if directive[0] == 'listen':
|
||||
addr = obj.Addr.fromstring(directive[1])
|
||||
parsed_server['addrs'].add(addr)
|
||||
@@ -503,6 +512,11 @@ def _add_directive(block, directive, replace):
|
||||
See _add_directives for more documentation.
|
||||
|
||||
"""
|
||||
directive = nginxparser.UnspacedList(directive)
|
||||
if len(directive) == 0:
|
||||
# whitespace
|
||||
block.append(directive)
|
||||
return
|
||||
location = -1
|
||||
# Find the index of a config line where the name of the directive matches
|
||||
# the name of the directive we want to add.
|
||||
|
||||
@@ -83,7 +83,7 @@ class NginxConfiguratorTest(util.NginxTest):
|
||||
filep = self.config.parser.abs_path('sites-enabled/example.com')
|
||||
self.config.parser.add_server_directives(
|
||||
filep, set(['.example.com', 'example.*']),
|
||||
[['listen', '5001 ssl']],
|
||||
[['listen', ' ', '5001 ssl']],
|
||||
replace=False)
|
||||
self.config.save()
|
||||
|
||||
@@ -265,7 +265,8 @@ class NginxConfiguratorTest(util.NginxTest):
|
||||
|
||||
@mock.patch("certbot_nginx.configurator.tls_sni_01.NginxTlsSni01.perform")
|
||||
@mock.patch("certbot_nginx.configurator.NginxConfigurator.restart")
|
||||
def test_perform(self, mock_restart, mock_perform):
|
||||
@mock.patch("certbot_nginx.configurator.NginxConfigurator.revert_challenge_config")
|
||||
def test_perform_and_cleanup(self, mock_revert, mock_restart, mock_perform):
|
||||
# Only tests functionality specific to configurator.perform
|
||||
# Note: As more challenges are offered this will have to be expanded
|
||||
achall1 = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
@@ -291,7 +292,11 @@ class NginxConfiguratorTest(util.NginxTest):
|
||||
|
||||
self.assertEqual(mock_perform.call_count, 1)
|
||||
self.assertEqual(responses, expected)
|
||||
self.assertEqual(mock_restart.call_count, 1)
|
||||
|
||||
self.config.cleanup([achall1, achall2])
|
||||
self.assertEqual(0, self.config._chall_out) # pylint: disable=protected-access
|
||||
self.assertEqual(mock_revert.call_count, 1)
|
||||
self.assertEqual(mock_restart.call_count, 2)
|
||||
|
||||
@mock.patch("certbot_nginx.configurator.subprocess.Popen")
|
||||
def test_get_version(self, mock_popen):
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"""Test for certbot_nginx.nginxparser."""
|
||||
import copy
|
||||
import operator
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from pyparsing import ParseException
|
||||
|
||||
from certbot_nginx.nginxparser import (
|
||||
RawNginxParser, loads, load, dumps, dump)
|
||||
RawNginxParser, loads, load, dumps, dump, UnspacedList)
|
||||
from certbot_nginx.tests import util
|
||||
|
||||
|
||||
@@ -17,39 +19,39 @@ class TestRawNginxParser(unittest.TestCase):
|
||||
|
||||
def test_assignments(self):
|
||||
parsed = RawNginxParser.assignment.parseString('root /test;').asList()
|
||||
self.assertEqual(parsed, ['root', '/test'])
|
||||
parsed = RawNginxParser.assignment.parseString('root /test;'
|
||||
'foo bar;').asList()
|
||||
self.assertEqual(parsed, ['root', '/test'], ['foo', 'bar'])
|
||||
self.assertEqual(parsed, ['root', ' ', '/test'])
|
||||
parsed = RawNginxParser.assignment.parseString('root /test;foo bar;').asList()
|
||||
self.assertEqual(parsed, ['root', ' ', '/test'], ['foo', ' ', 'bar'])
|
||||
|
||||
def test_blocks(self):
|
||||
parsed = RawNginxParser.block.parseString('foo {}').asList()
|
||||
self.assertEqual(parsed, [[['foo'], []]])
|
||||
self.assertEqual(parsed, [[['foo', ' '], []]])
|
||||
parsed = RawNginxParser.block.parseString('location /foo{}').asList()
|
||||
self.assertEqual(parsed, [[['location', '/foo'], []]])
|
||||
parsed = RawNginxParser.block.parseString('foo { bar foo; }').asList()
|
||||
self.assertEqual(parsed, [[['foo'], [['bar', 'foo']]]])
|
||||
self.assertEqual(parsed, [[['location', ' ', '/foo'], []]])
|
||||
parsed = RawNginxParser.block.parseString('foo { bar foo ; }').asList()
|
||||
self.assertEqual(parsed, [[['foo', ' '], [[' ', 'bar', ' ', 'foo '], ' ']]])
|
||||
|
||||
def test_nested_blocks(self):
|
||||
parsed = RawNginxParser.block.parseString('foo { bar {} }').asList()
|
||||
block, content = FIRST(parsed)
|
||||
self.assertEqual(FIRST(content), [['bar'], []])
|
||||
self.assertEqual(FIRST(content), [[' ', 'bar', ' '], []])
|
||||
self.assertEqual(FIRST(block), 'foo')
|
||||
|
||||
def test_dump_as_string(self):
|
||||
dumped = dumps([
|
||||
['user', 'www-data'],
|
||||
[['server'], [
|
||||
['listen', '80'],
|
||||
['server_name', 'foo.com'],
|
||||
['root', '/home/ubuntu/sites/foo/'],
|
||||
[['location', '/status'], [
|
||||
['check_status', None],
|
||||
[['types'], [['image/jpeg', 'jpg']]],
|
||||
dumped = dumps(UnspacedList([
|
||||
['user', ' ', 'www-data'],
|
||||
[['\n', 'server', ' '], [
|
||||
['\n ', 'listen', ' ', '80'],
|
||||
['\n ', 'server_name', ' ', 'foo.com'],
|
||||
['\n ', 'root', ' ', '/home/ubuntu/sites/foo/'],
|
||||
[['\n\n ', 'location', ' ', '/status', ' '], [
|
||||
['\n ', 'check_status', ''],
|
||||
[['\n\n ', 'types', ' '],
|
||||
[['\n ', 'image/jpeg', ' ', 'jpg']]],
|
||||
]]
|
||||
]]])
|
||||
]]]))
|
||||
|
||||
self.assertEqual(dumped,
|
||||
self.assertEqual(dumped.split('\n'),
|
||||
'user www-data;\n'
|
||||
'server {\n'
|
||||
' listen 80;\n'
|
||||
@@ -60,10 +62,7 @@ class TestRawNginxParser(unittest.TestCase):
|
||||
' check_status;\n'
|
||||
'\n'
|
||||
' types {\n'
|
||||
' image/jpeg jpg;\n'
|
||||
' }\n'
|
||||
' }\n'
|
||||
'}\n')
|
||||
' image/jpeg jpg;}}}'.split('\n'))
|
||||
|
||||
def test_parse_from_file(self):
|
||||
with open(util.get_data_filename('foo.conf')) as handle:
|
||||
@@ -116,24 +115,29 @@ class TestRawNginxParser(unittest.TestCase):
|
||||
|
||||
def test_dump_as_file(self):
|
||||
with open(util.get_data_filename('nginx.conf')) as handle:
|
||||
parsed = util.filter_comments(load(handle))
|
||||
parsed[-1][-1].append([['server'],
|
||||
[['listen', '443 ssl'],
|
||||
['server_name', 'localhost'],
|
||||
['ssl_certificate', 'cert.pem'],
|
||||
['ssl_certificate_key', 'cert.key'],
|
||||
['ssl_session_cache', 'shared:SSL:1m'],
|
||||
['ssl_session_timeout', '5m'],
|
||||
['ssl_ciphers', 'HIGH:!aNULL:!MD5'],
|
||||
[['location', '/'],
|
||||
[['root', 'html'],
|
||||
['index', 'index.html index.htm']]]]])
|
||||
parsed = load(handle)
|
||||
parsed[-1][-1].append(UnspacedList([['server'],
|
||||
[['listen', ' ', '443 ssl'],
|
||||
['server_name', ' ', 'localhost'],
|
||||
['ssl_certificate', ' ', 'cert.pem'],
|
||||
['ssl_certificate_key', ' ', 'cert.key'],
|
||||
['ssl_session_cache', ' ', 'shared:SSL:1m'],
|
||||
['ssl_session_timeout', ' ', '5m'],
|
||||
['ssl_ciphers', ' ', 'HIGH:!aNULL:!MD5'],
|
||||
[['location', ' ', '/'],
|
||||
[['root', ' ', 'html'],
|
||||
['index', ' ', 'index.html index.htm']]]]]))
|
||||
|
||||
with open(util.get_data_filename('nginx.new.conf'), 'w') as handle:
|
||||
dump(parsed, handle)
|
||||
with open(util.get_data_filename('nginx.new.conf')) as handle:
|
||||
parsed_new = util.filter_comments(load(handle))
|
||||
self.assertEquals(parsed, parsed_new)
|
||||
parsed_new = load(handle)
|
||||
try:
|
||||
self.maxDiff = None
|
||||
self.assertEquals(parsed[0], parsed_new[0])
|
||||
self.assertEquals(parsed[1:], parsed_new[1:])
|
||||
finally:
|
||||
os.unlink(util.get_data_filename('nginx.new.conf'))
|
||||
|
||||
def test_comments(self):
|
||||
with open(util.get_data_filename('minimalistic_comments.conf')) as handle:
|
||||
@@ -145,20 +149,23 @@ class TestRawNginxParser(unittest.TestCase):
|
||||
with open(util.get_data_filename('minimalistic_comments.new.conf')) as handle:
|
||||
parsed_new = load(handle)
|
||||
|
||||
self.assertEquals(parsed, parsed_new)
|
||||
try:
|
||||
self.assertEquals(parsed, parsed_new)
|
||||
|
||||
self.assertEqual(parsed_new, [
|
||||
['#', " Use bar.conf when it's a full moon!"],
|
||||
['include', 'foo.conf'],
|
||||
['#', ' Kilroy was here'],
|
||||
['check_status', None],
|
||||
[['server'],
|
||||
[['#', ''],
|
||||
['#', " Don't forget to open up your firewall!"],
|
||||
['#', ''],
|
||||
['listen', '1234'],
|
||||
['#', ' listen 80;']]],
|
||||
])
|
||||
self.assertEqual(parsed_new, [
|
||||
['#', " Use bar.conf when it's a full moon!"],
|
||||
['include', 'foo.conf'],
|
||||
['#', ' Kilroy was here'],
|
||||
['check_status'],
|
||||
[['server'],
|
||||
[['#', ''],
|
||||
['#', " Don't forget to open up your firewall!"],
|
||||
['#', ''],
|
||||
['listen', '1234'],
|
||||
['#', ' listen 80;']]],
|
||||
])
|
||||
finally:
|
||||
os.unlink(util.get_data_filename('minimalistic_comments.new.conf'))
|
||||
|
||||
def test_issue_518(self):
|
||||
parsed = loads('if ($http_accept ~* "webp") { set $webp "true"; }')
|
||||
@@ -168,5 +175,62 @@ class TestRawNginxParser(unittest.TestCase):
|
||||
[['set', '$webp "true"']]]
|
||||
])
|
||||
|
||||
class TestUnspacedList(unittest.TestCase):
|
||||
"""Test the UnspacedList data structure"""
|
||||
def setUp(self):
|
||||
self.a = ["\n ", "things", " ", "quirk"]
|
||||
self.b = ["y", " "]
|
||||
self.l = self.a[:]
|
||||
self.l2 = self.b[:]
|
||||
self.ul = UnspacedList(self.l)
|
||||
self.ul2 = UnspacedList(self.l2)
|
||||
|
||||
def test_construction(self):
|
||||
self.assertEqual(self.ul, ["things", "quirk"])
|
||||
self.assertEqual(self.ul2, ["y"])
|
||||
|
||||
def test_append(self):
|
||||
ul3 = copy.deepcopy(self.ul)
|
||||
ul3.append("wise")
|
||||
self.assertEqual(ul3, ["things", "quirk", "wise"])
|
||||
self.assertEqual(ul3.spaced, self.a + ["wise"])
|
||||
|
||||
def test_add(self):
|
||||
ul3 = self.ul + self.ul2
|
||||
self.assertEqual(ul3, ["things", "quirk", "y"])
|
||||
self.assertEqual(ul3.spaced, self.a + self.b)
|
||||
self.assertEqual(self.ul.spaced, self.a)
|
||||
ul3 = self.ul + self.l2
|
||||
self.assertEqual(ul3, ["things", "quirk", "y"])
|
||||
self.assertEqual(ul3.spaced, self.a + self.b)
|
||||
|
||||
def test_extend(self):
|
||||
ul3 = copy.deepcopy(self.ul)
|
||||
ul3.extend(self.ul2)
|
||||
self.assertEqual(ul3, ["things", "quirk", "y"])
|
||||
self.assertEqual(ul3.spaced, self.a + self.b)
|
||||
self.assertEqual(self.ul.spaced, self.a)
|
||||
|
||||
def test_set(self):
|
||||
ul3 = copy.deepcopy(self.ul)
|
||||
ul3[0] = "zither"
|
||||
l = ["\n ", "zather", "zest"]
|
||||
ul3[1] = UnspacedList(l)
|
||||
self.assertEqual(ul3, ["zither", ["zather", "zest"]])
|
||||
self.assertEqual(ul3.spaced, [self.a[0], "zither", " ", l])
|
||||
|
||||
def test_get(self):
|
||||
self.assertRaises(IndexError, self.ul2.__getitem__, 2)
|
||||
self.assertRaises(IndexError, self.ul2.__getitem__, -3)
|
||||
|
||||
def test_rawlists(self):
|
||||
ul3 = copy.deepcopy(self.ul)
|
||||
ul3.insert(0, "some")
|
||||
ul3.append("why")
|
||||
ul3.extend(["did", "whether"])
|
||||
del ul3[2]
|
||||
self.assertEqual(ul3, ["some", "things", "why", "did", "whether"])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
|
||||
@@ -126,7 +126,7 @@ class NginxParserTest(util.NginxTest):
|
||||
nparser.add_server_directives(nparser.abs_path('nginx.conf'),
|
||||
set(['localhost',
|
||||
r'~^(www\.)?(example|bar)\.']),
|
||||
[['foo', 'bar'], ['ssl_certificate',
|
||||
[['foo', 'bar'], ['\n ', 'ssl_certificate', ' ',
|
||||
'/etc/ssl/cert.pem']],
|
||||
replace=False)
|
||||
ssl_re = re.compile(r'\n\s+ssl_certificate /etc/ssl/cert.pem')
|
||||
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
# Use bar.conf when it's a full moon!
|
||||
include foo.conf;
|
||||
# Kilroy was here
|
||||
check_status;
|
||||
server {
|
||||
#
|
||||
# Don't forget to open up your firewall!
|
||||
#
|
||||
listen 1234;
|
||||
# listen 80;
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
user nobody;
|
||||
worker_processes 1;
|
||||
error_log logs/error.log;
|
||||
error_log logs/error.log notice;
|
||||
error_log logs/error.log info;
|
||||
pid logs/nginx.pid;
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
include foo.conf;
|
||||
http {
|
||||
include mime.types;
|
||||
include sites-enabled/*;
|
||||
default_type application/octet-stream;
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
access_log logs/access.log main;
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
keepalive_timeout 0;
|
||||
gzip on;
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
server_name localhost;
|
||||
server_name ~^(www\.)?(example|bar)\.;
|
||||
charset koi8-r;
|
||||
access_log logs/host.access.log main;
|
||||
|
||||
location / {
|
||||
root html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
error_page 404 /404.html;
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
location = /50x.html {
|
||||
root html;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
proxy_pass http://127.0.0.1;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
root html;
|
||||
fastcgi_pass 127.0.0.1:9000;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
|
||||
}
|
||||
|
||||
location ~ /\.ht {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 8000;
|
||||
listen somename:8080;
|
||||
include server.conf;
|
||||
|
||||
location / {
|
||||
root html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name localhost;
|
||||
ssl_certificate cert.pem;
|
||||
ssl_certificate_key cert.key;
|
||||
ssl_session_cache shared:SSL:1m;
|
||||
ssl_session_timeout 5m;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
location / {
|
||||
root html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Common utilities for certbot_nginx."""
|
||||
import copy
|
||||
import os
|
||||
import pkg_resources
|
||||
import unittest
|
||||
@@ -82,12 +83,15 @@ def filter_comments(tree):
|
||||
|
||||
def traverse(tree):
|
||||
"""Generator dropping comment nodes"""
|
||||
for key, values in tree:
|
||||
for entry in tree:
|
||||
key, values = entry
|
||||
if isinstance(key, list):
|
||||
yield [key, filter_comments(values)]
|
||||
new = copy.deepcopy(entry)
|
||||
new[1] = filter_comments(values)
|
||||
yield new
|
||||
else:
|
||||
if key != '#':
|
||||
yield [key, values]
|
||||
yield entry
|
||||
|
||||
return list(traverse(tree))
|
||||
|
||||
|
||||
@@ -91,10 +91,10 @@ class NginxTlsSni01(common.TLSSNI01):
|
||||
# Add the 'include' statement for the challenges if it doesn't exist
|
||||
# already in the main config
|
||||
included = False
|
||||
include_directive = ['include', self.challenge_conf]
|
||||
include_directive = ['include', ' ', self.challenge_conf]
|
||||
root = self.configurator.parser.loc["root"]
|
||||
|
||||
bucket_directive = ['server_names_hash_bucket_size', '128']
|
||||
bucket_directive = ['server_names_hash_bucket_size', ' ', '128']
|
||||
|
||||
main = self.configurator.parser.parsed[root]
|
||||
for key, body in main:
|
||||
@@ -116,6 +116,7 @@ class NginxTlsSni01(common.TLSSNI01):
|
||||
|
||||
config = [self._make_server_block(pair[0], pair[1])
|
||||
for pair in itertools.izip(self.achalls, ll_addrs)]
|
||||
config = nginxparser.UnspacedList(config)
|
||||
|
||||
self.configurator.reverter.register_file_creation(
|
||||
True, self.challenge_conf)
|
||||
@@ -140,19 +141,19 @@ class NginxTlsSni01(common.TLSSNI01):
|
||||
document_root = os.path.join(
|
||||
self.configurator.config.work_dir, "tls_sni_01_page")
|
||||
|
||||
block = [['listen', str(addr)] for addr in addrs]
|
||||
block = [['listen', ' ', str(addr)] for addr in addrs]
|
||||
|
||||
block.extend([['server_name',
|
||||
block.extend([['server_name', ' ',
|
||||
achall.response(achall.account_key).z_domain],
|
||||
['include', self.configurator.parser.loc["ssl_options"]],
|
||||
['include', ' ', self.configurator.parser.loc["ssl_options"]],
|
||||
# access and error logs necessary for
|
||||
# integration testing (non-root)
|
||||
['access_log', os.path.join(
|
||||
['access_log', ' ', os.path.join(
|
||||
self.configurator.config.work_dir, 'access.log')],
|
||||
['error_log', os.path.join(
|
||||
['error_log', ' ', os.path.join(
|
||||
self.configurator.config.work_dir, 'error.log')],
|
||||
['ssl_certificate', self.get_cert_path(achall)],
|
||||
['ssl_certificate_key', self.get_key_path(achall)],
|
||||
[['location', '/'], [['root', document_root]]]])
|
||||
['ssl_certificate', ' ', self.get_cert_path(achall)],
|
||||
['ssl_certificate_key', ' ', self.get_key_path(achall)],
|
||||
[['location', ' ', '/'], [['root', ' ', document_root]]]])
|
||||
|
||||
return [['server'], block]
|
||||
|
||||
Reference in New Issue
Block a user