Skip to content

Commit

Permalink
Refactor _add_directive into separate functions (#5786)
Browse files Browse the repository at this point in the history
* Refactor _add_directive to separate functions

* UnspacedList isn't idempotent

* refactor parser in add_server_directives and update_or_add_server_directives

* update parser tests

* remove replace=False and add to update_or_add for replace=True in configurator

* remove replace=False and add to update_or_add for replace=True in http01

* update documentation
  • Loading branch information
ohemorange committed Mar 23, 2018
1 parent 693cb1d commit 8d0d42a
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 75 deletions.
18 changes: 9 additions & 9 deletions certbot-nginx/certbot_nginx/configurator.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ def _deploy_cert(self, vhost, cert_path, key_path, chain_path, fullchain_path):
cert_directives = [['\n ', 'ssl_certificate', ' ', fullchain_path],
['\n ', 'ssl_certificate_key', ' ', key_path]]

self.parser.add_server_directives(vhost,
cert_directives, replace=True)
self.parser.update_or_add_server_directives(vhost,
cert_directives)
logger.info("Deploying Certificate to VirtualHost %s", vhost.filep)

self.save_notes += ("Changed vhost at %s with addresses of %s\n" %
Expand Down Expand Up @@ -344,7 +344,7 @@ def _add_server_name_to_vhost(self, vhost, domain):
for name in vhost.names:
name_block[0].append(' ')
name_block[0].append(name)
self.parser.add_server_directives(vhost, name_block, replace=True)
self.parser.update_or_add_server_directives(vhost, name_block)

def _get_default_vhost(self, port):
vhost_list = self.parser.get_vhosts()
Expand Down Expand Up @@ -584,7 +584,7 @@ def _make_server_ssl(self, vhost):
# have it continue to do so.
if len(vhost.addrs) == 0:
listen_block = [['\n ', 'listen', ' ', self.DEFAULT_LISTEN_PORT]]
self.parser.add_server_directives(vhost, listen_block, replace=False)
self.parser.add_server_directives(vhost, listen_block)

if vhost.ipv6_enabled():
ipv6_block = ['\n ',
Expand Down Expand Up @@ -618,7 +618,7 @@ def _make_server_ssl(self, vhost):
])

self.parser.add_server_directives(
vhost, ssl_block, replace=False)
vhost, ssl_block)

##################################
# enhancement methods (IInstaller)
Expand Down Expand Up @@ -683,15 +683,15 @@ def _set_http_header(self, domain, header_substring):
['\n ', 'add_header', ' ', header_substring, ' '] +
constants.HEADER_ARGS[header_substring],
['\n']]
self.parser.add_server_directives(vhost, header_directives, replace=False)
self.parser.add_server_directives(vhost, header_directives)

def _add_redirect_block(self, vhost, domain):
"""Add redirect directive to vhost
"""
redirect_block = _redirect_block_for_domain(domain)

self.parser.add_server_directives(
vhost, redirect_block, replace=False, insert_at_top=True)
vhost, redirect_block, insert_at_top=True)

