diff --git a/letsencrypt/client/plugins/nginx/configurator.py b/letsencrypt/client/plugins/nginx/configurator.py index 3c00e5e50..88a440f21 100644 --- a/letsencrypt/client/plugins/nginx/configurator.py +++ b/letsencrypt/client/plugins/nginx/configurator.py @@ -159,8 +159,12 @@ class NginxConfigurator(object): matches = self._get_ranked_matches(target_name) if not matches: - # No matches at all :'( - pass + # No matches. Create a new vhost with this name in nginx.conf. + filep = self.parser.loc["root"] + new_block = [['server'], [['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])) elif matches[0]['rank'] in xrange(2, 6): # Wildcard match - need to find the longest one rank = matches[0]['rank'] diff --git a/letsencrypt/client/plugins/nginx/dvsni.py b/letsencrypt/client/plugins/nginx/dvsni.py index 30af2e7a1..0e4f125f6 100644 --- a/letsencrypt/client/plugins/nginx/dvsni.py +++ b/letsencrypt/client/plugins/nginx/dvsni.py @@ -52,9 +52,8 @@ class NginxDvsni(ApacheDvsni): vhost = self.configurator.choose_vhost(achall.domain) if vhost is None: logging.error( - "No nginx vhost exists with server_name or alias of: %s", + "No nginx vhost exists with server_name matching: %s", achall.domain) - logging.error("No default 443 nginx vhost exists") logging.error("Please specify server_names in the Nginx config") return None diff --git a/letsencrypt/client/plugins/nginx/obj.py b/letsencrypt/client/plugins/nginx/obj.py index f4b2f0f57..b2db6522a 100644 --- a/letsencrypt/client/plugins/nginx/obj.py +++ b/letsencrypt/client/plugins/nginx/obj.py @@ -97,7 +97,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods :ivar set addrs: Virtual Host addresses (:class:`set` of :class:`Addr`) :ivar set names: Server names/aliases of vhost (:class:`list` of :class:`str`) - :ivar array raw: The raw form of the parsed server block + :ivar list raw: The raw form of the parsed server block :ivar bool ssl: SSLEngine on in vhost :ivar bool enabled: Virtual host is enabled diff --git a/letsencrypt/client/plugins/nginx/parser.py b/letsencrypt/client/plugins/nginx/parser.py index 16480d75f..099a8e36d 100644 --- a/letsencrypt/client/plugins/nginx/parser.py +++ b/letsencrypt/client/plugins/nginx/parser.py @@ -253,7 +253,7 @@ class NginxParser(object): def add_server_directives(self, filename, names, directives, replace=False): - """Add or replace directives in server blocks identified by server_name. + """Add or replace directives in the first server block with names. ..note :: If replace is True, this raises a misconfiguration error if the directive does not already exist. @@ -267,14 +267,20 @@ class NginxParser(object): :param bool replace: Whether to only replace existing directives """ - if replace: - _do_for_subarray(self.parsed[filename], - lambda x: self._has_server_names(x, names), - lambda x: _replace_directives(x, directives)) - else: - _do_for_subarray(self.parsed[filename], - lambda x: self._has_server_names(x, names), - lambda x: x.extend(directives)) + _do_for_subarray(self.parsed[filename], + lambda x: self._has_server_names(x, names), + lambda x: _add_directives(x, directives, replace)) + + def add_http_directives(self, filename, directives): + """Adds directives to the first encountered HTTP block in filename. + + :param str filename: The absolute filename of the config file + :param list directives: The directives to add + + """ + _do_for_subarray(self.parsed[filename], + lambda x: x[0] == ['http'], + lambda x: _add_directives(x[1], [directives], False)) def get_all_certs_keys(self): """Gets all certs and keys in the nginx config. @@ -463,24 +469,28 @@ def _parse_server(server): return parsed_server -def _replace_directives(block, directives): - """Replaces directives in a block. If the directive doesn't exist in +def _add_directives(block, directives, replace=False): + """Adds or replaces directives in a block. If the directive doesn't exist in the entry already, raises a misconfiguration error. ..todo :: Find directives that are in included files. :param list block: The block to replace in :param list directives: The new directives. + """ - for directive in directives: - changed = False - if len(directive) == 0: - continue - for index, line in enumerate(block): - if len(line) > 0 and line[0] == directive[0]: - block[index] = directive - changed = True - if not changed: - raise errors.LetsEncryptMisconfigurationError( - 'LetsEncrypt expected directive for %s in the Nginx config ' - 'but did not find it.' % directive[0]) + if replace: + for directive in directives: + changed = False + if len(directive) == 0: + continue + for index, line in enumerate(block): + if len(line) > 0 and line[0] == directive[0]: + block[index] = directive + changed = True + if not changed: + raise errors.LetsEncryptMisconfigurationError( + 'LetsEncrypt expected directive for %s in the Nginx ' + 'config but did not find it.' % directive[0]) + else: + block.extend(directives) diff --git a/letsencrypt/client/plugins/nginx/tests/configurator_test.py b/letsencrypt/client/plugins/nginx/tests/configurator_test.py index 9399d42a6..a17fbb611 100644 --- a/letsencrypt/client/plugins/nginx/tests/configurator_test.py +++ b/letsencrypt/client/plugins/nginx/tests/configurator_test.py @@ -91,7 +91,7 @@ class NginxConfiguratorTest(util.NginxTest): self.assertEqual(results[name], self.config.choose_vhost(name).names) for name in bad_results: - self.assertEqual(None, self.config.choose_vhost(name)) + self.assertEqual(set([name]), self.config.choose_vhost(name).names) def test_more_info(self): self.assertTrue('nginx.conf' in self.config.more_info()) diff --git a/letsencrypt/client/plugins/nginx/tests/dvsni_test.py b/letsencrypt/client/plugins/nginx/tests/dvsni_test.py index 15d7dbdb5..7505b3751 100644 --- a/letsencrypt/client/plugins/nginx/tests/dvsni_test.py +++ b/letsencrypt/client/plugins/nginx/tests/dvsni_test.py @@ -75,12 +75,12 @@ class DvsniPerformTest(util.NginxTest): self.assertEqual([0], self.sni.indices) @mock.patch("letsencrypt.client.plugins.nginx.configurator." - "NginxConfigurator.save") - def test_perform(self, mock_save): + "NginxConfigurator.choose_vhost") + def test_perform(self, mock_choose): self.sni.add_chall(self.achalls[1]) - responses = self.sni.perform() - self.assertTrue(responses is None) - self.assertEqual(mock_save.call_count, 1) + mock_choose.return_value = None + result = self.sni.perform() + self.assertTrue(result is None) def test_perform0(self): responses = self.sni.perform() @@ -108,30 +108,31 @@ class DvsniPerformTest(util.NginxTest): self.assertTrue(['include', self.sni.challenge_conf] in http[1]) def test_perform2(self): - self.sni.add_chall(self.achalls[0]) - self.sni.add_chall(self.achalls[2]) + for achall in self.achalls: + self.sni.add_chall(achall) mock_setup_cert = mock.MagicMock(side_effect=[ challenges.DVSNIResponse(s="nginxS0"), - challenges.DVSNIResponse(s="nginxS1")]) + challenges.DVSNIResponse(s="nginxS1"), + challenges.DVSNIResponse(s="nginxS2")]) # pylint: disable=protected-access self.sni._setup_challenge_cert = mock_setup_cert responses = self.sni.perform() - self.assertEqual(mock_setup_cert.call_count, 2) + self.assertEqual(mock_setup_cert.call_count, 3) - self.assertEqual( - mock_setup_cert.call_args_list[0], mock.call(self.achalls[0])) - self.assertEqual( - mock_setup_cert.call_args_list[1], mock.call(self.achalls[2])) + for index, achall in enumerate(self.achalls): + self.assertEqual( + mock_setup_cert.call_args_list[index], mock.call(achall)) http = self.sni.configurator.parser.parsed[ self.sni.configurator.parser.loc["root"]][-1] self.assertTrue(['include', self.sni.challenge_conf] in http[1]) + self.assertTrue(['server_name', 'blah'] in http[1][-2][1]) - self.assertEqual(len(responses), 2) - for i in xrange(2): + self.assertEqual(len(responses), 3) + for i in xrange(3): self.assertEqual(responses[i].s, "nginxS%d" % i) def test_mod_config(self): diff --git a/letsencrypt/client/plugins/nginx/tests/parser_test.py b/letsencrypt/client/plugins/nginx/tests/parser_test.py index 21e96aa26..51ca03e5e 100644 --- a/letsencrypt/client/plugins/nginx/tests/parser_test.py +++ b/letsencrypt/client/plugins/nginx/tests/parser_test.py @@ -140,6 +140,16 @@ class NginxParserTest(util.NginxTest): ['foo', 'bar'], ['ssl_certificate', '/etc/ssl/cert2.pem']]) + def test_add_http_directives(self): + nparser = parser.NginxParser(self.config_path, self.ssl_options) + filep = nparser.abs_path('nginx.conf') + block = [['server'], + [['listen', '80'], + ['server_name', 'localhost']]] + nparser.add_http_directives(filep, block) + self.assertEqual(nparser.parsed[filep][-1][0], ['http']) + self.assertEqual(nparser.parsed[filep][-1][1][-1], block) + def test_replace_server_directives(self): nparser = parser.NginxParser(self.config_path, self.ssl_options) target = set(['.example.com', 'example.*'])