def _split_block(self, vhost, only_directives=None):
"""Splits this "virtual host" (i.e. this nginx server block) into
Expand Down Expand Up @@ -771,7 +771,7 @@ def _enable_redirect_single(self, domain, vhost):

# Add this at the bottom to get the right order of directives
return_404_directive = [['\n ', 'return', ' ', '404']]
self.parser.add_server_directives(http_vhost, return_404_directive, replace=False)
self.parser.add_server_directives(http_vhost, return_404_directive)

vhost = http_vhost

Expand Down Expand Up @@ -821,7 +821,7 @@ def _enable_ocsp_stapling_single(self, vhost, chain_path):

try:
self.parser.add_server_directives(vhost,
stapling_directives, replace=False)
stapling_directives)
except errors.MisconfigurationError as error:
logger.debug(error)
raise errors.PluginError("An error occurred while enabling OCSP "
Expand Down
4 changes: 2 additions & 2 deletions certbot-nginx/certbot_nginx/http_01.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,9 @@ def _make_or_mod_server_block(self, achall):
['return', ' ', '200', ' ', validation]]]]

self.configurator.parser.add_server_directives(vhost,
location_directive, replace=False)
location_directive)

rewrite_directive = [['rewrite', ' ', '^(/.well-known/acme-challenge/.*)',
' ', '$1', ' ', 'break']]
self.configurator.parser.add_server_directives(vhost,
rewrite_directive, replace=False, insert_at_top=True)
rewrite_directive, insert_at_top=True)
105 changes: 64 additions & 41 deletions certbot-nginx/certbot_nginx/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,29 +276,48 @@ def has_ssl_on_directive(self, vhost):

return False

def add_server_directives(self, vhost, directives, replace, insert_at_top=False):
def add_server_directives(self, vhost, directives, insert_at_top=False):
"""Add directives to the server block identified by vhost.
This method modifies vhost to be fully consistent with the new directives.
..note :: It's an error to try and add a nonrepeatable directive that already
exists in the config block with a conflicting value.
..todo :: Doesn't match server blocks whose server_name directives are
split across multiple conf files.
:param :class:`~certbot_nginx.obj.VirtualHost` vhost: The vhost
whose information we use to match on
:param list directives: The directives to add
:param bool insert_at_top: True if the directives need to be inserted at the top
of the server block instead of the bottom
"""
self._modify_server_directives(vhost,
functools.partial(_add_directives, directives, insert_at_top))

def update_or_add_server_directives(self, vhost, directives, insert_at_top=False):
"""Add or replace directives in the server block identified by vhost.
This method modifies vhost to be fully consistent with the new directives.
..note :: If replace is True and the directive already exists, the first
instance will be replaced. Otherwise, the directive is added.
..note :: If replace is False nothing gets added if an identical
block exists already.
..note :: When a directive with the same name already exists in the
config block, the first instance will be replaced. Otherwise, the directive
will be appended/prepended to the config block as in add_server_directives.
..todo :: Doesn't match server blocks whose server_name directives are
split across multiple conf files.
:param :class:`~certbot_nginx.obj.VirtualHost` vhost: The vhost
whose information we use to match on
:param list directives: The directives to add
:param bool replace: Whether to only replace existing directives
:param bool insert_at_top: True if the directives need to be inserted at the top
of the server block instead of the bottom
"""
self._modify_server_directives(vhost,
functools.partial(_add_directives, directives, replace, insert_at_top))
functools.partial(_update_or_add_directives, directives, insert_at_top))

def remove_server_directives(self, vhost, directive_name, match_func=None):
"""Remove all directives of type directive_name.
Expand Down Expand Up @@ -524,26 +543,17 @@ def _is_ssl_on_directive(entry):
len(entry) == 2 and entry[0] == 'ssl' and
entry[1] == 'on')

def _add_directives(directives, replace, insert_at_top, block):
"""Adds or replaces directives in a config block.
When replace=False, it's an error to try and add a nonrepeatable directive that already
exists in the config block with a conflicting value.
When replace=True and a directive with the same name already exists in the
config block, the first instance will be replaced. Otherwise, the directive
will be added to the config block.
..todo :: Find directives that are in included files.
:param list directives: The new directives.
:param bool replace: Described above.
:param bool insert_at_top: Described above.
:param list block: The block to replace in
def _add_directives(directives, insert_at_top, block):
"""Adds directives to a config block."""
for directive in directives:
_add_directive(block, directive, insert_at_top)
if block and '\n' not in block[-1]: # could be " \n " or ["\n"] !
block.append(nginxparser.UnspacedList('\n'))

"""
def _update_or_add_directives(directives, insert_at_top, block):
"""Adds or replaces directives in a config block."""
for directive in directives:
_add_directive(block, directive, replace, insert_at_top)
_update_or_add_directive(block, directive, insert_at_top)
if block and '\n' not in block[-1]: # could be " \n " or ["\n"] !
block.append(nginxparser.UnspacedList('\n'))

Expand Down Expand Up @@ -601,28 +611,20 @@ def _find_location(block, directive_name, match_func=None):
return next((index for index, line in enumerate(block) \
if line and line[0] == directive_name and (match_func is None or match_func(line))), None)

def _add_directive(block, directive, replace, insert_at_top):
"""Adds or replaces a single directive in a config block.
def _is_whitespace_or_comment(directive):
"""Is this directive either a whitespace or comment directive?"""
return len(directive) == 0 or directive[0] == '#'

See _add_directives for more documentation.
"""
directive = nginxparser.UnspacedList(directive)
def is_whitespace_or_comment(directive):
"""Is this directive either a whitespace or comment directive?"""
return len(directive) == 0 or directive[0] == '#'
if is_whitespace_or_comment(directive):
def _add_directive(block, directive, insert_at_top):
if not isinstance(directive, nginxparser.UnspacedList):
directive = nginxparser.UnspacedList(directive)
if _is_whitespace_or_comment(directive):
# whitespace or comment
block.append(directive)
return

location = _find_location(block, directive[0])

if replace:
if location is not None:
block[location] = directive
comment_directive(block, location)
return
# Append or prepend directive. Fail if the name is not a repeatable directive name,
# and there is already a copy of that directive with a different value
# in the config file.
Expand All @@ -647,7 +649,7 @@ def can_append(loc, dir_name):
for included_directive in included_directives:
included_dir_loc = _find_location(block, included_directive[0])
included_dir_name = included_directive[0]
if not is_whitespace_or_comment(included_directive) \
if not _is_whitespace_or_comment(included_directive) \
and not can_append(included_dir_loc, included_dir_name):
if block[included_dir_loc] != included_directive:
raise errors.MisconfigurationError(err_fmt.format(included_directive,
Expand All @@ -668,6 +670,27 @@ def can_append(loc, dir_name):
elif block[location] != directive:
raise errors.MisconfigurationError(err_fmt.format(directive, block[location]))

def _update_directive(block, directive, location):
block[location] = directive
comment_directive(block, location)

def _update_or_add_directive(block, directive, insert_at_top):
if not isinstance(directive, nginxparser.UnspacedList):
directive = nginxparser.UnspacedList(directive)
if _is_whitespace_or_comment(directive):
# whitespace or comment
block.append(directive)
return

location = _find_location(block, directive[0])

# we can update directive
if location is not None:
_update_directive(block, directive, location)
return

_add_directive(block, directive, insert_at_top)

def _is_certbot_comment(directive):
return '#' in directive and COMMENT in directive

Expand Down
9 changes: 4 additions & 5 deletions certbot-nginx/certbot_nginx/tests/configurator_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,7 @@ def test_save(self):
None, [0])
self.config.parser.add_server_directives(
mock_vhost,
[['listen', ' ', '5001', ' ', 'ssl']],
replace=False)
[['listen', ' ', '5001', ' ', 'ssl']])
self.config.save()

# pylint: disable=protected-access
Expand Down Expand Up @@ -206,9 +205,9 @@ def test_deploy_cert_requires_fullchain_path(self):
"example/chain.pem",
None)

@mock.patch('certbot_nginx.parser.NginxParser.add_server_directives')
def test_deploy_cert_raise_on_add_error(self, mock_add_server_directives):
mock_add_server_directives.side_effect = errors.MisconfigurationError()
@mock.patch('certbot_nginx.parser.NginxParser.update_or_add_server_directives')
def test_deploy_cert_raise_on_add_error(self, mock_update_or_add_server_directives):
mock_update_or_add_server_directives.side_effect = errors.MisconfigurationError()
self.assertRaises(
errors.PluginError,
self.config.deploy_cert,
Expand Down
29 changes: 11 additions & 18 deletions certbot-nginx/certbot_nginx/tests/parser_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,7 @@ def test_remove_server_directives(self):
mock_vhost.path = [0]
nparser.add_server_directives(mock_vhost,
[['foo', 'bar'], ['ssl_certificate',
'/etc/ssl/cert2.pem']],
replace=False)
'/etc/ssl/cert2.pem']])
nparser.remove_server_directives(mock_vhost, 'foo')
nparser.remove_server_directives(mock_vhost, 'ssl_certificate')
self.assertEqual(nparser.parsed[example_com],
Expand All @@ -226,8 +225,7 @@ def test_add_server_directives(self):
None, [10, 1, 9])
nparser.add_server_directives(mock_vhost,
[['foo', 'bar'], ['\n ', 'ssl_certificate', ' ',
'/etc/ssl/cert.pem']],
replace=False)
'/etc/ssl/cert.pem']])
ssl_re = re.compile(r'\n\s+ssl_certificate /etc/ssl/cert.pem')
dump = nginxparser.dumps(nparser.parsed[nparser.abs_path('nginx.conf')])
self.assertEqual(1, len(re.findall(ssl_re, dump)))
Expand All @@ -239,10 +237,8 @@ def test_add_server_directives(self):
mock_vhost.path = [0]
nparser.add_server_directives(mock_vhost,
[['foo', 'bar'], ['ssl_certificate',
'/etc/ssl/cert2.pem']],
replace=False)
nparser.add_server_directives(mock_vhost, [['foo', 'bar']],
replace=False)
'/etc/ssl/cert2.pem']])
nparser.add_server_directives(mock_vhost, [['foo', 'bar']])
from certbot_nginx.parser import COMMENT
self.assertEqual(nparser.parsed[example_com],
[[['server'], [['listen', '69.50.225.155:9000'],
Expand All @@ -264,8 +260,7 @@ def test_add_server_directives(self):
nparser.add_server_directives,
mock_vhost,
[['foo', 'bar'],
['ssl_certificate', '/etc/ssl/cert2.pem']],
replace=False)
['ssl_certificate', '/etc/ssl/cert2.pem']])

def test_comment_is_repeatable(self):
nparser = parser.NginxParser(self.config_path)
Expand All @@ -275,12 +270,10 @@ def test_comment_is_repeatable(self):
set(['.example.com', 'example.*']),
None, [0])
nparser.add_server_directives(mock_vhost,
[['\n ', '#', ' ', 'what a nice comment']],
replace=False)
[['\n ', '#', ' ', 'what a nice comment']])
nparser.add_server_directives(mock_vhost,
[['\n ', 'include', ' ',
nparser.abs_path('comment_in_file.conf')]],
replace=False)
nparser.abs_path('comment_in_file.conf')]])
from certbot_nginx.parser import COMMENT
self.assertEqual(nparser.parsed[example_com],
[[['server'], [['listen', '69.50.225.155:9000'],
Expand All @@ -299,8 +292,8 @@ def test_replace_server_directives(self):
target = set(['.example.com', 'example.*'])
filep = nparser.abs_path('sites-enabled/example.com')
mock_vhost = obj.VirtualHost(filep, None, None, None, target, None, [0])
nparser.add_server_directives(
mock_vhost, [['server_name', 'foobar.com']], replace=True)
nparser.update_or_add_server_directives(
mock_vhost, [['server_name', 'foobar.com']])
from certbot_nginx.parser import COMMENT
self.assertEqual(
nparser.parsed[filep],
Expand All @@ -310,8 +303,8 @@ def test_replace_server_directives(self):
['server_name', 'example.*'], []
]]])
mock_vhost.names = set(['foobar.com', 'example.*'])
nparser.add_server_directives(
mock_vhost, [['ssl_certificate', 'cert.pem']], replace=True)
nparser.update_or_add_server_directives(
mock_vhost, [['ssl_certificate', 'cert.pem']])
self.assertEqual(
nparser.parsed[filep],
[[['server'], [['listen', '69.50.225.155:9000'],
Expand Down

0 comments on commit 8d0d42a

Please sign in to comment.